[Java] JPA - 연관관계 매핑 기초

daheenamic·2025년 10월 30일
0

JAVA

목록 보기
35/41

JPA 연관관계 매핑 기초

  • 객체와 테이블의 연관관계 차이를 이해해야 한다.
  • 객체의 참조와 테이블의 외래키를 매핑한다

객체는 member.getTeam() 처럼 참조(Reference)로 관계를 탐색하는 반면 테이블은 TEAM_ID 외래키(Foreign Key)를 이용해서 관계를 찾는다.

이 간극을 메워주는 것이 JPA의 연관관계 매핑이다.


용어

  1. 방향(Direction): 단방향, 양방향
  2. 다중성(Multiplicity): 일대일, 일대다, 다대일, 다대다
  3. 연관관계의 주인(Owner): 외래키를 관리(등록,수정)하는 엔티티

연관관계가 필요한 이유

"객체 지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다."

예시 시나리오

  • 회원(Member)과 팀(Team)이 있다.
  • 회원은 하나의 팀에만 소속 될 수 있다. -> 회원 N:팀1 (다대일관계)

객체 vs 테이블 관계 표현의 차이

구분MemberTeam
테이블 모델member_id (PK), team_id (FK), nameteam_id (PK), name
객체 모델 (잘못된 설계)id, teamId, nameid, name
객체 모델 (정상적인 설계)id, name, Team teamid, name, List<Member> members

테이블은 FK로 연결되지만 객체는 참조 필드로 연결 되어야 객체지향적인 설계가 된다.
DB는 외래키 기반 관계, 객체는 참조 기반 관계, 이 둘을 매핑 시켜야 ORM이 완성된다.


단방향 연관관계

단방향 연관관계

@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
  • @ManyToOne: 다대일 관계
  • @JoinColumn: 외래키 컬럼 지정(TEAM_ID)

연관관계 저장

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team); // 단방향 연관관계 설정
em.persist(member);

연관관계 수정

Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

member.setTeam(teamB); // 새로운 팀으로 변경

양방향 연관관계와 연관관계의 주인

테이블은 FK 하나로 양방향 조회가 가능하지만 객체는 단방향 두개로 표현해야 한다.

객체방향설명
Member.team회원 → 팀다대일 단방향
Team.members팀 → 회원일대다 단방향
@OneToMany(mappedBy = "team") 
private List<Member> members = new ArrayList<>();
  • mappedBy = "team": 나는 주인이 아니고, Member 엔티티의 team 필드에 의해 매핑되었다 라는 뜻

객체 그래프 탐색

Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();

for (Member m : members) {
    System.out.println("m = " + m.getUsername());
}

-> findMember -> getTeam() -> getMembers() 순서로 객체 그래프를 따라간다.

객체는 가급적 단방향이 좋다. 단방향으로 충분히 설계가 가능하고 양방향은 조회(탐색) 편의 기능일 뿐이다.


연관관계의 주인과 mappedBy

이 부분이 좀 헷갈리지만 원리를 이해하면 단순하다.

객체 vs 테이블의 관계 개수

구분관계 개수예시
객체단방향 2개Member → Team, Team → Member
테이블외래키 1개MEMBER.TEAM_ID 하나로 양방향 가능

객체의 양방향 관계는 단방향 2개를 합친것이기 때문에 두 객체 중 하나만 외래키를 관리해야 한다.

연관관계 주인 규칙

규칙설명
외래키가 있는 쪽이 주인(즉, Member.team이 주인)
주인만 외래키 등록/수정 가능주인이 아닌 쪽은 읽기 전용
주인이 아닌 쪽은 mappedBy 속성으로 주인 지정
비즈니스 로직 기준 ❌ → 외래키 기준으로 판단해야 함

정리하면 외래키를 들고있는 쪽(Member)이 연관관계의 주인이다.


양방향 관계 설정시 주의할 점

주인에 값을 입력 안하는 실수

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
// ❌ 역방향(주인 아닌 쪽)만 설정
team.getMembers().add(member);
em.persist(member);

-> Member 테이블의 TEAM_IDnull로 들어간다.

올바른 설정

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
team.getMembers().add(member);

// 주인(Member.team)에 값 설정
member.setTeam(team);
em.persist(member);

왜 양쪽 다 넣어야 할까?

JPA는 영속성 컨텍스트(1차 캐시)를 사용하기 때문에 flush/clear 전에 조회하면 DB가 아닌 메모리의 상태를 반환한다. 그래서 한쪽만 세팅하면 캐시 내 객체 상태가 불일치 할 수 있다.

순수 객체 상태를 유지하기 위해 항상 양쪽 다 설정하는 습관을 들여야 한다. 하지만 매번 두줄의 코드를 쓰면 실수 할 수 있으므로 편의 메서드를 만들어 두면 좋다

연관관계 편의 메서드 생성

// Member.java
public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

혹은 반대쪽에서

// Team.java
public void addMember(Member member) {
    members.add(member);
    member.setTeam(this);
}

단 두 방식 중 하나만 사용해야 한다. 둘 다 호출하면 중복추가나 무한루프 위험이 있다.

이름은 단순한 setter 보다는 changeTeam(), addMember()처럼 의미 있는 메서드명이 좋다.


무한 루프 주의

양방향 매핑에서 toString(), lombok, JSON 변환 시, 서로 참조를 타고 돌다가 StackOverflowError가 발생할 수 있다.

예시

  • Member.toString(team.toString() → 다시 member.toString()
  • JSON 변환 시 양방향 필드를 순환 참조

해결 방법

  • toString() 직접 생성 (Lombok 자동생성 지향)
  • 엔티티를 Controller에서 직접 반환 금지
    -> 반드시 DTO로 변환해서 응답해야 함. (API 스펙 안정성 + 순환참조 방지)

양방향 매핑 정리

항목요약
단방향 매핑만으로도 기능 완성양방향은 탐색 편의 기능일 뿐
연관관계 주인은 외래키가 있는 쪽Member.team
주인만 DB 외래키 등록/수정 가능mappedBy 사용 X
주인이 아닌 쪽은 mappedBy로 주인 지정읽기 전용
항상 양쪽 값 세팅순수 객체 상태 유지
편의 메서드 작성 추천실수 방지
양방향 무한루프 주의DTO 변환 필수

JPA의 연관관계 매핑은 객체의 참조와 테이블의 외래키 사이의 간극을 메워주는 기술이다.

단방향 매핑으로 관계를 명확히 표현하고, 필요할 때만 양방향을 추가하여 외래키 기준으로 주인을 명확히 설정해야 한다.

그리고 항상 객체 그래프 일관성을 유지하는 습관이 중요하다.

0개의 댓글