요청/응답을 처리하는 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는 값 비교를 위해 equals와 hashCode를 반드시 오버라이드해야 한다.
하지만 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을 사용하는 것이 적절하지 않다는 점을 이번에 확실히 알게 되었다.
앞으로는 객체의 책임과 역할을 명확히 구분하고,
그에 맞는 도구와 설계 방식을 선택할 수 있는 개발자가 되도록 노력하고 싶다.
'Backend > Architecture' 카테고리의 다른 글
다양한 API 요청 구조에 대응하는 DTO 설계 방법 (2) | 2025.07.09 |
---|---|
API 요청과 DB 컬럼이 다를 때, DTO와 VO를 나누는 이유 (0) | 2025.07.01 |
MVC에 익숙했던 내가 도메인 구조를 선택한 이유 (2) | 2025.06.28 |