이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.
객체 연관관계
Member.team
) : 다대일(N:1) 관계Team.members
) : 일대다(1:N) 관계테이블 연관관계
TEAM_ID
) 하나로 양방향 조회 가능 → 처음부터 양방향 관계MEMBER JOIN TEAM
)TEAM JOIN MEMBER
)@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
//Getter, Setter ...
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
//==추가==//
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
//Getter, Setter ...
}
@OneToMany
속성 | 설명 | 기본값 |
---|---|---|
mappedBy | ◾ 연관관계의 주인 필드 선택 ◾ 값으로 반대쪽 매핑의 필드 이름 제공 | |
fetch | 글로벌 패치 전략 설정 | FetchType.LAZY |
cascade | 속성 전이 기능 사용 | |
targetEntity | ◾ 연관된 엔티티의 타입 정보 설정 ◾ 거의 사용하지 않음 ◾ 컬렉션 사용해도 제네릭으로 타입 정보 알 수 있음 |
public void biDirection() {
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); //팀 → 회원
//객체 그래프 탐색
for(Member member : members) {
System.out.println("member.username = " + member.getUsername());
}
}
//==결과==//
//member.username = 회원1
//member.username = 회원2
객체 연관관계
테이블 연관관계
엔티티를 양방향 연관관계로 설정 시 객체의 참조는 둘인데 외래 키는 하나
→ 둘 사이에 차이 발생
JPA에서 연관관계의 주인(Owner)을 지정
💡 연관관계의 주인(Owner)
연관관계를 맺고 있는 두 객체 중 테이블의 외래 키를 관리하는 객체
주인 | 주인x | |
---|---|---|
기능 | ◾ 데이터베이스 연관관계와 매핑 ◾ 외래 키 관리(등록, 수정, 삭제) | 읽기만 가능 |
mappedBy 사용 | X | O |
💡 연관관계의 주인 결정 = 외래 키 관리자 선택
class Member {
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
class Team {
@OneToMany
private List<Member> members = new ArrayList<Member>();
class Team {
@OneToMany(mappedBy="team") //MappedBy 속성의 값은
//연관관계의 주인인 Member.team
private List<Member> members = new ArrayList<Member>();
}
- 데이터베이스 테이블의 다대일(N:1), 일대다(1:N) 관계에서는 항상 다(N)쪽이 외래 키를 가진다.
- 다(N) 쪽인
@ManyToOne
은 항상 연관관계의 주인이 됨
→mappedBy
설정 불가(mappedBy
속성이 없는 이유)
public void testSave() {
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //연관관계 설정 member1 → team1
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //연관관계 설정 member2 → team1
em.persist(member2);
}
SELECT * FROM MEMBER;
MEMBER_ID | USERNAME | TEAM_ID |
---|---|---|
member1 | 회원1 | team1 |
member2 | 회원2 | team1 |
TEAM_ID
외래 키에 팀의 기본 키 값 저장된 상태Member.team
양방향 연관관계 설정 후 연관관계의 주인이 아닌 곳에만 값을 입력하지 않도록 주의한다.
public void testSaveNonOwner() {
//회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
//주인이 아닌 곳만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
}
SELECT * FROM MEMBER;
MEMBER_ID | USERNAME | TEAM_ID |
---|---|---|
member1 | 회원1 | null |
member2 | 회원2 | null |
Team.member
에만 값을 저장Member.team
에 아무 값도 입력하지 않음💡 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.
public void test순수한객체_양방향() {
//팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); //연관관계 설정 member1 → team1
member2.setTeam(team1); //연관관계 설정 member2 → team2
//팀에 소속된 회원 수 출력
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
//결과 : members.size = 0
Member.team
에만 연관관계 설정//회원 → 팀
member1.setTeam(team1);
member2.setTeam(team1);
team1.getMembers().add(member1); //팀 → 회원
public void test순수한객체_양방향() {
//팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); //연관관계 설정 member1 → team1
team1.getMembers().add(member1); //연관관계 설정 team1 → member1
member2.setTeam(team1); //연관관계 설정 member2 → team2
team1.getMembers().add(member2); //연관관계 설정 team1 → member2
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
//결과: members.size = 2
public void testORM_양방향() {
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
//양방향 연관관계 설정
member1.setTeam(team1); //연관관계 설정 member1 → team1
team1.getMembers().add(member1); //연관관계 설정 team1 → member1
em.persist(member1);
Member member2 = new Member("member2", "회원2");
//양방향 연관관계 설정
member2.setTeam(team1); //연관관계 설정 member2 → team2
team1.getMembers().add(member2); //연관관계 설정 team1 → member2
em.persist(member2);
}
Member.team
Team.members
💡 주의 사항
- 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
- 양쪽 모두 입력하지 않을 경우, 순수 객체 상태에서 심각한 문제가 발생할 수 있다.
∴ 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주는 것이 좋다.
한 번에 양방향 관계를 설정하는 메소드
member.setTeam(team);
team.getMembers().add(member);
Member
클래스의 setTeam()
메소드 수정public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
...
}
setTeam()
메소드 하나로 양방향 관계 모두 설정하도록 변경//연관관계 설정
member1.setTeam(team1);
member2.setTeam(team1);
//==기존 코드 삭제 시작==//
//teamA.getMembers().add(member1); //팀1 → 회원1;
//teamA.getMembers().add(member2); //팀1 → 회원2;
//==기존 코드 삭제 종료==//
public void testORM_양방향_리팩토링() {
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", "회원2");
member2.setTeam(team1); //양방향 설정
em.persist(member2);
}
setTeam()
메소드에는 버그가 존재한다.
member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); //member1이 여전히 조회된다.
member1.setTeam(teamA)
호출 직후
member1.setTeam(teamB)
호출 직후
teamB
로 변경 시 teamA → member1
관계 제거 x💡 참고
teamA → member1
관계가 제거되지 않아도 데이터베이스 외래 키 변경에는 문제 x
→ 관계를 설정한Team.members
가 연관관계의 주인이 아니기 때문- 연관관계의 주인인
Member.team
의 참조를member1 → teamB
로 변경
→ 데이터베이스의 외래 키가teamB
정상 참조- 새로운 영속성 컨텍스트에서
teamA
를 조회하여teamA.getMembers()
호출
=> 데이터베이스 외래 키의 관계 끊어진 상태 → 조회 x- 관계 변경 후 영속성 컨텍스트 유지 상태에서
teamA
의getMembers()
호출
→member1
반환
∴ 변경된 연관관계는 관계를 제거하는 것이 안전함
연관관계 변경 시 기존에 참조하고 있는 객체 존재한다면, 기존에 참조하고 있던 객체와의 연관관계를 삭제하는 코드를 추가해야 한다.
public void setTeam(Team team) {
//기존 팀과 관계를 제거
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
mappedBy
로 주인을 지정해야 한다.양방향 매핑 시 무한 루프에 주의할 것
Lombok이 자동으로 생성하는 toString()
을 사용하지 않도록 한다.
Member
의 toString()
호출
→ Team
의 toString()
의 members
가 member
의 toString()
호출
∴ 무한 루프 생성 및 스택오버플로우 발생
JSON 생성 라이브러리
DTO
로 변환해서 반환하도록 한다.연관관계의 주인으로 일대다(1:N)도 선택 가능
→ 성능과 관리 측면에서 권장하지 않는다.
ex) 팀 엔티티의 Team.members
📖 참고