[JPA] 연관 관계 매핑

심씨·2023년 12월 2일
1

JPA

목록 보기
1/3

인프런 자바 ORM 표준 JPA 프로그래밍 기본편 - JPA 연관 관계 매핑

자바 ORM 표준 JPA 프로그래밍 - 기본편
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편 내용을 기반으로 작성했습니다.

JPA에서 엔티티의 연관관계 매핑 시 다중성, 방향, 연관관계 주인 3가지 사항을 고려해야 합니다.

1. 다중성

다중성은 엔티티 간의 관계를 데이터베이스 테이블 간의 관계로 매핑하는 방법을 의미하며 유형으로는 @ManyToOne, @OneToMany, @OneToOne, @ManyToMany가 있습니다.

2. 방향

방향은 연관된 엔티티 사이의 상호 작용 방식을 의미하며 단방향, 양방향으로 나눠질 수 있습니다. 객체 지향 프로그래밍에서 단방향은 한 엔티티만 다른 엔티티에 대한 참조를 보유한 경우이며, 양방향은 양쪽에서 서로의 참조를 보유한 경우입니다.

3. 연관관계 주인

연관관계 주인은 양방향 연관관계에서 연관관계를 관리하는 엔티티를 나타냅니다.연관관계 주인은 대부분 외래 키를 갖고 있으며 관계를 제어합니다. 반대로 연관관계 주인의 반대편은 단순 조회 기능만 가능합니다.

1. 다대일[N:1]

1.1. 다대일 단방향

@Entity
public class Team {
	@Id 
    @GeneratedValue
	private Long id;
	// ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    // ...
}

Member 엔티티에서 Team 엔티티를 참조하는 필드 값 team만 있을 뿐, Team 엔티티에서 Member 엔티티를 참조하는 필드 값은 없습니다. JPA는 @ManyToOne, @OneToMany 테이블 매핑에서 항상 N 쪽인 테이블에 외래 키를 생성합니다. 따라서 테이블에는 N 쪽인 Member 쪽에 외래 키가 위치합니다.

1.2. 다대일 양방향

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    // ...
}

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;    
    // ...
}

단방향 연관관계와 비교했을 때 Team 엔티티에 members 필드가 추가되어 Team 엔티티와 Member 엔티티는 서로를 참조하고 있습니다. Team 엔티티에 members 필드가 추가되어도 Member 엔티티가 연관관계 주인이며 데이터베이스 테이블에는 어떠한 영향도 주지 않습니다.

또 Team 엔티티의 members 필드는 mappedBy 속성을 사용하여 Member 엔티티의 team 필드와 매핑해 줍니다. 만약 Team 엔티티의 members 필드를 mappedBy 속성으로 Member 엔티티와 연결해 주지 않으면 아래와 같은 SQL 쿼리문이 실행되면서 Member와 Team을 매핑해주는 새로운 Team_Member 테이블이 추가로 생성됩니다.

Hibernate: 
    
    create table Team_Member (
       Team_TEAM_ID bigint not null,
        members_MEMBER_ID bigint not null
    )
Hibernate: 
    
    alter table Team_Member 
       add constraint FKpsjmea2e134ab3x3gsdoyxepg 
       foreign key (members_MEMBER_ID) 
       references Member
Hibernate: 
    
    alter table Team_Member 
       add constraint FK4u1npo283vgqfk8lyxclihgnl 
       foreign key (Team_TEAM_ID) 
       references Team

2. 일대다[1:N]

2.1. 일대다 단방향

@Entity
public class Team {
	@Id 
    @GeneratedValue
	private Long id;
   
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    // ...
}

Team 엔티티에서 Member 엔티티를 참조하는 필드 값 members만 있을 뿐, Member 엔티티에는 Team 엔티티를 참조하는 필드 값은 없습니다. 엔티티 연관관계의 주인은 Team이지만 테이블에는 N 쪽인 Member 테이블에 외래 키가 생성됩니다.

Team 엔티티가 외래 키를 관리하지만 정작 외래 키는 Member 테이블에 존재하게 되는 모순적인 상황이 됩니다. 아래 코드와 같이 Member 테이블의 속성 값을 변경하기 위해서 Team 엔티티의 members 필드를 참조해야 합니다. 즉, 엔티티가 관리하는 외래 키가 다른 테이블에 존재하게 됩니다.


Member member = new Member();
em.persist(member);

Team team = new Team();
// Team 엔티티에서 Member 테이블 업데이트
team.getMembers().add(member);
em.persist(team);

Hibernate: 
    /* insert jpabook.jpashop.domain.Member
        */ insert 
        into
            Member
            (MEMBER_ID) 
        values
            (?)
Hibernate: 
    /* insert jpabook.jpashop.domain.Team
        */ insert 
        into
            Team
            (TEAM_ID) 
        values
            (?)
Hibernate: 
    /* create one-to-many row jpabook.jpashop.domain.Team.members */ update
        Member 
    set
        TEAM_ID=? 
    where
        MEMBER_ID=?

2.2. 일대다 양방향


@Entity
public class Team {
	@Id 
    @GeneratedValue
	private Long id;
   
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
    // ...
}

일대다 양방향 관계는 JPA에서 공식적을 존재하지 않으나 @JoinColumn 어노테이션 속성 값 insertable, updatable에 모두 false를 주어 읽기 전용 필드로 지정해 준다면 양방향처럼 사용 가능합니다.

3. 일대일[1:1]

"회원 한 명당 사물함을 한 개씩만 배정한다."라는 비즈니스 로직을 가정했을 때, 회원(Member)과 사물함(Locker)은 1:1 관계입니다.

또 비즈니스 관점에서 회원 테이블이 사물함보다 더 많은 조회가 예상되므로 회원 테이블을 주 테이블로 사물함을 상대 테이블로 가정하겠습니다.

다대일 관계의 반대는 일대다 관계지만 일대일 관계의 반대는 똑같이 일대일 관계입니다. 따라서 주 테이블이나 상대 테이블 중에 외래 키를 선택해서 생성할 수 있습니다.

3.1. 주 테이블에 외래 키 일대일 단방향


@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    // ...
}

Member 엔티티에서 Locker 엔티티를 참조하는 필드 값 locker만 있을 뿐, Locker 엔티티에서 Member 엔티티를 참조하는 필드 값은 없습니다. 데이터베이스 테이블에는 Member 쪽에 외래 키가 생성됩니다.

3.2. 주 테이블에 외래 키 일대일 양방향


@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id
    
    @OneToOne(mappedBy = "locker")
    private Member member;
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    // ...
}

단방향 연관관계와 비교했을 때 Locker 엔티티에 member 필드가 추가되어 Locker 엔티티와 Member 엔티티는 서로를 참조하고 있습니다. 데이터베이스 테이블에는 여전히 Member 테이블에만 외래 키가 생성됩니다.

3.3. 대상 테이블에 외래 키 일대일 단방향

주 테이블이 연관관계 주인이지만 외래 키는 대상 테이블에 존재하는 관계는 JPA에서 지원되지 않습니다.

3.4. 대상 테이블에 외래 키 일대일 양방향


@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @OneToOne
	@OneToOne(mappedBy = "member")
    private Locker locker;
    // ...
}

주 테이블에 외래 키 일대일 양방향 관계와 반대로 대상 테이블 Locker가 연관관계 주인이며 외래 키 또한 Locker 테이블에 생성됩니다. 결론적으로 일대일 관계는 연관관계 주인인 엔티티에서 본인의 외래 키를 직접 관리해야 합니다.

4. 다대다[N:M]

관계형 데이터베이스에서는 정규화된 테이블 2개로 N : M 관계를 표현할 수 없습니다. 따라서 두 테이블을 연결하는 테이블을 추가로 생성해 1 : N, M : 1 관계로 변경해 줘야 합니다.

4.1. 다대다 단방향


@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
    // ...
}

Member 엔티티에 @JoinTable 어노테이션을 주면 Member 테이블의 기본 키와 Product 테이블의 기본 키를 외래 키로 사용하는 MEMBER_PRODUCT 테이블이 생성되어 Memeber 테이블과 Product 테이블을 매핑해줍니다.

4.2. 다대다 양방향


@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id
     
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
    // ...
}

@Entity
public class Member {
	@Id 
    @GeneratedValue
	private Long id;
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
    // ...
}

다대일 양방향 관계와 유사하게 Product 엔티티에 Member 엔티티는 참조하는 필드 값 members를 추가해줍니다.

하지만 다대다 관계는 매핑하려는 테이블의 기본 키만 외래 키로 가져야 하는 등 제약 조건이 굉장히 많아 실제로는 사용을 지양하고 있습니다. 따라서 연결 테이블용 엔티티를 따로 추가해 주어 @OneToMany, @ManyToOne 관계로 변경해 줍니다.

profile
개발 뿌샤!

1개의 댓글

comment-user-thumbnail
2023년 12월 5일

너무 좋은 글입니다.

답글 달기