JPA) 연관관계 매핑 - 다양한 연관관계

Date:    Updated:

카테고리:

연관관계 매핑시 고려사항

  • @JoinColumn
    • 속성 설명 기본값  
      name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명  
      referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본키 컬럼명  
      foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.    
      unique, nullable, insertable, updatable, columnDefinition, table @Column의 속성과 같다.    
  • 다중성
    • 1:N(@OneToMany)
    • N:1(@ManyToOne)
    • 1:1(@OneToOne)
    • N:M(@ManyToMany)
      • 권장하지 않음.
  • 단방향, 양방향
    • 테이블
      • 외래키 하나로 양쪽 조인이 가능하다.
      • 방향이라는 개념 X
    • 객체
      • 참조용 필드가 있는 쪽으로만 참조 가능
      • 한쪽만 참조하면 단방향
      • 양쪽이 서로 참조하면 양방향
  • 연관관계의 주인
    • 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음.
    • 객체 양방향 관계는 A -> B, B -> A 처럼 참조가 2군데임.
    • 객체 양방향 관계는 참조가 2군데있음. 둘중 테이블의 외래키를 관리할 곳을 지정해야함.
    • 연관관계의 주인 : 외래키를 관리하는 참조
    • 주인의 반대편 : 외래키에 영향을 주지않고 단순 조회만 (read-only)

다대일 (N:1)

다대일의 관계에서 다(N)를 연관관계의 주인으로 설정한다.

  • @ManyToOne
    • 속성 설명 기본값
      optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. TRUE
      fetch 글로벌 페치 전략을 설정한다. @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
      cascade 영속성 전이 기능을 사용한다.  
      targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  
  • 다대일 단방향
    • image
    • 설계 의도
      • Member가 속한 Team을 추가하거나 수정하고 싶을 때
    • DB 입장에선 Member와 Team의 관계가 N:1이므로 외래키가 Member쪽에 속한다.
      • N쪽에 항상 외래키가 존재해야함.
    • 객체 입장에선 외래키가 존재하는 엔티티에 참조용 필드를 만들어서 연관관계를 설정하면 된다.
      @Entity
      public class Member {
        ...
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
      }
      
  • 다대일 양방향
    • image
    • 설계 의도
      • Member 입장에서 Member가 속한 Team도 추가하거나 수정할 수 있고
      • Team 입장에서 Team에 속한 Member들을 조회해야 할 때
    • 연관관계 주인으로 설정된 반대편에 참조용 객체를 생성하여 이어주면 된다.
      @Entity
      public class Team {
        ...
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
      }
      
    • 반대쪽에 참조용 객체를 추가한다고 해도 테이블에 영향을 주지않는다.
      • 어차피 read-only 이므로
    • 외래키가 있는 쪽이 연관관계의 주인이다.
    • 양쪽을 서로 참조하도록 개발한다.

가장 많이 사용하는 연관관계이며 다대일의 반대는 일대다 이다.

일대다 (1:N)

일대다의 관계에서 일(1)을 연관관계 주인으로 설정할 수 있다.

  • @OneToMany
    • 속성 설명 기본값
      mappedBy 연관관계의 주인 필드를 선택한다.  
      fetch 글로벌 페치 전략을 설정한다. @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
      cascade 영속성 전이 기능을 사용한다.  
      targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  
  • 일대다 단방향
    • image
    • 설계 의도
      • TeamTeam에 속한 Member들을 추가하거나 수정하고 싶을 때
    • 1의 입장인 Team에서 연관관계의 주인을 관리
      @Entity
      public class Team {
        ...
        @OneToMany
        @JoinColumn(name = "TEAM_ID")
        private List<Member> members = new ArrayList<>();
      }
      
      ...
      // 예제
      Member member = new Member();
      member.setUsername("member1");
      
      em.persist(member);
      
      Team team = new Team();
      team.setName("teamA");
      team.getMembers().add(member);  // 연관관계 주인 활용
      
      em.persist(team);
      
    • 일반적으로 수행되는 insert 쿼리에 추가로 update 쿼리가 실행 된다.
      • team 엔티티를 저장했지만 연관 관계는 반대편(Member) 테이블을 가리키고 있으므로 반대편 테이블의 update 쿼리가 한번 더 실행됨
      • 의도하지 않은 쿼리가 수행되므로 복잡도가 올라가며 및 성능상 좋지 않다.
    • 테이블 일대다 관계는 항상 다(N) 쪽에 외래키가 있음.
    • 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조가 된다.
    • @JoinColumn을 사용하지 않으면 조인 테이블 방식을 사용하므로 @JoinColumn을 꼭 써야한다.
      • 조인 테이블 방식 : 연관관계를 위해 두 테이블 사이에 중간 테이블을 생성하여 매핑한다 (좋지 않음)
    • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자!
  • 일대다 양방향
    • image
    • 설계 의도
      • TeamTeam에 속한 Member들을 추가하거나 수정하고 싶고
      • MemberMember가 속한 팀을 조회하고 싶을 때
    • 공식 스펙상으로 지원하지 않으나 야매 방식(insertable, updatable 속성을 사용하여 읽기전용 필드로 만듬)으로 처리할 수 있음
      @Entity
      public class Member {
        ...
        @ManyToOne
        @JoinColumn(name="TEAM_ID", insertable=false, updatable=false)
        private Team team;
      }
      
    • @JoinColumn의 속성으로 해당 필드를 수정하지 못하게 만듦으로 써 read-only로 만듬 (양방향 매핑 구현)
    • 복잡하게 하지말고 깔끔하게 다대일 양방향을 사용하자.

일대일 (1:1)

  • 주 테이블이나 대상 테이블 중에 외래키 선택 가능하다.
    • 주 테이블에 외래키
    • 대상 테이블에 외래키
  • 외래키에 데이터베이스 유니크 제약조건을 추가해야 함.

  • 주 테이블에 외래키 - 단방향
    • image
    • 설계 의도
      • Member(주 테이블)는 락커를 하나 소유할 수 있다.
      • 다대일(@ManyToOne) 단방향 매핑과 유사함
        @Entity
        public class Locker {
          @Id @GeneratedValue
          private Lond id;
        
          private String name;
        }
        
        @Entity 
        public class Member {
        
          @Id @GeneratedValue
          private Long id;
        
          @OneToOne
          @JoinColumn(name = "LOCKER_ID")
          private Locker locker;
        
          private String username;
        }
        
    • 추후에 하나의 Locker를 여러명의 Member가 사용할 수 있도록 (다대일) 관계를 확장할 수 있다. (DB 입장)
    • MemberLocker가 있는게 성능상으로 유리하다
      • ex) Member가 주 테이블이기 때문에 Member 조회는 필수적이다. Member가 가진 Locker 정보를 확인하기 위해 굳이 Locker를 대상으로 한번 더 조회하지 않아도 된다.
  • 주 테이블에 외래키 - 양방향
    • image
    • 설계 의도
      • Member는 락커를 하나 소유할 수 있고
      • Locker 는 자신을 소유한 Member를 알수있음.
          @Entity
          public class Locker {
            @Id @GeneratedValue
            private Lond id;
        
            private String name;
        
            @OneToOne(mappedBy= "locker")
            private Member member;
        
          }
        
          @Entity 
          public class Member {
        
            @Id @GeneratedValue
            private Long id;
        
            @OneToOne
            @JoinColumn(name = "LOCKER_ID")
            private Locker locker;
        
            private String username;
          }
        
    • 다대일 양방향 매핑처럼 외래키가 있는곳이 연관관계의 주인
      • 예제에선 Member가 주인
    • 반대편은 mappedBy를 적용
  • 주 테이블 외래키 정리
    • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래키를 두고 대상 테이블을 찾는다.
    • 객체지향 개발자가 선호한다.
    • JPA 매핑이 편리하다.
    • 장점 : 주 테이블만 조회해도 (외래키로) 대상 테이블에 데이터가 있는지 확인 가능하다. (성능상 유리)
    • 단점 : 값이 없으면 외래키에 null을 허용한다.
  • 대상 테이블에 외래키 - 단방향 (불가능)
    • image
    • Member 엔티티의 locker가 연관관계의 주인으로 설정하고 싶으나 외래키는 LOCKER에 있는 상황
    • 일대다 단방향 같은 상황
    • JPA에서 지원하지 않는다.
  • 대상 테이블에 외래키 - 양방향
    • image
    • Locker에 있는 member를 연관관계 주인으로 설정하고 (LOCKER 테이블에 외래키가 있으므로) 양방향 연관관계를 설정
      • 반대편인 Memberlocker를 읽기 전용으로 설정
    • 사실 주 테이블에 외래키 양방향과 매핑 방법은 동일하다.
  • 대상 테이블 외래키 정리
    • 대상 테이블에 외래키가 존재한다. (null 허용 문제 해결)
    • 전통적인 데이터베이스 개발자들이 많이 선호한다.
    • 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조가 유지된다. (확장에 유리)
    • 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다.
      • JPA 입장에서는 Member 객체를 로딩할 때 외래키 대상인 locker에 값이 있는지 없는지 알아야함.
      • 그러나 외래키는 반대편인 LOCKER가 관리하기 때문에 MEMBER 테이블 말고도 LOCKER 테이블까지 전부 조회 해봐야함.
      • 결국 주 테이블 과 대상 테이블 둘다 확인하는 과정을 필히 거치므로 지연 로딩이 아무 의미 없음. (그래서 항상 즉시 로딩이 됨)
        • 지연 로딩 : 실제 객체를 사용하는 시점에 조회
        • 즉시 로딩 : Join을 활용하여 연관된 객체까지 한번에 조회

다대다 (N:M) - 권장하지 않음

  • 관계형 데이터베이스
    • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
    • 연결 테이블을 추가해서 일대다 - 다대일 관계로 풀어내야 함.
    • image
  • 객체
    • 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
    • image
    • 단방향, 양방향 둘다 가능하다.
    • @ManyToMany를 사용한다.
    • @JoinTable로 연결 테이블을 지정할 수 있다.
      @Entity
      public class Product {
        @Id @GeneratedValue
        private Long id;
      
        private String name;
      
        @ManyToMany(mappedBy = "products")
        private List<Member> members = new ArrayList<>();
        // getter, setter
      }
      
      @Entity
      public class Member {
        @Id @GeneratedValue
        private Long id;
      
        private String name;
      
        @ManyToMany
        @JoinTable(name = "MEMBER_PRODUCT")
        private List<Product> products = new ArrayList<>();
        // getter, setter
      }
      
  • 다대다 매핑의 한계
    • image
    • 편리해 보이지만 실무에서 사용하면 안됨.
    • 연결 테이블이 단순히 연결만 하고 끝나지 않기 때문
    • 주문시간, 수량 같은 데이터가 들어올 수 있음.
      • 매핑 정보만 있어야 함.
    • 중간에 연결 테이블이 존재해서 쿼리를 예측할 수 없음
  • 다대다 매핑 극복
    • image
    • 연결 테이블 용 엔티티를 추가 (연결 테이블을 엔티티로 승격)
    • @ManyToMany -> @OneToMany, @ManyToOne
      @Entity
      public class Product {
        @Id @GeneratedValue
        private Long id;
      
        private String name;
      
        @OneToMany(mappedBy = "product")
        private List<MemberProduct> memberProducts = new ArrayList<>();
        // getter, setter
      }
      
      @Entity
      public class MemberProduct {
        @Id @GeneratedValue
        private Long id;
      
        @ManyToOne
        @JoinColumn(name = "MEMBER_ID")
        private Member member;
      
        @ManyToOne
        @JoinColumn(name = "PRODUCT_ID")
        private Product product;
      }
      
      @Entity
      public class Member {
        @Id @GeneratedValue
        private Long id;
      
        private String name;
      
        @OneToMany(mappedBy = "member")
        private List<MemberProduct> memberProducts = new ArrayList<>();
        // getter, setter
      }
      

📣 Reference

본 포스팅은 김영한님의 강의를 듣고 스스로 정리 및 추가한 내용입니다.

자바 ORM 표준 JPA 프로그래밍 - 기본편

댓글남기기