[JPA] 6. 주문 도메인 개발 - 주문, 주문상품 엔티티 개발

2025. 5. 14. 11:45·BE/SpringBoot
728x90

Order.java

@Entity
@Table(name="orders")
@Getter @Setter
public class Order {

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

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

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate; // 주문 시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문상태 [ORDER, CANCLE]

    // 연관관계 메서드
    // 연관관계 메서드를 사용하는 이유: 양방향 연관관계에서 한쪽에만 값을 넣고 반대쪽에는 값을 누락하는 실수를 방지하기 위해
    // "양쪽 모두를 한 번에 설정" 해주는 메서드를 만드는 것
    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 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;
    }

    // 비즈니스 로직
    /** 주문 취소 **/
    public void cancle(){
        // delivery의 상태가 이미 배송완료(DeliveryStatus.COMP)면,
        if(delivery.getStatus() == DeliveryStatus.COMP){
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCLE);
        for(OrderItem orderItem: orderItems){
            orderItem.cancle();
        }
    }

    // 조회 로직
    /** 전체 주문 가격 조회 **/
    public int getTotalPrice(){
        int totalPrice = 0;
        for(OrderItem orderItem: orderItems){
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
}

 

  • 주문 취소(cancel()): 주문 취소시 사용한다. 주문 상태를 취소로 변경하고 주문상품에 주문 취소를 알린다. 만약 이미 배송을 완료한 상품이면 주문을 취소하지 못하도록 예외를 발생시킨다.

주문 엔티티(Order.java)에서 이전과 달라진 부분들이 왜 달라졌는지 작성해보려고 한다.

연관관계 메서드

    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 void 연관관계_설정() {
    // 회원1 저장
    Member member1 = new Member("member1", "회원1");
    em.persist(member1);

    // 회원2 저장
    Member member2 = new Member("member2", "회원2");
    em.persist(member2);

    // 주문 생성
    Order order1 = new Order();
    Order order2 = new Order();

    // Order → Member 만 설정 (Member ← Order는 누락됨)
    order1.setMember(member1);
    order2.setMember(member2);

    em.persist(order1);
    em.persist(order2);
}

예를들어 이런 코드가 있다고 가정해보자.

이 코드에서는 Order.member만 설정했고, Member.orders에는 아무것도 들어가지 않는다.

위 코드를 실행하고 회원을 조회하면 orders가 비어 있을 수 있다. 왜냐하면 연관관계의 주인은 Order 엔티티인데, Order.member를 설정하지 않고, Member.orders에만 값을 추가했기 때문에 DB에는 실제 관계가 반영되지 않는다.

 

아래는 Order.member 와 Member.orders 양쪽에 관계를 설정하는 코드다.

Order order1 = new Order();
order1.setMember(member1);
member1.getOrders().add(order1);

Order order2 = new Order();
order2.setMember(member2);
member2.getOrders().add(order2);

즉, 1번과 2번의 코드는 같은 코드를 의미한다.

// 1
order.setMember(member);
member.getOrders().add(order);

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

그럼 연관관계의 주인은 대체 누구일까?

쉽게 말해서 @JoinColumn 어노테이션을 가지고 있는 쪽이 연관관계의 주인이다.

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

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

그러니 Member와 Order의 관계에서 Order가 연관관계의 주인이고, Delievery와 Order의 관계에서도 Order가 연관관계의 주인!

 

엥 근데 그럼 OrderItems는 mappedBy로 되어있으니까.. 연관관계의 주인은 OrderItems가 되어야 하는게 아닌가? 

연관관계 메서드가 OrderItems에서 관리되어야하는게 아닌가?

연관관계 메서드는 연관관계 주인에 꼭 있어야 하는 게 아니다. 비즈니스 흐름을 주도하는 쪽(즉, 관계를 조립하는 쪽)에 둬야한다.

 

그렇기 때문에 Order와 OrderItem의 관계에서 OrderItem이 연관관계의 주인이지만, Order가 생성될 때 여러 OrderItem을 담기 때문에 연관관계 메서드를 Order에 둔다.

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

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

setMember와 addOrderItem의 차이는

여기서 addOrderItem()은 편의 메서드이고, 실제로 DB에 반영되려면 주인 쪽인 OrderItem.order에 값을 넣어야 하므로 이 메서드 안에서 orderItem.setOrder(this)를 반드시 호출해야 한다는 점이다.

생성메서드

생성메서드는 객체를 만들고, 그 객체가 올바른 상태를 갖도록 설정을 한 번에 마무리해주는 메서드

  • createOrder(): 주문 엔티티를 생성할 때 사용한다. 주문 회원, 배송정보, 주문상품의 정보를 받아서 실제 주문 엔티티를 생성한다.

그럼 생성메서드를 왜 사용하는걸까? 그냥 Order order = new Order();로 선언해서 사용해도 되지 않을까?

(1) 객체의 일관성을 보장하기 위해

만약 객체를 new로 생성하면 필수 필드를 설정하지 않고 빠트리는 문제가 발생할 수 있다.

Order order = new Order();
order.setMember(member); // 설정 안 하면 문제 발생

(2) 가독성과 재사용성을 높이기 위해

생성자 없이 new로 설정할 경우 Order 객체가 필요한 곳에서 계속 필드를 설정해줘야 하기에 중복 코드가 발생한다.

// 생성자 없이 설정할 때
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
order.addOrderItem(item1);
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());

// 생성 메서드로 간결하게
Order order = Order.createOrder(member, delivery, item1);

OrderItem.java

@Entity
@Getter @Setter
public class OrderItem {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name="item_id")
    private Item item;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name="order_id")
    private Order order;

    private int orderPrice; // 주문 가격
    private int count; // 주문 수량

    // 생성 메서드
    public static OrderItem createOrderItem(Item item, int orderPrice, int count){
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count);

        item.removeStock(count);
        return orderItem;
    }

    // 비즈니스 로직
    public void cancle() {
        // 재고 수량을 다시 돌린다
        getItem().addStock(count);
    }

    // 조회 로직
    /** 주문상품 전체 가격 조회**/
    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}
  • 전체 주문 가격 조회: 주문 시 사용한 전체 주문 가격을 조회한다. 전체 주문 가격을 알려면 각각의 주문상품 가격을 알아야 한다. 로직을 보면 연관된 주문상품들의 가격을 조회해서 더한 값을 반환한다. (실무에서는 주로 주문에 전체 주문 가격 필드를 두고 역정규화 한다.)

Item.java

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
    @Id @GeneratedValue
    @Column(name="item_id")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();

    @OneToMany(mappedBy = "item")
    private Collection<OrderItem> orderItem;

    // 비즈니스 로직

    /** stock 증가 +*/
    public void addStock(int quantity){
        this.stockQuantity += quantity;
    }

    public void removeStock(int quantity){
        int resultStock = this.stockQuantity - quantity;
        if(resultStock < 0){
            throw new NotEnoughStockException("need more stock");
        }
        this.stockQuantity = resultStock;
    }
}

 

728x90

'BE > SpringBoot' 카테고리의 다른 글

[JPA] 6. 주문 도메인 개발 - 주문 검색 기능 개발  (0) 2025.05.20
[JPA] 6. 주문 도메인 개발 - 리포지토리/서비스/테스트  (0) 2025.05.17
[JPA] 5. 상품 도메인 개발  (0) 2025.05.14
[JPA] 4. 회원 도메인 개발  (0) 2025.05.13
[JPA] 3. 애플리케이션 구현 준비  (0) 2025.05.12
'BE/SpringBoot' 카테고리의 다른 글
  • [JPA] 6. 주문 도메인 개발 - 주문 검색 기능 개발
  • [JPA] 6. 주문 도메인 개발 - 리포지토리/서비스/테스트
  • [JPA] 5. 상품 도메인 개발
  • [JPA] 4. 회원 도메인 개발
DROPDEW
DROPDEW
💻 Developer | 기록하지 않으면 존재하지 않는다
  • DROPDEW
    제 2장 1막
    DROPDEW
  • 전체
    오늘
    어제
    • Dev (446) N
      • App·Android (1)
      • AI (12) N
      • BE (50)
        • HTTP 웹 기본 지식 (8)
        • SpringBoot (23)
        • 스프링부트와 JPA 활용 (0)
        • JAVA (1)
        • PHP (11)
      • FE·Client (23)
        • HTML (1)
        • React (19)
        • Unity (1)
      • Data (28)
        • Bigdata (6)
        • Database (1)
        • Python (0)
        • 빅데이터분석기사 (13)
      • Infra (1)
      • Activity (7)
        • Intern (0)
        • SK AI Dream Camp (0)
        • 구름톤 유니브 4기 (1)
        • 리모트 인턴십 6기 (3)
        • 봉사활동 (0)
        • 부스트캠프 AI Tech 8기 (3)
      • CS (8)
      • 취준 (12)
        • 자격증 (4)
        • 인적성·NCS (6)
        • 코테·필기·면접 후기 (2)
      • 코테 (270)
        • Algorithm (222)
        • SQL (35)
        • 정리 (13)
      • 인사이트 (27)
        • 금융경제뉴스 (7)
        • 금융용어·지식 (2)
        • 북마크 (7)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    수학
    그리디알고리즘
    정렬
    너비우선탐색
    다이나믹프로그래밍
    문자열
    최단경로
    오블완
    이분탐색
    매개변수탐색
    백준
    투포인터
    시뮬레이션
    자료구조
    구현
    그래프탐색
    브루트포스 알고리즘
    그래프이론
    티스토리챌린지
    누적합
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
DROPDEW
[JPA] 6. 주문 도메인 개발 - 주문, 주문상품 엔티티 개발
상단으로

티스토리툴바