๐Ÿƒ‍โ™‚๏ธ DEVLOG

[DEVLOG] @Builder์™€ @Builder.Default

๊ฐœ๋ฐœ์ž HOON 2023. 4. 12. 22:13

 

๐Ÿค” @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๋ฅผ ๊ฐ™์ด ์‚ฌ์šฉํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.