오늘, 12시가 넘었으니까 정확히는 어제, 연습 차원에서 팀 프로젝트로 진행했던 서비스에 기능을 추가하여 새로 프로젝트를 만들어 보는 중이었다. 양방향 연관관계 매핑을 사용해 보며 Entity를 구현하고 테스트를 작성했을 때, 오류가 발생했다.
우선 Member
와 Cafe
엔티티 코드이다. (import 부분은 제외했다)
// Member
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@NotNull
private String email;
@NotNull
private String password;
private String name;
private String phone;
@Enumerated(EnumType.STRING)
private Role role;
@OneToMany(mappedBy = "owner")
private List<Cafe> cafes = new ArrayList<>();
...
}
// Cafe
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Cafe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cafe_id")
private Long id;
private String cafeName;
private String cafeAddress;
private String cafePhone;
private String cafeIntroduction;
private String cafeReward;
private int cafeRewardStampsNumber;
@ManyToOne
@JoinColumn(name = "member_id")
private Member owner;
...
public void setOwner(Member owner) {
this.owner = owner;
owner.getCafes().add(this);
}
}
Cafe
의 소유자(사장)를 설정하는 로직을 수행하는 setOwner()
메서드를 테스트해 보았다.
@Test
void associationTest() {
Member member = Member.builder()
.id(1L)
.email("woopaca")
.password("woopaca")
.build();
Cafe cafe = Cafe.builder()
.id(1L)
.cafeName("포롱")
.build();
cafe.setOwner(member);
List<Cafe> cafes = member.getCafes();
assertThat(cafes.size()).isEqualTo(1);
}
이런.. NullPointerException
이 발생했다. 도대체 왜지… 진짜 한참을 고민했다. 뭔가 Builder 패턴의 문제인가 싶었다.
그래서 일단 무식하게 @Builder
애노테이션으로 들어가 보았다. 하지만 하나도 모르겠다.
그런데 상단에 링크가 하나 있었다. lombok의 Builder에 대한 공식 문서 같았다.
영어 울렁증이 왔지만 번역기도 돌려보고 모르는 단어는 하나하나 찾아보며 읽어보았다. 그러다 이런 문장을 발견했다!
In the builder: A 'setter'-like method for each parameter of the target: It has the same type as that parameter and the same name. It returns the builder itself, so that the setter calls can be chained, as in the above example.
대충 setter와 같은 역할을 수행한다고 이해했다. 그러면 new ArrayList<>()
를 매개변수로 전달해 줘야 하나 싶어서 테스트 코드를 수정해 봤다.
@Test
void associationTest() {
Member member = Member.builder()
.id(1L)
.email("woopaca")
.password("woopaca")
.cafes(new ArrayList<>()) // 이 부분을 추가했음
.build();
Cafe cafe = Cafe.builder()
.id(1L)
.cafeName("포롱")
.build();
cafe.setOwner(member);
List<Cafe> cafes = member.getCafes();
assertThat(cafes.size()).isEqualTo(1);
}
하.. 된다. 근데 이상하다. 필드에서 바로 new ArrayList<>()
로 생성해서 할당하는데 왜 null
이 할당되는 건지 도무지 모르겠다. 또 new Member()
로 생성하면 null
이 아니다. Builder 패턴을 사용했을 때만 null
이 할당된다.
@Test
void associationTest() {
Member member = new Member();
Cafe cafe = Cafe.builder()
.id(1L)
.cafeName("포롱")
.build();
cafe.setOwner(member);
List<Cafe> cafes = member.getCafes();
assertThat(cafes.size()).isEqualTo(1);
}
이것 때문에 졸린데 못 자고 있었다. 일단 내일 일찍 일어나기 위해 잠을 자고 일어나서 좀 더 알아봐야겠다. 흑….😢
드디어 찾았다.
If a certain field/parameter is never set during a build session, then it always gets 0 /
null
/ false.
만약 빌드 세션 중에서 특정한 필드나 파라미터가 설정되지 않으면 항상 0, null
, false가 된다.
바로 아래 해결법도 나와있었다.
... you can instead specify the default directly on the field, and annotate the field with
@Builder.Default
:@Builder.Default private final long created = System.currentTimeMillis();
필드에서 직접 기본값을 지정하고 @Builder.Default
로 필드에 애노테이션을 추가하면 된다고 한다.
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@NotNull
private String email;
@NotNull
private String password;
private String name;
private String phone;
@Enumerated(EnumType.STRING)
private Role role;
@OneToMany(mappedBy = "owner")
@Builder.Default
private final List<Cafe> cafes = new ArrayList<>();
...
}
사실 @Builder.Default
를 사용하지 않고 final
키워드만 사용해도, 필드를 설정하지 않았을 때 참조가 변경될 수 없기 때문에 null
이 들어갈 수 없어서 초기에 설정한 값이 유지되기는 한다. @Builder.Default
는 참조형 변수보다는 기본형 변수를 초기화할 때 사용하는 게 좋을 것 같다.
...
@OneToMany(mappedBy = "owner")
private final List<Cafe> cafes = new ArrayList<>();
...