이때까지 나도 테이블에서 외래키로 관리된 연관관계를 객체로 만들면 그 외래키를 VO에 만들어 쓰곤 했다..
내가 n년 동안 해왔던 게 사실은 객체지향적인 방법이 아니었다는 걸 알게 되었다.
JPA를 배우면 배울수록 이제야 알게 됐다는 게 아쉬울 뿐이다.
단방향 연관관계
테이블 구조는 그대로고, Member에 teamId가 아닌 Team 객체 자체로 연관관계를 설정한다.
(여러 명의 멤버가 한 팀에 소속될 수 있기 때문에 @ManyToOne이다.)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
이 구조로 member1의 소속 팀을 조회해 보자.
1. Team을 저장한다.
2. Member를 저장한다. 이때 Team을 같이 저장함으로써 단방향 연관관계를 설정한다.(JPA가 알아서 Team의 pk값을 찾아서 외래키로 넣어준다)
3. em.find로 Member를 조회한다.
4. 조회한 Member에서 바로 Team을 꺼낸다.
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//회원의 소속 팀을 조회할 때
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
여기서 member1의 팀을 TeamA -> teamB로 바꾸고 싶다면?
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
팀이 추가되더라도 Team을 꺼내서 수정하지 않고, Member에 set만 해주면 DB에 외래키 값이 update 된다.
양방향 매핑
양방향은 Team을 조회했을 때도 팀 소속 멤버들을 꺼낼 수 있도록 하는 것이다.
테이블 구조는 변하지 않는다.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name="TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); //관례로 ArrayList로 초기화해둠
}
Team 객체에서도 멤버 컬렉션을 추가해 연관관계를 양쪽으로 설정하는 것이다.
(Team 입장에선 본인 팀 1개당 여러 명의 member가 있으므로 @OneToMany이다.)
//회원 조회
Member findMember = em.find(Member.class, member.getId());
//팀 멤버 조회
List<Member> members = findMember.getTeam().getMembers();
이렇게 역방향으로 조회할 수 있다.
연관관계의 주인과 mappedBy
위에 Team.class의 @OneToMany(mappedBy = "team") 이 mappedBy가 뭘까?
이것을 이해하려면 객체와 테이블 간에 연관관계를 맺는 차이, 그리고 연관관계의 주인이라는 개념을 이해해야 한다.
객체와 테이블의 연관관계 차이
양방향 연관관계의 객체, 테이블 구조를 다시 보자.
굵은 글씨를 보면 객체의 연관관계는 2개다.
- 회원 → 팀 연관관계 1개(단방향)
- 팀 → 회원 연관관계 1개(단방향)
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
객체를 양방향으로 참조하려면 아래처럼 단방향 연관관계를 2개 만들어야 한다.
class A {
B b;
}
class B{
A a;
}
a.getB();
b.getA();
테이블의 연관관계는 1개다.
- 회원 ↔ 팀의 연관관계 1개(양방향)
테이블은 TEAM_ID라는 외래 키 하나로 두 테이블의 연관관계를 관리한다.
외래키 하나로 두 테이블의 연관관계를 관리한다? 테이블은 이 외래키 하나로 양쪽으로 조인할 수 있다는 것
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
연관관계의 주인
그러면 여기서 멤버를 바꾸고 싶거나 새로운 팀에 들어가고 싶을 때
Member의 team을 바꿔야 할까? Team의 members를 바꿔야 할까?
DB입장에선 외래키인 team_id 값만 바뀌면 된다. 이런 이유로 연관관계의 주인(Owner)을 지정해야 한다
누가 주인인가?
외래 키가 있는 곳을 주인으로 정해라.
= 여기서는 Member.team이 연관관계의 주인이다.
Team.members는 보통 주인의 반대편이라고 칭한다.
양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 외래 키가 있는 곳을 주인으로 정해라
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능.
- 주인은 mappedBy 속성 사용X, 반대편이 mappedBy 속성으로 주인 지정
양방향 매핑 시 가장 많이 하는 실수
양방향을 매핑할 때 주인의 반대편에만 연관관계를 설정하는 경우다.
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//멤버 저장
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 쪽)에만 연관관계 설정
team.getMembers().add(member); //**
em.persist(member);
이렇게 Team.members에만 member를 추가하면 DB에서 MEMBER 테이블을 조회했을 때 TEAM_ID에 null이 들어간다.
양방향 매핑 시 연관관계의 주인에 값을 입력해야 한다.
순수한 객체 관계를 고려하면 항상 양쪽에 값을 설정해야 한다. (객체의 연관관계는 단방향 관계 2개이므로)
//연관관계의 반대편에 설정
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team); //**
만약 주인(Member.team) 쪽에만 설정하고 반대편(Team.members)에는 하지 않는다면?
//team 저장
Team team = new Team();
team.setName("teamA");
em.persist(team);
//member 저장
Member member = new Member();
member.setUsername("member1");
member.setTeam(team); //주인에 team 설정
em.persist(member);
//team.getMembers().add(member); << 양쪽에 하지 않으면
//em.flush();
//em.clear();
//아무것도 조회되지 않음 = team은 순수 객체 상태기 때문
Team findTeam = em.find(Team.class, team.getId()); //1차 캐시에 없는 상태
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m.getUsername() = " + m.getUsername());
}
tx.commit();
중간에 쿼리를 날려주는 em.flush()를 주석처리한다면 Team.members는 설정해주지 않았기 때문에 1차 캐시에 없는 상태다. 그래서 Team의 members를 출력해도 아무것도 나오지 않는다.
이를 방지하기 위해 연관관계 편의 메서드를 생성하는 방법이 있다.
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void changeTeam(Team team) {
this.team = team;
//연관관계 편의 메서드
team.getMembers().add(this);
}
}
정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료 - DB에서 외래키 추가(단방향)만으로 연관관계 매핑이 된 것이므로..
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐
- JPQL에서 역방향으로 탐색할 일이 많다.
- 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 됨(테이블에 영향을 주지 않음)
[참고] 인프런 - 김영한 님의 자바 ORM 표준 JPA 프로그래밍-기본편 강좌를 보고 공부한 내용을 바탕으로 작성했습니다.