[BE] 연관관계 매핑 알아보기

Kwanni·2025년 6월 9일
0

BackEnd

목록 보기
3/4

들어가며

JPA에서 연관관계를 매핑하고 사용하는 방법을 공부하면서 실제로 실무에서는 어떻게 매핑하는지 궁금하여 블로깅하였다. 먼저 JPA에서의 연관관계 매핑 기본 개념을 설명하고, 실무에서의 과정을 알아보려 한다.

연관관계 매핑

객체 지향 프로그래밍의 객체 간 참조(reference)와 관계형 데이터베이스의 테이블 간 외래 키(Foreign Key)를 연결하는 과정

연관관계 매핑의 종류

  • 일대일(1:1)
    하나의 엔티티가 정확히 하나의 다른 엔티티를 참조하는 관계(한 명의 회원이 하나의 프로필을 가짐)
  • 일대다(1:N)
    하나의 엔티티가 여러 개의 다른 엔티티를 참조하는 관계(하나의 팀에 여러 명의 회원이 속함)
  • 다대일(N:1)
    여러 개의 엔티티가 하나의 다른 엔티티를 참조하는 관계(여러 명의 회원이 하나의 팀에 속함)
  • 다대다(N:M)
    여러 개의 엔티티가 여러 개의 다른 엔티티를 참조하는 관계(여러 명의 학생이 여러 개의 강의를 수강)

연관관계 매핑의 방향

  • 단반향: 한쪽 엔티티만 다른 쪽 엔티티를 참조하는 관계(회원 객체는 팀 객체를 참조할 수 있지만, 팀 객체에서는 회원 객체를 참조할 수 없음)
  • 양방향: 양쪽 엔티티가 서로 참조하는 관계(회원 객체에서 팀 객체를 참조할 수 있고, 팀 객체에서도 회원 객체를 참조할 수 있음)

양방향 연관관계에서는 외래 키를 관리하는 엔티티를 'Owner'로 지정해야한다. 그렇다면 Owner의 지정이 왜 필요한지 더 알아보자.

연관관계의 Owner

  1. 왜 필요한가?
    객체는 참조로 서로 연결되고, 테이블은 외래키로 관계를 맺는다. 하지만 객체의 양방향 관계는 실제 DB테이블에서는 외래키 하나로만 표현된다. 즉, 객체는 두 번 참조가 가능하지만, 테이블은 외래키 하나가 관계를 대표한다. 이때, 어느 쪽에서 외래키를 조작할지 명확하지 않으면, 데이터베이스와 객체 상태가 불일치할 수 있다. 따라서, JPA는 둘 중 한 곳만 외래키를 관리할 수 있다고 규칙을 둔다.

  2. Owner의 개념
    외래키(FK)를 실제로 관리하는 엔티티로써 DB의 외래키를 직접 소유하고, 외래키의 값을 등록, 수정, 삭제할 수 있다. 반대쪽은 읽기만 가능하다.

  3. 잘못 지정한다면?

  • 데이터 불일치
    Owner가 아닌 쪽에서 값을 변경해도 DB에 반영되지 않는다.
  • 예상치 못한 쿼리 발생
    전혀 상관없는 엔티티에서 update 쿼리가 발생할 수 있고, 의도와 전혀 다르게 저장될 수 있다.
  • 유지보수의 어려움
    어느 쪽이 실제로 외래키를 관리하는지 헷갈려서, 실수로 데이터가 꼬이거나, 객체-DB간의 동기화가 깨질 수 있다.
  1. 어떻게 해결할까?
  • 외래키가 있는 곳을 Owner로
    1:N, N:1 관계에서는 N쪽이 Owner가 되는 것이 일반적이다.
  • 양방향 매핑 시, Owner가 아닌 쪽은 반드시 mappedBy로 명시
// Member 엔티티 (주인)
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

// Team 엔티티 (주인이 아님)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
  • 연관관계 편의 메서드(Convenience Method)로 객체 상태 동기화
    연관관계가 변경될 때마다 양쪽 객체의 상태를 동기화한다.
public class Member {
    public void changeTeam(Team team) {
        this.team = team;
        if (!team.getMembers().contains(this)) {
            team.getMembers().add(this);
        }
    }
}

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

실무에서의 연관관계 매핑

성능과 유지보수성을 위해 연관고나계 매핑을 최소화하거나, 아예 사용하지 않고 ID로만 관리하기도 한다.

  1. 왜 최소화 하는가?
  • 성능 최적화
    연관관계 매핑을 많이 사용할수록 JPA는 객체 그래프를 자동으로 탐색하고, 지연 로딩이나 즉시 로딩 전략에 따라 예상치 못한 쿼리가 발생할 수 있다. 예상치 못한 쿼리의 예시는 실무에서의 전략을 설명한 후 다루겠다.
  • 유지보수성 향상
    연관관계가 복잡할수록 코드의 의도 파악이 어려워지고, 문제 추적 및 해결도 힘들어진다. 단순하고 명확한 구조가 유지보수에 유리하다.
  1. ID만으로 관계를 관리
  • 외래 키 대신 식별자만 저장
    엔티티 간의 직접적인 연관관계 매핑을 사용하지 않고, 관련 엔티티의 ID만 필드로 저장.
  • 조인이 필요할 때 직접 쿼리
    연관 데이터가 필요할 때는 ID를 이용해 별도의 쿼리로 직접 조회

이 전략을 사용하여 대규모 트래픽, 복잡한 도메인, 성능이 중요한 서비스에서 특히 많이 채택되고 있다.

예상치 못한 쿼리 문제

즉시 로딩 또는 지연 로딩의 문제로 예상치 못한 쿼리 문제가 발생한다고 하였다. 이를 간단히 알아보자.

  • 즉시 로딩(EAGER)에서의 문제
    • 연관관계가 많은 엔티티에서 즉시 로딩을 사용하면 여러 엔티티를 한 번에 조회할 때 JPA가 내부적으로 JOIN 쿼리를 생성하거나, 각 엔티티의 연관 엔티티를 추가 쿼리로 조회한다.
    • 예를 들어, 회원 1000명을 조회하면, 각 회원의 팀을 조회하는 쿼리가 1000번 추가로 실행될 수 있다. 이로 인해 N+1 문제가 발생한다. N+1 문제는 1번의 메인 쿼리와 N번의 추가 쿼리를 의미한다.
    • 따라서, 예기치 못한 JOIN이나 추가 SELECT 쿼리를 발생시키고, 복잡한 쿼리에서는 JOIN이 여러 번 일어나 성능이 급격히 저하될 수 있다.
  • 지연 로딩(LAZY)에서의 문제
    • 연관 엔티티를 실제로 사용할 때 쿼리를 실행한다.
    • for문 등으로 연관 엔티티에 반복 접근하면, 그때마다 추가 쿼리가 발생해 N+1 문제가 생긴다.
    • 예를 들어, 게시글(Post) 리스트를 조회한 후 각 게시글의 작성자(User)에 접근하면, 게시글 개수만큼 User 조회 쿼리가 추가로 실행된다.

즉시 로딩과 지연 로딩을 통해 연관관계가 많은 엔티티를 조회할 때 문제가 발생하는 것을 알 수 있었다.

참고자료

profile
Success is not an accident, Success is actually a choice.

0개의 댓글