방구석 컴퓨터/방구석 스프링

Lombok 생성자와 빌더

CCEO 2024. 11. 25. 21:56
반응형

Lombok에서의 생성자로는 `@NoArgsConstructor`, `@RequiredArgsConstructor`, `@AllArgsConstructor`가 있습니다.

 

우선 각각에 대해서 정리한 후에 어떤 식으로 사용되는지 또 `@Builder`와 함께 사용하는 것은 어떻게 하는지 등을 알아보겠습니다.


`@NoArgsConstructor`

기본 생성자(파라미터가 없는 생성자)를 생성합니다.

import lombok.NoArgsConstructor;

@NoArgsConstructor
public class ExampleEntity {
    private Long id;
    private String name;

    // JPA에서 프록시 생성을 위해 기본 생성자가 필요합니다.
    // 별도의 기본 생성자를 작성하지 않아도 자동 생성됩니다.
}

 

주로

  • JPA Entity 클래스에서 프록시 생성을 위해 기본 생성자가 필요할 때
  • 기본 생성자를 명시적으로 사용하거나 다른 프레임워크가 요구하는 경우

이 2가지 상황에서 주로 사용된다고 볼 수 있는데요.


JPA Entity 클래스에서 프록시 생성을 위해 기본 생성자가 필요할 때

`JPA Entity`에서 기본 생성자가 필요한 이유는 JPA에서 프록시 객체를 사용해 지연 로딩과 같은 기능을 제공하기 때문인데요.

프록시 객체를 생성하거나 데이터를 로드할 때 리플렉션을 사용해 객체를 초기화하기 때문에 기본 생성자가 반드시 필요합니다.

@Entity
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    // 리플렉션으로 User 객체 생성 시 기본 생성자가 없으면 에러 발생
}

 

그렇다면 `@AllArgsConstructor`로는 안되는 걸까요? (네. 안됩니다.)

JPA는 리플렉션을 사용하여 객체를 생성할 때, 매개변수가 없는 기본 생성자를 사용합니다.

즉, JPA는 객체를 초기화하기 위해 기본 생성자를 반드시 요구합니다.

 

그렇다면 지금 계속 얘기하는 프록시 객체와 리플렉션은 무엇일까요?

프록시 객체

프록시 객체는 대리 객체를 의미합니다.

실제 객체를 대신하여 행동하는 객체로, 주로 지연 로딩(Lazy Loading)과 같은 기능을 구현하기 위해 사용됩니다.

 

프록시 객체는 진짜 객체를 생성하거나 데이터를 로드하지 않고, 프록시 객체가 대신 행동합니다.

즉, 진짜 객체가 필요한 시점까지 "지연" 생성하는 것입니다.

 

예를 들어 JPA에서 엔티티가 데이터베이스에서 로드될 때, JPA는 프록시 객체를 먼저 반환합니다.

이 프록시 객체는 실제 데이터 대신 기본적인 메타데이터만 가지고 있다가, 진짜 데이터가 필요할 때(필드 접근 시) 데이터베이스에서 값을 가져옵니다.

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

// 실제 코드
User user = entityManager.getReference(User.class, 1L);

// 프록시 객체 반환
System.out.println(user.getClass()); // class com.example.User$$EnhancerByHibernate

 

위에 코드처럼 `entityManager.getRegerence()`로 엔티티를 요청하면, JPA는 프록시 객체를 반환합니다.

`user.getClass()`를 출력하면 현재 해당 객체는 프록시 객체이기 때문에 class com.example.User$$EnhancerByHibernate라는 값이 출력됩니다.

만약에 `user.getName()`처럼 데이터에 접근하면 실제 데이터베이스 쿼리가 실행되어 값을 로드합니다.

이를 통해 성능 최적화를 이루고 불필요한 데이터 로딩을 방지합니다.

 

리플렉션

리플렉션은 자바에서 클래스, 메서드, 필드, 생성자 등의 구조를 런타임에 동적으로 확인하거나 수정할 수 있는 기능입니다.

즉, 프로그램이 실행 중일 때 객체의 정보를 가져오거나 객체를 조작할 수 있는 기술입니다.

기본적으로, 코드는 컴파일 시점에 모든 클래스와 메서드 호출이 결정되지만, 리플렉션을 사용하면 실행 중에 코드 구조를 탐색하고 수정할 수 있습니다.

 

리플렉션에 대해서는 다른 글에서 좀 더 자세하게 다루도록 하겠습니다.


기본 생성자를 명시적으로 사용하거나 다른 프레임워크가 요구하는 경우

JPA 외에도 일부 프레임워크(Jackson, Hibernate, Spring Data 등)는 객체를 직렬화하거나 역직렬화할 때 기본 생성자가 필요합니다.

 

예를 들어, JSON 데이터를 역직렬화할 때 기본 생성자를 사용하여 빈 객체를 생성한 후, setter 메서드로 데이터를 채웁니다.

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.NoArgsConstructor;

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

    // setter, getter

    public static void main(String[] args) throws Exception {
        String json = "{\"name\":\"Alice\", \"age\":25}";
        ObjectMapper objectMapper = new ObjectMapper();
        
        // 기본 생성자가 없으면 역직렬화 시 에러 발생
        User user = objectMapper.readValue(json, User.class);
    }
}

 

Jackson은 JSON 데이터를 User 객체로 변환하는 과정에서 빈 객체를 생성하려고 시도하기 때문에, 기본 생성자가 없으면 실패하게 됩니다.


@RequiredArgsConstructor

final 필드나 @NonNull 필드를 포함한 생성자를 생성합니다.

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class ExampleService {
    private final ExampleRepository exampleRepository;  // final 필드
    private final String serviceName;                  // final 필드
    private String optionalField;                      // 초기화 필요 없음

    // 자동 생성된 생성자:
    // public ExampleService(ExampleRepository exampleRepository, String serviceName) {
    //     this.exampleRepository = exampleRepository;
    //     this.serviceName = serviceName;
    // }
}

 

주로

  • 필드 주입 대신 생성자 주입을 사용할 때 (final 필드 초기화)
  • 특정 필드가 필수적인 경우
  • @NonNull로 선언된 필드가 항상 값이 필요할 때

위의 상황들에서 사용합니다.

 

특히 생성자 주입 단계에서 자주 볼 수 있는데요.

@RequiredArgsConstructor
public class ExampleService {
    private final ExampleRepository exampleRepository;
}

 

위에 코드처럼 생성자 주입을 위해 final 필드만 초기화하는 생성자가 필요합니다.


@AllArgsConstructor

모든 필드를 파라미터로 받는 생성자를 생성합니다.

import lombok.AllArgsConstructor;

@AllArgsConstructor
public class ExampleDto {
    private Long id;
    private String name;

    // 자동 생성된 생성자:
    // public ExampleDto(Long id, String name) {
    //     this.id = id;
    //     this.name = name;
    // }
}

 

주로

  • 테스트나 유틸리티 코드에서 모든 필드를 초기화하면서 객체를 생성해야할 때
  • DTO에서 모든 데이터를 한 번에 전달받아야 할 때

사용합니다.

 

롬복의 `@Builder`는 내부적으로 `@AllArgsConstructor`로 생성된 생성자를 사용합니다.

@Builder
@AllArgsConstructor
public class ExampleDto {
    private String name;
    private int age;
}

각각의 어노테이션에 대해서 알아보았으니 이제는 자주 사용되거나 사용하면 안 되는 조합에 대해서 알아보겠습니다.

(위에서 나왔던 예시는 빼고 정리하겠습니다.)

 

우선 자주 사용되는 조합에 대해서 알아보겠습니다.

 

1. JPA Entity에서 `@NoArgsConstructor`와 `@AllArgsConstructor` 조합

JPA는 프록시 생성을 위해 기본 생성자가 필요하며, 모든 필드를 초기화하는 생성자가 유용하게 사용될 수 있습니다.

@Entity
@NoArgsConstructor
@AllArgsConstructor
public class ExampleEntity {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

2. JPA, Jackson 등 특정 프레임워크가 기본 생성자를 요구하는 상황에서 Builder를 사용하려면, `@NoArgsConstructor`와 `@AllArgsConstructor`를 함께 사용

JPA 엔티티에서는 기본 생성자가 필수이기 때문에, 빌더를 함께 사용하기 위해서는 기본 생성자도 추가되어야 합니다.

@Entity
@NoArgsConstructor // JPA가 기본 생성자를 요구
@AllArgsConstructor // @Builder가 모든 필드를 초기화하는 생성자를 요구
@Builder // 선택적으로 필드를 초기화할 수 있는 Builder 제공
public class UserEntity {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
}

 

혹은 Jackson을 통해 역직렬화(JSON 데이터를 객체로 변환)할 때, Jackson은 기본 생성자를 사용합니다.

@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleDto {
    private String name;
    private int age;
}

3. 상속 구조에서 빌더 패턴 사용을 위한 `@SuperBuilder` 사용

`@Builder`는 상속 구조를 지원하지 않으므로, 부모와 자식 필드 모두 빌더 패턴으로 생성하기 위해서는 `@SuperBuilder`를 사용해야 합니다.

@SuperBuilder
@AllArgsConstructor
public class Parent {
    private String parentField;
}

@SuperBuilder
@AllArgsConstructor
public class Child extends Parent {
    private String childField;
}

// 빌더 사용
Child child = Child.builder()
    .parentField("Parent Value")
    .childField("Child Value")
    .build();

같이 사용하면 안되는 경우

`@NoArgsConstructor`와 `@RequiredConstructor`는 동시에 사용하면 안 됩니다.

`@NoArgsConstructor`는 기본 생성자를, `@RequiredArgsConstructor`는 `final`이나 `@NonNull` 필드만 포함하는 생성자를 생성합니다.

 

즉, `@RequiredArgsConstructor`는 `final` 또는 `@NonNull` 필드를 초기화해야 하는데, `@NoArgsConstructor`는 이를 초기화하지 않고 객체를 생성할 수 있어 에러 가능성이 높습니다.

 

이런 문제를 해결하려면

 

`final` 필드에 기본값을 제공하거나

@RequiredArgsConstructor
@NoArgsConstructor
public class MyClass {
    private final String requiredField = "Default Value"; // 기본값
}

 

 

`@NoArgsConstructor`를 제거해야 합니다.

참고로, `@NonNull`은 필드나 매개변수의 null 값을 방지하여 필수 데이터가 누락되지 않도록 체크하는 기능을 합니다.
반응형