본문 바로가기

JPA

[JPA] 연관관계 매핑 1

엔티티들 간에는 다양한 연관관계가 있을 수 있다. 예를들어 회원 엔티티는 자신이 주문한 현황을 알기 위해 주문 엔티티와 연관관계가 있고, 주문 엔티티는 주문 상품을 알기위해 다시 상품엔티티와 연관관계를 맺는다. 객체에서의 연관관계는 참조(reference)를 통해 쉽게 구현이 가능하다. 하지만 관계형 데이터베이스에서는 외래키(foreign key)라는 개념을 사용해야 한다. 이번 포스팅에서는 JPA가 객체에서의 연관관계(참조)를 어떻게 DB의 연관관계(외래키)로 매핑하는지 알아보자.

 

 


객체의 테이블의 연관관계 (feat. 다대일 연관관계)

다음과 같은 요구사항이 있다고 해보자.

 

- 회원과 팀이 있다.

- 회원은 하나의 팀에 소속되어야 한다.

- 하나의 팀에는 여러 회원이 소속되어 있다.

 

위와 같이 팀은 여러명의 회원을 가질 수 있고, 회원은 단 하나의 팀에만 소속될 수 있는 관계를 회원과 팀이 다대일(N:1)관계에 있다고 한다. 객체와 테이블을 이용해 회원과 팀의 연관관계를 구현해보자.

 


객체에서의 단방향 관계

객체는 참조를 통해 연관관계를 생성한다. 이때 참조를 한쪽에서만 하면 단방향 관계가 되는 것이다.

public class Member {
    public Long id;
    public Team team;		// Team을 참조
    public String username;
}

public class Team {
    public Long id;
    public String teamname;
}

위 코드를 보면 Member는 Team을 참조하고 있지만, Team은 Member를 참조하고 있지 않다. Member에서는 Team 객체에 접근할 수 있지만 Team에서는 Member 객체에 접근할 수 없다. 즉, Member에서 Team으로 단뱡항으로 연관관계에 맺어진것이다.

객체에서의 연관관계는 하나의 참조가 하나의 방향을 만드므로, 단방향 밖에 존재하지 않는다. 그렇다면 양방향 연관관계를 어떻게 구현할 수 있을까? 단방향 연관관계를 두개를 만들면 양방향 연관관계가 된다.

 

아래 코드와 같이 Team도 Member를 참조하게 되면 단방향 연관관계가 두개 생기게 되고 두 객체는 양방향으로 연관관계를 형성하게 된다.

public class Member {
    public Long id;
    public Team team;		// Team을 참조
    public String username;
}

public class Team {
    public Long id;
    public List<Member> members;	// Member를 참조
    public String teamname;
}

 


 

데이터베이스(테이블)에서의 단방향 연관관계

테이블은 외래키(foreign key)를 통해 연관관계를 형성한다. 회원 테이블(MEMBER)는 TEAM_ID를 외래키로 가짐으로써 TEAM 테이블과 연관관계를 맺는다.

데이터베이스는 조인(join)을 통해 데이터에 접근하게 되는데 이때 회원테이블을 통해 팀 테이블의 데이터를 접근한것과 팀 테이블을 통해 회원테이블에 접근하는 것이 동시에 가능한다. 즉, 데이터 베이스는 반드시 양뱡향 연관관계이다. 조인을 어떻게 하느냐에 따라 양뱡향 모두 데이터 접근이 가능하다. 

SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

 


연관관계 매핑

위에서는 엔티티의 연관관계를 객체와 테이블로 구현해봄으로써, 객체와 DB가 연관관계에서 갖는 관점의 차이에 대해 알아보았다. 이제 JPA를 이용해 객체의 연관관계를 테이블로 매핑해보자.

 


연관관계 매핑

객체에서의 연관관계와 테이블의 연관관계를 매핑하기 위해서는 크게 2가지 정보가 필요하다.

 

  1. 어떠한 종류의 연관관계인가? (일대일, 다대일, 일대다, 다대다)
  2. 외래키가 무엇인가?

회원테이블과 팀테이블을 다시 보면, 일단 다대일(N:1) 관계이고, TEAM_ID를 외래키로 갖는다.  회원 엔티티 코드는 @JoinColumn 애노테이션과 @ManyToOne 애노테이션를 통해 위 관계를 매핑한다.

@Entity
public class Member {
	
    @Id
    @Column(name="MEMBER_ID")
    public Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    // 연관관계 설정
    public void setTeam(Team team) {
    	this.team = team;
    }
    
    ...
}

일단 @ManyToOne은 엔티티가 다대일 관계임을 의미한다. 만약 일대다라면 @OneToMany, 다대다라면 @ManyToMany, 일대일이라면 @OneToOne을 쓰면 된다.

 

@JoinColumn은 외래키를 매핑하게 된다. name 속성을 통해 외래키의 이름을 지정한다.   


연관관계의 주인

회원 엔티티는 @ManyToOne을 통해 다대일 연관관계임을 선언하고, @JoinColumn을 통해 외래키를 매핑해주었다. 이번에는 매핑한 팀 엔티티를 살펴보자.

@Entity
public class Team {

	@Id
    @Column(name='TEAM_ID')
    private Long id;
    
    private String teamname;
    
    @OneToMany(mappedBy="team")
    private List<Member> members = new ArrayList<>();
    
    ...
}

팀 엔티티 입장에서는 일대다(1:N)관계이므로 @OneToMany 애노테이션을 지정해주었다. 이때 속성으로 mappedBy를 설정해주는데 mappedBy는 무슨 역할을 할까?

 

위에서 객체는 단방향 연관관계만은 가진다고 말했다. 즉, 양방향 연관관계는 단뱡향 연관관계 2개를 지정함으로써 형성된다. 반면 데이터베이스는 양방향 연관관계를 형성함에 있어, 하나의 테이블이 외래키를 관리한다. 즉, 참조는 둘인데 외래키는 하나만 존재한다. 결국 두 참조(관계) 사이에서는 차이가 발생하고, 두 참조중 하나의 참조가 외래키를 관리해야 한다는데 이 참조 혹은 관계를 연관관계의 주인이라고 한다.

 

mappedBy 속성은 연관관계의 주인을 지정하는 역할을 한다. mappedBy 속성이 설정된 엔티티는 연관관계의 주인이 되지 않는다. 즉, 위 관계에서는 mappedBy 속성이 지정된 팀 엔티티는 연관관계에 주인이 되지 않는다. 다시 말해 회원 엔티티가 연관관계의 주인이 된다.


연관관계의 주인은 누구?

기본적으로 연관관계의 주인은 외래키가 있는 곳이 해야한다.

위 테이블을 보면 MEMBER 테이블이 외래키를 가지고 관리한다. 즉, 회원 엔티티가 연관관계의 주인이 된다.

다대일 관계에서는 보통 데이터베이스의 특성상 '다'쪽이 외래키를 관리하게 된다. 즉, '다'에 해당하는 쪽이 보통 연관관계의 주인이된다. 

 


연관관계 저장, 조회 (feat. JPA)

이제 JPA를 통해 연관관계를 저장하고 조회해보자.

 


저장

public void saveExample() {

    // team1 저장
    Team team1 = new Team(1L, "team1");
    em.persist(team1);
    
    // member1 저장
    Member member1 = new Member(2L, "member1");
    member1.setTeam(team1);		// 연관관계 설정
    em.persist(member1);
}

setTeam() 메서드를 통해 간단하게 연관관계를 설정할 수 있다. 이후 저장하면 된다. (매우 간단...) 

그런데 이렇게만 코드를 짜면 팀 엔티티는 멤버에 대한 정보를 받지 못하게 된다. 문제가 되지 않을까? 문제가 되지 않는다. 왜냐하면 회원 테이블만이 외래키를 관리하기 때문이다. 즉, 실제로 팀 테이블은 회원에 관한 정보를 애초에 저장하지 않는다.

 

하지만 실제 코드를 작성하는 과정에서 객체단위로 코드를 테스트하거나 처리하는 경우가 있다. 이 경우 팀 객체가 멤버에 대한 정보가 없을 경우, 문제가 발생할 수 있다. 이를 해결하기위해서는 연관관계 편의 메서드라는 것이 필요한다. 


연관관계 편의 메서드

public void saveExample() {

    // team1 저장
    Team team1 = new Team(1L, "team1");
    em.persist(team1);
    
    // member1 저장
    Member member1 = new Member(2L, "member1");
    member1.setTeam(team1);		// 연관관계 설정
    em.persist(member1);
}

위의 코드를 테이블의 관점에서 보면 전혀 문제가 없다. 왜냐하면 외래키를 회원 테이블만 관리하기때문에 회원 객체에만 팀 정보를 설정해주면 충분하다. 하지만 객체의 관점에서 보면 문제가 발생한다. 바로 멤버 객체의 팀에 소속되었음에도 팀 객체에는 회원 정보가 없는 것이다. 이 문제를 해결하기 위해서는 다음과 같이 코드를 수정해야 한다.

public void saveExample() {

    // team1 저장
    Team team1 = new Team(1L, "team1");
    em.persist(team1);
    
    // member1 저장
    Member member1 = new Member(2L, "member1");
    
    member1.setTeam(team1);		// 연관관계 설정
    team1.getMembers().add(member1) 	// 팀에도 회원정보를 넣어줌
    
    em.persist(member1);
}

위와 같이 회원의 팀에 대한 정보가 업데이트되면 팀도 동시에 회원에 대한 정보를 업데이트 해야한다. 그렇기 때문에 처음부터 setTeam / addMembers와 같은 연관관계내의 데이터를 수정하는 메서드는 동시에 처리되도록 메서드를 작성하는데 이러한 메서드를 연관관계 편의 메서드라고 한다. 

 

@Entity
public class Member {
	
    @Id
    @Column(name="MEMBER_ID")
    public Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    // 연관관계 편의 메서드
    public void setTeam(Team team) {
    	this.team = team;
        team.getMembers().add(this)
    }
    
    ...
}

멤버 엔티티의 setTeam() 메서드를 보면 team필드를 업데이트함과 동시의 team 객체의 members필드에도 자신(this)을 추가해준다. 

 

연관관계 편의 메서드의 주의점

만약 Member1이 Team1에서 Team2로 팀이 바뀌었다고 해보자. 그렇다면 다음과 같이 코드를 작성할수 있을 것이다.

public void saveExample() {

    // team1 저장
    Team team1 = new Team(1L, "team1");
    em.persist(team1);
    
    // team2 저장
    Team team2 = new Team(2L, "team2");
    em.persist(team2);
    
    // member1 저장
    Member member1 = new Member(1L, "member1");
    member1.setTeam(team1);		// 연관관계 편의 메서드
    em.persist(member1);
   
    // member1, team2 조회
    Member findMember = em.find(1L, Member.class);
    Team findTeam = em.find(2L, Team.class);
    findMember.setTeam(findTeam); 	// 팀 변경
}

위의 코드는 중대한 문제가 있다. 위의 코드를 도식화한 다이어 그램을 살펴보자.

 

회원은 정상적으로 소속된 팀을 변경했지만, 문제는 Team1은 여전히 Member1을 소속된 회원을 가지고 있다는 것이다. 즉, 연관관계 편의 메서드를 작성할때는 이전의 연관관계를 완전히 해제하고, 새로운 연관관계를 맺어야한다. 그런 점에서 연관관계 편의 메서드 setTeam은 다음과 같이 수정되어야 한다.

// 수정전
public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

// 수정 후
public void setTeam(Team team) {
	
    if (this.team != null) {
    	this.team.getMembers().remove(this);	// 기존의 연관관계를 해제
    }
    
    this.team = team;
    team.getMembers().add(this);
}

마무리

이번 포스팅에서는 객체와 DB간의 연관관계의 관점 차이를 알아보고 JPA를 통해 어떻게 매핑할 수 있는지 알아보았다. 다음 포스팅에서는 각 관계별(다대일,일대일,다대다)로 어떻게 코드를 작성해야 하는지 알아보자.