💡 발단
발단 부분은 Java적인 문제이고, 본론은 이 다음에 나오니 건너뛰시길 추천드립니다.
프로젝트를 진행하다가 엔티티 생성자쪽에서 에러가 발생했다.
Builder 생성자에서 엔티티 컬렉션 필드를 할당하는 쪽에서 생긴 문제인데, 너무 당연하게 생각했어서 더 많은 생각을 하게 됐다.
Spring 프로젝트를 2번 하는 동안 컬렉션 필드를 생성자에서 만들어본 적이 없었던 것.
상황은 이렇다.
컬렉션 필드를 addAll()로 할당하는 부분에서 NPE(Null Pointer Exception)가 발생한 것.
문제는 NULL값이 들어갈 때였다.
// ... 필드
private List<FeedImage> feedImages = new ArrayList<>();
@Builder
private Feed(String title, String content, List<FeedImage> feedImages, User author) {
this.feedImages.addAll(feedImages);
}
addAll은 내부적으로 파라미터에 대해 toArray()를 호출하고, 이때 NULL값이라면 NPE를 발생시키게 된다.
이건 Spring 실력이 아닌 Java 실력의 문제였다..
그래서 당연히, NULL 비교 연산을 하고 생성자에서 할당해주면(...!) 해결되겠다고 생각했다.
지금 생각해보면 저럴 필요가 없었다.
애초에 addAll에 NULL 인자를 주면서 생긴 문제이지, addAll 자체의 문제가 아니기 때문이다.
다른 로직은 다 유지하되 생성자에서 NULL 비교 연산만 추가해주면 되는 문제였다.
아래는 위의 피드백을 반영했다면 생겨났을 코드.
// ...
private List<FeedImage> feedImages; // 필드
// ...
@Builder
private Feed(List<FeedImage> feedImages) { // 생성자
// ...
this.feedImages = Objects.isNull(feedImages) ? new ArrayList<>() : feedImages;
}
그러나 팀원분께서 피드백을 주셨고, 이건 위험한 방식이라는걸 알게 됐다.
❗컬렉션을 필드 초기화하지 않을 경우 생기는 문제
1. NPE(Null Pointer Exception)
누군가가 일반적인 생성자를 사용하지 않고 @NoArgsConstructor을 사용해 객체를 생성할 수 있다.
말 그대로 new 명령을 통해 새로 생성하게 되면, 필드의 값은 NULL 상태가 된다.
물론 보통은 @NoArgsConstructor(access = AccessLevel.PROTECTED) 처럼 외부에서 호출할 수 없도록 막아두지만, 리스크를 원천 봉쇄하는 것이 좋다.
2. ⭐ Hibernate가 컬렉션을 읽지 못할 수 있음
컬렉션 필드 그 자체 (참조 값)가 변경되어버리면 Hibernate는 정상작동하지 않는다.
Hibernate가 엔티티를 영속화할 때 컬렉션을 감싸서 자기만의 내장 컬렉션으로 변경하기 때문이다.
그래서 이 문제를 방지하기 위해 필드에서 빠르게 컬렉션을 초기화하고 해당 컬렉션을 바꾸는 행위를 막도록 하는 것.
3. 기존 비즈니스 로직에 맞지 않음
위에서 구현하려던건 이미지를 추가하는 것이다.
생성자라서 문제가 없었지만 메서드에서 필드값을 아예 다른 컬렉션으로 바꿔버린다면, 기존의 이미지들은 사라질 것이다.
컬렉션 필드 자체를 바꿔치기할게 아니라면,
아니 비즈니스적으론 바꿔치기를 한다고 해도, 직접 참조값 할당을 해주는 일은 앞으로 절대 하지 말자.
만약 엔티티 통째로 @Builder를 쓴다면?
이 경우와는 달리 빌더 + @AllArgsConstructor 패턴으로 엔티티를 생성하는 경우,
빌더에서는 호출되지 않은 인자에 대해선 필드에 NULL을 할당해버린다. 필드 초기화를 한다고 해도!
땨라서 @Builder.Default를 써야 한다.
@Builder
@AllArgsConstructor
public class Feed {
// ...
@OneToMany( // ...
@Builder.Default
private List<FeedImage> FeedImageList = new ArrayList<>();
Java 기본기, Spring 기본기에 충실하자.
참고
'Back-end > Spring Boot' 카테고리의 다른 글
[Spring Boot] logback으로 CloudWatch에 서버 요청 로깅하기 (1) | 2023.10.06 |
---|---|
[Spring JPA] 페이지네이션 일대다 FETCH JOIN 문제와 default_batch_size (0) | 2023.09.25 |
[Spring Boot] 커서 페이징(no offset)에서 Page 대신 Slice 사용하기 (1) | 2023.09.11 |
[Spring JPA] Hibernate에서 지원하지 않는 MySQL 랜덤 함수 직접 만들기 + Expressions 파헤쳐보기 (0) | 2023.09.06 |
[Spring Boot] 개발 환경 분리와 ddl-auto 재앙 방지 + @Profile (0) | 2023.09.03 |