안녕하세요. 이번 포스팅은 저번 포스팅에 이어 본격적으로 Account Controller에 대해 설계를 시작해보도록 하겠습니다.
기본적으로 Rest API 설계를 다음의 원칙으로 진행하겠습니다.
1. Controller는 url 매핑 이외의 비즈니스 로직에 대해 알면 안 된다.
2. 모든 비즈니스 로직은 Service에서 처리한다.
3. Service에서 Repository와 연결되는 코드는 모두 private접근자를 사용하여 외부에서 접근이 불가능하게 설계한다.
4. RequestDto와 ResponseDto를 설계하여 Entity를 전달받거나 Entity를 반환하지 않는다.
이 정도의 큰 틀을 잡고 시작하겠습니다.
우선 User package에 Dto package, UserService, UserController, UserRepository를 생성합니다.
전체 디렉토리 구조는 다음과 같습니다.
우선 UserRepository부터 살펴보겠습니다.
UserRepository.java
package com.BlogWithSpringBoot.User;
import org.springframework.data.jpa.repository.JpaRepository;
// 자동 Bean 등록 @Repository 생략가능
public interface UserRepository extends JpaRepository<User, Long> {
}
@Repository는 생략이 가능합니다.
JPA사용을 위해 JpaRepository를 상속받아 줍니다. UserEntity와 PrimaryKey의 데이터형인 Long을 인자로 받습니다.
UserController는 다음과 같습니다.
UserController.java
package com.BlogWithSpringBoot.User;
import com.BlogWithSpringBoot.User.Dto.SignupRequestDto;
import com.BlogWithSpringBoot.User.Dto.UserResponseDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class UserController {
@Autowired
UserService userService;
@PostMapping(value = "/Account/SignUp")
public Long SignUp(@RequestBody SignupRequestDto signupRequestDto) {
return userService.UserSignup(signupRequestDto);
}
@GetMapping("/Account/UserDetails/{id}")
public UserResponseDto getUserDetail(@PathVariable Long id) {
return userService.UserDetails(id);
}
@GetMapping("/Account/UserDetails")
public List<UserResponseDto> getUserDetailsAll() {
return userService.UserDetailsAll();
}
@GetMapping("/Account/UserDetailsPage/{page_no}/{page_size}")
public List<UserResponseDto> getUserDetailPage(@PathVariable int page_no, @PathVariable int page_size) {
Pageable pageable = PageRequest.of(page_no, page_size, Sort.by("createdDate"));
return userService.getUserDetailByPage(pageable);
}
}
UserService를 @Autowired를 활용하여 주입받습니다.
위에서부터 순서대로,
회원가입 , 특정 유저 정보 읽기, 모든 유저정보 읽기, Page단위로 유저정보 읽기입니다.
Signup Request Dto는 JSON형태로 데이터를 주고받기 위해서 @RequestBody 어노테이션을 붙여주었습니다.
각각 URL에 맞는 service method를 호출해주었습니다.
UserService.java
package com.BlogWithSpringBoot.User;
import com.BlogWithSpringBoot.RoleType;
import com.BlogWithSpringBoot.User.Dto.SignupRequestDto;
import com.BlogWithSpringBoot.User.Dto.UserResponseDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/*
* Controller should not know SignUpDto to User method
* Controller should not directly Connect to Repository
*
* */
private User signUpDtoToUser(SignupRequestDto signupRequestDto) {
return User.builder().username(signupRequestDto.getUsername())
.email(signupRequestDto.getEmail())
.password(signupRequestDto.getPassword())
// TODO bcrypt...
.role(RoleType.ROLE_USER).build();
}
private Long SaveUserToRepo(User user) {
userRepository.save(user);
return user.getId();
}
private UserResponseDto FindUserDetailsById(Long id) {
User user = userRepository.findById(id).orElseThrow(()-> {
return new IllegalArgumentException("no user mathing user id");
});
return new UserResponseDto(user);
}
private List<UserResponseDto> FindUserDetailsByPage(Pageable pageable) {
Page<User> ListUser = userRepository.findAll(pageable);
return ListUser.stream()
.map(UserResponseDto::new)
.collect(Collectors.toList());
}
private List<UserResponseDto> UserListToUserResponseList () {
List<User> ListUser = userRepository.findAll();
return ListUser.stream()
.map(UserResponseDto::new)
.collect(Collectors.toList());
}
/*
* Public method
*
* */
public Long UserSignup(SignupRequestDto signupRequestDto) {
User user = signUpDtoToUser(signupRequestDto);
return SaveUserToRepo(user);
}
public UserResponseDto UserDetails(Long id) {
return FindUserDetailsById(id);
}
public List<UserResponseDto> UserDetailsAll() {
return UserListToUserResponseList();
}
public List<UserResponseDto> getUserDetailByPage(Pageable pageable) {
return FindUserDetailsByPage(pageable);
}
}
Controller와 맞닿아 있는 메서드의 경우 간단하게 private method를 호출하는 형식으로 짜보았고, 아직 User의 정보를 save 할 때에 해쉬를 적용하여 저장하지 않았습니다. 다음 포스팅에서 진행할 예정입니다.
그리고 각각 Read method에 적용할 ResponseDto를 짜서 stream을 활용하여 User 정보와 UserResponseDto를 각각 mapping 해주었습니다. 그리고 UserResponseDto를 반환해주었습니다.
각각 Dto는 다음과 같습니다.
package com.BlogWithSpringBoot.User.Dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor @AllArgsConstructor @Builder
public class SignupRequestDto {
private String username;
private String password;
private String email;
}
package com.BlogWithSpringBoot.User.Dto;
import com.BlogWithSpringBoot.User.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor @AllArgsConstructor @Builder
public class UserResponseDto {
private String username;
private String password;
private String email;
}
현재는 두 클래스에서 갖는 변수가 똑같아서 구분의 의미가 없어 보일 수 있지만, 추후에 Entity가 복잡해지면 분리하여 Request용 Dto, Response용 Dto를 구분하여 구현하는 게 좋습니다.
그러면 간단하게 postman을 통하여 API테스트를 진행해보겠습니다.
기본적으로 aaa ~ eee까지 5번의 SignUp을 진행해 주었습니다.
정상적으로 id가 반환이 되는 것을 확인할 수 있습니다.
workbench에서도 정상적으로 5개의 data가 들어있는 것을 확인할 수 있습니다.
우선 특정 유저 정보를 반환받아보겠습니다.
다음과 같이 요청을 보내고,
정상적으로 반환된 것을 확인할 수 있습니다.
paging관련 url도 테스트해보겠습니다.
2개씩 Paging 하고, 0번째 page를 반환받았습니다.
이렇게 UserController의 C, R기능에 대해 설계해 보았습니다.
물론 Security적용과 비밀번호 해싱, 등등할 작업이 많고, View를 디자인하다 보면 변경점이 생길 수 있지만 우선 간단하게 설계해 보았습니다.
다음 포스팅에서는 비밀번호 해싱, U, D 기능 구현과 테스트 코드 작성까지 진행해보겠습니다.
*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.
'Backend > Spring' 카테고리의 다른 글
[Spring] 전통적인 Spring의 Transaction과 JPA의 OSIV 전략 (0) | 2021.07.13 |
---|---|
[SpringBoot] PasswordEncoder 사용하기 (0) | 2021.07.12 |
[Spring/JPA] 영속성 컨텍스트 이해하기 (2) | 2021.06.29 |
[SpringBoot] 블로그 프로젝트 #2 JPA 설정 및 Entity 생성 (0) | 2021.06.29 |
[SpringBoot] 블로그 프로젝트 #1 JSP 설정 (0) | 2021.06.27 |