[JPA] 연관관계 기초
연관관계가 필요한 이유
연관관계가 필요한 이유를 말하기 앞서 간단한 예제 시나리오를 만들어보겠다.
예제 시나리오
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체를 테이블에 맞추어 모델링(연관관계가 없는 객체)
(참조 대신에 외래키를 그대로 사용한다.)
Member.java
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String username;
@Column(name = "TEAM_ID")
private Long teamId;
}
Team.java
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
JpaMain.java
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("memberA");
member.setTeamId(team.getId()); // teamId를 직접 설정
em.persist(member);
em.flush(); // 플러시 발생시킴
em.clear(); // 영속성 컨텍스트 초기화
// 1. 회원 객체 조회
Member findMember = em.find(Member.class, member.getId());
// 2. 회원의 팀 ID 조회
Long findTeamId = findMember.getTeamId();
// 3. 팀 객체 조회
Team findTeam = em.find(Team.class, findTeamId);
회원의 팀 객체를 조회하기 위해선 회원 객체를 조회한 후 회원의 팀 ID를 조회하고 회원 팀 객체를 조회해야 한다.
이렇게 되면 DB를 통해 계속 데이터를 조회해야 하므로 이것은 매우 객체지향스럽지 않다.
결국, 객체를 테이블에 맞추어 데이터 중심으로 모델링하면 협력관계를 만들 수 없다.
- 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다.
단방향 연관관계
객체 지향 모델링(객체 연관관계 사용)
Member 엔티티에 Team 엔티티 매핑
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String username;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
- @ManyToOne : 회원 입장에서 하나의 팀에 여러 회원이 존재할 수 있으므로 다대일 관계이다. 이것을 @ManyToOne어노테이션으로 매핑한다.
- @JoinColumn : Team 엔티티의 어떤 컬럼과 매핑할지 명시한다.(외래키를 지정한다.) 여기서는 Team 테이블의 TEAM_ID 컬럼과 매핑된다.
ORM매핑
이제 위 그림과 같이 객체를 통한 연관관계가 성립하게 된다.
JpaMain.java
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("memberA");
member.setTeam(team); // team 객체 그대로 설정
em.persist(member);
em.flush(); // 플러시 발생시킴
em.clear(); // 영속성 컨텍스트 초기화
// 1. 회원 객체 조회
Member findMember = em.find(Member.class, member.getId());
// 2. 회원의 팀 ID 조회
//Long findTeamId = findMember.getTeamId();
// 3. 팀 객체 조회
//Team findTeam = em.find(Team.class, findTeamId);
Team findTeam = findMember.getTeam();
객체지향 모델링을 통해 Member 엔티티에서 Team을 조회할 수 있게 되었다.
양방향 연관관계와 연관관계의 주인
양방향 매핑
이전 예제에서 Member 클래스에 Team객체를 추가하고 @ManyToOne 어노테이션을 설정하여 Member 엔티티에서 Team 엔티티를 조회할 수 있도록 연관관계를 매핑하였다. 그렇다면 반대로 Team 엔티티에서 Member 엔티티를 조회할 수 있을까? 지금은 불가능하다.
데이터베이스에서는 외래키만 설정해 준다면 MEMBER 테이블과 TEAM 테이블에서 양쪽으로 join 하여 양방향으로 조회가 가능하다.
하지만 객체에서는 불가능하다.
객체에서 양방향 연관관계를 맺으려면, 즉 Team엔티티에서 Member 엔티티를 조회하려면 Team 엔티티에 List에 member 엔티티를 추가해줘야 한다.
Team 엔티티에 Member 엔티티 매핑
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Member클래스의 관계 맺어져있는 변수명
private List<Member> members = new ArrayList<>(); // 초기화해두는 것은 관례이다. add할때 NPE방지
}
- @OneToMany(mappedBy = "team") : 팀 입장에서 하나의 팀에 여러 회원이 존재할 수 있으므로 일대다 관계이다. 이것을 @OneToMany어노테이션으로 매핑한다. mappedBy 에는 매핑할 엔티티에 관계가 맺어져 있는 변수명을 작성한다.
- private List<Member> members = new ArrayList<>() : ArrayList로 초기화해두는 것은 관례이다. 추후 NPE를 방지할 수 있다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("memberA");
member.setTeam(team); // team 객체 그대로 설정
em.persist(member);
em.flush(); // 플러시 발생시킴
em.clear(); // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
System.out.println("member : "+m.getUsername()); // memberA 출력
}
이제 Member에서 Team으로, Team에서 Member로 조회할 수 있다. 이것이 양방향 연관관계이다.
그런데 Member 엔티티에는 @joinColumn으로 작성해 줬는데 Team엔티티의 mappedBy는 무엇일까?
⭐️연관관계의 주인과 mappedBy⭐️
객체와 테이블이 관계를 맺는 차이
객체는 연관관계가 2가지가 있다.
- Member → Team (단방향)
- Team → Member (단방향)
테이블은 연관관계가 1가지이다.
- Member ↔ Team (양방향)
객체의 양방향 연관관계
객체의 양방향 관계는 사실 양방향관계가 아니라 서로 다른 단방향 관계 2개이다. 이것을 우리는 억지로 양방향이라고 부르는 것이다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
테이블의 양방향 연관관계
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가진다.(양쪽으로 JOIN 할 수 있다.)
잠시 그림을 보자.
만약 Member가 새로운 팀에 들어가고 싶다면, Member 엔티티의 Team 엔티티를 바꿔야 할지, Team에 있는 Members를 바꿔야 할지 모른다. 하지만 DB입장에서는 객체가 참조를 어떻게 하든 MEMBER 테이블의 외래키인 TEAM_ID값만 바뀌면 된다.
그래서 객체에서는 둘 중 하나에서 외래키를 관리해야 한다. Member 엔티티에서 외래키를 관리할지, Team 엔티티에서 외래키를 관리해야 할지. 이것이 바로 연관관계의 주인(Owner)이다.
연관관계의 주인(Owner)
양방향 매핑규칙
- 객체의 두 관계 중 하나를 연관관계 주인으로 지정
- 연관관계의 주인만이 외래키를 관리(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성 사용 X (~에 의해서 매핑이 되었다고 이해하자)
- 주인이 아니면 mappedBy 속성으로 주인 지
누구를 연관관계 주인으로 정할까?
- 외래키가 있는 곳을 주인으로 정해라. == 다(Many) 쪽을 연관관계 주인으로 정해라.
- 여기서는 Member.team이 연관관계주인
헷갈리면 Member가 변경되었을 경우 어떤 테이블이 UPDATE 되어야 할지 생각해 보자.
Member의 Team이 변경되면 MEMBER 테이블의 TEAM_ID가 UPDATE 되어야 한다.
반대입장으로 생각했을 때 Team의 Member가 변경되더라도 MEMBER 테이블의 TEAM_ID가 변경되어야 한다.
그렇기 때문에 객체에서 연관관계의 주인은 Member가 되어야 한다.
결국에 DB에 외래키가 있는 곳을 주인으로 정하면 된다.
ex) 실생활에 비유 → 자동차와 바퀴가 있다고 가정할 때 바퀴를 연관관계의 주인으로 정하면 된다.
양방향 매핑 시 가장 많이 하는 실수(연관관계의 주인에 값을 입력하지 않음)
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("memberA");
// 역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
해당 코드는 역방향만 연관관계를 설정하였다.
연관관계의 주인인 Member 엔티티에 Team 엔티티를 입력하지 않았기 때문에 MEMBER테이블의 TEAM_ID는 null이 들어간다.
양방향 매핑 시 연관관계의 주인에 값을 입력해야 한다.(순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다.)
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team); //**
em.persist(member)
연관관계의 주인에 값을 설정 시 TEAM_ID 값이 들어가는 것을 확인할 수 있다.
그러면 연관관계의 주인에 값만 설정하면 되는데 ( → member.setTeam(team) )
왜 역방향도 값을 설정할까? ( → team.getMembers().add(member) )
JPA 입장에서 보면 잘 동작은 하지만 객체 지향적으로 보면 양쪽에 값을 넣어주지 않았으므로 한쪽에만 값이 존재하기 때문이다.
순수한 객체관계를 고려하여 항상 양쪽 다 값을 입력해 주자.
양방향 연관관계 주의
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.
- 연관관계 편의 메서드를 생성하자
@Entity
public class Member {
//...
/* 연관관계 편의 메서드 */
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
또는
@Entity
public class Team {
//...
/* 연관관계 편의 메서드 */
public void addMember(Member member) {
member.setTeam(this);
members.add(member);
}
}
연관관계 편의 메서드를 한쪽에만 생성하고 객체를 둘 다 설정하도록 메서드를 작성함으로써 연관관계 편의를 제공할 수 있다.
* 연관관계 편의메서드는 꼭 한쪽에만 생성하자. 양쪽으로 생성 시 잘못하여 무한루프 될 수 있다.
* 메서드명을 setXXX로 지어주지 말자.. setter의 관례 때문에 문제가 될 수 있다.
- 양방향 매핑 시 무한 루프를 조심하자
- 예: toString(), lombok, JSON 생성 라이브러리
ex) toString() 양쪽 생성 시 무한호출..
- 예: toString(), lombok, JSON 생성 라이브러리
양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐
- JPQL에서 역방향으로 탐색할 일이 많음
- 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 됨(테이블에 영향을 주지 않음
연관관계의 주인을 정하는 기준
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안 된다.
- 연관관계의 주인은 외래키의 위치를 기준으로 정해야 한다.
해당 글은 인프런의 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 정리한 내용입니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com