테이블은 "외래키"로 조인하여 연관된 테이블을 찾는다.
객체는 "참조"를 통해 연관된 객체를 찾는다.
team_id
라는 외래키가 존재한다.@Entity
public class Member {
...
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@ManyToOne
@JoinColumn
team_id
는 Member 테이블에 생성될 FK 컬럼명이다.@Entity
public class Member {
...
@ManyToOne // Memeber 입장에서 작성한다.
@JoinColumn(name = "team_id") // 실제 DB의 FK명를 명시한다.
private Team team;
}
@Entity
public class Team {
...
@OneToMany(mappedBy = "team") // 주인이 되는 변수명을 명시한다.
private List<Member> members = new ArrayList<>();;
}
Member.getTeam()
Team.getMembers()
객체의 양방향은 서로 다른 단방향 2개를 말한다.
객체를 양방향으로 참조하게 하려면, 단방향 2개를 매핑해야 한다.
mappedBy
속성 지정// 1. 팀 생성
Team chelsea = new Team();
chelsea.setName("chelseaFC");
entityManager.persist(chelsea);
// 2. 멤버 생성 (주인 쪽에서만 연관관계 설정)
Member palmer = new Member();
palmer.setName("colePalmer");
palmer.setTeam(chelsea); // ✅ 주인 쪽 설정
entityManager.persist(palmer);
// 3. 팀에서 멤버 조회 (1차 캐시에서 조회)
Team findTeam = entityManager.find(Team.class, chelsea.getId());
List<Member> members = findTeam.getMembers(); // ❌ 비어있음
for (Member member : members) {
System.out.println("memberName: " + member.getName()); // 출력 안됨
}
Team.getMembers()
에는 아무 값도 들어 있지 않다.// 1. 팀 생성
Team chelsea = new Team();
chelsea.setName("chelseaFC");
entityManager.persist(chelsea);
// 2. 멤버 생성 및 연관관계 설정
Member palmer = new Member();
palmer.setName("colePalmer");
palmer.setTeam(chelsea); // ✅ 주인 쪽 설정
chelsea.getMembers().add(palmer); // ✅ 주인이 아닌 쪽도 설정
entityManager.persist(palmer);
// 3. 영속성 컨텍스트 조회 (1차 캐시에서 조회)
Team findTeam = entityManager.find(Team.class, chelsea.getId());
List<Member> members = findTeam.getMembers(); // ✅ palmer 포함됨
for (Member member : members) {
System.out.println("memberName: " + member.getName()); // 출력: colePalmer
}
// Member Entity (주인)
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
...
public void updateTeam(Team team) {
this.team = team; // ✅ 주인 업데이트
member.getTeam().add(this); // ✅ 주인X 업데이트
}
컨트롤러에서 엔티티 객체를 그대로 응답하면, 무한루프에 빠질 수 있다.
Jackson 라이브러리가 Member → Member.team → team.members ...
순으로 계속 직렬화 하다가 StackOverflowError가 발생할 수 있다.
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
StackOverflowError
에러 발생@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "team_id")
@ToString.Exclude // ✅ toString에서 제외
private Team team;
}
@Entity
public class Team {
@OneToMany(mappedBy = "team")
@ToString.Exclude // ✅ toString에서 제외
private List<Member> members = new ArrayList<>();
}
@ToString.Exclude
어노테이션을 사용한다.@ManyToOne
)이 연관관계의 주인이 된다.❓ 왜 외래키 기준으로 주인을 설정해야 할까?
비즈니스 로직을 기준으로 주인을 정하면 논리적으로는 맞지만 혼란이 생길 수 있음
ex)
Team이 주인이라고 설정해도, 외래키(fk)는Member.team_id
에 있음💡 외래키의 위치를 기준으로 주인을 설정하면
- 설계가 직관적이고 단순해진다.
- 영속성 전이(cascade), orphanRemoval 등 부가 설정이 덜 복잡하다.
- 실수로 주인이 아닌 쪽에서 값을 바꾸는 오작동을 줄일 수 있다.
- 성능 면에서도 외래키 기준(물리적) 주인 설정이 유리하다.
→ 복잡한 동기화 로직을 피하고 불필요한 쿼리를 줄일 수 있다.
김영한 자바 ORM 표준 JPA 프로그래밍
내배캠 튜터님들