-
[JPA] Json 다형성을 활용한 DTO 설계와 확장Spring/JPA 2024. 11. 26. 12:59728x90반응형
API를 설계하다 보면 서로 다른 요청 데이터 타입에 공통 필드가 반복적으로 존재하는 경우가 많습니다.
이 상황에서 새로운 공통 필드가 추가되거나 변경될 때 모든(1억개) DTO를 수정해야 하는 번거로움이 발생합니다.DTO(Data Transfer Object)를 쓰는 이유?
"Controller에서는 DTO로 요청 정보를 받는 게 좋다."라는 말을 개발 공부를 시작했을 무렵 접하게 되었습니다.하지만 이유를 정확히 알지 못한 상태에서 사용해 오다가 이번에 JPA 공부를 하면서 그
jangto.tistory.com
1. 문제사항
처음 API를 설계할 때 공통 내용을 요청받는 DTO를 각각 따로 만들었다고 가정합니다.
예를 들어 BookRequest, AlbumRequest, MovieRequest라는 요청 DTO가 아래와 같이 있습니다.@Getter @Setter @NoArgsConstructor public class BookRequest { private String name; private int price; private int stockQuantity; } @Getter @Setter @NoArgsConstructor public class AlbumRequest { private String name; private int price; private int stockQuantity; private String artist; } @Getter @Setter @NoArgsConstructor public class MovieRequest { private String name; private int price; private int stockQuantity; private String director; }
현재 DTO를 보면 name, price, stockQuantity가 중복 정의 되어있습니다.
만약 공통 필드가 추가되거나 변경된다면 이를 사용하는 모든 DTO를 수정해야 하는 문제가 발생하게 되고 이는 유지 보수 비용이 증가합니다.
2. 해결 방법: @JsonTypeInfo와 @JsonSubTypes를 이용한 공통 DTO 설계
이 문제를 해결하기 위해 공통 상위 DTO를 정의하고 이를 Jackson의 다형성을 활용하여 확장하는 방식으로 설계를 변경하게되면 @JsonTypeInfo와 @JsonSubTypes를 사용하여 하위 DTO로 자동으로 매핑할 수 있습니다.
※ Jackson은 역직렬화 시 Reflection을 활용하기 때문에 기본 생성자는 필수 사항입니다.
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ @JsonSubTypes.Type(value = BookRequest.class, name = "book"), @JsonSubTypes.Type(value = AlbumRequest.class, name = "album"), @JsonSubTypes.Type(value = MovieRequest.class, name = "movie") }) @Getter @Setter @NoArgsConstructor public class ItemRequest { private String name; private int price; private int stockQuantity; }
위와 같이 공통 필드를 담고 있는 상위 DTO를 만들고 하위 DTO는 이를 상속하여 만들어 줍니다.
@Getter @Setter @NoArgsConstructor public class BookRequest extends ItemRequest { private String genre; } @Getter @Setter @NoArgsConstructor public class AlbumRequest extends ItemRequest { private String artist; } @Getter @Setter @NoArgsConstructor public class MovieRequest extends ItemRequest { private String director; }
3. 요청 데이터 예시
Jackson은 상위 DTO에서 정의한 type 필드를 사용해 요청 데이터를 하위 DTO로 매핑해 줍니다.
BookRequest JSON 예시
{ "type": "book", "name": "실전! 스프링 부트와 JPA 활용1", "price": 88000, "stockQuantity": 1, "genre": "Programming" }
AlbumRequest JSON 예시
{ "type": "album", "name": "가까운 듯 먼 그대여", "price": 18000, "stockQuantity": 10, "artist": "카더가든" }
MovieRequest JSON 예시
{ "type": "movie", "name": "베테랑2", "price": 23000, "stockQuantity": 15, "director": "류승완" }
4. Controller 구현
이제 공통 상위 DTO인 ItemRequest를 통해 요청 데이터를 처리할 수 있기 때문에 유연성과 확장성을 높일 수 있습니다.
특히 DTO의 개수가 많을 수록 유지보수에 좋습니다.
이러한 확장 구조에서 개인적으로 GET 방식을 사용하게 되면 이점이 사라진다고 느껴지기 때문에 POST 방식을 선호합니다.
@RestController @RequiredArgsConstructor public class ItemController { private final ItemService itemService; @PostMapping("/api/v1/item") public ResponseEntity<String> createItem(@RequestBody ItemRequest request) { String result = itemService.createItem(request); return ResponseEntity.ok().body(result); } }
5. 결과
이러한 설계로 인해 만약 공통 필드인 stockQuantity를 quantity로 변경하더라도 코드 변경점이 N개에서 1개로 감소하게 되며 특히나 이 방식은 특히 Entity 상속 구조와 함께 사용할 경우 더욱 강력한 효과를 주는 설계가 됩니다.
6. 예외 처리
예제 상태에서 type필드에 잘못된 값이 들어왔을 때 HttpMessageNotReadableException이 발생하게 되는데 이를 독립적으로 처리하기 위해서는 아래와 같이 예외 핸들러를 만든 후 상위 DTO에서 상속해서 독립적으로 해결할 수 있습니다.
예외 핸들러 구현
public class ItemRequestJsonException { @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<String> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) { if (ex.getCause() instanceof InvalidTypeIdException typeIdException) { String invalidType = typeIdException.getTypeId(); return ResponseEntity.badRequest().body(String.format("유효하지 않은 'type' 값입니다: '%s'", invalidType)); } return ResponseEntity.badRequest().body("잘못된 요청입니다."); } }
상위 DTO 독립적인 예외처리
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ @JsonSubTypes.Type(value = BookRequest.class, name = "book"), @JsonSubTypes.Type(value = AlbumRequest.class, name = "album"), @JsonSubTypes.Type(value = MovieRequest.class, name = "movie") }) @Getter @Setter @NoArgsConstructor public class ItemRequest extends ItemRequestJsonException { private String name; private int price; private int stockQuantity; }
7. 고려 사항
Jackson을 사용한 역직렬화는 코드의 유연성과 확장성을 높이는 데 유리하지만 Reflection을 사용하기 때문에 성능에 영향을 미칠 수 있기 때문에 요구 사항에 맞게 적절히 최적화하고 성능을 모니터링하여 필요한 경우 개선 작업을 진행하는 것이 중요하다고 생각합니다.
728x90반응형'Spring > JPA' 카테고리의 다른 글