티스토리 뷰

스프링 부트 REST API WEB 프로젝트

깃헙 링크

https://github.com/choiwoonsik/springboot_RestApi_App_Project/tree/main/restApiSpringBootApp

수행 목록

  1. 환경구성 및 helloworld 출력
  2. H2 DB 연동
  3. Swagger API 문서 연동
  4. REST API 설계
  5. RestControllerAdvice를 이용한 통합 예외 처리
  6. Entity - DTO 분리
  7. MessageSource를 이용해 예외 메시지 다국화
  8. JPA Aduting을 이용해 객체 생성시간/수정시간 적용
  9. 스프링 시큐리티 + Jwt를 이용해서 인증 및 권한 체크
  10. 스프링 시큐리티 AuthenticationEntryPoint, AccessDenied로 인증 및 인가 예외처리
  11. Jwt AccessToken + RefreshToken으로 보안성과 사용자 편의성 고도화하기
  12. JUnit Test (단위 테스트)
  13. JUnit Test (통합 테스트)
  14. OAuth 2.0 정리
  15. OAuth 2.0 카카오 로그인 part.1 Authorization code + Token 발급
  16. OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
  17. OAuth 2.0 카카오 로그인 테스트 검증
  18. 환경별 설정을 위해서profile 분리하기 

 


지금까지는 User엔티티를 그대로 API 요청의 응답값으로 반환하여 사용하였다. 이렇게 반환된 값을 가지고 Controller 및 Service에 사용하게 될 경우 해당 로직들이 Entity의 속성값와 의존관계를 맺게된다.

 

하지만 Entity는 매우 Core한 객체로 사용범위도 매우 광범위하고 모든 데이터를 갖고 있는 객체로 이러한 엔티티와 서비스가 의존관계를 갖게 하는것은 유지보수 측면에서나 관리측면에서 매우 부적합하다.

 

또한 Entity는 한번 변경되면 영향을 끼치는 범위가 매우 큰데, Response, Request 용 DTO는 View를 위한 용도이므로 변경이 자주 발생하게 된다.

 

예를들어 Service에 정의된 findAllUser()를 통해 api 응답값을 받고, findAllUser를 통해 받은 List<User>에서 Entity내 선언된 "name"을 그대로 사용하여 {name=?}비교를 통해 특정 이름을 갖는 객체를 찾는다고 해보자.

 

이때 Entity의 "name"을 "username"으로 변경이 발생하면 Service영역의 코드까지 변경해야 하는등 영향이 매우 크다.

 

정리 - DTO와 Entity를 구별하지 않으면 생기는 문제점

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
    • 널값을 방어하기 위해 @NotNull 등을 달아줘야 하는 등 Entity에 대해 추가 제약이 붙는다 
  • API마다 필수 요소들이 다를수 있는데도 일률적으로 제한해야 한다.
  • 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty, @NotNull)
  • 엔티티가 변경되면 API 스펙이 같이 변한다.
    • 즉 api가 Entity에 의존적이게 된다.

결론

Controller에서는 Entity와 구분하여 API 요청에 맞는 DTO를 구현하여 사용하자.


정리한 내용에 따라 Entity -> DTO 변환작업을 진행한다.

dto/UserRequestDto, UserResponseDto 클래스 생성
UserRequestDto
@Getter
@Setter
@NoArgsConstructor
public class UserRequestDto {

    private String email;
    private String name;

    @Builder
    public UserRequestDto(String email, String name) {
        this.email = email;
        this.name = name;
    }

    public User toEntity() {
        return User.builder()
                .email(email)
                .name(name)
                .build();
    }
}
UserResponseDto
@Getter
public class UserResponseDto {
    private final Long userId;
    private final String email;
    private final String name;

    public UserResponseDto(User user) {
        this.userId = user.getUserId();
        this.email = user.getEmail();
        this.name = user.getName();
    }
}

응답 DTO를 @Setter를 빼서 무분별하게 수정되지 않도록한다. 어짜피 DTO로 받은 객체는 영속성 관리도 받지 않는 객체이므로 값을 변경할 필요가 없다.

 

@Service : service/UserService 클래스 생성
@Service
@AllArgsConstructor
public class UserService {
    private UserJpaRepo userJpaRepo;

    @Transactional
    public Long save(UserRequestDto userDto) {
        userJpaRepo.save(userDto.toEntity());
        return userJpaRepo.findByEmail(userDto.getEmail()).getUserId();
    }

    @Transactional(readOnly = true)
    public UserResponseDto findById(Long id) {
        User user = userJpaRepo.findById(id)
                .orElseThrow(UserNotFoundCException::new);
        return new UserResponseDto(user);
    }

    @Transactional(readOnly = true)
    public UserResponseDto findByEmail(String email) {
        User user = userJpaRepo.findByEmail(email);
        if (user == null) throw new UserNotFoundCException();
        else return new UserResponseDto(user);
    }

    @Transactional(readOnly = true)
    public List<UserResponseDto> findAllUser() {
        return userJpaRepo.findAll()
                .stream()
                .map(UserResponseDto::new)
                .collect(Collectors.toList());
    }

    @Transactional
    public Long update(Long id, UserRequestDto userRequestDto) {
        User modifiedUser = userJpaRepo
                .findById(id).orElseThrow(UserNotFoundCException::new);
        modifiedUser.setNickName(userRequestDto.getNickName());
        return id;
    }

    @Transactional
    public void delete(Long id) {
        userJpaRepo.deleteById(id);
    }
}
  • GET요청에 대한 응답으로 UserResponseDto를 반환
  • POST, PUT 요청에 대해서는 UserRequestDto를 받도록 함

 

@Controller : UserController 클래스 수정
@Api(tags = {"1. User"})
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1")
public class UserController {

    private final UserService userService;
    private final ResponseService responseService;

    @ApiOperation(value = "회원 단건 검색", notes = "userId로 회원을 조회합니다.")
    @GetMapping("/user/id/{userId}")
    public SingleResult<UserResponseDto> findUserById
            (@ApiParam(value = "회원 ID", required = true) @PathVariable Long userId,
             @ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
        return responseService.getSingleResult(userService.findById(userId));
    }

    @ApiOperation(value = "회원 단건 검색 (이메일)", notes = "이메일로 회원을 조회합니다.")
    @GetMapping("/user/email/{email}")
    public SingleResult<UserResponseDto> findUserByEmail
            (@ApiParam(value = "회원 이메일", required = true) @PathVariable String email,
             @ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
        return responseService.getSingleResult(userService.findByEmail(email));
    }

    @ApiOperation(value = "회원 목록 조회", notes = "모든 회원을 조회합니다.")
    @GetMapping("/users")
    public ListResult<UserResponseDto> findAllUser() {
        return responseService.getListResult(userService.findAllUser());
    }

    @ApiOperation(value = "회원 등록", notes = "회원을 등록합니다.")
    @PostMapping("/user")
    public SingleResult<Long> save(
            @ApiParam(value = "회원 이메일", required = true) @RequestParam String email,
            @ApiParam(value = "회원 이름", required = true) @RequestParam String name) {
        UserRequestDto user = UserRequestDto.builder()
                .email(email)
                .name(name)
                .build();
        return responseService.getSingleResult(userService.save(user));
    }

    @ApiOperation(value = "회원 수정", notes = "회원 정보를 수정합니다.")
    @PutMapping("/user")
    public SingleResult<Long> update (
            @ApiParam(value = "회원 ID", required = true) @RequestParam Long userId,
            @ApiParam(value = "회원 이름", required = true) @RequestParam String nickName) {
        UserRequestDto userRequestDto = UserRequestDto.builder()
                .nickName(nickName)
                .build();
        return responseService.getSingleResult(userService.update(userId, userRequestDto));
    }

    @ApiOperation(value = "회원 삭제", notes = "회원을 삭제합니다.")
    @DeleteMapping("/user/{userId}")
    public CommonResult delete(
            @ApiParam(value = "회원 아이디", required = true) @PathVariable Long userId) {
        userService.delete(userId);
        return responseService.getSuccessResult();
    }
}
  • UserService에서 <UserResponseDto>를 받아서 해당 객체로 처리하도록 진행했다.
  • 요청을 보낼때는 UserRequestDto를 생성해서 보낸다.

 

결론

엔티티는 이제 UserJpaRepo에서만 사용되고 엔티티의 변경이 Controller나 Service에 가지않게 변경되었다.

  • 엔티티의 이름이 변경되어도 Service에서 사용하는 Entity의 Get, Set 메서드만 이름에 맞춰 수정하면 되므로 영향이 매우 제한적이다.
반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday