๐ค @Builder๊ฐ ๋ฌ๋ ค์๋ ํด๋์ค์ ํ๋๋ฅผ ์ด๊ธฐํํด์คฌ๋๋ฐ, ์ ๋น๋ ํจํด์ ์ฌ์ฉํ๋ฉด ์ด๊ธฐํ ๊ฐ์ด ์๋ Null์ด ๋์ค์ง?
๐ช GTAccountInfo ์ํฐํฐ์ ์ฝ๋ ์ผ๋ถ
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "gt_account_info")
public class GTAccountInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ACCOUNT_ID")
private long id;
@Getter
@Column(name = "account_email")
private String accountEmail;
@Getter
@Column(name = "account_pw")
private String accountPW;
@OneToMany(mappedBy = "accountInfo", fetch = FetchType.LAZY)
private List<GTAccountUserRoleInfo> roleByThisAccountList = new ArrayList<>();
public List<GTUserRole> getRoles(){
return roleByThisAccountList.stream()
.map(GTAccountUserRoleInfo::getUserRole)
.map(GTUserRoleInfo::getUserRole)
.collect(Collectors.toList());
}
// ...
}
๐ชTest ์ฝ๋์ ์ผ๋ถ ๋ฐ์ท
@Test
@DisplayName("๐ค 1. ๊ณ์ ์ ๋ณด ์์ฑ ๋ฐ ์ ์ฅ ํ
์คํธ : ์ฑ๊ณต ์ผ์ด์ค")
@Transactional
public void testForCreateNewAccountInfoAndSave(){
GTAccountInfo mockAccountInfo = GTAccountInfo.builder()
.accountEmail("test@example.com")
.accountPW("1234")
.build();
GTAccountInfo savedAccountInfo = accountInfoRepository.save(mockAccountInfo);
assertEquals(savedAccountInfo.getAccountEmail(), "test@example.com");
assertEquals(savedAccountInfo.getAccountPW(), "1234");
assertEquals(savedAccountInfo.getRoles().size(), 0);
}
ํ ์คํธ ์ฝ๋์์ ์์ ๊ฐ์ด, Builder ํจํด์ ์ฌ์ฉํด์ ์ด๋ฉ์ผ๊ณผ PW๋ฅผ ์ฑ์ ๋ฃ์ด ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํด๋์ต๋๋ค.
Entity ํด๋์ค์์, roleByThisAccountList์ ๊ฒฝ์ฐ, new ArrayList<>()๋ก ํ ๋น๋์ด ์๊ธฐ ๋๋ฌธ์ null์ผ ๊ฒฝ์ฐ๋ ์๋ค๋ผ๊ณ ์๊ฐํ์ต๋๋ค.
๊ทธ๋ฌ๋, ํ ์คํธ์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํด๋ณด๋,
warning : @Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default. If it is not supposed to be settable during building, make the field final. private List<GTAccountUserRoleInfo> roleByThisAccountList = new ArrayList<>();
java.lang.NullPointerException at com.example.gtmvcserverside.member.domain.GTAccountInfo.getRoles(GTAccountInfo.java:41) ...
์ ๊ฐ์ด roleByThisAccountList๊ฐ null์ธ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
๊ฒฝ๊ณ ํํ์ ์ฝ์ด๋ณด๋ฉด, @Builder๋ Initializing expression์ ์ ์ฒด ๋ฌด์ํ๊ณ , default๋ก ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด, @Builder.Default๋ฅผ ์ฌ์ฉํด๋ผ๊ณ ํฉ๋๋ค.
class Example<T> {
private T foo;
private final String bar;
private Example(T foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public static <T> ExampleBuilder<T> builder() {
return new ExampleBuilder<T>();
}
public static class ExampleBuilder<T> {
private T foo;
private String bar;
private ExampleBuilder() {}
public ExampleBuilder foo(T foo) {
this.foo = foo;
return this;
}
public ExampleBuilder bar(String bar) {
this.bar = bar;
return this;
}
@java.lang.Override public String toString() {
return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")";
}
public Example build() {
return new Example(foo, bar);
}
}
}
์ ์ฝ๋๋, @Builder๋ฅผ ํตํด ๋ด๋ถ์ ์ผ๋ก ๋ง๋ค์ด์ง ์ฝ๋์ ๋ํ ๋ด์ฉ์ ์ฃผ์์์ ๊ฐ์ ธ์จ ๊ฒ์ ๋๋ค.
builder ํจํด์ ํ์ฉํด์ foo์ bar๋ฅผ ์ธํ ํ ์ ์์ต๋๋ค.
์ด inner class์ธ ExampleBuilder<T>๋, ๋ด๋ถ์์ ์ด๊ธฐํ ํ๋ ์ฝ๋๊ฐ ์ด๋์๋ ์์ต๋๋ค.
๋ฐ๋ผ์ @Builder๊ฐ ๋ฌ๋ ค์๋ ํด๋์ค์ ๋ํด ๋น๋ ํจํด์ผ๋ก ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ค๊ณ ํ๋ฉด, ํ๋๊ฐ initialized ๋์ด์๋๋ผ๋, ๋น๋ํจํด์ ์ฌ์ฉํ๋ฉด์ ๊ฐ์ ์ฝ์ ํ์ง ์์ผ๋ฉด null์ด ๋ค์ด๊ฐ ์ ๋ฐ์ ์์ต๋๋ค.
๋ฐ๋ผ์ ์ด๊ธฐํ๋ ์ํ๋ฅผ @Builder๋ฅผ ์ฌ์ฉํ๋ฉด์ ์ ์งํ๊ณ ์ถ๋ค๋ฉด, ํ๋์ @Builder.Default๋ฅผ ์ ์ฉํ๋ฉด ๋ฉ๋๋ค.
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "gt_account_info")
public class GTAccountInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ACCOUNT_ID")
private long id;
@Getter
@Column(name = "account_email")
private String accountEmail;
@Getter
@Column(name = "account_pw")
private String accountPW;
@Builder.Default
@OneToMany(mappedBy = "accountInfo", fetch = FetchType.LAZY)
private List<GTAccountUserRoleInfo> roleByThisAccountList = new ArrayList<>();
public List<GTUserRole> getRoles(){
return roleByThisAccountList.stream()
.map(GTAccountUserRoleInfo::getUserRole)
.map(GTUserRoleInfo::getUserRole)
.collect(Collectors.toList());
}
// ...
}
์ด์ ๋, ์ํ๋๋๋ก ์ด๊ธฐํ๊ฐ ๋๊ณ ์ ์์ ์ผ๋ก ํ ์คํธ๊ฐ ํต๊ณผํ๋ ๋ชจ์ต์ ๋ณผ ์ ์์ต๋๋ค.
๐ค @Builder์ ์ถ๊ฐ ์ง์
์ถ๊ฐ์ ์ผ๋ก @Builder์ ์ฃผ์์ ํ ๋ฒ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
If a member is annotated, it must be either a constructor or a method. If a class is annotated, then a package-private constructor is generated with all fields as arguments (as if @AllArgsConstructor(access = AccessLevel.PACKAGE) is present on the class), and it is as if this constructor has been annotated with @Builder instead. Note that this constructor is only generated if you haven't written any constructors and also haven't added any explicit @XArgsConstructor annotations. In those cases, lombok will assume an all-args constructor is present and generate code that uses it; this means you'd get a compiler error if this constructor is not present.
@Builder ์ด๋ ธํ ์ด์ ์ ์ฝ์ ํ๋ฉด, ๋ชจ๋ ํ๋๋ฅผ ์ธ์๋ก ์๊ตฌํ๋ package-privateํ ์์ฑ์๋ฅผ ์๋์ผ๋ก ์์ฑํ๊ณ , ๋ด๋ถ์ ์ผ๋ก ์ด๋ฌํ ์์ฑ์๊ฐ ์๋ค๋ ๊ฐ์ ํ์ ์ฝ๋๋ฅผ generate ํฉ๋๋ค.
์ด๋ฌํ ๋ชจ๋ ํ๋๋ฅผ ์ธ์๋ก ์๊ตฌํ๋ package-privateํ ์์ฑ์๋ ์ด๋ ํ ์์ฑ์๋ฅผ ์์ฑํ์ง ์์์ผํ๊ณ , ๋ค๋ฅธ @XArgsConstructor(No, Required..) ์ญ์ ๋ช ์ํ์ง ์์ ๊ฒฝ์ฐ์ ์์ฑ๋๋ค๊ณ ๋์์์ต๋๋ค.
์ฆ, JPA๋ฅผ ์ฌ์ฉํ๋ฉด์ @NoArgsConstructor๋ฅผ ๋ฐ๋์ ํฌํจํด์ผ ํ๋ ๊ฒฝ์ฐ์๋ ๋ชจ๋ ํ๋๋ฅผ ์ธ์๋ก ์๊ตฌํ๋ package-privateํ ์์ฑ์๊ฐ ์์ฑ๋์ง ์์ต๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ Entity ํด๋์ค์์ @Builder๋ฅผ ์ฌ์ฉํ๊ณ ์ ํ๋ค๋ฉด,
@NoArgsConstructor๊ณผ @AllArgsConstructor๋ฅผ ๊ฐ์ด ์ฌ์ฉํด์ฃผ๋ฉด ๋ฉ๋๋ค.