
Jpa 공부하면서 정말 어려웠던 개념이 몇가지 있는데 그 중 하나가 연관관계 매핑이다.
jdbcTemplate, mybatis를 사용한 경험이 있는 나는 fk를 이용해 테이블들의 연관된 정보를 가져오는 것에 익숙해져있었기 때문이다. 그러나 Jpa 연관관계를 익히기 위해선, 객체들 간의 관계와 테이블 간의 관계의 차이점을 잘 알아야 한다.
테이블과 객체의 차이
- 테이블은
외래키(fk)로 join을 사용하여 연관된 테이블을 찾는다. - 객체는
참조를 사용하여 연관된 객체를 찾는다.
여기 예시를 보면 더 잘 이해가 될 것 이다.
[ 객체 연관관계 ] 단방향이다.
| Member |
| id |
| Team team |
| username |
↓
| Team |
| id |
| name |
[ 테이블 연관관계 ]
| member |
| member_id (pk) |
| team_id (fk) |
| username |
| team |
| team_id (pk) |
| name |
Member는 Team에 속해있는 멤버들 중 한명이다.
Member는 Team 하나를 가질 수 있지만, Team은 Member 여러명을 가질 수 있다.
테이블 연관관계 매핑
jdbcTemplate이나 mybatis를 이용하여 테이블 데이터를 조회할때는 아래와 같이 테이블 연관관계를 그대로 이용하여 클래스를 만들었다.
public class Member {
private Long id;
private String username;
private Long teamId;
}
public class Team {
private Long id;
private String name;
}
그래서 member의 팀을 조회하고 싶다면 member의 teamId를 이용하여 다시 조회해야 한다.
테이블 연관관계를 통한 team 조회
Member findMember = em.find(Member.class, member.getId());
Long teamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, teamId);
이를 객체 지향 모델링으로 변경한것이 Jpa에서의 연관관계이다.
Jpa(객체) 연관관계 매핑
단방향 매핑
public class Member {
private Long id;
private String username;
@ManyToOne
@JoinColumn( name = "team_id")
private Team team;
}
team필드 외의 다른 필드는 편의상 jpa 컬럼들을 붙여주지 않았다.
Jpa에서의 단방향 연관관계 매핑은 이렇게 한다.
- 먼저 Member클래스에서 Team team 자체 필드를 선언한다.
- Member : Team = m : 1 관계이기 때문에
@ManyToOne을 붙여준다. - 그리고
@JoinColumn을 명시해 준다. 안붙여줘도 동작하지만, default값이 별로이기 때문에 직접 명시해주는 것이 좋다. team_id라는 테이블에서의 fk라고 생각하면 된다.
여기까지가 단방향 매핑이다. Member에서 Team을 조회할 수 있지만, Team에서는 Member를 조회할 수 없다.
단방향 매핑을 통한 team 조회
Member member = new Member();
Team team = new Team();
em.persist(team);
member.setTeam(team);
em.persist(member);
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
이렇게 참조를 이용하여 member에서 team을 바로 조회할 수 있다.
사실상 단방향 조회만 필요할 때가 많다. 그렇기 때문에 양방향 조회가 필요하지 않는다면 단방향 매핑 하나만 사용하는 것이 좋다.
그런데 Member에서 Team을 조회하는 것 + Team에서도 그 team에 속해있는 Member들을 조회하고 싶을 수가 있다.
그럴때 양방향 매핑(= 단방향 매핑 2개임)을 사용한다.
양방향 매핑 (단방향 2개)
양방향 매핑은 어떻게 할 수 있을까?
Member 엔티티는 단방향과 동일하다. 그리고 Team엔티티에 아래와 같이 컬렉션을 추가해주면 된다.
public class Team {
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
Team은 멤버를 여러개 가질 수 있고, Member는 team 하나만 가질 수 있기 때문에
@OneToMany 어노테이션을 붙여준다. 그리고 mappedBy 속성을 통해 Member의 team 필드와 매핑된다고 명시해주면 된다.
그러면, team에서도 member의 리스트를 조회하는 것이 가능해진다.
양방향 매핑을 통한 team 조회
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
List<Member> members = findTeam.getMembers();
mappedBy과 연관관계 주인
여기서 왜 굳이 mappedBy라는 속성이 필요한지 의문이 생긴다.
그냥 @OneToMany적고 끝내면 되는거 아닌가?
여기서 다시 객체와 테이블이 연관관계를 맺는 차이에 대해 상기해보자
객체 연관관계
- 회원 -> 팀 연관관계 1개 (단방향)
- 팀 -> 회원 연관관계 1개 (단방향)
테이블 연관관계
- 회원 <-> 팀의 연관관계 1개 (양방향)
객체는 단방향 매핑 2개가 필요하지만 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리할 수 다.
객체의 양방향 관계를 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 객체를 양방향으로 참조하려면 단방향 연관관계 2개가 필요하다.
둘 중 하나로 외래키를 관리해야 한다.
member.setTeam(teamA)를 하면 team이 업데이트 될 것이고, team.getMembers().add(memberA)해도 업데이트 될것이다. 이 둘중 무엇을 믿어야 할까.
둘 중 하나를 주인으로 만들고, 나머지는 조회만 하게 하도록 룰을 정했다.
Member의 team과, Team의 members 중 하나만 값을 업데이트할 수 있다.
양방향 매핑 규칙
- 객체의 두 관계 중
하나를 연관관계의주인으로 지정 - 연관관계의 주인만이 외래키를 관리(등록, 수정 가능)
- 주인이 아닌 쪽은 읽기(조회)만 가능
- 주인은 mappedBy속성 사용 X
- 주인이 아니면 mappedBy 속성으로 주인 지정
위의 예시에서는 Team.members필드에 mappedBy 속성을 사용했으므로 members를 통한 수정은 무시된다. DB 업데이트에 영향을 줄 수 없다.
누구를 주인으로 ?
외래키가 있는 곳을 주인으로 정한다. 여기서는 Member.team이 연관관계의 주인이다.
잘못된 예시 (주의할점)
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
em.persist(member);
이렇게 하면 등록된 member1의 team_id값은 null값이 들어간다. 즉 team값이 안들어간 것이다.
member.setTeam(team) 이 코드를 통해 주인으로 등록된 Member.team을 통해 값을 등록해야지 제대로 값이 들어간다.
수정된 예시
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); //이 코드를 통해 연관관계가 매핑이 완료된다.
team.getMembers().add(member); //이코드는 안써도 DB상 문제는 없지만, 객체지향 개념에 맞게 써준다.
em.persist(member);
자바코드를 쓸때는 둘다 쓴다. 그것이 객체지향 개념에 맞기 때문이다. member에 team을 설정한다면, team에도 member를 추가하는 것이 이치에 맞다. 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다는 것이다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다. 어차피 team에 member를 추가하는 것은 DB상으로는 무시될 것이기 때문에 실제 코드를 짤때는 양쪽 다 작성해주는 것을 권장한다.
연관관계 편의 메서드
양방향 연관관계는 결국 양쪽 다 신경 써야 한다. member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.
그래서 Member 클래스의 setTeam() 메서드를 수정해서 setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 할 수 있다.
public class Member{
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
// ...
}
이렇게 수정한 메서드를 사용하는 코드를 보겠습니다.
public void test() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member("member2", "회원1");
member2.setTeam(team1);
em.persist(member2);
}
이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라고 한다.
연관관계 편의 메서드 작성 시 주의할 점
member.setTeam(team1);
member.setTEam(team2);
Member findMember = teamA.getMember(); // member1이 여전히 조회된다.
teamB로 변경할 때 teamA ➡️ member1 관계를 제거하지 않았기 때문에 teamA.getMember() 메서드를 실행했을 때 member1이 남아있다. 따라서 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
Member 클래스 setTeam() 메서드
public void setTeam(Team team){
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
this.team은 member의 기존 team이다.
this.team이 null이 아니면 이 member객체는 team이 있음을 의미함으로, 해당 팀의 멤버에서 삭제시켜 준다.
'JPA' 카테고리의 다른 글
| [ JPA ] 사이드 프로젝트 중 만난 문제 (연관관계) (0) | 2023.02.03 |
|---|---|
| [ JPA ] 프로젝트 도중 만난 에러 ( feat. 트랜잭션 ) (0) | 2023.01.12 |
| [ Jpa ] 영속성 컨텍스트 (0) | 2022.12.08 |