Publish:

νƒœκ·Έ:

μΉ΄ν…Œκ³ λ¦¬:

μ—”ν‹°ν‹° ν΄λž˜μŠ€μ— @NoArgsConstructor(access = AccessLevel.PROTECTED) λ₯Ό λΆ™μ΄λŠ” 이유

JPA λ₯Ό μ‚¬μš©ν•˜λ‹€λ³΄λ©΄ Entity ν΄λž˜μŠ€μ— @NoArgsConstructor(access = AccessLevel.PROTECTED) λ₯Ό λΆ™μ—¬μ„œ κΈ°λ³Έ μƒμ„±μžμ˜ 생성 λ²”μœ„λ₯Ό μ œν•œν•˜λŠ” κ²½μš°κ°€ μžˆλŠ”λ° μ™œ ꡳ이 protected λ₯Ό λΆ™μ΄λŠ”μ§€, λ‹€λ₯Έ μ ‘κ·Ό μ œν•œμžλŠ” μ™œ μ•ˆλ˜λŠ”μ§€μ— λŒ€ν•΄ μ•Œμ•„λ³΄μž.

μ§€μ—°λ‘œλ”©κ³Ό ν”„λ‘μ‹œ 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // 생성 λ©”μ†Œλ“œ(createOrder) λ₯Ό ν†΅ν•΄μ„œλ§Œ μ—”ν‹°ν‹°λ₯Ό λ§Œλ“€λ„λ‘ν•œλ‹€.
public class Order {

  @Id
  @GeneratedValue
  @Column(name = "order_id")
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "member_id")
  private Member member;

  @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
  private List<OrderItem> orderItems = new ArrayList<>();

  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  @JoinColumn(name = "delivery_id")
  private Delivery delivery;

  private LocalDateTime orderDate;

  @Enumerated(EnumType.STRING)
  private OrderStatus status;

  public void setMember(Member member) {
    this.member = member;
    member.getOrders().add(this);
  }

  public void addOrderItem(OrderItem orderItem) {
    orderItems.add(orderItem);
    orderItem.setOrder(this);
  }

  public void setDelivery(Delivery delivery) {
    this.delivery = delivery;
    delivery.setOrder(this);
  }

  /**
   * 객체 생성 static factory method
   * 
   */
  public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
    Order order = new Order();
    order.setMember(member);
    order.setDelivery(delivery);
    for (OrderItem orderItem : orderItems) {
      order.addOrderItem(orderItem);
    }
    order.setStatus(OrderStatus.ORDER);
    order.setOrderDate(LocalDateTime.now());
    return order;
  }

  ...
}

JPA μ—μ„œ λ‹€λŒ€μΌ 연관관계λ₯Ό λ§Ίμ„λ•Œ λΆˆν•„μš”ν•œ 쿼리가 μƒμ„±λ˜λŠ” 것을 λ°©μ§€ν•˜κΈ° μœ„ν•΄ @ManyToOne(fetch = FetchType.LAZY) 둜 μ§€μ—°λ‘œλ”©μ„ ν•˜κ²Œλ˜λŠ”λ° FetchType.EAGER 와 달리 μ‹€μ œλ‘œ find ν•œ Order κ°μ²΄μ—μ„œ member λ₯Ό νƒμƒ‰ν•˜λŠ” μ‹œμ μ— member λ₯Ό μΏΌλ¦¬ν•˜κ²Œ λœλ‹€.

1
2
Order findOrder = em.find(Order.class, id);  // 1
findOrder.getMember().getName();   // 2

μœ„ μ½”λ“œμ—μ„œ getMember().getName() 을 ν˜ΈμΆœν•˜λŠ” μ‹œμ μ— μ‹€μ œλ‘œ select * from member where memberId = ? 쿼리가 μ‹€ν–‰λœλ‹€. 즉, 졜초 findOrder 만 μ‘°νšŒν•œ μƒνƒœ(1번) μΌλ•ŒλŠ” Order μ—”ν‹°ν‹° μ•ˆμ— member μ—”ν‹°ν‹°κ°€ μ—†λ‹€λŠ” μ˜λ―Έκ°€ λœλ‹€.

κ·ΈλŸ¬λ‚˜ κ·Έλ ‡κ²Œ λ˜λŠ” 경우 μœ„ μ½”λ“œλŠ” NullPointerException 였λ₯˜κ°€ λ°œμƒν•  것이닀. null 에닀가 getName() 은 λΆˆκ°€λŠ₯ν•˜κΈ° λ•Œλ¬Έ. JPA λŠ” 이런 상황을 λ°©μ§€ν•˜κΈ° μœ„ν•΄ Lazy λ‘œλ”©μ˜ λŒ€μƒμ΄ λ˜λŠ” Member μ—”ν‹°ν‹°λ₯Ό κ°μ‹ΈλŠ” ν”„λ‘μ‹œ 객체λ₯Ό λ§Œλ“€μ–΄ μš°μ„  findOrder 객체λ₯Ό λ§Œλ“ λ‹€.

img.png ν”„λ‘μ‹œ 객체

ν”„λ‘μ‹œ κ°μ²΄λŠ” 내뢀에 Member μ—”ν‹°ν‹°λ₯Ό 가지고 μžˆλŠ”λ°, getName() 으둜 μ‹€μ œ κ·Έ μ—”ν‹°ν‹°λ₯Ό ν˜ΈμΆœν•˜λŠ” μ‹œμ μ— DB 에 쿼리가 λ‚ μ•„κ°€κ³ , Member μ—”ν‹°ν‹°μ˜ 값이 μ±„μ›Œμ§„λ‹€. μ•„λ§ˆλ„ μ΄λ•Œ member.setName("AAA") λ“±κ³Ό 같이 Member 엔티티에 값을 μ±„μ›Œ 1μ°¨ μΊμ‹œμ— μ €μž₯ν•˜κ²Œ 될텐데, λŸ°νƒ€μž„ ν™˜κ²½μ—μ„œ member μ—”ν‹°ν‹° 객체λ₯Ό μƒμ„±ν•˜κΈ° μœ„ν•΄ λ¦¬ν”Œλ ‰μ…˜μ„ μ‚¬μš©ν•˜κ²Œ λ˜λŠ” 경우 κΈ°λ³Έ μƒμ„±μžκ°€ ν•„μš”ν•˜λ‹€. (μ΄λŠ” JPA 뿐만 μ•„λ‹ˆλΌ Mybatis μ—μ„œ 쿼리λ₯Ό VO 에 λ§€ν•‘ν•˜λŠ” κ²½μš°μ—λ„ λ§ˆμ°¬κ°€μ§€λ‹€.)

즉, ν”„λ‘μ‹œ 객체둜 λ‘œλ”©λ˜λŠ” 엔티티에 @NoArgsConstructor(access = AccessLevel.PRIVATE) 같이 μ™ΈλΆ€μ—μ„œ μƒμ„±μžλ₯Ό μ‚¬μš©ν•  수 μ—†λŠ” κ²½μš°μ—” μ• μ΄ˆμ— 였λ₯˜κ°€ λ°œμƒν•œλ‹€. img_1.png μƒμ„±μž μ ‘κ·Ό μ œν•œμžλŠ” public, protected 만 κ°€λŠ₯ν•˜λ‹€

μ •λ¦¬ν•˜μžλ©΄ JPA μ—μ„œ μ—”ν‹°ν‹°λŠ” Public μ΄κ±°λ‚˜ Protected μƒμ„±μžλ§Œμ„ κ°€μ Έμ•„ ν•œλ‹€.

βœ…οΈ μ—”ν‹°ν‹° κ°„μ˜ κΈ€λ‘œλ²Œ λ‘œλ”© μ „λž΅(lazy / eager) 이 μ•„λ‹ˆλΌ νŠΉμ • λ‘œμ§μ—μ„œ ν•„μš”ν•œ 쿼리(ex. findAll() 같은 리슀트 쑰회 쿼리)λŠ” JPQL 둜 μž‘μ„±ν•˜κ²Œ λœλ‹€. JPQL 의 κ²½μš°μ—” λ°”λ‘œ DB 둜 쿼리가 λ‚ μ•„κ°€κΈ° λ•Œλ¬Έμ— κΈ€λ‘œλ²Œ μ—”ν‹°ν‹° λ‘œλ”© μ „λž΅μ— 영ν–₯받지 μ•ŠμŒ. 이런 κ²½μš°μ—” @ManyToOne κ΄€κ³„λŠ” fetch join 으둜 κ°€μ Έμ˜€κ³ , @OneToMany κ΄€κ³„λŠ” batch size 섀정을 톡해 in() 쿼리둜 κ°€μ Έμ˜€λ©΄ 쿼리 호좜 횟수λ₯Ό μ΅œμ†Œν™” ν•  수 μžˆλ‹€.

@NoArgsConstructor(access = AccessLevel.PUBLIC) 으둜 μ„€μ •ν•˜λ©΄ μ•ˆλ˜λ‚˜?

κΈ°λ³Έ μƒμ„±μžμ˜ μ ‘κ·Ό μ§€μ •μžκ°€ Public μ΄κ±°λ‚˜ Protected 이여야 ν•˜λŠ” μ΄μœ λŠ” μ•Œκ² λŠ”λ°, 그럼 @NoArgsConstructor 둜 μ“°λŠ”κ±΄(Public) μ–΄λ–€ λ¬Έμ œκ°€ μžˆλ‚˜?

이 κ²½μš°λŠ” κΈ°λŠ₯μƒμ˜ λ¬Έμ œκ°€ μ•„λ‹ˆλΌ 휴먼 μ—λŸ¬λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•¨μ΄λ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional
public Long order(Long memberId, Long itemId, int count) {
  // μ—”ν‹°ν‹° 쑰회
  Member member = memberRepository.findOne(memberId);
  Item item = itemRepository.findOne(itemId);

  // 배솑정보
  Delivery delivery = new Delivery();
  delivery.setAddress(member.getAddress());
  delivery.setStatus(DeliveryStatus.READY);

  // μ£Όλ¬Έμƒν’ˆ 생성
  OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

  // μ£Όλ¬Έ 생성
  Order order = Order.createOrder(member, delivery, orderItem);

  // μ£Όλ¬Έ μ €μž₯
  orderRepository.save(order);
  return order.getId();
}

μœ„μ™€ 같은 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ—μ„œ μ£Όλ¬Έμƒν’ˆκ³Ό μ£Όλ¬Έ μ—”ν‹°ν‹°λ₯Ό λ§Œλ“€λ•Œ 정적 νŒ©ν† λ¦¬ λ©”μ†Œλ“œλ₯Ό μ΄μš©ν•΄ 객체λ₯Ό λ§Œλ“€μ–΄λ‚΄κ³  μžˆλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
// Order Entity
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
  Order order = new Order();
  order.setMember(member);
  order.setDelivery(delivery);
  for (OrderItem orderItem : orderItems) {
    order.addOrderItem(orderItem);
  }
  order.setStatus(OrderStatus.ORDER);
  order.setOrderDate(LocalDateTime.now());
  return order;
}

Order μ—”ν‹°ν‹° 내뢀에 객체λ₯Ό λ§Œλ“œλŠ” λ©”μ†Œλ“œλ₯Ό 놔두고 μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ μ—”ν‹°ν‹°κ°€ ν•„μš”ν•œ 경우 이 λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•˜λ„λ‘ ν•œλ‹€. μ΄λ ‡κ²Œ ν•˜λŠ” μ΄μœ λŠ” 엔티티와 κ΄€λ ¨λœ λ©”μ†Œλ“œλ₯Ό ν•œ ν΄λž˜μŠ€μ— λͺ¨μ•„λ†“μŒμœΌλ‘œμ¨ 응집도가 μ˜¬λΌκ°€κ³ , 같이 μž‘μ—…ν•˜κ±°λ‚˜ λ‹€λ₯Έ μ‚¬λžŒμ΄ μœ μ§€λ³΄μˆ˜λ₯Ό ν•˜λŠ” 경우 μ™ΈλΆ€μ—μ„œ 객체 생성을 λ°©μ§€ν•˜λŠ”λ°μ— λͺ©μ μ΄ μžˆλ‹€.

λ”°λΌμ„œ μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ new Order() λ₯Ό νƒ€μ΄ν•‘ν•˜λŠ” μˆœκ°„ 컴파일 μ—λŸ¬λ₯Ό λ§Œλ‚˜κ²Œ λœλ‹€. λ§Œμ•½ 객체생성을 막지 μ•ŠμœΌλ©΄, μ–΄λ””μ„œλ“  객체의 값이 변경될 수 있고 이λ₯Ό μΆ”μ ν•˜κΈ° νž˜λ“€λ‹€.

μ΄λ ‡κ²Œ μ—”ν‹°ν‹° μ•ˆμ— λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ κ΅¬μ„±ν•˜λŠ” 방식을 도메인 λͺ¨λΈ νŒ¨ν„΄(Domain Model Pattern) 이라고 ν•˜κ³ , λ°˜λŒ€λ‘œ 엔티티에 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μ—†κ³  μ„œλΉ„μŠ€ 계측에 μžˆλŠ” 경우λ₯Ό νŠΈλžœμž­μ…˜ 슀크립트 νŒ¨ν„΄(Transaction Script Pattern) 이라고 ν•œλ‹€.

정리

  • ν”„λ‘μ‹œ 객체 생성 μ‹œ κΈ°λ³Έ μƒμ„±μžκ°€ ν•„μš”ν•˜κΈ° λ•Œλ¬Έ
  • 객체 생성 λ©”μ†Œλ“œλ₯Ό ν†΅ν•΄μ„œλ§Œ 객체λ₯Ό μƒμ„±ν•˜λ„λ‘ κ°•μ œν•˜κΈ° μœ„ν•¨
λ°©λ¬Έν•΄ μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€! λŒ“κΈ€,지적,ν”Όλ“œλ°± μ–Έμ œλ‚˜ ν™˜μ˜ν•©λ‹ˆλ‹€πŸ˜Š

λŒ“κΈ€λ‚¨κΈ°κΈ°