Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Develog

코드스테이츠 50일차 본문

코드스테이츠

코드스테이츠 50일차

안형준 2022. 7. 6. 18:47

엔티티 간의 연관 관계 매핑

연관 관계 매핑이란?

테이블 설계, 클래스 다이어그램 설계를 통해 회원과 주문, 주문과 커피와의 관계를 도출했었던 것과 같이 엔티티 클래스 간의 관계를 만들어주는 것이 바로 연관 관계 매핑이다.

연관 관계 매핑은 참조하는 방향성을 기준으로 생각했을때 단방향 연관 관계와 양방향 연관 관계로 구분할 수 있다.

그리고, 엔티티 간에 참조할 수 있는 객체의 수에 따라서 일대다(1:N), 다대일(N:1), 다대다(N:N), 일대일(1:1)의 연관 관계로 나눌 수 있다.

 

단방향 연관 관계

Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있으므로, Order를 참조할 수 있지만

Order 클래스는 Member 클래스에 대한 참조 값이 없으므로 Order 입장에서는 Member 정보를 알 수 없다.

이처럼 한쪽 클래스만 다른 쪽 클래스의 참조 정보를 가지고 있는 관계를 단방향 연관 관계라고 한다.

 

양방향 연관 관계

Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있고, Member 클래스를 참조할 수 있다.

Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있고, Member 클래스를 참조할 수 있다.

결론적으로 두 클래스가 모두 서로의 객체를 참조할 수 있으므로, Member는 Order 정보를 알 수 있고, Order는 Member 정보를 알 수 있다.

이처럼 양쪽 클래스가 서로의 참조 정보를 가지고 있는 관계를 양방향 연관 관계라고 한다.

JPA는 단방향 연관 관계와 양방향 연관 관계를 모두 지원하는 반면에 Spring Data JDBC는 단방향 연관 관계만 지원한다.

 

일대다 단방향 연관 관계

일대다의 관계란 일(1)에 해당하는 클래스가 다(N)에 해당하는 객체를 참조할 수 있는 관계를 의미한다.

위 그림과 같이 한명의 회원이 여러 건의 주문을 할 수 있으므로 Member와 Order는 일대다 관계이다.

그리고 Member만 List 객체를 참조할 수 있으므로 단방향 관계이다.

일대다 단방향 연관 관계를 가지고 있다고 볼 수 있다.

 

테이블 간의 관계에서는 일대다 중에서 ‘다’에 해당하는 테이블에서 ‘일’에 해당하는 테이블의 기본키를 외래키로 가진다.

따라서 ORDERS 테이블이 MEMBER 테이블의 기본키인 member_id를 외래키로 가진다.

그러나 Order 클래스가 ‘테이블 관계에서 외래키에 해당하는 MEMBER 클래스의 참조값’을 가지고 있지 않기때문에 일반적인 테이블 간의 관계를 정상적으로 표현하지 못하고 있다.

따라서, Order 클래스의 정보를 테이블에 저장하더라도 외래키에 해당하는 MEMBER 클래스의 memberId 값이 없는채로 저장이된다.

이러한 문제로 일대다 단방향 매핑은 잘 사용하지 않는다.

 

다대일 연관 관계

다대일의 관계란 다(N)에 해당하는 클래스가 일(1)에 해당하는 객체를 참조할 수 있는 관계를 의미한다.

여러 건의 주문은 한 명의 회원에 속할 수 있으므로 Order와 Member는 다대일 관계이다.
그리고 Order만 Member 객체를 참조할 수 있으므로 단방향 관계이므로 다대일 단방향 연관 관계를 가지고 있다.

ORDERS 테이블이 MEMBER 테이블의 member_id를 외래키로 가지듯이 Order 클래스가 Member 객체를 외래키처럼 가지고 있다.

즉, 다대일 단방향 매핑은 테이블 간의 관계처럼 자연스러운 매핑 방식이기 때문에 JPA의 엔티티 연관 관계 중에서 가장 기본으로 사용되는 매핑 방식이다.

 

코드로 보는 다대일 연관 관계 매핑 방법

  • 다(N)에 해당하는 Order 클래스
@ManyToOne   // (1)

    @JoinColumn(name = "MEMBER_ID")  // (2)

    private Member member;



    public void addMember(Member member) {

        this.member = member;

    }



    public enum OrderStatus {

        ORDER_REQUEST(1, "주문 요청"),

        ORDER_CONFIRM(2, "주문 확정"),

        ORDER_COMPLETE(3, "주문 완료"),

        ORDER_CANCEL(4, "주문 취소");



        @Getter

        private int stepNumber;



        @Getter

        private String stepDescription;



        OrderStatus(int stepNumber, String stepDescription) {

            this.stepNumber = stepNumber;

            this.stepDescription = stepDescription;

        }

    }

먼저 (1)과 같이 @ManyToOne 애너테이션으로 다대일의 관계를 명시하고, (2)와 같이 @JoinColumn 애너테이션으로 ORDERS 테이블에서 외래키에 해당하는 컬럼명을 적어준다.

다대일 단방향 연관 관계이기 때문에 다(N)쪽에서만 설정을 해주면 매핑 작업은 끝난다.

 

다대일 매핑을 이용한 회원과 주문 정보 저장

// (1)

        em.persist(member);



        Order order = new Order();

        order.addMember(member);     // (2)

        em.persist(order);           // (3)



        tx.commit();



// (4)

        Order findOrder = em.find(Order.class, 1L);



        // (5) 주문에 해당하는 회원 정보를 가져올 수 있다.

        System.out.println("findOrder: " + findOrder.getMember().getMemberId() +

                        ", " + findOrder.getMember().getEmail());

먼저 주문을 하기 위해서는 회원 정보가 필요하다.

(1)에서 회원 정보를 저장한다.

(1)에서 저장한 회원 정보의 주문 정보를 저장하기 위해서 (2)와 같이 order 객체에 member 객체를 추가한다.

order 객체에 추가된 member 객체는 외래키의 역할을 한다.

(2)와 같이 추가되는 member 객체는 이 MEMBER_ID 같은 외래키의 역할을 한다고 생각하면 한다.

(3)에서 주문 정보를 저장한다.

(4)에서는 등록한 회원에 해당하는 주문 정보를 조회하고 있다.

(5)에서 findOrder.getMember()와 같이 주문에 해당하는 회원 정보를 가져와서 출력하고 있다.

(5)에서 findOrder.getMember().getMemberId()와 같이 객체를 통해 다른 객체의 정보를 얻을 수 있는 것을 객체 그래프 탐색이라고 한다.

이처럼 다대일 관계에서는 일(1)에 해당하는 객체의 정보를 얻을 수 있다.

 

다대일 매핑에 일대다 매핑 추가

카페 주인 입장에서는 이 주문을 누가 했는지 주문한 회원의 회원 정보를 알아야 할 경우에는 다대일 매핑을 통해 주문한 사람의 정보를 조회할 수 있어야 한다.

하지만 다대일 매핑만으로는 member 객체를 통해 내가 주문한 주문 정보인 order 객체들을 조회할 수 없다.

이 경우, 다대일 매핑이 되어 있는 상태에서 일대다 매핑을 추가해 양방향 관계를 만들어주면 된다.

  • 일대다에서 일(1)에 해당하는 Member 클래스
// (1)

    @OneToMany(mappedBy = "member")

    private List<Order> orders = new ArrayList<>();



    public Member(String email) {

        this.email = email;

    }



    public Member(String email, String name, String phone) {

        this.email = email;

        this.name = name;

        this.phone = phone;

    }



    public void addOrder(Order order) {

        orders.add(order);

    }

다대일 매핑에 일대다 매핑을 추가해서 양방향 관계를 만들어주었다.

 

(1)의 @OneToMany(mappedBy = "member")

일대다 단방향 매핑의 경우에는 mappedBy 애트리뷰트의 값이 필요하지 않다.

mappedBy 는 참조할 대상이 있어야 하는데 일대다 단방향 매핑의 경우 참조할 대상이 없기 때문이다.

 

솔직히 mappedBy 의 값으로 뭘 지정해야 되는지 알기가 어렵다.

MEMBER 테이블의 기본키 컬럼인 MEMBER_ID의 값을 지정합니다.

그렇다면 Order 클래스에서 외래키의 역할을 하는 필드는 무엇인가 생각해보자

바로 member 필드이다.

따라서 (1)에서 mappedBy 의 값이 “member”가 되는 것이다.

 

mappedBy 의 값으로 무얼 지정해야할지 도저히 모르겠다면 두 가지만 기억하자.

(1) 두 객체들 간에 외래키의 역할을 하는 필드는 무엇인가?

(2) 외래키의 역할을 하는 필드는 다(N)에 해당하는 클래스 안에 있다.

 

다대일 매핑에 일대다 매핑을 추가하여 주문 정보 조회

member.addOrder(order); // (1)

        order.addMember(member); // (2)



        em.persist(member);   // (3)

        em.persist(order);    // (4)



        tx.commit();



// (5)

        Member findMember = em.find(Member.class, 1L);



        // (6) 이제 주문한 회원의 회원 정보를 통해 주문 정보를 가져올 수 있다.

        findMember

                .getOrders()

                .stream()

                .forEach(findOrder -> {

                    System.out.println("findOrder: " +

                            findOrder.getOrderId() + ", "

                            + findOrder.getOrderStatus());

                });

    }
  • 먼저 (1)에서 member 객체에 order 객체를 추가해 준다.

ㄴmember 객체에 order 객체를 추가해주지 않아도 테이블에는 member 정보와 order 정보가 정상적으로 저장이 된다.

ㄴ하지만 member 객체에 order 객체를 추가해주지 않으면 (5)에서 find() 메서드로 조회한 member 객체로 order를 그래프 탐색하면 order 객체를 조회할 수 없다.

ㄴ이유는 바로 find() 메서드가 1차 캐시에서 member 객체를 조회하는데 (1)에서 order를 추가해주지 않으면 1차 캐시에 저장된 member 객체는 order를 당연히 가지고 있지 않기 때문이다.

  • 그리고 (2)에서 order 객체에 역시 member 객체를 추가해준다.

ㄴorder 객체에 member 객체를 추가해주는 이유는 다대일 관계에서 보았듯이 member가 order의 외래키 역할을 하기 때문에 order 객체 저장 시, 반드시 필요하다.

ㄴ만약에 order 객체에 member 객체를 추가해주지 않으면 ORDERS 테이블에 저장된 주문 정보의 MEMBER_ID 필드가 null이 될 것이다.

ㄴ즉 외래키로 참조할 객체 정보(member)가 없기 때문이다.

  • (3)에서 회원 정보를 저장하고, (4)에서 주문 정보를 저장한다.
  • (5)에서 방금 저장한 회원 정보를 1차 캐시에서 조회한다.
  • 일대다 양방향 관계를 매핑했기 때문에 (6)과 같이 find() 메서드로 조회한 member로부터 객체 그래프 탐색을 통해 List 정보에 접근할 수 있다.

 

다대다 연관 관계

실무에서는 다대다의 관계를 가지는 테이블을 설계하는 경우도 굉장히 많다.

방금 살펴봤던 커피 주문 샘플 애플리케이션에서도 주문(Order)과 커피(Coffee)의 관계는 다대다 관계이다.

하나의 주문에 여러개의 커피가 속할 수 있고, 하나의 커피는 여러 주문에 속할 수 있으니 다대다 관계인 것이다.

 

그렇다면 JPA에서 다대다에 해당하는 엔티티 클래스는 어떻게 매핑해야 할까?

테이블 설계 시, 다대다의 관계는 중간에 테이블을 하나 추가해서 두 개의 일대다 관계를 만들어주는 것이 일반적인 방법이다.

다대다 관계의 테이블을 두 개의 1대다 관계로 설계한 예

ORDER_COFFEE 테이블은 ORDERS 테이블의 외래키와 COFFEE 테이블의 외래키를 가지고 있다.

테이블 설계가 되었으니 이제 클래스 간의 연관 관계 매핑을 하면 된다.

 

일대일 연관관계

일대일 연관 관계 매핑은 다대일 단방향 연관 관계 매핑과 매핑 방법은 동일하다.

단지 @ManyToOne 애너테이션이 아닌 @OneToOne 애너테이션을 사용한다는 차이만 있다.

일대일 단방향 매핑에 양방향 매핑을 추가하는 방법도 다대일에 일대다 매핑을 추가하는 방식과 동일하다.

단, 역시 @ManyToOne 애너테이션이 아닌 @OneToOne 애너테이션을 사용한다.

 

엔티티 간의 연관 관계 매핑 권장 방법

  • 일대다 매핑은 사용하지 않는다.
  • 제일 먼저 다대일 단방향 매핑부터 적용한다.
  • 다대일 단방향 매핑을 통해 객체 그래프 탐색으로 조회할 수 없는 정보가 있을 경우, 그때 비로소 양방향 매핑을 적용한다.

 

총정리

  • Spring Data JDBC는 엔티티 간에 단방향 매핑만 지원하지만 JPA는 단방향과 양방향 매핑을 모두 지원한다.
  • JPA는 엔티티 간에 일대다, 다대일, 다대다, 일대일 연관 관계 매핑을 지원한다.
  • 일대다 관계는 외래키를 가지고 있어야 할 엔티티에 외래키 역할을 하는 객체 참조가 없기때문에 가급적 사용하지 않는 것이 좋다.
  • 다대일 매핑(@ManyToOne)은 다대일에서 ‘다’에 해당하는 엔티티에서 사용한다.
  • 일대다(@OneToMany) 양방향 매핑은 다대일에서 ‘일’에 해당하는 엔티티에서 사용한다.
  • 다대다 연관 관계 매핑은 두 개의 다대일 단방향 매핑을 적용하고, 필요한 경우 양방향 매핑을 적용한다.
  • 일대일 연관 관계 매핑 방식은 @OneToOne 애너테이션을 사용한다는 것 외에 @ManyToOne 단방향 방식, 양방향 방식과 동일하다.

'코드스테이츠' 카테고리의 다른 글

코드스테이츠 52일차  (0) 2022.07.09
코드스테이츠 51일차  (0) 2022.07.07
코드스테이츠 48-49일차  (0) 2022.07.04
코드스테이츠 47일차  (0) 2022.07.03
코드스테이츠 46일차  (0) 2022.06.30