TIL/내일배움캠프

[Spring/Lombok] 생성자 애너테이션 알아보기 — @AllArgsConstructor, @NoArgsConstructor, @RequiredArgsConstructor

bu119 2025. 9. 16. 21:00
728x90
반응형

📝 TIL (Today I Learned) — 2025.09.16. 화요일 

✍️ 이 글은 내일배움캠프에서 Spring 입문 강의를 들으며 학습한 내용을 정리한 TIL입니다.


🤔 Lombok을 왜 써야 할까요?

Java로 개발하다 보면 같은 패턴의 코드를 반복해서 작성하게 됩니다.

특히 클래스마다 생성자, getter, setter를 만드는 작업은 시간이 많이 들고, 가독성도 떨어집니다.

 

예를 들어, 간단한 상품 클래스를 하나 만든다고 해봅시다:

// 😵 Lombok 없이 작성하면...
public class Product {
    private String name;
    private int price;
    
    // 기본 생성자
    public Product() {}
    
    // 모든 필드 생성자
    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }
    
    // getter, setter 등등...
}

 

하지만 Lombok을 사용하면 단 두 줄로 같은 결과를 만들 수 있습니다:

// 😊 Lombok 사용하면...
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    private String name;
    private int price;
}

 

👉 Lombok은 이런 반복 코드를 애너테이션 한 줄로 해결해주는 라이브러리입니다.


🏗️ @AllArgsConstructor — "모든 필드를 한 번에!" 

모든 필드를 매개변수로 받는 생성자를 자동으로 만들어줍니다

@AllArgsConstructor
public class Product {
    private String name;
    private int price;
}

// 컴파일하면 이런 생성자가 자동 생성됩니다:
// public Product(String name, int price) {
//     this.name = name;
//     this.price = price;
// }

 

✅ 언제 사용하면 좋을까요?

  • DTO 클래스: REST API에서 데이터를 주고받을 때
  • 불변 객체: 한 번 만들면 값을 바꾸지 않는 객체
@AllArgsConstructor
@Getter
public class ProductDto {
    private String name;
    private int price;
}

// 사용 예시
ProductDto dto = new ProductDto("맥북 프로", 2500000);

 

👉 이렇게 하면 프론트엔드에서 받은 JSON을 객체로 변환하거나, 응답 DTO로 직관적으로 데이터를 담을 수 있습니다.

 

⚠️ 조심해야 할 경우

JPA 엔티티에는 사용하지 않는 것이 좋습니다.

DB가 자동으로 생성하는 id까지 생성자 매개변수로 받아야 해서 혼란스럽습니다.

// ❌ 이렇게 하면 안 됩니다
@AllArgsConstructor
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;      // DB가 만드는 건데 생성자로 받아야 함
    private String name;
}

// 사용할 때도 이상해집니다
Product product = new Product(null, "맥북");  // id에 null을 넣어야 함

🎯 @NoArgsConstructor — "빈 생성자가 필요합니다!"

매개변수가 하나도 없는 기본 생성자를 만들어줍니다.

@NoArgsConstructor
public class Product {
    private String name;
    private int price;
}

// 컴파일하면 이런 생성자가 자동 생성됩니다:
// public Product() {}

 

✅ 언제 반드시 필요할까요?

1️⃣ JPA 엔티티 (DB와 연결되는 클래스)

DB와 연결되는 객체는 JPA가 내부적으로 자동 생성해야 하기 때문에 반드시 기본 생성자가 필요합니다.
즉, 개발자가 직접 new로 만드는 것이 아니라, JPA가 알아서 객체를 만들 수 있도록 준비해두는 것입니다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

 

💡 왜 PROTECTED를 사용할까요?

JPA는 내부적으로 리플렉션(자바가 몰래 객체를 생성하는 기술)을 이용합니다.

이 과정에서는 접근 제어자가 크게 중요하지 않아서 PUBLIC, PROTECTED, PACKAGE, PRIVATE 모두 동작합니다.

하지만 실무에서는 PROTECTED를 가장 많이 사용합니다.

  • JPA는 객체를 만들 수 있도록 열어두면서,
  • 외부에서 new Product()로 무분별하게 생성하는 것을 막기 위해서 사용합니다.

👉 접근 제어자별 차이

  • PUBLIC : 어디서든 호출 가능
  • PROTECTED : JPA는 사용 가능, 외부 직접 호출 제한 (권장)
  • PACKAGE : 같은 패키지 내부에서만 호출 가능
  • PRIVATE : JPA는 사용 가능하지만 테스트나 별도 생성 메서드 없이는 불편

 

2️⃣ JSON 변환 - REST API 통신할 때

프론트엔드와 JSON 데이터를 주고받을 때도 빈 생성자가 필요합니다.

@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter
public class ProductDto {
    private String name;
    private int price;
}

// JSON → 객체 변환 과정:
// 1. Jackson이 new ProductDto()로 빈 객체 만듦
// 2. setter로 JSON 값들을 하나씩 넣음

💎 @RequiredArgsConstructor — "꼭 필요한 것만!" 

final이나 @NonNull이 붙은 중요한 필드만 매개변수로 받는 생성자를 만들어줍니다.

@RequiredArgsConstructor
public class Product {
    private final String name;   // 필수 - 생성자에 포함 ✅
    private final int price;     // 필수 - 생성자에 포함 ✅
    private String description;  // 선택 - 생성자에 포함 ❌
}

// 컴파일하면 이런 생성자가 자동 생성됩니다:
// public Product(String name, int price) {
//     this.name = name;
//     this.price = price;
// }

 

✅ 왜 좋을까요? 

  • 실수 방지: 중요한 값을 깜빡하고 안 넣는 실수를 막아줍니다
  • 안전한 객체: final로 만들면 나중에 값을 바꿀 수 없어서 더 안전합니다
  • JPA 권장 패턴: DB 엔티티 만들 때 가장 많이 사용하는 방식입니다

 

실무에서 이렇게 활용합니다:

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // JPA용
@RequiredArgsConstructor                            // 개발자용
public class Product {
    @Id @GeneratedValue
    private Long id;
    
    private final String name;     // 상품명은 필수
    private final int price;       // 가격도 필수
    private String description;    // 설명은 나중에 추가해도 됩니다
    
    // 나중에 설명을 추가하는 메서드
    public void updateDescription(String description) {
        this.description = description;
    }
}

// 사용할 때
Product product = new Product("맥북", 2000000);  // 필수 정보만 넣어서 생성
product.updateDescription("Apple M3 chip");      // 부가 정보는 나중에

📋 실무에서는 어떻게 조합할까요? (실무 조합 패턴)

상황별로 권장하는 Lombok 애너테이션 조합은 다음과 같습니다:

상황 추천 조합 이유
JPA 엔티티 @NoArgsConstructor(PROTECTED) + @RequiredArgsConstructor JPA 호환성 + 필수값 강제
DTO 클래스 @AllArgsConstructor + @NoArgsConstructor JSON 변환 호환성
Service 계층 @RequiredArgsConstructor 의존성 주입 자동화

 

예시 — JPA 엔티티

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@RequiredArgsConstructor
@Getter
public class Product {
    @Id @GeneratedValue
    private Long id;

    private final String name;
    private final int price;
}

 

👉 이렇게 하면 JPA가 객체를 만들 때는 문제없고,
개발자는 new Product("맥북", 2000000)처럼 필수값만 넣어 깔끔하게 객체를 생성할 수 있습니다.

 

예시 — Service 계층

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository productRepository;

    public void save(String name, int price) {
        productRepository.save(new Product(name, price));
    }
}

 

👉 이렇게 하면 Service 클래스에서 @RequiredArgsConstructorRepository가 자동 주입되고,
비즈니스 로직에서는 불필요한 setter 없이 new Product(name, price)필수값만 넣어 간결하게 객체를 생성할 수 있습니다.


🚨 자주 하는 실수들

1️⃣ final 필드와 @NoArgsConstructor만 사용

// ❌ 컴파일 에러 발생!
@NoArgsConstructor
public class Product {
    private final String name;  // final은 반드시 초기화해야 하는데...
}

 

✅ 해결: @RequiredArgsConstructor와 함께 사용합니다.

// ✅ 이렇게 해결하세요
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@RequiredArgsConstructor
public class Product {
    private final String name;
}

 

2️⃣ JPA 엔티티에 @AllArgsConstructor 사용

// ❌ 잘못된 패턴
@AllArgsConstructor
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;  // DB가 만드는 건데 생성자로 받습니다
    private String name;
}

 

✅ 해결: 필수 값만 받도록 @RequiredArgsConstructor 사용

// ✅ 올바른 패턴
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@RequiredArgsConstructor
public class Product {
    @Id @GeneratedValue
    private Long id;
    private final String name;  // 필수값만 생성자로
}

 

3️⃣ DTO에서 기본 생성자 누락

// ❌ JSON 변환 시 에러 발생
@RequiredArgsConstructor
public class ProductDto {
    private final String name;
}

 

✅ 해결: @AllArgsConstructor + @NoArgsConstructor 함께 사용

// ✅ 기본 생성자 추가
@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter
public class ProductDto {
    private String name;  // final 제거
}

✨ 오늘의 결론

Lombok은 단순히 반복 코드를 줄여주는 도구가 아니라, 클래스의 역할과 객체 생성 방식을 어떻게 설계할 것인지 결정하는 중요한 도구라는 점을 배웠습니다.

특히 엔티티에서는 @NoArgsConstructor(PROTECTED)와 @RequiredArgsConstructor 조합이 사실상 표준처럼 사용되며, DTO나 서비스 계층에서는 다른 조합이 적절할 수 있다는 것을 알게 되었습니다.

앞으로는 “이 클래스는 어떤 역할을 맡고 있으며, 어떤 필드가 필수적인가, 외부에서 객체를 어떻게 생성해야 하는가” 를 먼저 고민한 뒤, 상황에 맞는 Lombok 애너테이션을 선택하겠습니다.

728x90
반응형