Spring과 Lombok을 사용한다면 꼭 알고 있어야 하는 생성자 관련 어노테이션들이다.
아래 Lombok 공식 문서와 블로그를 참고로 글을 작성하였다.
- @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor
- Lombok 사용상 주의점(Pitfall)
Overview
@NoArgsConsructor, @RequiredArgsConstructor, @AllArgsConstructor,
이 3형제는 이름에서부터 어떤 역할을 하는 어노테이션들인지 대략 짐작이 간다.
일단 3형제 뒤에 붙은 Constructor라는 단어로부터,
이 3형제는 프로그래머 대신 생성자를 만들어주는 역할을 한다는 것을 유추할 수 있다.
그러니까, 객체 내부에 선언되어 있는 특정 field마다 딱 1개의 값을 생성자 parameter로 받고,
이 값을 단순히 field에 assign 해주는 생성자를 생성한다는 의미다.
그 앞에 prefix처럼 붙은 NoArgs(NoArguments), RequiredArgs(RequiredArguments), AllArgs(AllArguments)도 대충 예상이 가는데 (Argument와 Parameter의 차이는 이 글을 참고),
아래 샘플로 만들어둔 Customer(손님) 객체를 변형해가면서 하나씩 자세히 알아보자.
public class Customer {
private Long id;
private String name;
private int age;
}
@NoArgsConstructor
얘는 파라미터가 없는 기본 생성자를 만들어준다.
즉, @NoArgsConstructor가 붙어있는 객체의 인스턴스를 만들 때,
아래처럼 argument를 하나도 넘기지 않으며 생성자 호출을 할 수 있다.
@NoArgsConstructor
public class Customer {
private Long id;
private String name;
private int age;
}
Customer customer = new Customer();
하지만 만약, 항상 초기화가 필요한 final이 붙은 field가 있는데 @NoArgsConstructor을 사용한다면 compile error가 발생할 것이다.
대신, @NoArgsConstructor(force = true)처럼 force라는 옵션에 true 값을 주면, 모든 final fields는 0 / false / null로 초기화된다.
그런데 field에 final이 아닌 @NonNull 같은 제약이 있는 어노테이션이 붙어있다면,
얘는 force = true 옵션을 주어도 생성자에 들어가지 않기 때문에 나중에 프로그래머가 할당해주어야 한다.
hibernate나 Service Provider Interface 같은 특정 Java 구성에서 필요로 하고,
주로 @Data나 어노테이션을 생성하는 생성자 등과 함께 사용된다.
@RequiredArgsConstructor
얘는 특별한 처리가 필요한 각 field마다 하나의 parameter를 갖는 생성자를 생성해준다.
초기화되지 않은 모든 final fields와,
선언될 때 초기화되지 않은 @NonNull로 표시된 field까지 parameter를 가진다.
특히 @NonNull이 달려있는 field의 경우, 생성되는 생성자 내부에 명시적인 null 체크 로직 또한 생성된다.
그래서 만약 @NonNull이 붙어있는 field 중 어떠한 것이라도 null 값을 포함한다면 NullPointerException이 발생하게 된다.
생성자의 Parameter의 순서는 클래스 내부에서 선언된 field의 순서로 매칭 된다.
사용 방법은 아래와 같다.
@RequiredArgsConstructor
public class Customer {
private final Long id;
private String name;
private int age;
}
Customer customer = new Customer(3L);
@AllArgsConstructor
얘는 클래스 내부에 선언된 모든 field마다 하나의 parameter를 가진 생성자를 생성한다.
@NonNull이 붙어있는 field의 경우, 역시나 생성되는 생성자 내부에 해당 parameter에 null check 로직이 생성된다.
@RequiredArgsConstructor와 마찬가지로, 생성자의 Parameter의 순서는 클래스 내부에서 선언된 field의 순서로 매칭 된다.
사용 방법은 아래와 같다.
@AllArgsConstructor
public class Customer {
private final Long id;
private String name;
private int age;
}
Customer customer = new Customer(2L, "김철수", 23);
Alternate Form
이 3형제는 alternate form, 즉 위 사용법과는 조금 다른 대체 형식으로도 사용이 가능한데, 다음과 같다.
우선 3형제가 생성하는 생성자의 접근자는 모두 private이고,
만들어진 이 private 생성자를 감싸는 추가적인 static factory method를 사용하여 인스턴스 생성이 가능하다는 것이다.
이 모드는 위 어노테이션 3형제의 옵션 중 staticName이라는 것에 값을 주면 되는데,
예를 들어, @RequiredArgsConstructor(staticName="of")라고 주게 되면 of라는 키워드를 사용할 수 있다.
이런 static factory matod를 사용하게 되면,
new MapEntry<String, Ingeger>("foo", 5);와 같이 생성자 호출을 하지 않고
MapEntry.of("foo", 5)처럼 훨씬 짧으면서 뜻이 명확한 코드로 생성자 호출이 가능하다.
주의사항 및 단점
@AllArgsConstructor와 @RequiredArgsConstructor는 심각한 버그를 발생할 수 있어서 사용 시에 주의하거나,
아예 사용을 권하지 않는 경우도 있다.
이번에는 아래와 같은 Order이라는 객체를 만들어 살펴보자.
@AllArgsConstructor
public class Order {
private int cancelAmount;
private int orderAmount;
}
// 취소수량 4개, 주문수량 5개
Order order = new Order(4, 5);
위에서 언급했던 것처럼 @AllArgsConstructor 어노테이션은 생성자를 생성할 때,
class 내부에 선언된 field의 순서인 cancelPrice, orderPrice 순으로 생성자 파라미터를 생성한다.
그런데 만약 프로그래머가 orderAmount와 cancelAmount가 선언된 순서가 마음에 들지 않아,아래처럼 바꾸게 된다면?
@AllArgsConstructor
public class Order {
private int orderAmount;
private int cancelAmount;
}
이 경우, IDE가 제공해주는 리팩토링은 작동하지 않게 되고, lombok도 변화를 알아채지 못한다.
심지어 orderAmount, cancelAmount는 int라는 동일한 type을 갖고 있어 더욱 버그를 잡기 어렵다.
그래서 기존에 사용하던 new Order(4, 5)로 객체의 인스턴스를 만들어도 아무 에러없이 잘 동작할테지만,
실제로 입력되는 값은 취소수량과 주문수량이 뒤바뀌어 들어가는 심각한 비즈니스 로직 에러를 발생시킨다.
이 때문에 @AllArgsConstructor와 @RequiredArgsConstructor의 사용을 금지하는 것이 좋다는 주장도 있고,
그 대신, IDE 자동 생성 기능 등으로 아래처럼 생성자를 직접 만들고,
필요한 경우에는 직접 만든 생성자에 @Builder 어노테이션을 붙이는 것을 권장하기도 한다.
이 방법은 파라미터 순서가 아닌 이름으로 값을 설정하기 때문에 리팩토링에 유연하게 대응이 가능하다.
public class Order {
private int cancelAmount;
private int orderAmount;
@Builder
private Order(int cancelAmount, int orderAmount) {
this.cancelAmount = cancelAmount;
this.orderAmount = orderAmount;
}
}
// field 순서를 변경해도 에러가 없다.
Order order = Order.builder().orderAmount(5).cancelAmount(4).build();
끝!