실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 : 엔티티 분석 및 설계

jkky98·2024년 9월 20일
0

Spring

목록 보기
43/77

도메인 모델, 테이블 설계

회원은 여러 상품을 주문할 수 있으며 한번 주문에 대해 여러 상품을 선택 가능하다.

여러 개의 주문에 대해서 동일한 상품이 선택될 수 있고 여러 상품이 하나의 주문에 들어갈 수 있어 주문과 상품은 N:M(다대다) 관계를 형성한다. RDB에서나 엔티티에서도 이러한 N:M 관계를 그대로 구현하는 방식은 사용하지 않는다. 따라서 중개테이블(위의 사진에서 주문상품에 해당)을 두어 1:N, N:1로 풀어내도록 한다.

실습에서 여러 경험을 해보고자 다대다 관계또한 구현해보도록 한다. 카테고리는 상위 카테고리, 하위 카테고리 등을 가질 수 있다. 전자제품이면서, 스마트폰인 상품 갤럭시S25가 존재할 수 있고 또 동일하게 아이폰17이라는 상품또한 전자제품이면서 스마트폰이다. 카테고리 요소에 해당하는 "전자제품", "스마트폰"은 전자제품이 상위 카테고리이며 스마트폰이 하위 카테고리이다.

상품(Item) 엔티티에 대해 자식 클래스 엔티티로 도서, 음반, 영화를 구분하도록 하며 단일 테이블 전략을 사용할 것이고 DTYPE 칼럼으로 구분할 것이다.

Domain:: Member

@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") // 하나의 회원이 여러 상품 주문 // 연관관계 주인 -> Order // mappedBy를 통해 읽기 전용이 되는것.
    private List<Order> orders = new ArrayList<>();
}

@Embedded

Address는 city, street, zipcode 필드를 가진 @Embeddable 클래스이다. @Embedded 애노테이션은 엔티티 클래스에 다른 클래스를 임베드(내장)할 때 사용한다. 이 기능을 통해 복합적인 속성을 가진 객체를 보다 구조적으로 표현할 수 있다. 그냥 Address에 해당하는 정보들을 Member의 필드가 아닌 따로 객체화 해서 다루고 싶을 때 이렇게 사용하면 된다.

Order 엔티티와 1대다 관계를 가진다. 양방향 연관관계이므로 연관관계의 주인을 설정해주어야 한다. 일대다, 다대일 양방향 연관관계이므로 연관관계의 주인을 정해야 한다. 이때 외래 키가 존재하는 곳을 연관관계의 주인으로 정하는 것이 좋다.

Domain:: Order


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

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

    @ManyToOne(fetch = FetchType.LAZY) // 여러 주문에 대해 하나의 멤버 가능
    @JoinColumn(name = "member_id") // FK 설정
    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);
    }
}

member 필드에 JoinColumn 애노테이션을 통해 FK 설정을 해준다. 양방향 연관관계가 형성되었다면 mappedBy를 통해 양방향 연관관계의 주인을 지정할 수 있다. 외래키가 존재하는 Order쪽에 연관관계의 주인을 형성해주기 위해서는 mappedBy를 Member쪽에 지정해주어야 한다. 이를 통해 읽기전용과 같은 필드로 변경되어 Order 테이블에서만 Member를 관리할 수 있도록 한다.

Domain:: Item


@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", fetch = FetchType.LAZY)
    private List<Category> categories = new ArrayList<>();
}
////////

@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item {

    private String artist;
    private String etc;
}

Item 엔티티는 추상 클래스이다 구현을 위한 Book, Album, Movie를 위의 Album처럼 @DiscriminatorValue("A")를 통해 dtype으로 구분될 수 있도록 구성한다.

카테고리와 다대다 관계를 구성하기 위해 위아 같이 애노테이션을 달고 카테고리를 설계한다.

Domain:: Category


@Entity
@Getter @Setter
public class  Category {

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

    private String name;

    @ManyToMany
    @JoinTable(name = "category_item",
            joinColumns = @JoinColumn(name = "category_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id")
    )
    private List<Item> items = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>();

    // 연관관계 메서드 //

    public void addChildCategory(Category child) {
        this.child.add(child);
        child.setParent(child);
    }
}

Item과 다대다 관계를 풀어내기 위해 @JoinTable을 통해 중개테이블을 구성해볼 수 있다. 그리고 상위, 하위 카테고리 구현을 위한 parent, child 컬럼을 만들어주고 내부의 FK를 걸어준다.

참고: 실무에서는 @ManyToMany 를 사용하지 말자

@ManyToMany 는 편리한 것 같지만, 중간 테이블( CATEGORY_ITEM )에 컬럼을 추가할 수 없고, 세밀하게 쿼리
를 실행하기 어렵기 때문에 실무에서 사용하기에는 한계가 있다. 중간 엔티티( CategoryItem 를 만들고 @ManyToOne , @OneToMany 로 매핑해서 사용하자. 정리하면 다대다 매핑을 일대다, 다대일 매핑으로 풀어내
서 사용하자.

엔티티 설계시 주의점

Setter

실습의 편의상 Setter를 열어놓았지만 실무에서는 닫아놓고 필요한 경우에만 메서드를 작성해서 사용하는 것이 옳다. Getter는 사용하더라도 값을 바꿀 문제는 없지만(조회에 대한 리소스 낭비만 있을 뿐이다.) Setter는 데이터 자체를 바꿔버릴 수 있으므로 심각한 사용자 에러를 낼 수 있기에 Setter를 남발해서는 안된다.

FetchType.LAZY

@XToOne 애노테이션의 경우 디폴트가 즉시로딩이다. 즉시로딩은 어떤 SQL이 실행될지 추적이 어렵다. 예로 Member를 JPQL로 selectAll 조회를 할 때 List Order를 확보하기 위해 모든 Order를 조회하는 N번의 쿼리를 날릴 것이다.

즉 연관된 엔티티가 모두 항상 딸려나오는 것이 FetchType.EAGER이다. 그러므로 모든 연관관계에 대해 LAZY 적용은 필수적이다.

컬렉션 초기화

일단 필드에서 초기화하는 것이null 문제에서 안전하다. 하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만 약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생 할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.

profile
자바집사의 거북이 수련법

0개의 댓글