Spring/JPA

JPA 연관관계 매핑 기초

묠니르묘묘 2022. 3. 8. 16:36

엔티티는 다른 엔티티와 연관관계가 대부분 있다.

그럴 경우 어떻게 표현할까?

 

단방향과 양방향이 있다.

  • 회원 → 팀 ( 회원은 팀을 참조한다 )
  • 팀 → 회원 ( 팀은 회원을 참조한다 )

위의 두 가지 관계에서 하나만 성립하는 경우 단방향 관계, 두 가지 관계 모두 참조하면 양방향 관계라고 한다.

단방향 연관관계

다대일(N:1) 연관관계, 단방향

객체 연관관계에서는 단방향 관계이기에 member → team 조회는 가능하지만 반대의 경우는 할 수 없다.

그래서 Member.team필드를 통해서 팀을 알 수 있지만, 반대로 팀은 회원을 알 수 없다.

 

테이블 연관관계에서는 양방향 관계이기에 양 쪽 조회가 가능하다.

MEMBER 테이블의 TEAM_ID 외래키로 MEMBER JOIN TEAM 과 TEAM JOIN MEMBER 둘 다 가능하다.

 

  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래키로 연관관계를 맺는다.

 

객체와 테이블의 가장 큰 차이점은 참조를 통한 객체 연관관계는 언제나 단방향이라는 것이다.

객체간에 연관관계를 양방향으로 하려면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.

즉, 단방향 관계를 반대쪽에서도 만든다는 것이다. (단방향 2개 == 양방향 1개)

 

 

 

단방향 연관관계 매핑

다대일(N:1) 연관관계, 단방향

JPA를 사용해서 객체 연관관계와 테이블 연관관계를 매핑하면 위 그림처럼 된다.

회원 객체의 Member.team 필드와 회원 테이블의 MEMBER.TEAM_ID 외래키 컬럼이 매핑되는 것이다.

 

회원 엔티티

@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    private String username;
    
    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

 

팀 엔티티

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

 

 

@ManyToOne 이란?

다대일(N:1) 관계라는 매핑 정보이다.

테이블 연관관계를 보면 회원과 팀의 관계가 N:1로 표현되어 있다.

@ManyToOne 속성

 

@JoinColumn 이란?

@JoinColumn은 외래키를 매핑할 때 사용한다.

name 속성에는 매핑할 외래키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래키로 연관관계를 맺으므로 이 값을 지정한다.

이 어노테이션은 생략할 수 있다.

만약 생략한다면 외래키를 찾을때 기본 전략을 사용하게 된다.

  • 기본 전략 : 필드명 + _ + 참조하는 테이블의 컬럼명 (e.g. team_TEAM_ID 외래키를 사용)

@JoinColumn 속성

 

 

양방향 연관관계 매핑

양방향 객체 연관관계

객체 연관관계에서는 단방향 관계를 하나 더 반대쪽에 추가하였다.

Team은 Member를 여러명 가질 수 있으므로 List 컬렉션으로 추가한다.

이 때 테이블 연관관계는 이미 양방향으로 조회가 가능하므로 변경점이 없다.

 

팀 엔티티

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
    
    //==추가==//
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

코드도 회원 엔티티는 변경점이 없지만 팀 엔티티에는 List와 @OneToMany가 추가되었다.

팀의 입장에선 일대다 관계이므로 @OneToMany 매핑 정보를 사용했다.

mappedBy 속성은 양방향 매핑일 때 사용하는데, 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

즉, Member의 team을 해주면 된다.

 

그렇다면 mappedBy는 왜 하는 걸까?

객체에서 양방향 연관관계는 서로 다른 단방향 연관관계 2개를 잘 묶어서 보이게 한 것이다.

테이블은 외래키 하나로 두 테이블의 연관관계를 관리한다.

양방향 연관관계에서 객체의 참조는 둘인데 외래키는 하나가 되는것이므로 차이점이 발생하게 된다.

그래서 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계 주인이라 한다.

 

연관관계의 주인만이 DB 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)할 수 있다.

반면 주인이 아닌 쪽은 읽기만 가능한 것이다.

그래서 어떤 연관관계를 주인으로 정할지 결정하는 것이 mappedBy 속성이다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

보통 테이블에 외래키가 있는 쪽이 주인이다.

 

 

양방향 연관관계 주의점

주인이 아닌 곳에 값을 입력하여 저장이 안되는 경우가 많다.

그렇다면 연관관계의 주인에만 값을 저장하고, 주인이 아닌 곳에는 저장하지 않아도 될까?

이에 대한 답변은 그렇지 않다.

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

회원 -> 팀을 설정했다면, 팀 -> 회원도 설정해야 한다.

즉, 객체의 양방향 연관관계에서는 양쪽 모두 관계를 맺어줘야한다.

 

 

 

연관관계 편의 메소드

양방향 연관관계는 양쪽 관계 다 신경써야한다.

member.setTeam(team); // 연관관계 주인 , member -> team
team.getMembers().add(member); // 주인이 아니므로 저장 시 사용되지 않음, team -> member

이런 식으로 각각 회원 -> 팀 설정, 팀 -> 회원 설정 호출을 하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨지는 경우도 있다.

그렇기에 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다.

public class Member {
    private Team team;
    
    public void setTeam(Team team) {
    	this.team = team;
        team.getMembers().add(this);
    }
    ...
}

Member 클래스에서 setTeam() 메서드에서는 보통 매개변수로 받은 team을 this.team에 넣어주고 끝난다.

여기서 매개변수로 받은 team의 Member에 현재 Member클래스를 추가하면 아까 보았던 두 코드를 합친것과 동일하다.

member -> team 설정을 하는 동시에 team -> member 설정을 하는 것이다.

 

 

연관관계 편의 메소드 작성 시 주의사항

member1.setTeam(teamA); // member1 은 teamA에 소속되었다.
member1.setTeam(teamB); // member1 은 teamB에 소속되었다.
Member findMember = teamA.getMember(); // teamA에서는 여전히 member1이 조회된다.

member1이 teamA에 소속되었다가 teamB에 소속되었다.

우리는 회원은 하나의 팀에만 소속된다는 것을 알고 있다.

그렇지만 teamA에서 조회하면 member1이 여전히 조회된다.

이렇기에 teamB로 변경할 때 teamA -> member1 관계를 제거해야한다.

public void setTeam(Team team) {
    // 기존 팀이 있을 시 관계 제거
    if(this.team != null) {
    	this.team.getMember().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}

영속성 컨텍스트가 살아있는 상태에서 teamA를 조회해서 member1이 나온 경우이다.

그러므로 teamB로 바꾼 후 새로운 영속성 컨텍스트에서 teamA를 조회하면 나오지 않는다.

하지만 안전하려면 위와 같이 관계를 제거하는 것이 안전하다.

 

 


자바 ORM 표준 JPA 프로그래밍 / 김영한 지음 / 에이콘출판주식회사 출판

자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한 / 인프런 강의