어제 오늘 내일

[Spring Boot / JPA] "누가 주인인가?" 헷갈리는 연관관계 매핑 (N:1, 1:N, mappedBy) 완벽 정리 본문

IT/SpringBoot

[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. 반대쪽에도 값 세팅 (조회용)
}

이렇게 연관관계 편의 메서드를 작성해서 한 번에 처리하는 것을 강력 추천합니다.

 


 

마치며

오늘의 결론입니다.

  1. 외래 키(FK)가 있는 곳이 무조건 연관관계의 주인이다.
  2. 주인에게만 값을 입력/수정해야 DB에 반영된다.
  3. mappedBy는 "나는 주인이 아니니 조회만 할게"라는 뜻이다.
  4. 헷갈리면 일단 N:1 단방향으로 다 만들고, 꼭 필요할 때만 양방향을 추가하자.

이 원칙만 지키면 JPA 연관관계 때문에 밤새는 일은 없을 겁니다!

다음 포스팅에서는 "쿼리 한 번 날렸는데 100방이 더 나간다고?" JPA 성능 최악의 이슈인 N+1 문제와 해결법(Fetch Join)에 대해 알아보겠습니다.

 

 

반응형
Comments