티스토리 뷰

728x90
반응형

 

 

 

지난 글에선 JPA API를 활용해 Entity ↔ Table 간의 매핑을 살펴보았다.

 

2022.08.31 - [개발/Spring] - [Spring]JPA - Entity ↔ DB Table Mapping

 

[Spring]JPA - Entity ↔ DB Table Mapping

지난 글에서 결국 JPA의 목적은 Object ↔ DB Table 매핑 (설계) 영속성 컨텍스트 (JPA 내부 동작) 라고 정리하고 영속성 컨텍스트에 대해 알아봤었다. 2022.08.31 - [개발/Spring] - [Spring]JPA(Java Persist..

gnidinger.tistory.com

이번 글에선 조금 더 까다로운 엔티티 간의 매핑에 대해서 알아보자.

 

 

Entity ↔ Entity Mapping

 

JPA에서 엔티티 간의 매핑은 가장 중요하면서도 까다로운 부분 중 하나라고 한다.

 

그 이유는 외래 키로 직관적인 관계를 맺는 테이블과 다르게 엔티티는 객체 참조를 통해 관계를 맺기 때문이며,

 

테이블은 외래 키 하나로 양방향의 연관관계를 갖지만 객체의 참조를 통한 엔티티의 연관관계는 항상 단방향이기 때문이다.

 

아래에서 알아볼 엔티티의 양방향 연관관계도 사실 단방향 연관관계를 중첩한 것에 불과하다.

 

차근차근 하나씩 살펴보자.

 

시작하기 전에 연관관계 매핑은 크게 세 가지 고려사항이 있는데, 각각 아래와 같다.

 

  • 방향성

    • 단방향
    • 양방향
  • 다중성 - 연관관계의 주인을 푸른색으로 표시

    • 대다(1 : N)
    • 대일(N : 1)
    • 대다(N : N)
    • 대일(1 : 1)
  • 연관관계의 주인 - 연관관계를 맺은 두 엔티티 중 외래 키를 관리하는 엔티티

 

단방향 연관관계

 

위 그림에선 Member 클래스가 Order 객체를 원소로 가진 List<Order> 객체를 가지고 있으므로 Order를 참조할 수 있다.

 

따라서 Member는 Order의 정보에 접근할 수 있지만 Order는 Member의 정보에 접근하지 못한다.

 

위 그림은 상황이 반대인데, Order는 Member의 정보에 접근할 수 있지만 Member는 Order의 정보에 접근하지 못한다.

 

이와 같이 한쪽 클래스가 다른 쪽 클래스의 참조 정보를 일방적으로 가진 관계를 단방향 연관관계라 한다.

 

양방향 연관관계

 

양방향 연관관계는 두 개의 단방향 연관관계를 합쳤다고 생각하면 된다.

 

Member와 Order 클래스가 모두 서로의 정보에 접근할 수 있는 참조 객체를 가지고 있다.

 

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

 

대다(1 : N) 단방향 연관관계

 

일대다(1 : N) 관계란 일(1)에 해당하는 클래스가 다(N)에 해당하는 객체를 참조하는 것을 말한다.

 

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

 

또한 Member만 List<> 객체를 참조할 수 있으므로 단방향 관계라고 할 수 있다.

 

일대다 단방향 관계는 실제로는 잘 사용되지 않는데

 

ORDERS 테이블은 MEMBER 테이블의 기본키인 member_id를 외래 키로 가지지만

 

Order 클래스는 Member 클래스의 참조값을 가지지 못해 memberId 값이 없는 채로 정보가 저장되기 때문이다.

 

추가로 특이하게도 외래 키는 무조건 다(ORDER) 쪽에 존재하지만, 관리를 일(MEMBER)에서 하게 된다.

 

대일(N : 1) 단방향 → 양방향 연관관계

 

다대일(N : 1) 단방향 연관관계는 다(N)에 해당하는 클래스가 일(1)에 해당하는 객체를 참조하는 관계이다.

 

위에서 본 것과 같이 한 명의 회원이 여러 건의 주문을 가질 수 있으므로 Order와 Member는 다대일이며,

 

Order만 Member를 참조할 수 있으므로 단방향이라고 할 수 있다.

 

데이터베이스는 무조건 다(N) 쪽이 외래 키를 가지기 때문

 

ORDERS 테이블이 MEMBER 테이블의 member_id를 외래 키로 가지며,

 

이와 매치되도록 Order 클래스가 Member 객체를 가지고 있는 것을 확인할 수 있다.

 

이런 자연스러움 때문에 JPA에서는 다대일 연관관계를 기본으로 사용한다.

 

추가로, 다대일 매핑이 되어 있는 상태에서 일대다 매핑을 추가하면 손쉽게 양방향 연관관계를 얻을 수 있다.

 

코드로 확인하자.

 

다대일 단방향 연관 관계 매핑

 

먼저 지난 글에 작성했던 Order 클래스에 매핑 코드를 추가한다.

...

@Entity(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    ...

    @ManyToOne // 다대일 관계 명시
    @JoinColumn(name = "MEMBER_ID") // ORDER TABLE의 Foreign Key 컬럼명
    private Member member;

    public void addMember(Member member) {
        this.member = member;
    }

    public enum OrderStatus {
        ...
    }
}
  • @ManyToOne - 다대일 연관관계 명시
  • @JoinColumn - Join 할 컬럼명(외래 키) 지정

단방향 연관관계이기 때문에 위 코드만 추가하면 간단하게 매핑은 끝난다.

 

다대일 단방향 연관관계를 이용한 정보 저장 및 조회

 

Config 클래스를 우리 목적에 맞게 수정하자.

package com.gnidinger.many_to_one;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

@Configuration
public class JpaManyToOneUniDirectionConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            mappingManyToOneUniDirection();
        };
    }

    private void mappingManyToOneUniDirection() {
        tx.begin();

        // 회원 정보 생성
        Member member = new Member("gni@gmail.com", "gni", "010-1234-5678");

        em.persist(member); // 회원 정보 저장

        Order order = new Order();
        order.addMember(member); // addMember()를 이용해 member 객체(외래키 역할) 추가

        em.persist(order); // 주문 정보 저장

        tx.commit();

        Order findOrder = em.find(Order.class, 1L); // 해당 회원의 주문 정보 조회

        // 주문 정보 출력
        System.out.println("findOrder: " + findOrder.getMember().getMemberId() + ", "
                + findOrder.getMember().getEmail());
    }
}

// 출력 결과
findOrder: 1, gni@gmail.com

 

다대일 단방향 관계에서 일에 해당하는 정보를 쉽게 얻을 수 있음을 확인할 수 있다.

 

하지만 위와 같은 상황에선 회원은 자신의 주문정보를 확인하지 못하게 된다.

 

즉, 다대일 매핑에 일대다 매핑을 추가해 양방향 관계를 만들 필요가 생긴다.

 

다대일 매핑에 일대다 매핑 추가(단방향 → 양방향)

 

...

public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    ...

    @OneToMany(mappedBy = "member") // 추가
    private List<Order> orders = new ArrayList<>();
    
    public void addOrder(Order order) { // 추가
        orders.add(order);
    }

    public Member(String email) {
        ...
    }
}

Member 클래스를 간단하게 수정했다.

 

조금 헷갈리는 부분은 @OneToMany의 속성에 'order'가 아닌 'member'를 추가하는 것인데,

 

단순하게 외래 키 역할을 하는 필드를 적는다고 기억하면 쉽다.

 

참고로 단방향 일대다 연관관계의 경우 @OneToMany의 속성 지정이 필요하지 않다.

 

다대일 + 일대다 양방향 연관관계를 이용한 정보 저장 및 조회

 

package com.gnidinger.many_to_one;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

@Configuration
public class JpaManyToOneBiDirectionConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            mappingManyToOneBiDirection();
        };
    }

    private void mappingManyToOneBiDirection() {
        tx.begin();

        // 회원 정보 생성
        Member member = new Member("gni@gmail.com", "gni", "010-1234-5678");

        Order order = new Order();

        member.addOrder(order); // addOrder()를 이용해 order 객체 추가 -> 조회를 위해
        order.addMember(member); // addMember()를 이용해 member 객체(외래키 역할) 추가

        em.persist(member); // 회원 정보 저장
        em.persist(order); // 주문 정보 저장

        tx.commit();

        Member findMember = em.find(Member.class, 1L); // 해당 회원의 주문 정보 조회

        findMember.getOrders().stream() // member로부터 order 정보 출력
                .forEach(findOrder -> {
                    System.out.println("findOrder: " + findOrder.getOrderId() + ", "
                            + findOrder.getOrderStatus());
                });
    }
}

// 출력 결과
findOrder: 1, ORDER_REQUEST

Config 클래스를 수정한 뒤, 조회한 member로부터 order 정보를 출력하는 것을 확인할 수 있다.

 

참고로 addOrder(order)를 이용해 order 객체를 추가하지 않으면 조회가 불가능하다.

 

대다(N : N) 연관관계

 

Spring Data JDBC를 다루던 글에서 말했듯이, 다대다 관계는 두 개의 다대일 관계로 나눠서 적용한다.

 

2022.08.29 - [개발/Spring] - [Spring]Spring Data JDBC - 도메인 엔티티&테이블 설계

 

[Spring]Spring Data JDBC - 도메인 엔티티&테이블 설계

지난 글에선 JDBC, ORM, Spring Data JDBC에 대해 살펴보았다. 2022.08.26 - [개발/Spring] - [Spring]JDBC(Java DataBase Connectivity) 2022.08.26 - [개발/Spring] - [Spring]SQL Mapper vs. ORM 2022.08.26 -..

gnidinger.tistory.com

두 개의 일대대 관계가 아닌 두 개의 다대일 관계인 이유는 위에서 설명했다.

 

첨부한 글에서도 예를 들었듯이 주문(Order)과 커피(Coffee)의 관계가 대표적인 다대다 관계이다.

 

이런 경우, 두 개의 테이블 사이에 중간 테이블을 만드는 것으로 문제를 해결한다.

 

이제 ORDER_COFFEE 테이블에 매칭 되는 orderCoffee 클래스를 생성하고,

 

두 개의 다대일 연관관계를 만들어주면 된다.

 

대일(1  : 1) 연관관계

 

일대일 연관관계는 @OneToOne 애너테이션을 사용한다는 것을 제외하면 다대일 매핑과 동일하다.

 

양방향 연관관계 역시 다대일에서 했던 방법에 애너테이션만 추가하면 된다.

 

요약

 

  • 테이블은 외래 키 하나로 양방향의 연관관계를 갖는다.
  • 객체의 참조를 통한 엔티티의 연관관계는 항상 단방향이다.
  • 엔티티의 양방향 관계는 단방향의 중첩으로 구현한다. 즉, @ManyToMany는 사용하지 않는다.
  • 일대다 연관관계 역시 단독으로 사용하지 않는다.
  • 외래 키를 관리하는 엔티티를 연관관계의 주인이라고 하며, 모든 매핑을 외래 키를 중심으로 한다.
  • 다대일 단방향 매핑을 기준으로 설계 후 필요하다면 일대다 매핑을 중첩해 양방향으로 만든다.
  • 일대일 연관관계는 @OneToOne을 사용하는 것을 제외하면 다대일과 동일하다.
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함