본문 바로가기

VO에 Lombok을 쓰면 안 되는 이유

@cojoop 2025. 7. 2. 20:49

요청/응답을 처리하는 DTO와 DB 매핑 또는 도메인 로직을 담당하는 VO를 각각 만들면서, DTO에 적용한 것처럼 VO에도 Lombok의 @Getter, @Setter, @Builder 을 자연스럽게 사용했다. 

 

그런데 리뷰 도중 “VO에는 Lombok을 쓰면 안 좋아요”라는 피드백을 들었다.

처음엔 의아했지만, 그 이유를 하나씩 살펴보니 VO에는 Lombok 사용을 지양해야 하는 명확한 근거들이 있었다.

 

따라서 이번 포스팅에서는 VO에 Lombok을 쓰면 안 되는 이유를 중심으로 정리해보려 한다.


🔧 Lombok이란?

Lombok은 Java 코드에서 반복적으로 작성되는 getter/setter, constructor, equals/hashCode 등의 코드를 애노테이션 기반으로 자동 생성해주는 라이브러리를 말한다.

 

일반적으로 Java 클래스에서는 아래처럼 많은 Boilerplate 코드가 필요하다.

public class User {
    private String name;
    private int age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

 

하지만 Lombok을 사용하면 다음과 같이 간단히 줄일 수 있다.

@Getter
@Setter
public class User {
    private String name;
    private int age;
}

 

대표적으로 다음과 같은 기능을 제공한다.

Lombok 어노테이션 설명
@Getter, @Setter 모든 필드에 getter/setter 자동 생성
@ToString toString 메서드 자동 생성
@EqualsAndHashCode equals, hashCode 자동 생성
@Builder 빌더 패턴 적용
@NoArgsConstructor, @AllArgsConstructor 생성자 자동 생성

 

Lombok은 DTO, API 응답 객체, 단순 POJO 클래스 등에서 매우 유용하게 사용된다.

Lombok은 언제 주로 쓰는가?

Lombok은 주로 다음과 같은 목적의 클래스에서 사용된다.

대상 설명 Lombok 사용 여부
✅ DTO (Data Transfer Object) API 요청/응답, 계층 간 데이터 전달용 적극 사용 가능
✅ 테스트용 모델 빠르게 객체 생성 후 검증할 때 사용 권장
⚠️ JPA Entity 불변성·프록시·지연 로딩 등 고려 필요 조건부 사용 (getter만 권장)
❌ VO (Value Object) 도메인 모델의 핵심 객체, 불변 필요 사용 지양

 

데이터 전달이나 단순 객체 조립이 목적일 경우에는 Lombok이 유리하지만 도메인 설계 또는 비즈니스 규칙을 내포하는 객체에는 Lombok을 최소화하는 게 좋다.

VO에는 왜 Lombok을 쓰면 안 되는가?

❌ 1. 불변성(Immutable)을 깨뜨림

VO는 본질적으로 불변(immutable)해야 한다.

즉, 생성 후 내부 값이 변경되어서는 안된다.

 

하지만 Lombok의 @Setter, @NoArgsConstructor외부에서 필드를 변경할 수 있는 구조를 만든다.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Address {
    private String city;
    private String zipCode;
}
  • 위 코드는 address.setCity("Seoul")처럼 필드 값을 외부에서 수정할 수 있어 VO의 핵심 철학에 위배됩니다.

❌ 2. 유효성 검증이 불가능

VO는 내부에 입력값 검증 로직을 강제해야 한다.

하지만 Lombok을 사용하면 setter나 생성자가 자동 생성되어 검증이 빠질 위험이 생길 수 있다.

 

명시적으로 생성자를 만들면, 아래처럼 검증 로직을 포함시킬 수 있다.

public Address(String city, String zipCode) {
    if (city == null || city.isEmpty()) throw new IllegalArgumentException("City is required");
    this.city = city;
    this.zipCode = zipCode;
}

❌ 3. equals/hashCode/toString이 자동화되며 설계가 흐려짐

VO는 값 비교를 위해 equalshashCode를 반드시 오버라이드해야 한다.

하지만 Lombok의 @EqualsAndHashCode, @ToString모든 필드를 무조건 포함하거나, 순환 참조로 인해 오작동할 수 있다.

 

도메인 규칙에 맞는 커스터마이징이 필요하며, 자동 생성보다는 명시적으로 구현하는 편이 안전합니다.

❌ 4. VO의 의도를 코드에서 읽기 어려워짐

VO는 도메인 설계의 일환이며, 그 자체가 모델링의 결과이다.

Lombok을 쓰면 코드가 간결해지긴 하지만, 왜 이 값들이 필요하고, 어떤 제약이 있고, 어떤 방식으로 생성되는지를 코드만 보고 파악하기 어려워진다.


좋은 VO 구조란?

좋은 VO는 불변성, 명확한 생성 방식, 정합성 보장, 의미 중심 설계를 갖춰야 한다.

 

✔ 좋은 VO의 특징

  • 모든 필드는 private final
  • setter 없이, 생성자만 존재
  • 생성자에서 유효성 검사 수행
  • equals, hashCode는 값 기준으로 구현
  • 필요할 경우 toString 커스터마이징
  • @JsonCreator와 같은 Jackson 어노테이션으로 역직렬화 설정

 

✔ 예시

public class Email {
    private final String address;

    public Email(String address) {
        if (address == null || !address.contains("@")) {
            throw new IllegalArgumentException("Invalid email address");
        }
        this.address = address;
    }

    public String getAddress() {
        return address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Email)) return false;
        Email email = (Email) o;
        return address.equals(email.address);
    }

    @Override
    public int hashCode() {
        return address.hashCode();
    }

    @Override
    public String toString() {
        return address;
    }
}

 

마무리

VO는 단순한 데이터 묶음이 아니라, 값 그 자체의 의미와 불변성을 보장해야 하는 객체이기 때문에

Lombok을 사용하는 것이 적절하지 않다는 점을 이번에 확실히 알게 되었다.

 

앞으로는 객체의 책임과 역할을 명확히 구분하고,

그에 맞는 도구와 설계 방식을 선택할 수 있는 개발자가 되도록 노력하고 싶다.

cojoop
@cojoop :: cojoop.dev

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차