요구사항과 관련된 전체 엔티티는 위와 같다.
이번 포스트에서는 이들 중 회원과 관련된 엔티티에 대해서 집중적으로 다뤄보고자 한다.
회원이 주문을 하기 때문에, 회원이 주문리스트를 가지는 것은 얼핏 보면 잘 설계한 것처럼 보이지만, 객체 세상과 실제 세계는 다르다. 실무에서는 회원이 주문을 참조하지 않고, 주문이 회원을 참조하는 것으로 충분하다. 여기서는 일대다, 다대일의 양방향 연관관계를 설명하기 위해서 추가했을 뿐 실무에서는 단방향 연관관계면 충분하다.
이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메소드를 제공하는 것이 가장 이상적이다. 하지만 실무에서 엔티티의 데이터를 조회할 일은 많으므로, Getter의 경우 모두 열어두는 것이 편리하다. Getter는 아무리 호출해도 호출 하는 것 만으로 어떤 일이 발생하지 않기 때문이다.
하지만 Setter는 문제가 다르다. Setter를 호출하면 데이터가 변한다. Setter를 제한 없이 열어두면 엔티티가 도대체 왜 변경되는지 추적하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확하도록 변경을 위한 비즈니스 메소드를 별도로 제공해야 한다.
[회원 엔티티]
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
[주문 엔티티]
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.Lazy)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order")
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);
}
}
public enum OrderStatus {
ORDER, CANCEL
}
[배송 엔티티]
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "order", fetch = FetchType.LAZY)
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
}
public enum DeliveryStatus {
READY, COMP
}
[주소 값 타입]
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
값 타입은 불변객체 즉 변경 불가능하게 설계 해야한다.
@Setter를 제거하고, 생성자에서 값을 모두 초기화하여 변경 불가능한 불변객체로 만들어야 한다. 값 타입의 공유참조는 매우 위험하기 때문이다.
JPA 스펙상 엔티티나 임베디드 타입은 자바 기본 생성자를 public 또는 protected로 설정해야 한다. public으로 두는 것보다는 protected로 설정하는 것이 불변객체의 입장에서는 더 좋다.
JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플렉션 같은 기술을 사용할 수 있도록 지원해야하기 때문이다.
Order와 Delivery는 일대일 연관관계이다. 위에서 설계한대로 테이블이 만들어진다면 아마 다음과 같은 형태일 것이다.
즉 Order와 Delivery에 독립적인 PK가 각각 존재하고 Order에 delivery_id라는 FK가 존재하는 형태이다.
그런데 일대일 관계에서 설명했듯이 이는 효율적이지 않은 설계이다.
왜냐하면 어차피 일대일 관계이므로 order_id와 delivery_id가 서로 미러링 하면 되기 때문이다.
두 PK를 미러링하게 하려면 @MapsId를 이용하면 된다.
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Delivery delivery;
private LocalDateTime orderDate; // 주문시간
@Enumerated(EnumType.STRING)
private OrderStatus status; // 주문상태 [ORDER, CANCEL]
}
@Entity
@Getter @Setter
public class Delivery {
@Id
private Long id;
@MapsId
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; // READY, COMP
}
이런식으로 @MapsId를 이용하면 Order의 id와 Delvery의 id를 미러링 시킬 수 있다.
Order의 id를 Delivery의 id에 할당하면 되므로 Delivery의 id에는 @GeneratedValue를 붙일 이유가 없어진다. 그리고 다음과 같이 테이블이 만들어 질 것이다.
전이랑 비교하면 Order 테이블에서 FK인 delivery_id가 사라졌음을 알 수 있다.
그리고 Order와 Delivery의 id가 order_id로 통일되었음을 알 수 있다.
실제로 값을 집어넣으면 다음과 같이 나오게 된다.
※ 주의
Delivery에 있는 order부분에 @JoinColumn을 안붙여도 작동을 하는 경우가 있다.
만약 @JoinColumn을 붙이지 않으면 Delivery의 id의 컬럼명은 ORDER_ORDER_ID로 자동으로 지어진다. id 이름이 지어지는 규칙은 엔티티명_ID명 이다.
그런데 Delivery를 설계할 때 ORDER_ORDER_ID로 컬럼명을 지었을리가 없다. 그래서 맞춰줘야 하는데 이 때 필요한 것이 @JoinColumn이다. 여기서 name = "order_id"라고 설정을 해줘야 order_id의 컬럼에 알맞는 값이 들어가게 된다.