이 글은 김영한 님의 저서 「자바 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.teamTeam.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
📖 참고