평소와 같이 아무생각 없이 Builder를 사용해서 Entity를 작성했다.
Member Entity
package com.playdata.eungae.member.domain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.hibernate.annotations.DynamicInsert;
import com.playdata.eungae.base.BaseEntity;
import com.playdata.eungae.hospital.domain.Hospital;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@DynamicInsert
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
@Table(name = "member")
@Entity
public class Member extends BaseEntity {
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Id
private Long memberSeq;
// 페이징 처리를 위해 List -> Page로 리펙토링 필요함
// 즐겨찾기 병원 목록
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Hospital> hospitals = new ArrayList<>();
@Column(nullable = false, unique = true, updatable = false)
private String email;
@Column(nullable = false, length = 30)
private String password;
@Column(nullable = false, length = 20)
private String name;
@Column(nullable = false, length = 15)
private String phoneNumber;
@Column(nullable = false)
private String birthDate;
@Column(nullable = false)
private String address;
@Column(nullable = false)
private String addressDetail;
@Column(nullable = false)
private String zipCode;
private Integer xCoordinate;
private Integer yCoordinate;
//0이면 일반 로그인 1이면 카카오 로그인
@Column(columnDefinition = "varchar(1) default '0'")
private boolean kakaoCheck;
public void setHospitals(Hospital... hospitals) {
this.hospitals.addAll(List.of(hospitals));
Arrays.stream(hospitals).forEach((hospital -> hospital.setMember(this)));
}
public void remove(Hospital hospital) {
this.hospitals.remove(hospital);
hospital.setMember(null);
}
}
하지만 해당 엔티티에 연관관계 편의 메서드인 setHospitals를 실행하자 정말 뜬금없이 NullPointerException이 발생했다.
처음에는 Stream이 잘못된지 알고 애꿎은 코드만 만지작 거리다 이유를 찾았다.
Builder 어노테이션을 적용시 필드에 설정해둔 초기화가 작동하지 않는다.
상상도 못한 이유로 한참을 해맸더니 머리가 어지러웠다.
그렇다면 Builder 어노테이션을 적용시 어째서 필드가 초기화 되지 않을까?
이유는 객체를 생성할때 new 를 사용해서 객체를 생성하면 미리 필드에 지정해둔 초기값이 세팅되지만 Builder는 필드에 선언되어있는 초기값과 무관하게 직접 내가 원하는 값을 대입해주기 때문이다.
그럼 한번 테스트해보자.
실험에 사용될 Test class
package com.playdata.eungae.member.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Test {
private String name = new String("테스트 입니다.");
}
테스트를 위해 필드값이 단 하나뿐인 class를 선언해줬다.
new로 TestClass를 직접 생성하는 방법
@Test
public void builderTest() throws Exception {
//given
TestClass testClass = new TestClass();
//when
String testClassName = testClass.getName();
System.out.println("testClassName = " + testClassName);
//then 결과값이 null이 아니면 테스트 성공
Assertions.assertThat(testClassName).isNotNull();
}
객체가 생성되며 지정해준 필드값이 자동으로 생성된 모습이다.
그렇다면 Builder로 생성한 객체는 어떻게 될까?
@Test
public void builderTest() throws Exception {
//given
TestClass testClass = TestClass.builder()
.build();
//when
String testClassName = testClass.getName();
System.out.println("testClassName = " + testClassName);
//then
Assertions.assertThat(testClassName).isNull();
}
builder에 명시되지 않은 필드에는 null이 들어가는 모습을 확인할 수 있다.
예방법
예방법은 간단하다
@Builder.Default
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Hospital> hospitals = new ArrayList<>();
필드에 @Builder.Default를 선언해줘 객체가 Build될때 기본값을 설정해주면 해결!
'코딩딩 > Error' 카테고리의 다른 글
Spring Data JPA 사용시 Repository BeanCreationException (0) | 2024.01.09 |
---|---|
파이썬 셀레니움 웹 스크레핑 시 hidden에 숨겨져있는 src 경로 가져오기 (3) | 2024.01.08 |
OneToOne 양방향 관계 지연로딩 안먹히는 문제 해결법 (0) | 2024.01.05 |
OneToOne 관계에서 N+1 문제가 발생하는 이유 (2) | 2024.01.04 |
Null값이 들어올 수 없는 @PathVariable에도 Wrapper class를 사용해야 할까? (1) | 2023.12.28 |