이 글은 이번에 토이 프로젝트인 “Daily”를 개발하면서 생긴 궁금증에 대해서 글을 작성 해보고자 한다.
Spring Data JPA를 사용하여 Entity를 구현할 때 매번 컬렉션 타입의 필드에 대해서 초기화를 했었던 것에 대한 궁금증이 생겨 이렇게 공부를 진행 해보게 되었다.
private List<String> userNames = new ArrayList<>();
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DailyLike extends Common {
// ...생략
private List<String> userNames = new ArrayList<>();
// 생략...
}
JPA를 사용하여 Entity를 작성하다보면 다른 타입의 멤버 변수들과는 다르게 컬렉션 타입은 선언과 동시에 초기화 하는 것을 확인 할 수 있다.
어떠한 이유로 컬렉션 타입은 선언과 동시에 초기화를 진행해줄까?
아래의 컬렉션을 필드 초기화 하지 않을 경우의 예시들을 들어보자.
private List<String> userNames;
누군가 @NoArgsConstructor
어노테이션 등을 통한 방법으로 객체를 생성한다고 해보자.
이 경우에는 자연스럽게 userNames
컬렉션 필드의 값은 NULL이 된다.
이 때 NPE 문제를 일으키기 좋기 때문에 원천적으로 이러한 문제를 막아두는 것이 좋다.
만약 값이 초기화 되어있다면 NPE가 아닌 [] 빈 컬렉션
이 값으로 주입되어있을 것이다.
@Entity
@Getter
@Builder
@AllArgsConstructor
public class DailyLike extends Common {
// ...생략
private List<String> userNames = new ArrayList<>();
// 생략...
}
이는 @AllArgsConstructor
어노테이션과 클래스 레벨의 @Builder
어노테이션 사용을 통해 객체를 사용할 때도 마찬가지이다.
이 때는 추가적으로 선언과 초기화를 해주었다고 하더라도 필드의 값이 빈 컬렉션이 아니라 NULL이 저장된다.
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은 컬렉션을 인스턴스 변수로 두고 생성자에서 이를 주입받는 방식이다.
이번에 개발을 하던 중 추가적으로 평상시에는 크게 궁금증을 가지지 않았던 롬복의 @Builder
와 @~ArgsConstructor
어노테이션들간의 관계에 대해서 궁금증이 생겨서 알아보려고 한다.
만약 @Builder
와 @NoArgsConstructor
어노테이션을 동시에 사용하려고 할 때 컴파일 에러가 발생한다.
이 때 @AllAgrsConstructor
어노테이션을 두 어노테이션과 함께 써주면 컴파일 에러가 해결된다.
어떤 이유로 해결 되는 것일까?
먼저 컴파일 에러가 생기는 이유는 @Builder
를 통한 빌더로 객체를 생성할 때 필요한 전체 생성자가 없기 때문이다.
빌더로 필요한 파라미터 또는 전체 파라미터를 받아 객체를 만들 수 있어야 하는데, 이 때 전체 생성자가 필요하다는 것이다.
그런데 여기서 신기한 부분이 있다.
@NoArgsConstructor
없이 @Builder
만을 사용한다면, 에러가 발생하지 않는다는 것이다.
그 이유는 아래의 @Builder
내부 주석을 살펴보면 알 수 있다.
위 주석 중 아래와 같은 내용의 설명이 있다.
만약 클래스에
@~ArgsConstructor
어노테이션을 작성하지 않은 경우에@Builder
는 자동으로@AllAgrsConstructor
과 같은 역할을 하는 전체 파라미터에 대한 생성자가 생성이 된다.
그러나 @NoArgsConstructor
등의 @~ArgsConstructor
어노테이션을 작성하면 이렇게 자동으로 전체 파라미터에 대한 생성자가 생성되지 않기에 생기는 오류이다.