일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- StringBuffer
- etf
- 알고리즘
- 자바
- 기업분석
- 금리인하
- 잉여현금흐름
- 다형성
- 그리디 알고리즘
- 인플레이션
- 접근제어자
- javascript
- object
- 무디스
- 주린이
- 미국주식
- 제태크
- 배당성장
- 금리인상
- FCF
- mco
- 백준
- 객체지향
- 오버라이딩
- Java
- XLF
- 현금흐름표
- 프로그래머스
- 주식
- S&P500
- Today
- Total
오늘의하루
[JPA] Json 다형성을 활용한 DTO 설계와 확장 본문
API를 설계하다 보면 서로 다른 요청 데이터 타입에 공통 필드가 반복적으로 존재하는 경우가 많습니다.
이 상황에서 새로운 공통 필드가 추가되거나 변경될 때 모든(1억개) DTO를 수정해야 하는 번거로움이 발생합니다.
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을 사용하기 때문에 성능에 영향을 미칠 수 있기 때문에 요구 사항에 맞게 적절히 최적화하고 성능을 모니터링하여 필요한 경우 개선 작업을 진행하는 것이 중요하다고 생각합니다.