뭐라도 끄적이는 BLOG

JPA 05.02 - 양방향 연관관계와 연관관계의 주인 본문

Java/JPA

JPA 05.02 - 양방향 연관관계와 연관관계의 주인

Drawhale 2021. 4. 16. 10:39

양방향 연관관계

이제 팀에서도 회원으로 접근할 수 있도록 양방향 연관관계로 매핑해 보겠습니다. 먼저 객체 연관관계를 살펴보겠습니다. 회원과 팀은 다대일 관계이며 반대로 팀에서 회원은 일대다 관계인 것을 볼 수 있습니다. 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 합니다. 그러므로 Team.members를 List 컬렉션으로 추가하였습니다.

 

데이터베이스 테이블은 이전과 같이 외래 키 하나로 양방향으로 조회할 수 있으므로 추가할 내용은 전혀 없습니다.

 

양방향 연관관계 매핑

@Entity
@Getter
@Setter
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String name;

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

회원 엔티티에는 변경한 부분이 없습니다.

 

@Entity
@Getter
@Setter
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

팀엔티티에 컬렉션인 List<Member> members를 추가하였습니다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했습니다. mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 됩니다. 반대쪽 매핑이 Member.team이므로 team을 값으로 주었습니다.

 

Member findMember = em.find(Member.class, member.getId());

Team findTeam = findMember.getTeam();
List<Member> members = findTeam.getMembers();
members.stream().forEach(m-> System.out.println(m.getName()));

이제 team에서 member들을 찾아올 수 있습니다. 반대방향으로 객체 그래프를 탐색할 수 있게 되었습니다.

findMember 전 Member를 생성하고 team을 주는 부분이 있다면 flush와 clear를 해주어야 합니다. 해주지 않으면 1차 캐시의 Team에는 아직 Member의 정보가 담겨 있지 않아 출력이 되지 않습니다. 그리고 Lombok을 사용하고 있다면 @ToString을 조심해 주셔야 합니다.

 

연관관계의 주인

@OneToMany는 이해가 쉬우나 문제는 mappedBy 속성입니다. 단순이 @OneToMany만 쓰는것이 아니라 mappedBy가 필요한 이유를 알아보겠습니다.

 

엄밀히 이야기 하면 객체에는 양방향 연관관계라는 것이 없습니다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐입니다. 반면 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있습니다. 

객체의 연관관계 테이블 연관관계
회원 → 팀
팀 → 회원
회원 ↔ 팀

위 표와 같이 양방향 연관관계를 관리하는 포인트는 객체는 2곳 테이블은 1곳입니다. 이런 차이로 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인(Owner)이라고 합니다.

 

양방향 매핑의 규칙 : Owner

양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다. 반면 주인이 아닌쪽은 읽기만 할 수 있습니다. 연관관계의 주인으로 정할지는 mappedBy 속성을 보고 알 수 있습니다.

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

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것입니다.

연관관계의 주인은?

연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 합니다. 여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 됩니다. 주인이 아닌 Team.members에는 mappedBy="team"으로 주인이 아님을 설정해 주어야 합니다.

만약 Team.members를 선택하게 된다면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 합니다. 이 경우 Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야 할 외래 키는 MEMBER 테이블에 있기 때문입니다.

연관관계 주인은 비즈니스적으로 중요한 것이 아닙니다. 단순히 테이블쪽에서 N쪽이 연관관계의 주인이 되야 합니다.

 

DB에선 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가집니다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없습니다. 따라서 @ManyToOne에는 mappedBy속성이 없습니다.

주의점

연관관계의 주인이 아닌 객체에 관계에 해당하는 값을 넣으면 예상하지 못한 일이 발생할 수 있습니다.

Member member = new Member();
member.setName("member1");
em.persist(member);

Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);

위 코드처럼 연관관계의 주인이 아닌 Team에 member를 넣었을때 Member의 외래키가 제대로 들어가지 않는 상황이 발생합니다. mappedBy로 설정된 관계는 JPA에서 읽기 전용이기 때문에 설정에 아무런 영양을 끼치지 않아 이런 상황이 발생한 것입니다.

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);

team.getMembers().add(member);

 

이렇게 양쪽으로 값을 입력해주는 것은 가능합니다. 연관관계의 주인만 값을 입력해주는것 보다 이 방법이 더 안전하게 객체를 사용할 수 있습니다. 이렇게 양쪽에 값을 세팅해 주지 않고 flush clear까지 하지 않는다면 1차 캐시에 있는 team.members에는 값이 아무것도 들어있지 않아 사용할 수 없는 상태가 되어버리기 때문입니다. 결론적으로 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정해야 합니다.

 

아예 setTeam을 할때 team.members에 값을 집어넣는 메소드를 만드는 것도 한 방법입니다. 이러면 깜빡하고 한쪽만 값을 넣는 불상사를 줄일 수 있습니다.

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

setTeam메소드에 이렇게 넣어도 되지만 getter, setter관례때문에 다른 이름으로 변경하는것도 좋은 방법입니다.

public void changeTeam(Team team){
    this.team = team;
    team.getMembers().add(this);
}

team.members에 중복이 있는지 체크를 할 수 도 있습니다. Team에 이런 편의 메서드를 넣어도 상관없지만 둘중 하나만 사용하시길 권장 드립니다. 상황에 따라 둘중 하나를 정하면 됩니다.

 

그리고 양방향 매핑시 무한 루프를 조심해야합니다.

toString(), lombok, JSON 생성 라이브러리에서 이러한 오류를 발생시킬 수 있는 코드를 만들어 버릴수 있습니다. JSON생성 라이브러리의 경우 엔티티를 직접 Response로 보내줄때 문제가 발생합니다. 그래서 컨트롤러에서 엔티티를 직접 반환하면 안됩니다.

엔티티를 직접 반환하면 안되는 또다른 이유로 엔티티가 변경이 되면 API자체가 변경되어 버리기 때문이기도 합니다. 컨트롤러에서 반환을 하는 객체는 DTO로 따로 만들어서 반환하는 것이 좋습니다.

정리

JPA에서 설계는 단방향 매핑만으로 이미 연관관계 매핑은 완료가 된상태입니다. 처음 JPA설계를 할때 단방향 매핑만으로 모든 설계를 끝내는 것이 좋습니다. 양방향 매핑은 반대 방향으로 조회 기능이 추가가 되는것 뿐입니다. 객체 입장에서도 양방향으로 설계해서 좋을건 없습니다.

단방향 매핑을 잘 하고 나중에 필요하면 양방향(JPQL 포함)을 추가해도 됩니다. 이는 테이블에 영향을 주지 않기 때문에 개발 중 언제든지 가능합니다.

 

연관관계의 주인을 정하는 기준은 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됩니다. 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 합니다.

이 방식을 사용한다면 고민할 거리가 줄어들게 됩니다.


※ 참고 자료

 

 

자바 ORM 표준 JPA 프로그래밍 - 교보문고

자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA 기초 이론과 핵심 원리, 그리고

www.kyobobook.co.kr

 

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

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

www.inflearn.com

 

Documentation - 5.4 - Hibernate ORM

Idiomatic persistence for Java and relational databases.

hibernate.org

 

반응형