| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- html
- 배열
- 자바문법
- 스프링부트
- 문자열
- vscode
- junit
- 자바스크립트
- junit5
- 자바
- Visual Studio Code
- Array
- math
- IntelliJ
- 인텔리제이
- input
- 정규식
- 테스트자동화
- SpringBoot
- Java
- string
- HashMap
- js
- list
- java테스트
- 단위테스트
- CSS
- Eclipse
- ArrayList
- javascript
- Today
- Total
어제 오늘 내일
[Spring Boot / JPA] "누가 주인인가?" 헷갈리는 연관관계 매핑 (N:1, 1:N, mappedBy) 완벽 정리 본문
[Spring Boot / JPA] "누가 주인인가?" 헷갈리는 연관관계 매핑 (N:1, 1:N, mappedBy) 완벽 정리
hi.anna 2026. 3. 13. 00:24
JPA를 쓰다 보면 Member를 저장했는데 Team에는 회원이 안 들어가 있거나, 반대로 Team을 저장했는데 Member의 외래 키가 null인 황당한 경우를 겪게 됩니다.
이건 100% "연관관계의 주인"을 잘못 설정했기 때문입니다. 객체지향 설계와 데이터베이스 설계의 차이를 이해하면, 이 문제는 아주 쉽게 해결됩니다.
1. 객체 vs 테이블: 패러다임의 불일치
가장 먼저 이 그림을 머릿속에 넣어야 합니다.
- 테이블(DB):
MEMBER테이블에 있는TEAM_ID(외래 키) 하나만 있으면, 멤버도 팀을 찾을 수 있고 팀도 멤버를 찾을 수 있습니다. (양방향 가능) - 객체(Java):
Member객체에Team필드가 있어야 팀을 찾을 수 있고, 반대로Team객체에도List<Member>가 있어야 멤버를 찾을 수 있습니다. (사실상 단방향 2개)
문제는 여기서 발생합니다.
"멤버가 팀을 바꿨을 때 외래 키를 업데이트해야 할까? 아니면 팀에서 멤버 리스트를 바꿨을 때 외래 키를 업데이트해야 할까?"
JPA는 룰을 정했습니다. "둘 중 하나만 외래 키를 관리해라!" 이것이 바로 연관관계의 주인입니다.
2. 주인은 누구? (Foreign Key를 가진 자!)
고민할 필요 없습니다. DB 테이블을 봤을 때 외래 키(FK)를 가지고 있는 쪽이 무조건 주인입니다.
- Member (N):
team_id(FK)를 가지고 있음 -> 주인! - Team (1): FK 없음 -> 주인이 아님 (거들 뿐)
주인만이 등록, 수정, 삭제를 할 수 있습니다. 주인이 아닌 쪽은 오직 조회(Read)만 가능합니다.
3. 가장 많이 쓰는 N:1 (Many To One) 단방향
"다대일(N:1)" 관계가 실무의 90%입니다. (예: 멤버와 팀, 게시글과 댓글)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne // 나는 N, 팀은 1
@JoinColumn(name = "TEAM_ID") // FK 이름 지정 (주인 선언!)
private Team team;
}
@JoinColumn이 붙은 쪽이 주인입니다.- 이제
member.setTeam(team)을 하면 DB의TEAM_ID가 변경됩니다.
4. 양방향 매핑과 mappedBy의 비밀
개발하다 보면 팀에서도 소속된 멤버 목록을 보고 싶을 때가 있죠? 이때 반대쪽에 List를 추가합니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
// 나는 주인이 아니야! Member 클래스의 'team' 필드가 주인이야.
// 나는 그저 거울일 뿐이니, 나를 건드려도 DB에 영향 주지 마.
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
💡 mappedBy가 뭐죠?
- 의미: "나는 매핑 당했다(수동태)." 즉, "나는 연관관계의 주인이 아니다."라고 선언하는 것입니다.
- 역할: 여기에 값을 아무리 넣어도(
team.getMembers().add(member)) DB에는 아무런 변화가 일어나지 않습니다. 오직 조회용입니다.
5. 실수하기 딱 좋은 포인트 (주의사항)
① 주인이 아닌 쪽에만 값을 넣지 마세요!
Team team = new Team();
Member member = new Member();
// 주인이 아닌 쪽에만 넣음 (망함)
team.getMembers().add(member);
em.persist(team);
em.persist(member);
결과: MEMBER 테이블의 TEAM_ID는 여전히 null입니다. 주인(member.setTeam)이 일을 안 했기 때문이죠.
② 양쪽 다 값을 넣어주세요! (편의 메서드)
JPA 입장에서는 주인만 챙기면 되지만, 객체지향적으로는 양쪽 다 값이 있어야 안전합니다.
// Member 클래스 안에 메서드 생성
public void changeTeam(Team team) {
this.team = team; // 1. 주인에게 값 세팅 (DB 반영됨)
team.getMembers().add(this); // 2. 반대쪽에도 값 세팅 (조회용)
}
이렇게 연관관계 편의 메서드를 작성해서 한 번에 처리하는 것을 강력 추천합니다.
마치며
오늘의 결론입니다.
- 외래 키(FK)가 있는 곳이 무조건 연관관계의 주인이다.
- 주인에게만 값을 입력/수정해야 DB에 반영된다.
mappedBy는 "나는 주인이 아니니 조회만 할게"라는 뜻이다.- 헷갈리면 일단 N:1 단방향으로 다 만들고, 꼭 필요할 때만 양방향을 추가하자.
이 원칙만 지키면 JPA 연관관계 때문에 밤새는 일은 없을 겁니다!
다음 포스팅에서는 "쿼리 한 번 날렸는데 100방이 더 나간다고?" JPA 성능 최악의 이슈인 N+1 문제와 해결법(Fetch Join)에 대해 알아보겠습니다.
