DB

[JPA] 다대다(N:M) 관계를 지양하는 이유와 해결법

날아 2024. 4. 20. 05:06

토이 프로젝트를 진행하며 연관관계 매핑을 하던 중 다대다 관계를 만난 적이 있다. 

게시글(Project)과 카테고리(Skill)가 바로 그 관계였다. 

다대다 관계매핑은 최대한 지양하는 것이 좋다고 배웠기에 중간 테이블을 만들어 1:N 과 N:1 관계로 풀어주었다. 

위와 같이 @ManyToMany를 사용하지 않은 이유를 복습하기 위해 글로 정리해보려 한다. 

 

 

1. 다대다 관계

먼저, 프로젝트에서 정의한 도메인은 다음과 같다.

 

  • Project : 개발 프로젝트 인원을 구하는 모집글 테이블
  • Skill : 카테고리 (개발 언어) 테이블 

 

ERD는 아래와 같다. (관계형 데이터베이스에서는 다대다 관계가 없기 때문에 연결을 할 수 없어 이는 생략했다...)

 

위의 관계가 다대다 관계가 되는 이유는, 

하나의 모집글은 여러 개의 카테고리를 가질 수 있으며, 하나의 카테고리는 여러 개의 모집글에 포함될 수 있기 때문이다. 

 

이러한 다대다 관계의 경우 반드시 정규화를 통해 중간테이블을 만들어야 한다. 

 

 

관계형 데이터베이스에서는 다대다 관계가 안되는 이유는?

관계형 데이터베이스에서 2개의 테이블 만으로 다대다 관계를 구현하는 것은 불가능하다. 

project 테이블에 게시글 1, 게시글 2가 존재하고 skill 테이블에 java, mysql이 존재한다고 가정하고,

게시글 1이 java, skill 을 가지고 있고, 게시글 2 역시 java를 가지고 있다고 가정해보자. 

 

이러한 상황에서 java의 경우 게시글 1과 게시글 2를 포함하고 있기 때문에 데이터 중복이 발생하는 문제점이 생긴다. 

id (skill 테이블의 id) id (project 테이블의 id) name (skill 테이블의 name)
101 100 java
101 100 java
201 200 mysql

 

이는 제 2 정규형에 위배되기 때문에 정규화를 통해 중간 테이블을 놓는 것이다. 

(즉, project_skill 이라는 중간 테이블을 통해서 일대다, 다대일 관계로 풀어내는 것이다.)

id (project_skill 테이블의 id) id (skill 테이블의 id) id (project 테이블의 id)
1 101 100
2 101 100
3 201 200

 

 

객체는 다대다 관계를 표현할 수 있다.

하지만 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계를 표현할 수 있다. 

이 차이를 해결하기 위해 JPA 내에서는 @ManyToMany 어노테이션을 통해 다대다 관계를 지원한다.

하지만, @ManyToMany는 실무에서 절대 사용하면 안된다.

 


2. @ManyToMany

JPA는 @ManyToMany 어노테이션을 통해  객체의 다대다 관계를 관계형 데이터베이스 내의 일대다, 다대일 관계로 손쉽게 연결해준다. 

사용법은 편리하지만 실무에서는 지양된다.

 

왜냐하면 @ManyToMany는 project_skill 테이블의 역할을 하는 중간 테이블이 어노테이션 안에 숨겨져 있다. 

따라서 추가 정보를 넣는 것 자체가 불가능하다. 

하지만, 실제 개발을 하다보면 연결 테이블이 단순히 연결만 하고 끝나지 않고 추가 정보를 넣어야 할 때가 있다. 

 

또한 코드가 내부에 숨겨져 있기 때문에 N+1 과 같은 의도하지 않은 추가적인 쿼리가 나갈 가능성이 높다. 

 


3. 연결 테이블을 직접 구현 (@OneToMany, @ManyToOne 으로 풀어내기)

따라서 해당 문제를 해결하기 위해 연결 테이블용 엔티티를 추가하는 방식을 채택했다.

즉, @ManyToMany 내 숨겨져있던 매핑 테이블의 존재를 바깥으로 꺼낸 것이다.

그리고 작성인, 작성일 등과 같은 추가적인 컬럼을 추가해주었다. 

 

해당 코드는 아래와 같다. 

 

@Getter
@ToString
@EqualsAndHashCode(of = "id")
@Entity
public class ProjectSkill {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    private Project project;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "skill_id")
    private Skill skill;

    protected ProjectSkill() {}

    private ProjectSkill(Project project, Skill skill) {
        setProject(project);
        this.skill = skill;
    }

    public static ProjectSkill of(Project project, Skill skill) {
        return new ProjectSkill(project, skill);
    }
    
    .
    .
    .
(이하생략)
	.
    .
    .
}

 

 

비식별 관계로 독립적인 PK를 놓은 이유

사실 이 부분에 대해서는 개발 당시에 큰 고민을 하지 않았었는데, sqld를 준비하며 식별/비식별 관계를 깊게 공부하면서 결론적으로는 위의 방식으로 코드를 짜기를 잘 했다는 생각이 든다.

만약 식별 관계로 복합키를 사용했다면 두 테이블에 종속되기 때문에 유연한 개발이 불가능하지 않았을까..?