연관관계의 종류
- Many-To-One
- One-To-Many
- One-To-One
- Many-To-Many
DB 테이블과 객체의 차이
- DB 테이블
- 외래 키 컬럼을 생성하고 외래 키와 조인을 사용해서 연관된 테이블을 조회
- 객체
- 연관된 객체를 선언하고 참조를 통해 객체를 조회
객체의 방향성
설명
- DB에선 방향이라는 개념이 존재하지 않는다
SELECT * FROM member JOIN team ON member.team_id = team.id;
SELECT * FROM team JOIN member ON member.team_id = team.id;
- 두 SQL은 동일한 결과를 표시
- 객체는 방향이라는 개념이 존재한다(단방향 참조, 양방향 참조)
- 단방향 참조
- Member에
getTeam()
메소드가 존재하지만 Team에는 getMembers()
메소드가 존재하지 않는 경우
- Team에
getMembers()
메소드가 존재하지만 Member에는 getTeam()
메소드가 존재하지 않는 경우
- 양방향 참조
- Member에
getTeam()
메소드가 존재하고 Team에도 getMembers()
메소드가 존재하는 경우
- 서로 다른 단방향이 2개 있는 것(반대 방향으로도 조회할 수 있는 기능이 추가된 것)
- 단방향 매핑만 해도 연관관계 매핑은 완료된다
단방향(Unidirectional)
- 연관된 두 Entity 중 하나의 Entity에서만 상대편을 참조(컴포지션)하고 있는 상태
- 예시
- Many 객체(Member)에서만 One 객체(Team) 조회 가능
- One 객체(Team)에서만 Many 객체(Member) 조회 가능
양방향(Bidirectional)
- 연관된 두 Entity에서 서로 상대편을 참조(컴포지션)하고 있는 상태
- 예시
- Many 객체(Member)에서도 One 객체(Team)를 조회할 수 있고, One 객체(Team)에서도 Many 객체(Member) 조회 가능
1. 양방향 연관관계의 참조측(referencing side), 소유측(owning side)
- 양방향 매핑일 때
(mappedBy = "소유측 Entity에 선언된 참조측 Entity의 변수명")
으로 참조측과 소유측 설정 가능
- 예시:
@OneToMany(mappedBy = "team")
mappedBy
를 선언하더라도 DB에 영향을 미치지 않음
참조측(referencing side)
mappedBy
가 선언된 Entity
- 양방향 연관관계의 참조측은 Read만 가능
getMembers().get(0)
을 수정 후 teamRepository.save(team)
하더라도 DB에 반영되지 않음
소유측(owning side)
- 양방향 연관관계의 소유측은 CRUD 전부 가능
getMembers().get(0)
을 수정 후 memberRepository.save(member)
하면 DB에 반영됨
- JPA에서 외래 키를 관리(등록, 수정)하는 Entity
- 일반적으로 외래 키를 가지고 있는 Many Entity를 연관관계의 주인으로 설정한다
2. 양방향 매핑에서 주의할 점
1. 1차 캐시
Team team = new Team();
team.setName("한국");
Member member = new Member();
member.setName("A");
member.setTeam(team);
// 영속성 컨텍스트에 저장
entityManager.persist(member);
entityManager.persist(team);
// 쿼리하면 DB가 아닌 영속성 컨텍스트의 1차 캐시에서 가져온다
Team foundTeam = entityManager.find(Team.class, team.getId());
List<Member> members = foundTeam.getMembers();
// Member A가 있는데도 불구하고 1이 아닌 0이 출력된다
System.out.println(members.size());
- Member와 Team을 영속성 컨텍스트에 저장한 뒤(트랜잭션 커밋되지 않은 상태)
find()
메소드로 조회하면 1차 캐시에서 조회하게 된다
member.setTeam()
메소드를 사용했기 때문에 member.team을 가져올 수 있지만, team.members에는 아무 것도 들어있지 않다
- team.members에 값을 저장하는 방법
member.setTeam(team)
과 함께 team.getMembers().add(member)
를 실행한다
- Member의
setTeam(Team team)
메소드 안에 team.getMembers().add(this)
코드를 추가하면 편하게 사용 가능
- 이 방법을 사용한다면 기본 setter 메소드는 네이밍 때문에 헷갈릴 수도 있으므로
setTeamBidirectional()
같은 새로운 메소드를 생성해서 사용하는 것을 권장한다 (하단의 예시1 참고)
- Member의
setTeam(team)
을 사용하지 않고 Team에 addMembersBidirectional()
메소드를 생성 후 member.setTeam(this)
와 this.members.add(member)
코드를 추가해서 사용해도 된다 (하단의 예시2 참고)
- 위의 메소드는 Member와 Team에서 중복으로 사용하면 버그가 발생할 수 있으므로 Member에 생성할지 Team에 생성할지 하나만 선택해서 사용한다
entityManager.clear()
로 영속성 컨텍스트를 비우면 find()
메소드는 1차 캐시가 아닌 DB에서 가져오기 때문에 team.members에 값이 들어있는 것을 확인할 수 있다
- 영속성 컨텍스트의 내용을 비우기 전
entityManager.flush()
나 entityManager.commit()
메소드로 먼저 DB에 반영 필수
- 예시1
@Entity
public class Member {
/* ... */
public void setTeamBidirectional(Team team) {
this.team = team;
team.getMembers.add(this);
}
}
@Entity
public class Team {
/* ... */
public void addMembers(Member member) {
member.setTeam(this);
this.members.add(member);
}
}
2. 순환 참조
- 양방향 매핑에서
toString()
재정의
- Many 객체(Member)와 One 객체(Team)에 둘 다
toString()
을 재정의하면 순환참조에 의해 StackOverFlow가 발생할 수 있다
- 최소한 한 쪽은
toString()
에서 반대편 객체를 매핑하지 않도록 주의: @ToString(exclude = "members")
- 양방향 매핑에서 JSON 생성 라이브러리
- 객체를 JSON으로 변환하는 경우 다음과 같은 오류가 발생한다
- Team 안에
members
객체가 있으므로 연관된 모든 Member 객체를 찾는다
- Member의 Team을 확인하는 순간 1번으로 돌아가서 다시 그 Team의 모든 Member 객체를 찾는다
- 무한 반복
- Spring은 REST Controller에서 jackson 라이브러리가 객체를 자동으로 JSON으로 변환하기 때문에 Controller에서 Entity를 그대로 응답하지 않도록 한다(DTO 사용)
3. 양방향 매핑 사용 여부
- 양방향 매핑은 코드상으로 확인하기 어려운 신경써야 할 부분이 많기 때문에 웬만하면 양방향 매핑은 사용하지 않는 것을 권장하며, 꼭 필요한 경우만 사용한다