내가 Collection을 선언 즉시 초기화 했던 이유 + @AllArgsConstructor과 @Builder를 같이 사용했던 이유

Kevin·2024년 6월 7일
2
post-thumbnail

🤣 서론

이 글은 이번에 토이 프로젝트인 “Daily”를 개발하면서 생긴 궁금증에 대해서 글을 작성 해보고자 한다.

Spring Data JPA를 사용하여 Entity를 구현할 때 매번 컬렉션 타입의 필드에 대해서 초기화를 했었던 것에 대한 궁금증이 생겨 이렇게 공부를 진행 해보게 되었다.

private List<String> userNames = new ArrayList<>();


😎 왜 Collection을 선언 즉시 초기화할까?

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DailyLike extends Common {

    // ...생략
    
    private List<String> userNames = new ArrayList<>(); 

    // 생략...
}

JPA를 사용하여 Entity를 작성하다보면 다른 타입의 멤버 변수들과는 다르게 컬렉션 타입은 선언과 동시에 초기화 하는 것을 확인 할 수 있다.

어떠한 이유로 컬렉션 타입은 선언과 동시에 초기화를 진행해줄까?

아래의 컬렉션을 필드 초기화 하지 않을 경우의 예시들을 들어보자.

private List<String> userNames;

  1. NPE 문제

누군가 @NoArgsConstructor 어노테이션 등을 통한 방법으로 객체를 생성한다고 해보자.

이 경우에는 자연스럽게 userNames 컬렉션 필드의 값은 NULL이 된다.

이 때 NPE 문제를 일으키기 좋기 때문에 원천적으로 이러한 문제를 막아두는 것이 좋다.

만약 값이 초기화 되어있다면 NPE가 아닌 [] 빈 컬렉션이 값으로 주입되어있을 것이다.

@Entity
@Getter
@Builder
@AllArgsConstructor
public class DailyLike extends Common {

    // ...생략
    
    private List<String> userNames = new ArrayList<>(); 

    // 생략...
}

이는 @AllArgsConstructor 어노테이션과 클래스 레벨의 @Builder 어노테이션 사용을 통해 객체를 사용할 때도 마찬가지이다.

이 때는 추가적으로 선언과 초기화를 해주었다고 하더라도 필드의 값이 빈 컬렉션이 아니라 NULL이 저장된다.


  1. Hibernate 문제

userNames 컬렉션 자체의 참조 값이 변경되어버리면 Hibernate 차원에서 문제가 발생할 수 있다.

컬렉션 참조 값이 변경 된다는 것은 아래와 같은 예시이다.

Entity entity = new Entity();
entity.setId(1L);
entity.setUserNames(new ArrayList<>()); // 참조값 설정
entityManager.persist(entity);

// 나중에 코드의 다른 부분에서...
List<String> anotherList = new ArrayList<>();
anotherList.add("Alice");
anotherList.add("Bob");

entity.setUserNames(anotherList); // 참조값 변경

// 그리고 나중에...
entityManager.merge(entity); // 이 부분에서 예상치 못한 동작이 발생할 수 있다.

컬렉션 자체의 참조 값 변경으로 인해서 예상치 못한 동작이 발생할 수 있는 이유는 Hibernate는 엔티티를 영속화(persist)할 때 컬렉션을 래퍼 클래스(PersistentBag)로 감싸서 내장 컬렉션으로 변경하기 때문이다.


내장 컬렉션으로 변경하는 이유는 Hibernate가 컬렉션의 데이터가 추가 되었는지 등을 인식할 수 있어야 하기 때문이다.
→ 이를 기술적으로 풀면, 컬렉션으로 참조하고 있는 대상을 추적하고 관리하기 위해서이다.

public class PersistentBag extends AbstractPersistentCollection implements List {

  protected List bag;
  ...

  public PersistentBag(SharedSessionContractImplementor session, Collection coll) {
     super( session );
     providedCollection = coll;
     if ( coll instanceof List ) {
        bag = (List) coll;
     }
     else {
        bag = new ArrayList( coll );
     }
     setInitialized();
     setDirectlyAccessible( true );
  }

}

위 코드는 PersistentBag의 내부 코드이며, 코드를 보면 알 수 있겠지만 PersistentBag은 컬렉션을 인스턴스 변수로 두고 생성자에서 이를 주입받는 방식이다.




🥰 왜 @AllArgsConstructor과 @Builder를 같이 사용할까?

이번에 개발을 하던 중 추가적으로 평상시에는 크게 궁금증을 가지지 않았던 롬복의 @Builder@~ArgsConstructor 어노테이션들간의 관계에 대해서 궁금증이 생겨서 알아보려고 한다.

만약 @Builder@NoArgsConstructor 어노테이션을 동시에 사용하려고 할 때 컴파일 에러가 발생한다.

이 때 @AllAgrsConstructor 어노테이션을 두 어노테이션과 함께 써주면 컴파일 에러가 해결된다.

어떤 이유로 해결 되는 것일까?

먼저 컴파일 에러가 생기는 이유는 @Builder를 통한 빌더로 객체를 생성할 때 필요한 전체 생성자가 없기 때문이다.

빌더로 필요한 파라미터 또는 전체 파라미터를 받아 객체를 만들 수 있어야 하는데, 이 때 전체 생성자가 필요하다는 것이다.

그런데 여기서 신기한 부분이 있다.

@NoArgsConstructor 없이 @Builder 만을 사용한다면, 에러가 발생하지 않는다는 것이다.

그 이유는 아래의 @Builder 내부 주석을 살펴보면 알 수 있다.

위 주석 중 아래와 같은 내용의 설명이 있다.

만약 클래스에 @~ArgsConstructor 어노테이션을 작성하지 않은 경우에 @Builder는 자동으로 @AllAgrsConstructor 과 같은 역할을 하는 전체 파라미터에 대한 생성자가 생성이 된다.

그러나 @NoArgsConstructor 등의 @~ArgsConstructor 어노테이션을 작성하면 이렇게 자동으로 전체 파라미터에 대한 생성자가 생성되지 않기에 생기는 오류이다.

profile
Hello, World! \n

0개의 댓글