본문 바로가기
Study/자바 ORM 표준 JPA 프로그래밍

6장. 다양한 연관관계 매핑

반응형

본 포스트는 사내 스터디로 진행되는 자바 ORM 표준 JPA 프로그래밍 관련 정리입니다

 

인프런 강의도 있습니다! 짱짱

https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com


6장.  다양한 연관관계 매핑

  • 엔티티의 연관관계를 매핑할 때는 다음 3가지 고려사항이 있다
    • 다중성
      • 다대일(@ManyToOne)
      • 일대다(@OneToMany)
      • 일대일(@OneToOne)
      • 다대다(@ManyToMany)
    • 단방향, 양방향
      • 테이블은 외래 키(FK) 하나로 조인을 사용해서 양방향으로 쿼리가 가능하므로 사실상 방향이라는 개념이 없다
      • 객체는 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있다
      • 객체 관계에서 한 쪽만 참조하는 것을 단방향, 양쪽에서 서로 참조하는 것을 양방향이라고 한다
    • 연관관계의 주인
      • JPA는 두 객체 연관관계 중 하나를 정해서 데이터베이스 외래 키(FK)를 관리하는데 이것을 연관관계의 주인이라 한다
      • 외래 키를 가진 테이블과 매핑한 엔티티(일반적으로 다쪽에 해당하는)가 외래 키를 관리하는 게 효율적이므로 보통 이곳을 연관관계의 주인으로 선택
      • 주인이 아닌 방향은 외래 키를 변경할 수 없고 읽기만 가능하다

 

6-1. 다대일(@ManyToOne)

  • 데이터베이스 테이블의 일대다, 다대일 관계에서 외래 키는 항상 다쪽에 있다
  • 따라서 연관관계의 주인은 항상 다쪽이다 ( 외래 키가 있는 곳으로 설정해야 효율적이다 )
  • 다대일 단방향(N:1)
    // N:1 다대일 단방향
    
    @Entity
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
        
        ...
    }​

    // 다대일
    
    @Entity
    public class Team {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        ...
    }​

    • Team 객체와 @ManyToOne의 관계를 맺고 있는 것을 확인할 수 있다
    • Team(일) 대 Member(다) 관계이며, 다쪽인 Member에 @JoinColumn(외래 키)가 설정되어 있다
    • 단방향이기 때문에 Member에서 Team쪽의 참조만 가지고 있고 Team쪽에서는 다른 참조관계를 갖고있지 않다
  • 다대일 양방향(N:1, 1:N)
    // 다대일
    
    @Entity
    public class Team {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
        
        ...
        
        public void addMember(Member member) {
            this.members.add(member);
            if (member.getTeam() != this) {
                member.setTeam(this);
            }
        }
        
        ...
    }​
  • // N:1 다대일 단방향
    
    @Entity
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
        
        ...
        
        public void setTeam(Team team) {
            this.team = team;
            
            if (false == team.getMembers().contains(this)) {
                team.getMembers().add(this);
            }
        }
        
        ...
    }​
    • 기존에 아무 참조관계도 없던 Team 클래스에 @OneToMany(일대다) 관계가 설정되었습니다
    • 양방향 관계에서는 연관관계 주인이 아닌 곳에서는 mappedBy 속성을 통해 반대편의 연관관계 주인을 나타내줘야합니다
    • 양방향 관계에서는 객체 그래프 탐색을 통해 서로를 조회할 수 있으므로 무한 루프에 빠질 수 있습니다
      • 둘 중 한 곳에서만 셋팅되는 것을 막기위해 한 쪽에서 Team과 Member 둘 다 참조관계를 셋팅해 주고 있고, 이를 편의 메서드라고 부릅니다
      • Team 객체의 addMember 메서드, Member 객체의 setTeam 메서드 설정 중 예외처리 하는 부분이 무한 루프를 방지하기 위한 구문입니다
      • Lombok의 @Data, @ToString 등의 자동으로 Generate 되는 코드들도 조심해야합니다 ( 서로 ToString 메서드를 호출하다가 무한 루프에 빠짐 )

 

6-2. 일대다(@OneToMany)

  • 일다다 관계는 위에서 설명했던 다대일(@ManyToOne)의 반대 방향입니다
  • 사용 가능 컬렉션 타입
    • Collection
    • List
    • Set
    • Map
  • 일대다 단방향(1:N)
    // 일대다 단방향
    
    @Entity
    public class Team {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        @OneToMany
        @JoinColumn(name = "TEAM_ID")
        private List<Member> members = new ArrayList<>();
        
        ...
    }​

    • 일대다 단방향 관계에서도 역시 외래 키를 관리하는 연관관계의 주인을 설정해야하는데, 코드에서 보시다시피 외래 키를 갖는 테이블(Member)이 아니라 참조하는 곳에서 @JoinColumn을 관리하고 있습니다
    • 단방향에서는 Member Entity가 외래 키를 매핑할 수 있는 참조 필드가 없기 때문에 반대쪽인 Team Entity에서 관리하는 것을 확인 할 수 있습니다
    • 만약 일대다 단방향 관계에서 @JoinColumn을 명시하지 않으면, JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 Join Table 전략을 기본으로 사용해서 매핑합니다
    • 일대다 단방향 매핑의 단점
      • 매핑한 객체가 관리하는 외래 키가 다른 테이블에 존재
      • 즉 매핑된 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리시 INSERT 쿼리 한 번에 데이터 삽입을 할 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE 쿼리가 추가적으로 실행된다
      • 따라서 일대다 단방향보다는 다대일 양방향 매핑을 권장한다

 

6-3. 일대일(1:1)

  • 일대일 관계는 양쪽이 서로 하나의 관계만 갖는다
  • 일대일 관계는 반대도 일대일 관계
  • 일대일 관계는 양쪽 모두에서 외래 키를 가질 수 있다
    • 주 테이블이 외래 키를 갖는 경우, 주 테이블에 @JoinColumn을 이용해 테이블에 외래키 명시
    • 대상 테이블이 외래 키를 갖는 경우, 주 테이블에 @OneToOne(mappedBy="")를 설정
  • 프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩(Lazy)으로 설정해도 즉시 로딩(Eagar)된다
    • 정확히 말하면 JPA는 일대일 관계에서 무조건적인 Lazy loading을 지원하지 않는다
    • JPA는 객체의 참조가 프록시 기반으로 동작하는데, 연관관계가 있는 객체는 참조를 할때 기본적으로 null이 아닌 프록시 객체를 반환한다
    • 하지만 @OneToOne 관계에서 null이 허용되는 경우는 프록시 형태로 null 객체를 반환할 수 없기 때문에 지연 로딩을 할 수 없다
    • @OneToMany는 배열 형태로 원소가 없는 컬렉션을 지원하기 때문에 가능
    • 지연로딩 발동 조건
      • nullable이 허용되지 않는 1:1 관계, 참조 객체가 optional=false로 지정할 수 있는 관계여야한다
      • 양방향이 아닌 단방향 1:1 관계여야한다
      • @PrimaryKeyJoin은 허용되지 않는다. 부모,자식간 엔티티의 조인컬럼이 모두 PK인 경우

 

6-4. 다대다(N:M)

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다 ( PK를 잡을 수 없음 )
  • 따라서 1:N, N:1로 풀어내야하며, 중간에 연결 테이블(정션 테이블, 매핑 테이블)을 추가적으로 설계해야한다
  • 하지만 객체는 관계형 데이터베이스와 다르게 객체 2개로 다대다 관계를 만들 수 있다
  • 다대다 단방향
    // 다대다: 단방향
    @Entity
    public class Member {
    	@Id
        @Column(name = "MEMBER_ID")
        private String id;
        
        private String username;
        
        @ManyToMany
        @JoinTable(name = "MEMBER_PRODUCT",
                   joinColumns = @JoinColumn(name = "MEMBER_ID"),
                   inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
        private List<Product> products = new ArrayList<>();
        
        ...
    }

    • Member 엔티티와 Product 엔티티를 @ManyToMany, 다대다로 연관관계를 설정했다
    • N:M 관계를 풀기위해서는 정션테이블, 즉 중간에 매핑테이블이 필요하기 때문에 @JoinTable 설정을 통해 따로 엔티티를 만들지 않고 풀어낼 수 있다
    • @JoinTable
      • name: 연결 테이블을 지정
      • joinColumns: 현재 방향에서 매핑한 조인 컬럼 정보 지정
      • inverseJoinColumns: 반대 방향에서 매핑한 조인 컬럼 정보 지정
    • 따라서 Member 엔티티에 Product 연관관계를 추가 후 persist를 호출하면 아래와 같은 쿼리가 발생한다
      
      # @ManyToMany
      INSERT INTO PRODUCT ...
      INSERT INTO MEMBER ...
      INSERT INTO MEMBER_PRODUCT ...
    • @ManyToMany 조회
      • Code
        
        // find
        public void find() {
            Member member = entityManager.find(Member.class, "member1");
            List<Product> products = member.getProducts(); // 객체 그래프 탐색
            for (Product product : products) {
                System.out.println("product.name = " + product.getName());
            }
        }
      • Lazy 로딩인 member.getProducts() 호출시 발생 쿼리
        
        # @ManyToMany
        SELECT 
            *
        FROM
            MEMBER_PRODUCT MP
        INNER JOIN 
            PRODUCT P 
            ON MP.PRODUCT_ID = P.PRODUCT_ID
        WHERE
            MP.MEMBER_ID=?
    • 다대다 양방향
      • 기존의 양방향처럼 연관관계의 주인을 설정하여 @JoinTable 설정을 하고, 연관관계의 주인이 아닌 곳에서는 mappedBy를 통해 연관관계 주인을 알려주면 된다
      • Code
        
        // @ManyToMany 반대방향
        @Entity
        public class Product {
        	@Id
            private String id;
            
            @ManyToMany(mappedBy = "products")
            private List<Member> members;
            
            ...
        }

        • mappedBy를 통해 Member 엔티티의 "products" 필드 속성이 연관관계 주인임을 알려줬다
    • @ManyToMany의 한계
      • @ManyToMany 설정은 연결 테이블(정션, 매핑)을 자동으로 처리해주므로, 코드가 단순해지고 편리하지만 한계가 존재한다
      • 문제점
        • 실무를 하다보면 연결 테이블에는 단순히 매핑된 엔티티들의 키값만 들어가지 않는다 ( @ManyToMany, @JoinTable로는 컬럼 추가가 불가능하다.. )
        • 따라서 연결테이블을 관리하는 엔티티를 직접 설계하는 것이 효율적이다
    • 참고: 식별 관계 vs 비식별 관계
      • 식별 관계: 부모 테이블의 기본 키를 받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 것을 식별 관계라고 한다
      • 비식별 관계: 단순히 외래 키로만 사용하는 것을 비식별 관계라고 한다
728x90
반응형

'Study > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

5장. 연관관계 매핑 기초  (0) 2021.09.04
4장. 엔티티 매핑  (0) 2021.08.31
3장. 영속성 관리  (0) 2021.08.22
1장. JPA 소개  (0) 2021.08.09