JPA

[JPA] 연관관계 매핑 2

아, 그래요? 2022. 8. 7. 15:03

연관관계 매핑 1편에서는 연관관계 매핑이 필요한 이유와 구현하는 방법에 대해 알아보았다. 1편에서 이미 예시를 통해 다대일, 일대다 연관관계는 살펴보았으므로 2편에서는 일대일(1:1), 다대다(N:N) 연관관계에 대해 자세히 알아보자.

 


일대일(1:1) 연관관계

일대일 관계는 하나의 객체가 다른 하나의 객체만을 참조할 수 있고, 반대 방향으로도 마찬가지인 연관관계이다. 예를 들어 다음과 같은 상황이다.

  • 한명의 학생은 하나의 사물함만을 가질 수 있다.
  • 하나의 사물함은 한명의 학생 것이다.

이런 경우, 학생과 사물함은 일대일 연관관계라고 할 수 있다.

 


일대일 연관관계의 주인

일대다 연관관계의 경우, '다'쪽인 외래키(foreign key)를 가지고 연관관계의 주인이 되었다. 하지만 일대일 관계는 어느쪽이 연관관계의 주인이 되도 상관이 없다.

 

보통 일대일 연관관계도 두 테이블이 주테이블과 대상테이블로 구분 되는데 위 예시에서는 학생이 주 테이블, 사물함이 대상 테이블이 된다(학생이 사물함을 소유한다는 관점) 이때 주 테이블이 연관관계의 주인인 경우와 대상 테이블이 연관관계의 주인인 경우 다음과 같은 관점 차이가 발생한다.

 

  1. 주 테이블이 연관관계의 주인
    • 주 테이블이 외래키를 가지고 관리한다. 그렇게 되면 외래키를 마치 대상 테이블에 접근할때 객체 참조처럼 사용할 수 있다. 이 방법의 장점은 주 테이블에만 접근해도 외래키를 통해 연관된 대상 테이블까지 쉽게 접근할 수 있다. 보통 객체지향 관점에서 선호하는 방식이다.
  2. 대상 테이블이 연관관계의 주인
    • 전통적인 데이터베이스에서처럼 대상 테이블이 외래키를 관리하게 된다. 그렇게 되면 만약 주테이블과 대상 테이블이 일대다(1:N)관계가 되어도 구조의 변경없이 사용할 수 있다. 보통 데이터베이스의 관점에서 선호하는 방식이다.

 

이제 코트를 통해 살펴보자


 

주 테이블이 연관관계의 주인

1. 단방향

@Entity
public class Member {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String username;
    
    @OneToOne
    @JoinColumn(name="LOCKER_ID")
    private Locker locker;
}

@Entity
public class Locker {
	
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    private int lockernum;
}

@JoinColumn 애노테이션을 통해 외래키를 지정해준다.

 

2. 양방향

@Entity
public class Member {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String username;
    
    @OneToOne
    @JoinColumn(name="LOCKER_ID")
    private Locker locker;
}

@Entity
public class Locker {
	
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    private int lockernum;
    
    @OneToOne(mappedBy="locker")
    private Member member;
}

mappedBy 속성을 통해 Locker가 연과관계의 주인이 아님을 선언하고, 회원 객체를 참조한다.

 


 

대상 테이블이 연관관계의 주인

1. 단방향

@Entity
public class Member {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String username;
}

@Entity
public class Locker {
	
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    private int lockernum;
    
    @OneToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;
}

@JoinColumn을 연관관계의 주인인 사물함 테이블에 지정해준다.

 

2. 양방향

@Entity
public class Member {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String username;
    
    @OneToOne(mappedBy='member')
    private Locker locker;
}

@Entity
public class Locker {
	
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    private int lockernum;
    
    @OneToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;
}

 


다대다(N:N) 연관관계

다대다 연관관계는 두 엔티티가 서로 여러개를 참조할 수 있는 관계를 말한다. 아래 예시를 보자.

  • 한명의 회원은 여러 모임에 들어갈 수 있다.
  • 하나의 모임에는 여러명의 회원이 소속되어 있다.

위의 예시에서는 회원이 여러개의 모임을 참조할 수 있고, 반대로 모임도 여러개의 회원을 참조할 수 있다. 즉, 회원과 모임은 다대다(N:N) 연관관계에 있는것이다.


 

다대다(N:N) => 일대다(1:N) - 다대일(N:1)

다대다 관계는 데이터베이스의 테이블로는 구현할 수 없다. 그렇기 때문에 연결테이블을 추가하여 다대다 관계를 일대다-다대일 관계로 풀어서 사용하게 된다. 아래 다이어그램을 보자.

 

이러한 관계는 DB에서 표현할 수가 없다.
MEMBER_TEAM 테이블이 연결테이블이다.

회원테이블과 팀테이블 사이에 둘을 이어주는 연결 테이블 MEMBER_TEAM을 추가한다. MEMBER_TEAM 테이블은 회원과 팀 데이터를 쌍으로 가지고 있어 어떤 회원이 어떤 팀에 있는지를 저장하고 있다.

 

이제 코드로 구현해보자.


구현

단방향

@Entity
public class Member {
	
    @Id @Column(name='MEMBER_ID')
    private Long id;
    
    private string username;
    
    @ManyToMany
    @JoinTable(
    	name = "MEMBER_TEAM",
        joinColumns = @JoinColumn(name="MEMBER_ID"),
        inverseJoinColumns = @Joincolumn(name="TEAM_ID")
    )
    private List<Team> teams = new ArrayList<>();
}

@Entity
public class Team {
	
    @Id @Column(name="TEAM_ID")
    private Long id;
    
    private String teamname;
    
    ...
}

회원만 팀을 참조하는 단방향 관계이다. 일단 다대다 관계를 나타내는 @ManyToMany 애노테이션을 붙여준다. 그리고 @JoinTable 애노테이션을 통해 연결 테이블을 매핑한다. 

 

양방향

@Entity
public class Member {
	
    @Id @Column(name='MEMBER_ID')
    private Long id;
    
    private string username;
    
    @ManyToMany
    @JoinTable(
    	name = "MEMBER_TEAM",
        joinColumns = @JoinColumn(name="MEMBER_ID"),
        inverseJoinColumns = @Joincolumn(name="TEAM_ID")
    )
    private List<Team> teams = new ArrayList<>();
}

@Entity
public class Team {
	
    @Id @Column(name="TEAM_ID")
    private Long id;
    
    private String teamname;
    
    @ManyToMany(mappedBy='team')
    private List<Member> members = new ArrayList<>();
}

양방향 관계의 경우 단뱡향 관계의 코드에서 Team에 회원리스트 참조만 넣어주면 된다. 이때 @ManyToMany 애노테이션을 통해 다대다관계를 지정하고, mappedBy 속성을 통해 연관관계의 주인을 설정한다.


연결테이블의 한계, 연결 엔티티

@JoinTable을 통해 구현한 연결 테이블에는 아주 큰 한계점이 있다. 바로 설정한 키 값외에 다른 칼럼을 추가할 수 없다는 것이다.

아래의 다이어그램을 보자.

기존의 연결 테이블에 추가적으로 역할(ROLE)과 가입일(JOINDATE) 칼럼이 추가되었다. @JoinTable 애노테이션을 통해서는 위의 테이블을 매핑할 수 없다. 그렇기 때문에 개발자가 직접 엔티티를 작성하여 매핑해야 한다. 아래의 코드를 보자

@Entity
public class Member {
	
    @Id @Column(name='MEMBER_ID')
    private Long id;
    
    private string username;
    
    @OneToMany(mappedBy="member")
    private List<MemberTeam> memberTeams = new ArrayList<>();
}

@Entity
public class Team {
	
    @Id @Column(name="TEAM_ID")
    private Long id;
    
    private String teamname;
    
    @OneToMany(mappedBy='team')
    private List<MemberTeam> memberTeams = new ArrayList<>();
}

회원 테이블과 팀 테이블 모두 연결테이블과 일대다 관계로 매핑되게 된다. 그러므로 @OneToMany 애노테이션을 지정하게된다.

 

@Entity
public class MemberTeam {
	
    @Id @Column(name="MEMBER_TEAM_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    private String role;
    
    private Date joinDate;
}

MemberTeam은 연결 엔티티이다. 회원, 팀과 각각 다대일 관계를 갖는다. 엔티티를 직접 구현함으로써 키 값 외에도 다른 데이터를 가질 수 있다.

 

추가) 일대일/다대다 관계모두 연관관계 편의 메서드를 구현해놓는것을 추천한다(1편참고)

 


마무리

두편에 걸져 JPA가 제공하는 연관관계 매핑에 대해 알아보았다.