Spring/Spring Boot

[Spring Boot] @NoargsConstructor(AccessLevel.PROTECTED), @Builder

JoonYong 2024. 7. 23. 14:09

이번 프로젝트를 진행하면서 JPA Entity 클래스의 설계를 더욱 견고하게 하기 위해 @NoArgsConstructor(access = AccessLevel.PROTECTED) 기능을 활용하게 되었습니다. 이 접근 방식을 통해 객체의 상태를 관리하고, 명확한 설계 의도를 표현하는 방법을 최적화할 수 있었습니다. 특히, JPA를 사용하는 Spring 애플리케이션에서 엔티티 클래스의 기본 생성자 접근을 제어하는 것이 얼마나 중요한지 알게 되었습니다.

 

1. JPA와 기본 생성자

JPA는 엔티티 클래스를 인스턴스화할 때 기본 생성자를 사용합니다. 따라서, 모든 엔티티 클래스는 기본 생성자를 제공해야 합니다. 기본 생성자가 없으면 JPA는 엔티티 클래스를 인스턴스화할 수 없고, 이는 곧 런타임 예외로 이어집니다.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Profile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long profileId;

    @Column(nullable = false, unique = true)
    private String userId;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private String introduce;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id", nullable = false, columnDefinition = "bigint")
	private Account account;

}

위 코드는 JPA가 요구하는 기본 생성자를 제공하지만, 기본 생성자가 공개(public)되어 있어 외부에서 자유롭게 인스턴스화할 수 있습니다. 이는 객체의 불완전한 상태를 초래할 수 있습니다.

 

2. @NoArgsConstructor(access = AccessLevel.PROTECTED)의 도입

기본 생성자의 접근 제어를 protected로 설정하면 외부에서 직접적으로 객체를 생성할 수 없도록 제한할 수 있습니다. 대신, JPA와 같은 프레임워크에서는 여전히 기본 생성자를 사용할 수 있습니다.

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Profile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long profileId;

    @Column(nullable = false, unique = true)
    private String userId;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private String introduce;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id", nullable = false, columnDefinition = "bigint")
	private Account account;

}

 

3. 장점

  • 객체 불변성 강화: 외부에서 기본 생성자를 사용할 수 없으므로 객체 생성 시 필드가 불완전하게 초기화될 가능성을 줄입니다.
  • 명확한 설계 의도 표현: 기본 생성자가 protected로 설정되어 클래스 설계 의도를 명확히 합니다. 이는 클래스의 인스턴스화가 제한됨을 의미합니다.
  • JPA와의 호환성: 기본 생성자가 protected로 설정되어도 JPA는 여전히 이를 사용하여 엔티티를 인스턴스화할 수 있습니다.

 

4. 단점 및 고려 사항

  • 테스트 코드 작성 시 번거로움: 기본 생성자를 사용할 수 없으므로 빌더 패턴이나 다른 생성 방법을 사용해야 합니다.
  • 외부에서 객체 생성 제약: 외부에서 객체를 생성할 필요가 있는 경우, 추가적인 접근 방법(예: 공개 생성자, 정적 팩토리 메서드 등)을 제공해야 합니다.

 

5. 빌더 패턴 활용

다음은 JoinRequest를 통해 사용자가 회원 가입을 요청할 때, Profile 객체를 빌더 패턴을 통해 생성하는 예시입니다. 빌더 패턴을 사용하면 객체 생성 시 필요한 필드를 명확히 하고, 객체의 불완전한 상태를 방지할 수 있습니다.

Profile profile = Profile.builder()
        .userId(joinRequest.profile().userId())
        .nickname(joinRequest.profile().nickname())
        .introduce("소개 글") // 기본 소개 글 설정
        .account(account)
        .build();

 

6. 결론

@NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하면 JPA와 호환되면서 외부 접근을 제한할 수 있습니다. 이는 클래스의 인스턴스화를 제한하여 객체 상태를 더 잘 관리하고, 설계 의도를 명확히 표현할 수 있는 장점이 있습니다. 따라서, JPA 엔티티 클래스에서 객체의 불변성을 강화하고자 할 때 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 고려해보는 것이 좋습니다.

이번 프로젝트에서 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 통해 JPA 엔티티 클래스의 설계를 개선하고, 객체의 상태를 더 잘 관리할 수 있었습니다. 이를 통해 설계 의도를 명확히 표현하고, 엔티티 클래스를 더욱 견고하게 만들 수 있었습니다.