연관관계 매핑

김하영·2023년 6월 18일

출처: 스프링 부트 핵심 가이드 - 장정우 지음
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=296591989

설계가 복잡해지면 각 도메인에 맞는 테이블을 설계하고 연관관계를 설정해서 Join등의 기능을 활용하게 된다.
JPA를 사용하는 어플리케이션에서도 테이블의 연관관계엔티티의 연관관계로 표현할 수 있다!
다만 객체와 테이블은 성질이 다르기 때문에 정확한 연관관계를 표현할 수는 없다.

연관관계 매핑 종류와 방향

어떤 엔티티를 중심으로 연관 엔티티를 보느냐에 따라 연관관계의 상태가 달라진다.

  • One to One (일대일)

  • One to Many (일대다)

  • Many to One (다대일)

  • Many to Many (다대다)

  • 예시 )
    공급입장에서는 여러 상품을 남품할 수 있으므로 → 일대다
    상품입장에서는 하나의 공급업체에 속하므로 → 다대일

    데이터베이스와 JPA의 차이

  • 데이터베이스 :

    • 두 테이블의 연관관계를 설정하면 외래키를 통해 서로 조인해서 참조하는 구조로 생성
  • JPA를 사용하는 객체지향 모델링 :

    • 엔티티간 참조방향을 설정할 수 있음
    • 데이터 베이스와 관계를 일치시키기 위해 양방향 설정해도 무관
    • 비즈니스 로직관점에서 단방향 관계로 설정해도 해결되는 경우가 많음!
    • 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
    • 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식

    JPA: 객체에서 양방향 개념은 양쪽에서 단방향으로 서로 매핑하는 것

  • 연관관계가 설정되면 한테이블에서 다른테이블의 기본값을 외래키로 가짐

    • 이러한 관계에서는 주인(Owner)라는 개념이 사용됨
    • 주인 : 외래키를 가진 테이블이 관계의 주인, 주인은 외래키를 사용가능, but 상대 엔티티는 읽는 작업만 수행가능

일대일 매핑

하나의 상픔에 하나의 상품정보만 매핑되는 구조

  • 상품 정보 엔티티에서 상품필드에 @OneToOne어노테이션과 @JoinColumn(name = "product_number") 어노테이션을 붙인다.

  • @OneToOne :

    • 다른 엔티티 객체(여기서는 상품)를 필드로 정의 했을때, 일대일 연관관계로 매핑하기위해 사용

    • 조회 시 Hibernate구문을 보면 상품과 상품 정보 left join이 사용됨

    • 엔티티 조회 시, 연관된 엔티티도 함께 조회(즉시 로딩)

    • 그 이유는 @OneToOne 의 기본 fetch() 전략이 EAGER 즉, 즉시 로딩이기 때문

    • @OneToOneoptional() 메소드 : 기본값은 true로, 매핑되는 값이 nullable이라는 뜻이다.

      • null 허용을 안하고 싶다면 상품정보 엔티티에서 상품 필드에 @OneToOne(optional = false)를 추가
        • 이때는 Hibernate에서 쿼리가 left join이 아니라 inner join!
  • @JoinColumn(name = "product_number") :

    • 매핑할 외래키 설정

    • 기본값이 설정되어 있어 자동으로 이름을 매핑하지만 name 속성을 사용해 원하는 칼럼명을 지정하는 것이 좋다.

    • @JoinColumn을 선언하지 않으면 엔티티를 매핑하는 중간 테이블이 생가면서 관리 포인트가 늘어나 좋지 않음!!!

    • 속성 :

      • name: 매핑할 외래키의 이름 설정
      • referencedColumnName: 외래키가 참조할 상대 테이블의 칼럼명 지정
      • foreignKey: 외래키를 생성하며 지정할 제약조건 설정(unique, nullable, insertable, updatable 등)

일대일 양방향 매핑


JPA의 일대일 양방향 매핑에서 주인 개념 적용하기 - @OneToOne(mappedBy="")

앞서 상품정보 엔티티에 @OneToOne, @JoinColumn 어노테이션을 붙여주었으므로 상대 엔티티인 상품 엔티티의 상품정보 필드에 @OneToOne를 붙여준다.

  • 여러 테이블끼리 연관관계가 설정 되어 있어 여러 left outer join이 설정되는 것은 괜찮지만, 이 경우처럼 양쪽에서 외래키를 가지고 left outer join이 두번 수행 되는 경우에는 효율성이 떨어지게 된다.
  • 실제 DB에서는 주인(Owner)개념이 적용되어 테이블 간 연관관계를 맺으면 한쪽 테이블이 외래키를 갖는 구조로 이루어짐!
  • 따라서 JPA에서도 실제 데이터베이스의 연관관계를 반영하여 한쪽 테이블에서만 외래키를 바꿀 수 있도록 정하는 것이 좋음!

    mappedBy : 어떤 객체가 주인인지 표시하는 속성!!
    엔티티는 양방향으로 매핑하되 한쪽에게만 외래키를 주기

  • mappedBy에 들어가는 값은 연관관계를 갖고 있는상대 엔티티에 있는 연관관계 필드의 이름이 됨
    • 상품 엔티티의 상품정보 필드에 붙은 어노테이션을 @OneToOne(mappedBy="상품")으로 바꿔준다
    • 상품 정보가 상품 엔티티의 주인이 됨!

    JPA의 일대일 양방향 매핑에서 순환참조 제거하기

  • 두 엔티티를 일대일 양방향 매핑하고 주인을 정해주었다면, test코드에서 toString을 실행하는 시점에서 StackOverFlowError가 발생한다.
  • 양방향으로 연관관계가 설정되면, toString을 사용할때 순환참조 발생!!

    따라서 필요한 경우가 아니라면, 대체로 단방향으로 연관관계 설정!!
    양방향 설정이 필요한 경우, 순환참조 제거를 위해 exclude를 사용하여 toString에서 제외 설정 필요!

    • @ToString.Exclude 어노테이션을 상품 엔티티의 상품 정보 필드에 붙임!

다대일, 일대다 매핑

다대일 단방향 매핑

  • 일반적으로 외래키를 갖는 쪽이 주인!
  • 상품 엔티티의 공급업체 필드에 @ManyToOne어노테이션 추가!
  • 외래키를 설정해주고, join 하는 컬럼의 이름을 지정해 주기 위해 @JoinColumn(name="provider_id") 어노테이션 추가!
  • 순환참조를 막기위해 @ToString.Exclude어노테이션 추가!
  • 상품 엔티티에서 단방향으로 공급업체 엔티티 연관관계를 맺고 있으므로 상품 레포지토리만으로 공급업체 객체 조회 가능!

다대일 양방향 매핑

  • 반대로 공급업체를 통해 상품을 조회하기 위해 일대다 연관관계 설정

  • 공급업체 엔티티의 상품필드에 @OneToMany 어노테이션 추가

    • @OneToMany의 기본 fetch전략은 lazy이다.
    • 지연 로딩 방식을 사용하면 no session에러가 발생할 겅우 EAGER로 fetch 전략 변경해준다.

      지연로딩과 즉시 로딩
      엔티티라는 객체의 개념으로 데이터베이스를 구현했으므로 연관관계를 가진 각 엔티티 클래스에는 연관관계가 있는 객체들이 필드에 존재하게 됨!
      지연로딩: 연관관계와 상관없이 즉각 해당 엔티티의 값만 조회하고 싶을때
      즉시로딩: 연관관계를 가진 테이블의 값도 조회하고 싶을때

  • 순환참조 방지를 위해 @ToString.Exclude 어노테이션 추가

  • 일대다 연관관계의 경우 여러 엔티티가 포함될 수 있으므로 컬랙션(Collections, List, Map) 형식으로 필드생성

  • 주인인 상품엔티티에 외래키 관리 위임을 위해 @OneToMany(mappedBy="provider) 설정

  • 공급업체 엔티티는 주인이 아니라서 외래키 관리 불가!

    • 따라서 공급업체를 등록한 후 각 상품 객체에 설정하는 작업을 통해 데이터 베이스에 저장

    • 공급업체 엔티티에서 정의한 productList필드에 상품 객체를 추가하는 방식으로 데이터 베이스에 레코드를 저장하게 되면 해당 데이터는 DB에 반영 안됨!

      주인은 상품 엔티티이다.
      따라서 공급업체 엔티티 클래스를 수정해도 어플리케이션을 가동해보면 칼럼은 변경되지 않는다.
      즉, mappedBy로 설정된 필드는 칼럼에 적용되지 않는다!
      양쪽에서 연관관계를 설정하고 있을때, RDBMS 형식처럼 사용하기 위해 mappedBy를 통해 외래키 관리 위임~!!!

일대다 단방향 매핑

  • 일대다 양방향 매핑에서는 어느 엔티티 클래스도 연관관계의 주인이 될 수 없기 때문에 다루지 않겠음!

  • 상품 분류 엔티티에서 @OneToMany, @JoinColumn을 사용하면 상품 엔티티에서 별도의 설정을 하지 않아도 일대다 단방향 연관관계 매핑 됨.

    @JoinColumn은 앞에서 말했다시피 필수가 아님! 이 어노테이션을 사용하지 않으면 중간 테이블로 Join테이블이 생성되는 전략이 채택됨.

  • 일대다 단방향 관계의 단점
    • 매핑의 주체가 아닌 반대테이블에 외래키가 추가 됨
    • 다대일 구조와 다르게 외래키를 설정하기 위해 다른 테이블에 대한 update쿼리를 발생시킴
  • 이 문제를 해결하기 위해 일대다 양방향 연관관계를 사용하기 보다 다대일 연관관계를 사용하는 것이 좋음!

    일대다 단방향 관계에서는 다대일 구조와 다르게 외래키를 설정하기 위해 다른 테이블에 대한 update쿼리를 발생시킴
    따라서 일대다 양방향 연관관계를 사용하기 보다 다대일 연관관계를 사용하는 것이 좋음!!

다대다 매핑

  • 다대다 연관관계 매핑은 실무에서 거의 사용되지 않는 구성
  • 다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어진다.
  • 이 경우 교차엔티티라고 부르는 중간테이블을 생성해서 다대다 관계를 일대다 또는 다대일로 해소한다.

다대다 단방향 매핑

  • `@ManyToMany어노테이션 사용
  • 리스트로 필드를 가지는 객체에서는 외래키를 가지지 않기 때문에 별도의 @JoinColumn 설정할 필요 ❌
  • 생산업체 테이블에는 별도의 외래키가 추가 되지 않음, 데이터베이스에 추가로 중간 테이블이 설정됨!! @JoinTable(name="")으로 중간테이블이름을 정해줄 수 있음
  • 중간 테이블에서는 상품테이블과 상산업체테이블에서 id값을 가져와 두개의 외래키가 설정됨

다대다 양방향 매핑

  • 상품엔티티에도 @ManyToMany어노테이션 사용

  • 필요에 따라 mappedBy속성으로 주인 설정

  • 이렇게 설정하고 어플리케이션을 실행해도 데이터베이스의 테이블 구조는 변경 ❌

  • 중간테이블이 연관관계를 설정하고 있기 때문!!!

  • 다대다 연관관계를 설정하면 중간 테이블을 통해 연관된 엔티티의 값을 가져옴

  • 그러나 다대다 연관관계에서는 중간테이블이 생성되기 때문에 예기치 못한 쿼리가 생길 수 있음!!

  • 즉, 관리가 힘든 포인트가 발생하는 문제점!!

  • 해결: 중간 테이블 생성 대신 일대다 다대일로 연관관계를 맺을 수 있는 중간 테이블로 승격시켜 JPA에서 관리할 수 있도록 생성하자!

    양방향으로 연관관계가 설정되면, toString을 사용할때 순환참조 발생!!
    따라서 필요한 경우가 아니라면 대체로 단방향으로 연관관계 설정!!
    순환참조 제거를 위해 exclude를 사용하여 toString에서 제외 설정 필요!- @ToString.Exclude 를 한 엔티티의 필드에 붙여줌

영속성 전이

  • 특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것
  • 연관관계 여노테이션을 보면 cascade()라는 요소를 볼 수 있음
  • 이 요소는 영속성 전이를 설정하는데 사용됨!
  • 한 엔티티가casecad 요소의 값으로 주어진 영속상태의 변경이 일어나면 매핑으로 연관된 엔티티에도 동일한 동작이 일어나도록 전이 발생시킴
  • 사용 예: @OneToMany(mappedBy="provider", cascade = CascadeType.PERSIST)

영속성 전이 타입

  • ALL : 모든 영속 상태 변경에 대해 영속성 전이 적용
  • PERSIST : 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화
  • MERGE : 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
  • REMOVE : 엔티티를 제거할때 연관된 엔티티도 제거
  • REFRESH : 엔티티를 새로고침할 때 연관된 엔티티도 새로고침
  • DETACH : 엔티티를 영속성 컨텍스트에서 제외하며 연관된 엔티티도 제외

고아 객체

  • JPA에서 고아(orphan)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미한다.
  • JPA에는 이러한 고아 객체를 자동으로 제거하는 기능 존재
  • 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 이 기능은 사용하지 않는 것이 좋음!
  • 사용 예: @OneToMany(mappedBy="provider", cascade = CascadeType.PERSIST, orphanRemoval true)
profile
백엔드 개발자로 일하고 싶어요 제발

0개의 댓글