ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA : Entity 연관 관계 / @JoinColumn
    BE/스프링 2024. 11. 4. 22:05

     

    JPA  엔티티 간의 연관 관계

    JPA에서는 객체 간의 다양한 연관관계를 설정할 수 있으며, 각 연관관계는 주인 엔티티와 외래 키(FK)의 위치에 따라 결정

     

     

    • 1:1 관계 (One-to-One): 한 엔티티가 다른 엔티티와 1:1로 매핑되는 관계
    • 1:N 관계 (One-to-Many): 한 엔티티가 여러 엔티티와 매핑되는 관계
    • N:1 관계 (Many-to-One): 여러 엔티티가 한 엔티티와 매핑되는 관계
    • N:M 관계 (Many-to-Many): 여러 엔티티가 서로 여러 엔티티와 매핑되는 관계

     

    연관관계 JPA Annotation
    1:1 @OneToOne
    1:N @OneToMany
    N:1 @ManyToOne
    N:M @ManyToMany

     

     

     


     

     

    1.  1:1 (One-to-One) 관계

     

    1:1 관계에서는 외래 키를 어느 테이블에 두어도 관계가 성립

     

    -  단방향: 한 엔티티에서만 참조하는 방식

    -  양방향: 두 엔티티가 서로 참조하는 방식으로, mappedBy를 통해 관계의 주인을 설정

     

     

    @Entity
    public class Customer {
        @Id @GeneratedValue
        private Long id;
        private String name;
    
        @OneToOne
        @JoinColumn(name = "customer_detail_id", unique = true)
        private CustomerDetail customerDetail;
    }
    
    @Entity
    public class CustomerDetail {
        @Id @GeneratedValue
        private Long id;
        private String address;
        private String phoneNumber;
    
        @OneToOne(mappedBy = "customerDetail") // 관계의 주인을 Customer로 설정
        private Customer customer;
    }

     

     

    • 각 고객은 하나의 상세 정보를 가지고, 고객 상세 정보도 하나의 고객과만 연결
    • 외래 키가 있는 Customer 엔티티가 관계의 주인
    • CustomerDetail은 mappedBy를 통해 주인이 아님을 명시
    • 1:1 관계에서는 외래 키 컬럼에 unique = true를 설정하여 중복을 방지

     

     


     

     

    2.  1:N / N:1 (One-to-Many / Many-to-One) 관계

     

     

    -  단방향: 한 엔티티만 상대방을 참조하며, 외래 키는 N쪽 엔티티에 위치

    -  양방향: 양쪽 엔티티가 서로를 참조하며, @OneToMany 쪽에서는 mappedBy로 주인을 지정

     

     

    Customer가 여러 Order를 가질 수 있으며, Order는 특정 Customer에 속하는 단방향 예제

    @Entity
    public class Customer {
        @Id @GeneratedValue
        private Long id;
    
        private String name;
    
        @OneToMany
        @JoinColumn(name = "customer_id") // FK는 Order에 위치
        private List<Order> orders = new ArrayList<>();
    }

     

     

    Customer Order의 관계를 양방향으로 설정하여 Order에서도 Customer를 참조할 수 있도록한 양방향 예제

     

    @Entity
    public class Customer {
        @Id @GeneratedValue
        private Long id;
    
        private String name;
    
        @OneToMany(mappedBy = "customer")
        private List<Order> orders = new ArrayList<>();
    }
    
    @Entity
    public class Order {
        @Id @GeneratedValue
        private Long id;
    
        private String orderNumber;
        private LocalDateTime orderDate;
    
        @ManyToOne
        @JoinColumn(name = "customer_id") // FK 설정
        private Customer customer;
    }

     

    • 한 명의 Customer가 여러 개의 Order를 생성
    • Order 엔티티가 외래 키를 가지고 있어 주인이며, Customer 엔티티에서는 mappedBy = "customer"로 참조
    • 지연 로딩(Lazy Loading): @OneToMany는 기본적으로 LAZY이며, 필요할 때만 조회
    • N+1 문제: @OneToMany 관계는 컬렉션 조회 시 추가 쿼리가 발생 가능 ->  fetch join 또는 @BatchSize 같은 최적화 기법 사용

     


     

    3.  N:M (Many-to-Many) 관계와 연결 엔티티

     

    실무에서는 중간 테이블(연결 엔티티)을 사용해 다대다 관계를 1과 N:1로 분해하는 것이 일반적

    연결 엔티티는 다대다 관계에 추가 정보 저장 가능

     

     

    Customer FoodItem 사이의 연결 엔티티 OrderItem

    @Entity
    public class OrderItem {
        @Id @GeneratedValue
        private Long id;
    
        @ManyToOne
        @JoinColumn(name = "order_id")
        private Order order;
    
        @ManyToOne
        @JoinColumn(name = "food_item_id")
        private FoodItem foodItem;
    
        private int quantity;
        private int price;
        private LocalDateTime orderDate;
    }

     

     

    • OrderItem은 Order와 FoodItem 사이의 중간 테이블 역할  -> quantity, price, orderDate와 같은 추가 정보 저장 가능
    • 다중 @ManyToOne 설정: OrderItem에서 각각의 @ManyToOne 관계를 통해 Order와 FoodItem을 연결
    @Entity
    public class Order {
        @Id @GeneratedValue
        private Long id;
    
        private String orderNumber;
        private LocalDateTime orderDate;
    
        @OneToMany(mappedBy = "order")
        private List<OrderItem> orderItems = new ArrayList<>();
    }
    
    @Entity
    public class FoodItem {
        @Id @GeneratedValue
        private Long id;
    
        private String name;
        private int price;
    
        @OneToMany(mappedBy = "foodItem")
        private List<OrderItem> orderItems = new ArrayList<>();
    }

     

     

    • 다대다 관계를 1:1과 N:1로 분해 -> Order와 FoodItem은 각각 OrderItem과 1:1 관계를 가짐
    • 연결 엔티티를 통한 관계 관리: OrderItem을 통해 구매 수량과 가격 같은 정보를 유연하게 관리 가능

     


     

     

    @JoinColumn이란?

    @JoinColumn은 외래 키(FK) 컬럼을 명시적으로 설정하기 위해 사용
    관계의 주인을 설정하며, 외래 키가 위치할 엔티티와 컬럼 이름을 지정하는 역할을 함

     

    주요 속성

    1. name: 외래 키 컬럼의 이름을 지정합니다. 기본값은 참조 필드명_대상 테이블의 기본 키 이름 형식입니다.
    2. referencedColumnName: FK가 참조할 대상 테이블의 컬럼을 지정합니다. 생략 시 기본값은 대상 테이블의 기본 키 컬럼입니다.

     


     

    연관관계별 @JoinColumn 위치와 역할

     

    1.  1:1 관계

    • 외래 키가 위치한 엔티티가 관계의 주인

     

    Customer가 CustomerDetail을 참조할 때, Customer에 @JoinColumn(name = "customer_detail_id") 설정.

     

    @Entity
    public class Customer {
        @Id @GeneratedValue
        private Long id;
    
        @OneToOne
        @JoinColumn(name = "customer_detail_id")
        private CustomerDetail customerDetail;
    }

     

     

    2.  1:N / N:1 관계

    • N:1 쪽 엔티티가 외래 키를 가지고 관계의 주인
    • 주로 @ManyToOne 쪽에 @JoinColumn을 설정하여 외래 키를 관리

     

    Order에서 Customer를 참조할 때, Order @JoinColumn(name = "customer_id") 설정.

     

    @Entity
    public class Order {
        @Id @GeneratedValue
        private Long id;
    
        @ManyToOne
        @JoinColumn(name = "customer_id")
        private Customer customer;
    }

     

     

    3.  N:M 관계

    • 중간 테이블을 생성해 다대다 관계를 1과 N:1로 풀어냄
    • 중간 테이블을 명시적으로 연결 엔티티로 생성하고, 각각의 @ManyToOne 관계에 @JoinColumn을 설정

     

    Order에서 Customer를 참조할 때, Order @JoinColumn(name = "customer_id") 설정.

     

    @Entity
    public class OrderItem {
        @Id @GeneratedValue
        private Long id;
    
        @ManyToOne
        @JoinColumn(name = "order_id")
        private Order order;
    
        @ManyToOne
        @JoinColumn(name = "food_item_id")
        private FoodItem foodItem;
    }

     

     


     

     

    연속성 전이 (Cascade)

    연속성 전이란 한 엔티티가 다른 엔티티와 연관 관계를 맺고 있을 때, 특정 작업(예: 저장, 삭제, 업데이트 등)을 수행하면 자동으로 연관된 엔티티에도 해당 작업이 전이되도록 하는 기능

    연속성 전이를 설정하면 여러 엔티티를 하나의 작업으로 관리할 수 있어 편리함

     

     

    Cascade Type 종류

    • CascadeType.PERSIST: 연관된 엔티티를 함께 저장
    • CascadeType.MERGE: 연관된 엔티티를 함께 병합(업데이트)
    • CascadeType.REMOVE: 연관된 엔티티를 함께 삭제
    • CascadeType.REFRESH: 연관된 엔티티를 새로 고침
    • CascadeType.DETACH: 연관된 엔티티의 영속성 컨텍스트를 분리
    • CascadeType.ALL: 위의 모든 전이 유형을 포함

     

    Customer를 저장할 때 해당 Customer에 연결된 Order들도 함께 저장

    @Entity
    public class Customer {
        @Id @GeneratedValue
        private Long id;
        
        private String name;
        
        @OneToMany(mappedBy = "customer", cascade = CascadeType.PERSIST)
        private List<Order> orders = new ArrayList<>();
    }
    
    @Entity
    public class Order {
        @Id @GeneratedValue
        private Long id;
    
        private String orderNumber;
        private LocalDateTime orderDate;
    
        @ManyToOne
        @JoinColumn(name = "customer_id")
        private Customer customer;
    }

     

     


     

     

    지연 로딩 (Lazy Loading)

    지연 로딩은 연관된 엔티티를 실제로 필요할 때 조회하는 방식으로, 메모리와 성능 최적화에 도움을 줌
    반대 개념으로 즉시 로딩(Eager Loading)이 있으며, 이는 엔티티를 로드할 때 연관된 모든 엔티티를 즉시 조회하는 방식

     

    지연 로딩 설정

    • @ManyToOne @OneToOne은 기본적으로 즉시 로딩(EAGER)으로 설정
    • @OneToMany @ManyToMany는 기본적으로 지연 로딩(LAZY)으로 설정

     

    지연 로딩 명시적 설정 예제

    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();

     

     

    Customer Order의 관계에서 Order를 지연 로딩하도록 설정한 예제

    ->  Customer 엔티티를 조회할 때 orders 리스트는 초기화되지 않으며, 실제로 접근하는 시점에 데이터베이스에서 조회

    Customer customer = customerRepository.findById(customerId).orElseThrow();
    // 여기서는 orders가 아직 로딩되지 않음
    List<Order> orders = customer.getOrders(); // 이 시점에 데이터베이스에서 조회

     

     

    N+1 문제

    지연 로딩은 성능 최적화에 유용하지만, 반복적인 데이터베이스 접근을 초래할 수 있음

     

    예를 들어, Customer리스트를 조회하면서 각각의 Order를 지연 로딩으로 가져오면 Customer 조회(1번) + Order 조회(N번)으로 인해 N+1 문제가 발생할 수 있음

    -> Fetch Join을 사용하거나 @BatchSize를 설정하여 최적화

     

     

    Fetch Join 사용 예제

    : Customer Order를 한 번에 조회하여 N+1 문제를 해결

    List<Customer> customers = em.createQuery("SELECT c FROM Customer c JOIN FETCH c.orders", Customer.class).getResultList();

    'BE > 스프링' 카테고리의 다른 글

    JPA Auditing  (0) 2024.11.14
    QueryDSL + Projection  (1) 2024.11.13
Designed by Tistory.