ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Request DTO 불변 객체로 만들기 - JSON 역직렬화
    Programming/Java 2023. 5. 29. 01:10
    반응형

    들어가기 전에

    DTO 혹은 VO 객체가 한 번 생성하고 나서 값이 변경될 가능성이 없다면 Immutable 하게 생성하는 것이 좋다고 한다.

    Gotcha 프로젝트를 리팩토링하며 불변 객체화할 수 있는 클래스들을 불변 객체화 하는 것을 진행해보려고 한다.

     

    제일 간단한 기능을 기준으로 테스트해보고자한다.

    수정전 Controller, Service, Request DTO는 아래와 같다.

    Repository는 Spring Data JPA를 이용한 기본 메소드를 사용했다. 

     

    [DuplicateNicknameRequest]

    *참고)

    - @NoArgsConstructor는 Jackson에서 리플렉션을 통해 json 데이터로 객체를 생성할 수 있도록 함
    - @Builder 는 test code 작성시 객체를 생성하기 위해 붙임

    - @AllArgsConstructor는 Builder 사용시 기본생성자만 있을 경우 이용할 수 없어 추가해줌 ( -> 추가 공부 해보기!!)

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Schema(description = "중복 닉네임을 확인하는 요청")
    public class DuplicateNicknameRequest {
    
        @NotNull
        @Positive
        @Schema(description = "게임 방 Id")
        private Long roomId;
    
        @NotBlank
        @Schema(description = "유저 닉네임")
        private String nickname;
    
    }

    [ParticipantController]

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/game")
    @Slf4j
    public class ParticipantController {
    
        private final ParticipantService participantService;
    
        @PostMapping("/duplicate")
        public BaseResponse<Boolean> duplicateNickname(@Valid @RequestBody DuplicateNicknameRequest request) {
            Boolean isDuplicate = participantService.existDuplicateNickname(request);
            return new BaseResponse<>(isDuplicate);
        }
        
        ...
        
    }

    [ParticipantService]

    @RequiredArgsConstructor
    @Service
    public class ParticipantService {
    
        private final ParticipantRepository participantRepository;
        private final RoomRepository roomRepository;
    
        @Transactional(readOnly = true)
        public Boolean existDuplicateNickname(DuplicateNicknameRequest request) {
            checkRoomValidation(request.getRoomId());
            return checkDuplicateNickname(request.getRoomId(), request.getNickname());
        }
        
        private void checkRoomValidation(Long roomID) {
            boolean isExist= roomRepository.existsById(roomID);
            if(!isExist) {
                throw new RoomNotFoundException();
            }
        }
        
        private boolean checkDuplicateNickname(Long roomId, String nickname) {
    
            boolean isExist = participantRepository.existsParticipantByRoomIdAndNickname(roomId, nickname);
            if(isExist) {
                throw new DuplicateNicknameException();
            } else {
                return false;
            }
        }
    }

    서비스 흐름

    닉네임 중복체크를 하는 흐름은 다음과 같다.

    1. 유저가 특정 방의 Pin코드를 입력해 해당 방으로 접속
    2. 닉네임을 입력하고 중복 확인 요청(이 때, 닉네임은 방 1개당 유일해야 한다)
    3. 프론트에서 roomId와 nickname 값을 담아 Json 형태로 API 요청 보냄
    4. Controller에서 이를 받아 request 정보를 그대로 Service layer로 전달
    5. room 유효성 체크와 해당 room에 중복된 닉네임이 없는지 확인 후 결과값 전달

    Controller에서 Repository 까지 request의 정보가 가면서 값이 바뀌어야 할 부분이 존재하지 않기 때문에

    Request 객체를 불변객체화 하는 작업을 진행하고자 한다.

    Request DTO 불변객체로 수정하기

    모든 필드를 private final 형태로 만들고, 불필요한 생성자를 삭제해주면 아래와 같이 수정이 된다.

    - immutable하게 만들기 위해서는 각 필드의 내용이 생성과 동시에 할당되고 변하지 않아야 한다.
    - 따라서 기본생성자를 삭제해주었다.

    @Builder
    @Getter
    @AllArgsConstructor
    @Schema(description = "중복 닉네임을 확인하는 요청")
    public class DuplicateNicknameRequest {
    
        @NotNull
        @Positive
        @Schema(description = "게임 방 Id")
        private final Long roomId;
    
        @NotBlank
        @Schema(description = "유저 닉네임")
        private final String nickname;
    
    }

    그렇다면 테스트 결과는?

    통합 테스트 결과, 당연히 fail이 떴다.

    오류 로그는 아래와 같다. no Creators, like default constructor, exist

    - HttpRequest의 경우 Json 형태로 데이터가 전달이 되며, 보통은 기본 생성자가 있어야만 Jackson이 원래대로 리플렉션을 통해 객체를 생성할 수 있다.
    - 하지만 우리는 객체를 불변 객체를 만들어야 하다보니 기본 생성자를 만들 수 없는 상황이다.

    cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
    ...
    Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `....` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]

    해결 방법 1 - @JasonCreator로 직접 알려주기

    Jackson에게 어떻게 불변 객체를 생성해야 할지에 대한 정보를 전달하기 위해

    @JsonCreator 어노테이션과 @JsonProperty 어노테이션을 이용해 정보 전달

    - 모든 필드에 대한 @JsonProperty 정보를 전달해야하기 때문에 @AllArgsConstructor는 삭제 처리

    @Builder
    @Getter
    @Schema(description = "중복 닉네임을 확인하는 요청")
    public class DuplicateNicknameRequest {
    
        @NotNull
        @Positive
        @Schema(description = "게임 방 Id")
        private final Long roomId;
    
        @NotBlank
        @Schema(description = "유저 닉네임")
        private final String nickname;
    
        @JsonCreator
        public DuplicateNicknameRequest(
                @JsonProperty("roomId") Long roomId,
                @JsonProperty("nickname") String nickname) {
            this.roomId = roomId;
            this.nickname = nickname;
        }
    
    }

    다음과 같이 테스트가 잘 통과하는 것을 볼 수 있다!

    하지만 생긴 궁금증..

    @JsonCreator와 @JsonProperty를 이용해 값을 모두 입력해줄 경우...
    만약 Request에서 특정 필드가 Optional일 경우, 해당 필드를 제외한 다른 필드들로만 구성된 생성자를 또 만들어줘야 한다는 문제점,

    또한 필드가 많아질수록 코드의 가독성이 매우매우 떨어질 것 같다는 생각이 들었다.

    해결 방법 2 - @JsonDeserialize

    그러다 발견한 @JsonDeserialize와 Builder 패턴을 통한 방법!

    코드도 훨씬 가독성이 좋아졌다!

    @Builder
    @JsonDeserialize(builder = DuplicateNicknameRequest.DuplicateNicknameRequestBuilder.class)
    @Getter
    @Schema(description = "중복 닉네임을 확인하는 요청")
    public class DuplicateNicknameRequest {
    
        @NotNull
        @Positive
        @JsonProperty("roomId")
        @Schema(description = "게임 방 Id")
        private final Long roomId;
    
        @NotBlank
        @JsonProperty("nickname")
        @Schema(description = "유저 닉네임")
        private final String nickname;
    
    }

    마찬가지로 잘 통과됐다!

     

    참고 사이트

    https://shanepark.tistory.com/437

    https://www.baeldung.com/jackson-deserialize-immutable-objects

    반응형

    댓글

Designed by Tistory.