연관관계가 필요한 이유
![](https://blog.kakaocdn.net/dn/zvNiS/btsjRgJqFZq/q4TN17dUh0o5Vs2QOB5yK1/img.png)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
…
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
…
}
===
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
===
//조회
Member findMember = em.find(Member.class, member.getId());
//연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
member.setTeamId(team.getId());
처럼 외래키를 직접 설정해주어야한다.
- 또한,
Team findTeam = em.find(Team.class, team.getId());
처럼 식별자를 통해서 다시 조회해야 하므로 객체 지향적인 방법이 아니다.
단방향 연관관계
![](https://blog.kakaocdn.net/dn/lLnsX/btsjR7yDL7B/CfXXjZyKB7Vgx9gVkFeRy0/img.png)
![](https://blog.kakaocdn.net/dn/dr3IeB/btsjILqTCDg/rtD2MkXYNvIh004pqCHkKk/img.png)
public class Member {
@Id @GeneratedValue
private Long id;
@Column
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team 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();
member.setTeam(team);
처럼 팀을 세팅해주면, 자동으로 연관관계가 설정된다.
Team findTeam = findMember.getTeam();
처럼Member
를 한 번 가져오면,getter
를 통해서 객체지향적인 방법으로 연관객체를 가져올 수 있다.
양방향 연관관계
![](https://blog.kakaocdn.net/dn/dfrU14/btsjRigdqsW/Q6hO1XAKwxBJJjgjsKjbhk/img.png)
- 애초에 테이블에는 “양방향”이라는 개념이 없기 때문에, DB설계는 이전과 동일하다.
- 객체 연관관계만 양방향으로 바뀌었고,
Team
객체에members
속성이 추가되었다.
@Entity
public class Team{
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
...
}
===
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); // 역방향 조회
mappedBy
- 객체의 연관관계는 2개다.
- 회원 → 팀 연관관계 1개(단방향)
- 팀 → 회원 연관관계 1개(단방향)
- 객체의 양방향 관계는 양방향 관계가 아니라, 서로 다른 단방향 관계 2개를 양방향으로 표현한 것이다.
- 테이블의 연관관계는 1개다.
- 회원 ↔ 팀 연관관계 1개(양방향)
- 테이블은 외래 키(FK)하나로 두 테이블의 연관관계를 관리할 수 있다. 즉,
Member.team_id
하나로 양방향 연관관계를 갖는 것이다.
![](https://blog.kakaocdn.net/dn/b7jsrq/btsjQfRvmEQ/rvuixknL8LKLLxK09Fi671/img.png)
- 여기서 핵심은, Member의 team이 변경되었을 때 TEAM_ID(FK)가 변경되어야하는지, Team의 members가 변경되었을 때 TEAM_ID(FK)가 변경되어야 하는지 JPA는 알 수가 없다.
- 따라서, 둘 중 하나로 FK를 관리해야한다. FK가 아닌 나머지는 테이블에 영향을 주지 않는 데이터다.
연관관계 주인
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정해야한다.(위에 설명한 문제로 인해서)
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)할 수 있다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아닌 쪽은 읽기만 가능하다. 만약 등록, 수정할 경우 DB에 영향을 주지 않는다.
- 주인이 아닌쪽은 mappedBy 속성을 통해 주인을 지정해야한다.
class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
class Team {
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
주인을 누구로?
- 외래키가 있는 곳으로 주인을 정해야한다.
- 즉, “다”인 쪽이 주인이다.
연관관계 편의 메서드
필요성
Member member = new Member();
member.setName("member1");
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(member);
em.persist(team);
- 이렇게 짜면, 연관관계의 주인이 아닌
team
의members
속성을 수정한 것이므로 update가 되지 않는다.
Member member = new Member();
member.setName("member1");
Team team = new Team();
team.setName("TeamA");
member.setTeam(team);
em.persist(member);
em.persist(team);
- 따라서, 주인인
member
에setTeam
을 해주어야 FK update쿼리가 정상적으로 나가게 된다.
- 그렇다고, 관계의 주인에만 객체를 설정해주어도 문제가 발생한다.
- 1차 캐시에 team과 member가 모두 남아있기 때문에 em을 flush clear 해주지 않을 경우 team의 members 배열은 항상 비어있을 것이다.
- 따라서, 주인에만 값을 입력해주지 말고, 객체지향적인 관점에서 양쪽 모두 값을 입력해주는 것이 맞다.
- 어차피 mappedBy로 매핑된 필드는 jpa가 무시한다.
사용
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
- 이렇게, 양방향 매핑인 경우에 양쪽에 모두 추가해주는 편의성 메서드를 추가해주는게 좋다.
- 다만, 양쪽에 모두 연관관계 편의 메서드를 추가하는 것은 무한루프에 걸릴 수 있다.
- 어느 쪽에 할 지는 상황에 따라서 정해야한다.
무한루프 주의
- Lombok, JSON 생성 라이브러리 등을 통해서
toString()
을 자동 생성할 경우 무한루프에 빠지게 된다.
@Setter
public class Member{
@Override
public String toString(){
return "Member{" +
"id=" + id +
", team=" + team +
", name='" + name + '\'' +
", city='" + city + '\'' +
", street='" + street + '\'' +
", zipcode='" + zipcode + '\'' +
'}';
}
}
public class Team {
@Override
public String toString(){
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
", members=" + members +
'}';
}
}
- 여기서 Member.toString()을 호출하면, team.toString()을 호출하게 되고, team.toString()은 다시 member.toString()을 호출하게 된다.
- 따라서, toString()은 라이브러리로 만들지 말고, 직접 코딩하는 것이 좋다.
- controller에서 JSON형태로 변환할 때 객체 자체를 반환하지 말아야 한다.
- 객체 자체를 반환하면 자동으로 JSON으로 직렬화 해주기 때문에 문제를 바로 파악하기 힘들다.
양방향 매핑 정리
- 단방향 매핑으로만 설계해야한다. 객체든, 테이블이든 무조건 단방향으로 설계하자.
- 테이블을 모두 단방향으로 설계하고, 이후에 필요할 때 객체를 반대 방향으로도 매핑해주어서 양방향으로 만들어주자. 이렇게 하더라도 테이블 설계에 영향을 주지 않는다.
- 비지니스 로직으로 연관관계의 주인을 정하지 말자. 연관관계의 주인은 외래키의 위치로 정하자.
Uploaded by N2T
(23.06.11 16:47)에 작성된 글 입니다.