DTO 만들 때 Lombok 꿀팁 대방출!

지훈진·2022년 2월 6일
6

java

목록 보기
1/1

'훌륭한 프로그래머 되는 법' 포스팅을 끝내려고 하다보니... 정작 개발 이야기는 한번도 하지 않은 것 같아서, 오늘은 그걸 풀어보려 합니다.

DTO, Data Transfer Object. 한글로 직역하면 데이터 전송 객체입니다. 주로 계층간에 데이터를 전달할 때 사용합니다. 스타일마다 다르지만 저는 주로 dao, service까지는 entity 객체를 그대로 사용하고 controller에서 응답을 줄 때 dto로 바꿔서 응답을 줍니다. 엔티티의 필드 변화로 API의 응답값의 변화를 주는 것은 많은 사이드 이펙트를 발생시키기 때문입니다.

돌아와서 자바에서 DTO를 만들 때 주로 사용하는 코드를 보도록 하겠습니다.

@Data
public class UserDto {

    private String email;

    private String name;

    private Integer age;
    
    private LocalDateTime registeredAt;

}

주로 lombok의 Data 어노테이션을 사용하여 만듭니다. 간단하기 때문이죠. 하지만 이에 치명적인 문제가 있습니다.

Data 어노테이션은 총 5개, Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode 어노테이션이 합쳐져 있습니다. 여기에서 바로 Setter가 문제가 됩니다. 데이터 변조가 가능하다는 것이죠. 이는 생각보다 치명적인 문제입니다. '그냥 set 메서드 안쓰면 되는 거 아냐?' 라고 생각할 수 있지만, 코드는 혼자 작성하는 것이 아니며, 다른 개발자가 사용할 수 있습니다. 따라서 세터 사용을 막아야 합니다. 그래서 우리는 @Data 어노테이션을 이렇게 바꿀수 있습니다.

@Getter
@RequriredArgsConstructor // final 필드가 없어 추가할 필요 없음
@ToString
@EqualsAndHashCode
public class UserDto {
	...
}

이로써 문제 해결!

아닙니다... X나 구려요... 다른 방법을 찾아야 합니다. 나는 Data에서 Setter만 빼고 싶다고! 저렇게 어노테이션 덕지덕지 붙여놓느니 차라리 Intellij 자동완성으로 Setter빼고 다 만들겠어... 진짜 방법이 없을까...

방법이 있습니다! 바로 바로... AccessLevel을 이용하는 것입니다. Setter 어노테이션을 한번 까봅시다.

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
	/**
	 * If you want your setter to be non-public, you can specify an alternate access level here.
	 * 
	 * @return The setter method will be generated with this access modifier.
	 */
	lombok.AccessLevel value() default lombok.AccessLevel.PUBLIC;
    ...

어노테이션 기본 파라미터로 AccessLevel을 설정하고 기본으로 퍼블릭으로 되어있네요. 이걸 바꾸면 됩니다. 아예 안만드는 옵션도 있네요. 이걸 써보도록 합시다.

@Data
@Setter(AccessLevel.NONE)
public class UserDto {
    ...
}

이렇게 하면 Setter를 사용할 수 없게 됩니다. 이제 진짜 끝~이 아니아니죠... 만약에 나이를 업데이트 하면 DB에서 다시 읽어와야 하는데, 성능상의 문제가 있어 다시 읽어오지 않고 로직으로 DTO의 값을 변경만 해야한다. 이럴 땐 정말 Setter가 필요하게 됩니다. 다 안쓴다고 해버렸는데 어쩌나...

@Data
@Setter(AccessLevel.NONE)
public class UserDto {
    ....
    
    @Setter
    private Integer age;

}

간단합니다. 그냥 해당 어트리뷰트 위에 선언해주면 됩니다. 앞서 살펴보신 Setter 어노테이션 구현체에 보시면 @Target이라는 옵션이 있는데 필드와 타입이 있습니다. 초 간단히 설명드리면 필드가 각 속성이고 타입이 클래스입니다. age속성에 @Setter를 선언해줌으로써 setAge 메서드를 사용할 수 있게 됩니다.

아주 훌륭한 개비스콘이네요! 그리고 이 포스팅을 작성하다가 갑자기 생각난 방법도 공유합니다.

@Data
public class UserDto {

    private final String email;

    private final String name;

    private Integer age;

    private final LocalDateTime registeredAt;

}

로직적으로 변경될 일이 없는 속성들을 final로 선언해두면 됩니다. 이러면 setter가 생성되지도 않죠! 다만 제가 기억하기에 빈 생성자가 없으면 Jackson 라이브러리가 json으로 바꾸지 못하는 이슈가 있었던 거 같아요. 그렇다고 @NoArgsConstructor 어노테이션을 만들면 final 속성들에 기본값이 없어서 에러가 나고, 빈 생성자를 만들고 final 속성들에 기본값을 주면 추후에 값을 바꾸지 못하는 문제가 있습니다. 나중에 파볼일이 있으면 더 파보겠습니다. 그러면 여기서 끝~

profile
집사없는 개발 고양이

2개의 댓글

comment-user-thumbnail
2024년 4월 8일

지나가는 나그네 입니다ㅎ
DTO의 특징 중 하나가 setter/getter 를 가지는 가변객체 인데 내용은 불변객체로 생성해서 ReadOnly 특성을 가지도록 하는 것은 VO에 해당하는 것 같네요^^;
가변의 특징을 유지하면서 불변의 특징도 가질 수 있도록 DTO 내 필드를 VO로 사용하는 조합은 어떨까요?^^

1개의 답글