문제 상황
평소와 같이 1:N의 관계인 Entity를 생성하고 1에 해당하는 클래스에서 @OneToMany(mappedBy = "family")를 했습니다.
그리고 해당 List를 new ArrayList<>()로 미리 초기화를 해줬습니다. (Null 방지)
그리고 service 레이어에서 insertNewMember 메서드를 실행시켰는데, 바로 NullPointerException 발생..
해당 List가 Null 상태인 것을 발견했습니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Family extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long familyId;
@Column(nullable = false, unique = true, length = 50)
private String familyName;
@OneToMany(mappedBy = "family", cascade = CascadeType.ALL)
private List<FamilyMember> familyMemberList = new ArrayList<>();
public static Family createFamily(CreateFamilyRequest dto) {
return Family.builder()
.familyName(dto.getFamilyName())
.build();
}
public void insertNewMember(FamilyMember familyMember) {
familyMemberList.add(familyMember);
}
}
원인 분석
우선 디버깅을 통해서 insertNewMember에 넘어오는 파라미터 값인 FamilyMember를 살펴봤습니다.
그리고 해당 FamilyMember는 위의 static 메서드인 createFamily를 통해서 생성됐고 빌더 패턴을 이용했습니다.
클래스 필드 초기화
기본적으로 클래스로부터 객체가 생성될 때, 필드는 기본 초기값으로 설정됩니다.
초기값과 다르게 생성하고 싶다면, 필드에 미리 값을 지정해주는 방법과 생성자를 통해 전달하는 방법이 있습니다.
그리고 알아낸 사실은 빌더 패턴을 이용할 경우, 미리 지정해준 값은 무시되고 자동 초기값으로 생성되는 것이었습니다.
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class BuilderTest {
private String test = "test1";
private int num = 5;
private List<Integer> testList = new ArrayList<>();
}
테스트를 위해 BuilderTest라는 클래스를 만들고 필드는 각각 String, int, List로 미리 값을 지정해줬습니다.
@DisplayName("빌더 패턴을 사용할 경우 기본 초기값이 무시된다")
@Test
void Test() {
BuilderTest instanceFromDefaultConstructor = new BuilderTest();
assertThat(instanceFromDefaultConstructor.getTest()).isEqualTo("test1");
assertThat(instanceFromDefaultConstructor.getNum()).isEqualTo(5);
assertThat(instanceFromDefaultConstructor.getTestList()).isNotNull();
BuilderTest instanceFromBuilder = BuilderTest.builder().build();
assertThat(instanceFromBuilder.getTest()).isNull();
assertThat(instanceFromBuilder.getNum()).isEqualTo(0);
assertThat(instanceFromBuilder.getTestList()).isNull();
}
테스트 결과, 기본 생성자로 생성한 인스턴스의 값은 미리 지정해준 값으로 생성되지만
빌더를 이용해서 생성한 인스턴스는 필드 자료형의 기본 초기값으로 생성이 되는 것을 확인할 수 있었습니다.
결과
따라서 빌더 패턴을 통해서 인스턴스를 만들었는데, 해당 ArrayList는 값을 전달해주지 않아서 기본 초기값인 Null로 되어 있었고 해당 Null에 add 메서드를 실행해서 NullPointerException이 발생했던 것입니다.
public static Family createFamily(CreateFamilyRequest dto, String invitationCode) {
return Family.builder()
.familyName(dto.getFamilyName())
.familyMemberList(new ArrayList<>())
.build();
}
빌더로 생성 시 List도 new ArrayList로 초기화해줬더니 더 이상 Null 문제가 발생하지 않았습니다.
@Builder.Default
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class BuilderTest {
private String test = "test1";
private int num = 5;
@Builder.Default
private List<Integer> testList = new ArrayList<>();
}
저렇게 매번 Builder로 객체 생성 시 초기화해주는 것은 번거롭기도 하고, 놓칠 수 있습니다.
@Builder.Default 어노테이션을 이용하면 Builder를 통해 생성한 인스턴스의 기본 값을 세팅할 수 있습니다.