본문 바로가기
BackEnd/JPA

[JPA] 도메인 분석 설계(2)

by 개발 Blog 2024. 9. 26.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

엔티티 클래스 개발1

이번 글에서는 JPA의 엔티티 클래스 설계 및 개발을 다룬다. 예제에서는 학습을 목적으로, 엔티티 클래스에 Getter와 Setter를 모두 열어 두었다. 하지만 실제 실무에서는 좀 더 신중한 접근이 필요하다.

 

Getter와 Setter의 사용

  • 실무에서는
    • Getter: 대부분의 경우 열어두는 것을 추천한다. 엔티티의 데이터는 조회할 일이 많아 편리하다.
    • Setter: 가능한 한 열지 말고, 꼭 필요한 경우에만 사용하는 것이 좋다. 데이터 변경 시, 어디서 어떤 이유로 변경되었는지 추적하기 어렵기 때문에, 변경이 필요한 경우에는 비즈니스 로직 메서드를 따로 제공하는 것이 바람직하다.

참고: 가장 이상적인 것은 Getter와 Setter 모두 제공하지 않고, 필요한 메서드만 제공하는 방식이다. 하지만 실무에서는 엔티티의 데이터 조회가 빈번하므로, Getter는 대부분 열어두는 것이 좋다. 단, Setter는 데이터 변경 시 추적이 어려울 수 있기 때문에 비즈니스 로직을 통해 관리하는 것이 바람직하다.

 

item

Item은 상속을 위한 부모 클래스이다. 상속 구조에서 전략을 SINGLE_TABLE 방식으로 사용하여 한 테이블에 모든 상속받은 엔티티의 데이터를 관리한다. 그리고 각 엔티티는 dtype이라는 구분 컬럼을 통해 어떤 타입인지 구분한다.

package jpabook.jpashop.domain.item;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@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;
}

 

Album, Book, Movie

각각의 엔티티는 Item을 상속받아 DiscriminatorValue를 통해 각 엔티티가 어떤 타입인지 식별한다.

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item {
    private String artist;
    private String etc;
}
package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("B")
@Getter
@Setter
public class Book extends Item {
    private String author;
    private String isbn;

}
package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item {
    private String director;
    private String actor;
}

 

Address

임베디드 타입으로, 주소와 관련된 필드를 묶어 관리한다. Member와 Delivery 같은 엔티티에서 주소를 포함하는 방식으로 사용된다.

package jpabook.jpashop.domain;

import jakarta.persistence.Embeddable;
import lombok.Getter;

@Embeddable
@Getter
public class Address {

    private String city;
    private String street;
    private String zipCode;
}

 

Delivery

배달과 관련된 정보를 가진다. Enum 타입의 배달 상태(READY, COMP)를 저장할 때는 반드시 STRING으로 저장해야 한다. 숫자를 사용하게 되면 나중에 컬럼이 추가되었을 때 값이 밀리는 문제가 발생할 수 있기 때문이다.

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Delivery {
    @Id
    @GeneratedValue
    @Column(name = "delivery_id")
    private Long id;

    @OneToOne(mappedBy = "delivery")
    private Order order;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; // READY, COMP
}

 

DeliveryStatus와 OrderStatus

두 enum 클래스는 각각 배송 상태와 주문 상태를 관리하며, 명확한 상태 구분을 통해 시스템에서 데이터 흐름을 제어하는 데 사용된다.

package jpabook.jpashop.domain;

public enum DeliveryStatus {
    READY, COMP
}
  • READY: 배송 준비 상태를 나타낸다. 물품이 아직 출발하지 않고 준비 중일 때 사용된다.
  • COMP: 배송 완료 상태를 나타낸다. 물품이 최종 목적지에 도착하여 배송이 완료된 상태를 의미한다.
package jpabook.jpashop.domain;

public enum OrderStatus {
    ORDER, CANCEL
}
  • ORDER: 주문이 완료된 상태를 나타낸다. 고객이 상품을 주문한 직후의 상태이다.
  • CANCEL: 주문이 취소된 상태를 나타낸다. 고객이 주문을 취소했거나 시스템에서 취소 처리가 되었을 때 사용된다.

Member 엔티티

회원 정보를 저장하며, OneToMany 관계로 주문 정보를 참조한다. 식별자로 id를 사용하며, PK 컬럼명은 member_id를 사용한다. 이는 객체 지향적인 설계에서는 타입을 통해 구분할 수 있지만, 테이블에서는 구분이 어려워 관례상 테이블명 + id 형식을 따르는 것이다.

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@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<>();

}

MemberRepository

회원 정보를 저장하고 조회하는 리포지토리이다. EntityManager를 통해 영속성 컨텍스트에 엔티티를 저장하고, 회원의 ID로 회원 정보를 조회할 수 있다.

package jpabook.jpashop.domain;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;

@Repository
public class MemberRepository {
    @PersistenceContext
    private EntityManager em;

    public Long save(Member member) {
        em.persist(member);
        return member.getId();
    }

    public Member find(Long id) {
        return em.find(Member.class, id);
    }
}

 

 

Order와 OrderItem

주문과 주문 아이템을 관리하는 엔티티이다. Order는 회원과 배달 정보를 참조하며, OneToMany 관계로 여러 주문 아이템을 가진다. OrderItem은 주문과 상품 정보를 참조하며, 각각의 주문 내역과 상품 수량, 가격을 저장한다.

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

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

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

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

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

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

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

}
package jpabook.jpashop.domain;

import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class OrderItem {

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

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

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

    private int orderPrice;

    private int count;
}

 

MemberRepositoryTest

회원을 저장하고, 저장한 회원 정보를 조회하여 일치하는지 검증하는 테스트를 작성한다. JUnit과 Spring의 테스트 기능을 사용하여 트랜잭션을 관리하고, 테스트 환경에서 실제 동작을 확인한다.

package jpabook.jpashop;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.MemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(value = false)
    public void testMember() throws Exception {
        // given
        Member member = new Member();
        member.setUserName("memberA");

        // when
        Long savedId = memberRepository.save(member);
        Member findMember = memberRepository.find(savedId);

        // then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUserName()).isEqualTo(member.getUserName());
        Assertions.assertThat(findMember).isEqualTo(member);

    }


}

 

엔티티를 설계할 때는 객체의 역할과 책임을 명확히 정의하고, 불필요한 데이터 변경을 막기 위해 Setter의 사용을 최소화하는 것이 중요하다. 데이터 변경이 필요할 때는 비즈니스 로직을 통해 이루어지도록 설계하는 것이 바람직하다.