문제 발생
@Builder를 사용해서 엔티티의 1:N 관계를 정의하던 중 new ArrayList<>()로 필드를 초기화 해도
NullPointException 에러가 발생했다.
List가 Builder 패턴을 이용해서 엔티티를 초기화할 때 Null값으로 된 것이다.
Cannot invoke "java.util.List.add(Object)" because the return value of "semicolon.MeetOn_WhenToMeet.domain.when_to_meet.domain.WhenToMeet.getTimeTableList()" is null
java.lang.NullPointerException: Cannot invoke "java.util.List.add(Object)" because the return value of "semicolon.MeetOn_WhenToMeet.domain.when_to_meet.domain.WhenToMeet.getTimeTableList()" is null
at semicolon.MeetOn_WhenToMeet.domain.when_to_meet.application.WhenToMeetServiceTest.웬투밋_조회(WhenToMeetServiceTest.java:107)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
원인
Builder 패턴을 사용할 때 필드에서 초기화해준 값을 무시하고 builder.build에서 초기화해준 값이 없다면 자동으로 null로 초기화 해준다는 것이 문제였다.
즉, 필드에서 아무리 초기 값을 넣어주고 조건을 넣어줘도 Builder 패턴을 사용하면 무시된다는 것이다.
@Getter
@Entity
@NoArgsConstructor
public class WhenToMeet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private LocalDateTime startDate;
private LocalDateTime endDate;
private Integer startTime;
private Integer endTime;
private Long channelId;
@OneToMany(mappedBy = "whenToMeet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TimeTable> timeTableList = new ArrayList<>();
@Builder
public WhenToMeet(Long id, String title, LocalDateTime startDate, LocalDateTime endDate,
Integer startTime, Integer endTime, Long channelId, List<TimeTable> timeTableList) {
this.id = id;
this.title = title;
this.startDate = startDate;
this.endDate = endDate;
this.startTime = startTime;
this.endTime = endTime;
this.channelId = channelId;
this.timeTableList = timeTableList;
}
}
해결
1. Builder 패턴을 사용할 때 초기화
WhenToMeet whenToMeet = WhenToMeet.builder().id(1L).title("test")
.endDate(LocalDateTime.now())
.endTime(1)
.startDate(LocalDateTime.now())
.startTime(2)
.timeTableList(new ArrayList<>())
.build();
TimeTable timeTable1 = new TimeTable(1L, "test1", 1L, whenToMeet);
TimeTable timeTable2 = new TimeTable(2L, "test2", 1L, whenToMeet);
whenToMeet.getTimeTableList().add(timeTable1);
whenToMeet.getTimeTableList().add(timeTable2);
위 코드처럼 Builder 패턴으로 엔티티를 초기화할 때 List 값도 new ArrayList<>()로 초기화하여 빈 List 값을 넣어준다.
엔티티를 Builder 패턴으로 초기화할 때 매번 List도 초기화해야 한다는 단점이 있다.
2. @Builder.Default로 초기값 설정
@Builder
@Getter
@Entity
@NoArgsConstructor
public class WhenToMeet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private LocalDateTime startDate;
private LocalDateTime endDate;
private Integer startTime;
private Integer endTime;
private Long channelId;
@Builder.Default
@OneToMany(mappedBy = "whenToMeet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TimeTable> timeTableList = new ArrayList<>();
@Builder
public WhenToMeet(Long id, String title, LocalDateTime startDate, LocalDateTime endDate,
Integer startTime, Integer endTime, Long channelId, List<TimeTable> timeTableList) {
this.id = id;
this.title = title;
this.startDate = startDate;
this.endDate = endDate;
this.startTime = startTime;
this.endTime = endTime;
this.channelId = channelId;
this.timeTableList = timeTableList;
}
}
@Builder.Default 설정을 통해서 해당 List를 Builder 패턴을 이용하여 엔티티를 초기화할 때 자동으로 기본 값을 지정할 수 있다.
나는 클래스 단위에서 @Builder를 잘 사용하지 않고 생성자에 적용하기 때문에 클래스 단위로 바꾸거나 클래스에도 @Builder를 넣어줘야 했다.
삽질한 것
1. 그냥 생성자에서 초기화만 하기
@Getter
@Entity
@NoArgsConstructor
public class WhenToMeet {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private LocalDateTime startDate;
private LocalDateTime endDate;
private Integer startTime;
private Integer endTime;
private Long channelId;
@OneToMany(mappedBy = "whenToMeet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TimeTable> timeTableList = new ArrayList<>();
@Builder
public WhenToMeet(Long id, String title, LocalDateTime startDate, LocalDateTime endDate,
Integer startTime, Integer endTime, Long channelId, List<TimeTable> timeTableList) {
this.id = id;
this.title = title;
this.startDate = startDate;
this.endDate = endDate;
this.startTime = startTime;
this.endTime = endTime;
this.channelId = channelId;
this.timeTableList = new ArrayList<>();
}
되긴 된다. 대신 초기화를 빈 리스트로만 해야한다. 절때 이렇게 하면 안된다. 바보였나 보다
2. 생성자에서 초기화 후 값 넣기
@Getter
@Entity
@NoArgsConstructor
public class WhenToMeet {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private LocalDateTime startDate;
private LocalDateTime endDate;
private Integer startTime;
private Integer endTime;
private Long channelId;
@OneToMany(mappedBy = "whenToMeet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TimeTable> timeTableList = new ArrayList<>();
@Builder
public WhenToMeet(Long id, String title, LocalDateTime startDate, LocalDateTime endDate,
Integer startTime, Integer endTime, Long channelId, List<TimeTable> timeTableList) {
timeTableList = new ArrayList<>();
this.id = id;
this.title = title;
this.startDate = startDate;
this.endDate = endDate;
this.startTime = startTime;
this.endTime = endTime;
this.channelId = channelId;
this.timeTableList = timeTableList;
}
당연히 안된다. @Builder 생성자 안에서 초기화 해봤자 timeTableList가 따로 조건이 없다면 null로 들어오기 때문에
무조건 null이 들어가 NullPointException이 발생한다.
결론
클래스 단위에서 @Builder를 적용하고 @Builder.Default로 초기값을 설정해주는 것이 가장 가독성도 좋고 사용하기에도 편한 것 같다.
생성자 단위에서 @Builder를 생성자를 보고 사용하던 방식보단 위 방식으로 구현해야겠다.
진작에 검색하고 찾았어야 하는데 혼자 고쳐보겠다고 삽질하느라 시간이 좀 걸렸다....
하필이면 빈 리스트만 필요해서 삽질 1번이 맞는 것이라고 생각하고 구현하다가 테스트 코드에서 터졌다.
테스트 코드 사용을 생활화 하자!!
참조
'Dev > Spring' 카테고리의 다른 글
Controller Test하기 feat WebMvcTest (1) | 2024.06.02 |
---|---|
Spring Boot AWS S3 연결 (0) | 2024.04.27 |
Spring Cloud Eureka Swagger 연결하기 (2) | 2024.04.15 |
JWT 리프레시 토큰 Cookie에 저장하기 (2) | 2024.03.28 |
Private 메소드 테스트 고민 (1) | 2024.03.17 |