[TIL] JPA계의 포인터 (양방향 연관관계와 주인관계)

wannabeing·2025년 5월 1일
0

SPARTA-TIL

목록 보기
16/22
post-thumbnail

테이블은 "외래키"로 조인하여 연관된 테이블을 찾는다.
객체는 "참조"를 통해 연관된 객체를 찾는다.

단방향 연관관계

  • Member 테이블에 team_id라는 외래키가 존재한다.
  • 따라서 Member 엔티티는 Team 객체를 참조해야 한다.

Member Entity

@Entity
public class Member {
	...
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
  • @ManyToOne
    Member(N) ↔ Team(1) 관계를 나타내는 어노테이션이다.
  • @JoinColumn
    실제 DB에 생성될 외래키 컬럼 이름을 지정한다.
    여기서 team_id는 Member 테이블에 생성될 FK 컬럼명이다.

양방향 연관관계

  • ❗️테이블 연관관계에서는 달라진 점이 없다!!
    외래키 하나만 있으면 양쪽 테이블을 조인을 통해 찾을 수 있다.
  • 문제는 객체의 연관관계이다.
    Team은 Member를 참조할 수 있는 방법이 없다.

Member Entity (N)

@Entity
public class Member {
	...
    
    @ManyToOne // Memeber 입장에서 작성한다.
    @JoinColumn(name = "team_id") // 실제 DB의 FK명를 명시한다.
    private Team team;
}

Team Entity (1)

@Entity
public class Team {
	...
    
    @OneToMany(mappedBy = "team") // 주인이 되는 변수명을 명시한다.
    private List<Member> members = new ArrayList<>();;
}
  • ✅ mappedBy
    • 반대편 엔티티에 존재하는 나의 엔티티 변수명을 입력해준다.
    • 연관관계의 주인이 아니며(읽기 전용), 외래키를 관리하지 않는다.

객체와 테이블이 관계를 맺는 차이

객체의 연관관계 = 2개

  • 회원 → 팀 (단방향): Member.getTeam()
  • 팀 → 회원 (단방향): Team.getMembers()

테이블의 연관관계 = 1개

  • 외래키로 조인을 통해 양방향 관계를 맺는다고 볼 수 있음

객체의 양방향은 서로 다른 단방향 2개를 말한다.
객체를 양방향으로 참조하게 하려면, 단방향 2개를 매핑해야 한다.


❓ 그럼 우린 외래키를 어떤 기준으로 관리해야 될까?

양방향 관계의 주인(Owner)을 설정하자!

  • 💡 테이블 관계에서 외래키(fk)가 있는 쪽이 주인!
  • 연관관계 주인이 외래키(fk)를 관리(등록, 수정)
  • 주인이 아닌 쪽은 읽기만 가능
  • 따라서 주인이 아닌 쪽이 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()); // 출력 안됨
}
  • 이 경우 DB에 외래키(FK)가 정상적으로 설정된다.
  • 단, Team.getMembers()에는 아무 값도 들어 있지 않다.
  • 영속성 컨텍스트(1차 캐시)에는 값이 설정되어 있지 않기 때문이다.
    → 한 트랜잭션 내에는 양쪽 연결이 불완전하다.

⭐️⭐️⭐️ 양쪽에 값을 설정할 경우

// 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
}
  • DB에 외래키(FK) 정상적으로 설정된다.
  • 영속성 컨텍스트 (1차 캐시) 에도 값이 양쪽으로 설정되어 있다.
  • 한 트랜잭션 내에 양쪽 연결이 완전하다.
    → 양방향 연결일 경우, 동시에 값을 설정해주는 메서드를 호출하는 것이 좋다.
    둘 중에 한 군데에서 하자. 양쪽에만 메서드를 만들지 말자.
    // Member Entity (주인)
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
    
    ...
    
    public void updateTeam(Team team) {
       this.team = team; // ✅ 주인 업데이트
       member.getTeam().add(this); // ✅ 주인X 업데이트
    }

😇 양방향 관계에서 생기는 문제점

1. JSON 직렬화 무한루프

컨트롤러에서 엔티티 객체를 그대로 응답하면, 무한루프에 빠질 수 있다.
Jackson 라이브러리가 Member → Member.team → team.members ...
순으로 계속 직렬화 하다가 StackOverflowError가 발생할 수 있다.

✅ 해결방법

  • 엔티티 클래스를 그대로 반환하지말고, DTO로 변환해서 반환하자.
    엔티티 필드가 바뀌면 API 명세서 스펙이 바뀌게 되므로 큰 작업이 될 수 있다.

2. lombok의 toString 무한루프

@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<>();
}
  • member.toString()
    → team.toString()
    → members.toString()
    → 각 member.toString()
    → 다시 team.toString() ... 무한 반복
  • 결국 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 어노테이션을 사용한다.
  • lombok toString 사용할 때, 해당 필드를 제외하자.

✅ 정리하자면... (1:N, 1:1에서)

  • 단방향 매핑만으로 이미 연관관계 매핑은 충분히 가능하다.
    → 테이블 외래키(FK) 하나만으로 관계 설정이 완료된다.
  • 보통 외래키를 가진 쪽(@ManyToOne)이 연관관계의 주인이 된다.
  • 쿼리를 짜도 외래키(FK)를 갖고 있는 테이블로 쿼리를 짠다.
    만약 멤버의 주문목록을 멤버로 접근하여 쿼리를 짠다면 어렵게 접근하는 방법이다.
  • 양방향 매핑은 단방향 매핑을 양쪽으로 한 것이 아니다.
  • 양방향 매핑은 필요할 때, 추후에 추가해도 된다.
    → 테이블에는 영향이 전혀 없다.
    → 복잡한 JPQL을 짜다보면, 양방향 매핑이 필요할 때가 생길 수도 있다!

❓ 왜 외래키 기준으로 주인을 설정해야 할까?

비즈니스 로직을 기준으로 주인을 정하면 논리적으로는 맞지만 혼란이 생길 수 있음
ex) Team이 주인이라고 설정해도, 외래키(fk)는 Member.team_id에 있음

💡 외래키의 위치를 기준으로 주인을 설정하면

  • 설계가 직관적이고 단순해진다.
  • 영속성 전이(cascade), orphanRemoval 등 부가 설정이 덜 복잡하다.
  • 실수로 주인이 아닌 쪽에서 값을 바꾸는 오작동을 줄일 수 있다.
  • 성능 면에서도 외래키 기준(물리적) 주인 설정이 유리하다.
    → 복잡한 동기화 로직을 피하고 불필요한 쿼리를 줄일 수 있다.

김영한 자바 ORM 표준 JPA 프로그래밍
내배캠 튜터님들

profile
wannabe---ing

0개의 댓글