@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class FridgeRequest {
private LocalDate expiredAt;
private float quantity;
private String unit;
}
@Getter
@Builder
public static class FridgeResponse {
private Long fridgeId;
private String ingredientName;
private String ingredientIconUrl;
private ZonedDateTime expiredAt;
private float quantity;
private String unit;
private String freshness;
public static FridgeResponse from(Fridge fridge) {
return FridgeResponse.builder()
.fridgeId(fridge.getFridgeId())
.ingredientName(fridge.getIngredient().getIngredientName())
.ingredientIconUrl(fridge.getIngredient().getIngredientIconUrl())
.expiredAt(fridge.getExpiredAt() != null ? fridge.getExpiredAt().atZone(ZoneId.of("Asia/Seoul")) : null)
.quantity(fridge.getQuantity())
.unit(fridge.getUnit())
.freshness(fridge.getFreshness().getName())
.build();
}
}
}
기존에 깊은 고민 없이 생성자나 Getter, Setter 함수를 무작정 사용하고 있었다. 그로 인해 여러 가지 문제 사항들이 나타나고 있었다.
아래에서 사용하고 있는 어노테이션들을 각각 살펴보면서 괜찮은지, 괜찮지 않다면 문제는 무엇인지에 대해서 먼저 살펴보도록 하겠다.
생성자의 경우 이전 게시물 에서 얘기했듯이 같은 타입을 가진 항목의 순서가 바뀐 경우 문제를 인식하지 못할 위험이 있다.
따라서 @AllArgsConstructor 같은 생성자 어노테이션은 쓰지 말고 필요하다면 생성자를 직접 선언 후 @Builder를 사용하는 것이 좋다.
위 코드를 살펴보면 @Builder를 클래스에서 사용하고 있는데 이는 지양하는 편이 좋다.
@Builder를 클래스에 사용하는 것은 결국 @AllArgsConstructor를 추가한 것과 동일하기 때문에 생성자 선언 시 필요 없는 항목까지 자동으로 만들어진다.
이 때문에 생성자 선언을 명시적으로 하고 이 생성자에 @Builder를 선언하는 편이 좋다.
Setter는 어디서나 지양하는 게 좋은 어노테이션이다.
setOOO이라는 메서드의 의미 자체가 모호하고 외부에서 모든 속성들이 변경 가능하도록 열어주면 객체의 일관성을 해칠 수 있다.
@Getter, @NoArgsConstuctor 어노테이션 또한 사용하고 있는데 이는 필요한 어노테이션일까?
정답만 먼저 얘기하자면 이는 직렬화/역직렬화를 위해 DTO 에 선언이 필요한 어노테이션들이다.
직렬화는 Java 객체를 JSON 등의 데이터로 변경하는 것이고, 역직렬화는 JSON 등의 데이터를 Java 객체로 변경하는 것을 의미한다.
DTO는 @RequestBody, @ResponseBody가 동작하면서 리플렉션을 사용하여 직렬화/역직렬화를 하게 된다.
리플렉션이란 메타 정보를 이용하여 클래스의 메소드나 타입 등에 접근 가능하도록 도와주는 자바 API이다.
@RequsestBody, @ResponseBody를 통해 직렬화/역직렬화 시 Jackson은 MappingJackson2HttpMessageConverter에서 내부적으로 ObjectMapper를 사용한다.
ObjectMapper에서 역직렬화를 위해서 기본 생성자로 객체 생성 후 필드 값을 찾아서 바인딩해준다. 이를 위해 private 기본 생성자와 public getter/setter가 필요하다.
ObjectMapper에서 직렬화를 위해서 writeValueAsString이라는 메서드를 사용하며 Json 변환을 위해 필드 값에 접근하다. 이를 위해 public getter가 필요하다.
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FridgeRequest {
private LocalDate expiredAt;
private float quantity;
private String unit;
}
@Getter
public class FridgeResponse {
private final Long fridgeId;
private final String ingredientName;
private final String ingredientIconUrl;
private final ZonedDateTime expiredAt;
private final float quantity;
private final String unit;
private final String freshness;
@Builder
public FridgeResponse(Long fridgeId, String ingredientName, String ingredientIconUrl,
ZonedDateTime expiredAt, float quantity, String unit, String freshness) {
this.fridgeId = fridgeId;
this.ingredientName = ingredientName;
this.ingredientIconUrl = ingredientIconUrl;
this.expiredAt = expiredAt;
this.quantity = quantity;
this.unit = unit;
this.freshness = freshness;
}
public static FridgeResponse from(Fridge fridge) {
return FridgeResponse.builder()
.fridgeId(fridge.getFridgeId())
.ingredientName(fridge.getIngredient().getIngredientName())
.ingredientIconUrl(fridge.getIngredient().getIngredientIconUrl())
.expiredAt(fridge.getExpiredAt() != null ? fridge.getExpiredAt().atZone(ZoneId.of("Asia/Seoul")) : null)
.quantity(fridge.getQuantity())
.unit(fridge.getUnit())
.freshness(fridge.getFreshness().getName())
.build();
}
}
위에서 살펴본 문제에 따라 아래 5가지 원칙을 지키면서 DTO를 수정하기로 했다.