[Spring] JPA

Neo-Renaissance·2024년 12월 26일

JPA에서 다대다 연관관계(Many-to-Many Relationship)를 직접 사용하는 것은 권장되지 않는 경우가 많습니다. 그 이유는 여러 가지 제약과 관리상의 문제 때문입니다. 아래에 그 구체적인 이유를 설명합니다.

1. 중간 테이블(조인 테이블)에 대한 제어 부족

다대다 관계는 내부적으로 조인 테이블을 생성하여 두 엔티티 간의 매핑을 관리합니다.
하지만, JPA의 기본 다대다 매핑에서는 조인 테이블을 독립적으로 제어할 수 없습니다.

  • 문제:
    조인 테이블에 추가적인 속성(예: 생성일, 수정일 등)을 저장할 수 없습니다.
    조인 테이블을 직접 다룰 방법이 없어 관리가 어려워집니다.

  • 예시:

@Entity
public class Member {
    @ManyToMany
    @JoinTable(name = "member_team")
    private List<Team> teams;
}

JPA는 member_team이라는 중간 테이블을 자동 생성하지만, 이 테이블에 추가적인 정보를 넣을 수 없습니다.

2. 확장성 문제

다대다 관계는 실무에서 비즈니스 요구사항이 복잡해질수록 확장성이 떨어집니다. 조인 테이블에 비즈니스 속성(예: "회원이 팀에 가입한 날짜")을 추가해야 할 때, 기본 다대다 매핑으로는 불가능합니다.

  • 해결법:
    조인 테이블을 독립된 엔티티로 만들어 다대일 관계로 풀어내는 것이 좋습니다.

  • 예시 (권장 방식):

@Entity
public class MemberTeam {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Member member;

    @ManyToOne
    private Team team;

    private LocalDate joinDate; // 추가 속성
}

이렇게 하면 조인 테이블에 속성을 추가하거나, 조인 테이블을 명시적으로 다룰 수 있습니다.

3. 성능 문제

다대다 관계에서는 JPA가 조인 쿼리를 생성합니다. 데이터가 많아질수록 성능 저하가 발생할 수 있습니다.

  • 문제:
    두 테이블 간의 조인을 필요로 하기 때문에, 테이블 간의 관계가 복잡해질수록 SQL 쿼리가 비효율적일 수 있습니다.
    데이터베이스 인덱스를 제대로 설계하지 않으면 성능 문제가 더 심각해질 수 있습니다.

4. 데이터 무결성 제약 처리의 어려움

다대다 관계에서는 중간 테이블의 데이터 무결성을 보장하기 어렵습니다.
특히 다음과 같은 경우 문제가 발생할 수 있습니다:

  • 중복 데이터 삽입 방지.
  • 특정 조건에 따라 조인 테이블에서 데이터를 삭제해야 하는 경우.

5. 객체 지향 설계와 관계의 불일치

다대다 관계는 객체 지향 설계 관점에서 부자연스러운 경우가 많습니다.
실제 비즈니스 도메인에서는 대부분 중간 엔티티가 존재하며, 이를 모델링하지 않으면 객체 간의 관계를 제대로 표현할 수 없습니다.

  • 비즈니스 예시:
    "회원(Member)"과 "팀(Team)"이 다대다 관계라고 할 때, 실제로는 "회원이 팀에 가입한 날짜", "회원의 역할"과 같은 정보가 필요할 수 있습니다.
  • 이런 정보를 포함하려면 중간 엔티티를 모델링해야 합니다.

6. 직접 사용하는 경우의 제약

  • Cascade(영속성 전이) 옵션 적용이 어렵습니다.
    - 기본 다대다 관계에서는 중간 테이블의 엔티티가 없으므로 Cascade 설정이 복잡합니다.
  • Lazy Loading 문제
    - 다대다 관계의 지연 로딩 시 프록시 객체 처리와 쿼리 최적화가 어려운 경우가 많습니다.

실무 권장: 다대다를 다대일 + 일대다로 풀기

중간 엔티티를 사용하여 다대다 관계를 풀어서 표현하는 것이 일반적입니다.
이렇게 하면 비즈니스 로직을 쉽게 확장하고, JPA의 장점을 극대화할 수 있습니다.

@Entity
public class Member {
    @OneToMany(mappedBy = "member")
    private List<MemberTeam> memberTeams;
}

@Entity
public class Team {
    @OneToMany(mappedBy = "team")
    private List<MemberTeam> memberTeams;
}

@Entity
public class MemberTeam {
    @ManyToOne
    private Member member;

    @ManyToOne
    private Team team;

    private LocalDate joinDate; // 조인 테이블 속성
    private String role;        // 추가 속성
}

외래 키의 역할

  • 외래 키는 두 테이블 간의 연관성을 유지하는 중요한 요소입니다.
  • 데이터베이스에서 일대다 혹은 다대일 관계를 구현하려면 항상 외래 키가 필요합니다.
  • JPA는 객체와 관계형 데이터베이스 간의 매핑을 처리하므로, 외래 키의 위치와 매핑 방식을 명확히 정의해야 합니다.

외래 키 정의 방식

외래 키를 설정하는 방식은 관계의 주인(Owner)과 연관이 있습니다.

1. 다대일 관계 (Many-to-One)

다대일 관계에서 외래 키는 항상 자식 엔티티에 존재합니다.
다대일 관계는 JPA에서 가장 일반적이고 단순한 매핑입니다.

@Entity
public class Member {
    @ManyToOne
    @JoinColumn(name = "team_id") // 외래 키 지정
    private Team team;
}
  • @JoinColumn은 외래 키를 명시적으로 지정합니다.
  • 만약 @JoinColumn을 생략하면 JPA는 기본적으로 team_id라는 이름으로 외래 키를 생성합니다.

2. 일대다 관계 (One-to-Many)

일대다 관계는 외래 키가 자식 엔티티에 있어야 하므로, 관계의 주인은 Many 쪽입니다.
일반적으로 일대다 매핑에서 외래 키는 자식 엔티티에 정의되며, 아래와 같이 설정됩니다:

(1) 단방향 일대다

단방향 일대다 관계는 잘 사용되지 않습니다.

이유: 외래 키가 Many 쪽에 위치하는데, JPA는 기본적으로 One 쪽에서 관리하려 하므로 비효율적입니다.

@Entity
public class Team {
    @OneToMany
    @JoinColumn(name = "team_id") // 외래 키가 Member 테이블에 생성됨
    private List<Member> members;
}
  • @JoinColumn을 사용하여 외래 키를 지정합니다.
  • 하지만 이 방식은 데이터베이스에서 비효율적인 쿼리가 발생할 수 있습니다.

(2) 양방향 일대다

양방향 관계에서는 일반적으로 다대일 쪽을 관계의 주인(Owner)으로 설정하고, 일대다 쪽은 매핑만 담당합니다.

@Entity
public class Team {
    @OneToMany(mappedBy = "team") // 관계의 주인은 Member
    private List<Member> members;
}

@Entity
public class Member {
    @ManyToOne
    @JoinColumn(name = "team_id") // 외래 키 지정
    private Team team;
}
  • mappedBy를 통해 관계의 주인을 명시합니다.
  • 외래 키는 Member 테이블에 생성됩니다.

외래 키를 정하지 않을 수 있는 경우

JPA에서 기본 설정(Default Mapping)을 사용할 경우, 명시적으로 외래 키를 지정하지 않아도 됩니다.
JPA는 아래와 같은 규칙을 기반으로 외래 키를 자동 생성합니다:

  1. @ManyToOne → 기본적으로 자식 테이블에 필드명_필드ID 형식의 외래 키를 생성.
  2. @OneToMany → 자동 생성된 외래 키를 자식 테이블에서 관리.

예:

@Entity
public class Member {
    @ManyToOne
    private Team team; // team_id 외래 키가 자동 생성
}

위 코드에서 @JoinColumn을 생략하면 JPA는 기본적으로 team_id라는 외래 키를 생성합니다.

profile
if (실패) { 다시 도전; } else { 성공; }

0개의 댓글