요구사항 분석 - 회원

PPakSSam·2022년 2월 3일
0
post-thumbnail

요구사항과 관련된 전체 엔티티는 위와 같다.
이번 포스트에서는 이들 중 회원과 관련된 엔티티에 대해서 집중적으로 다뤄보고자 한다.

들어가기 전


회원엔티티에 주문리스트가 있다?

회원이 주문을 하기 때문에, 회원이 주문리스트를 가지는 것은 얼핏 보면 잘 설계한 것처럼 보이지만, 객체 세상과 실제 세계는 다르다. 실무에서는 회원이 주문을 참조하지 않고, 주문이 회원을 참조하는 것으로 충분하다. 여기서는 일대다, 다대일의 양방향 연관관계를 설명하기 위해서 추가했을 뿐 실무에서는 단방향 연관관계면 충분하다.

Getter, Setter에 대하여

이론적으로 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의 컬럼에 알맞는 값이 들어가게 된다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글