spring

관계형DB일때 N:M 연관관계 @ManyToMany

seungmin576 2025. 2. 24. 16:10

객체는 Collection을 사용하여 N:M 설정이 가능하지만 관계형 DB는 N:M 연관관계를 구현할 수 없다.

따라서 중간 테이블을 추가해서 1:n, n:1 관계로 설정하면 된다.

JPA를 사용하면 @ManyToMany로 위 과정을 쉽게 해결가능한데 문제가 있으니 주의하여야한다.

 

단방향 예시

@Entity
@Table(name = "language")
public class Language {

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

    private String name;

}



@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    @ManyToMany
    @JoinTable(
            name = "tutor_language",
            joinColumns = @JoinColumn(name = "tutor_id"),
            inverseJoinColumns = @JoinColumn(name = "language_id")
    )
    private List<Language> languages = new ArrayList<>();

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

 

 

 

양방향 예시

@Entity
@Table(name = "language")
public class Language {

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

    private String name;

    @ManyToMany(mappedBy = "languages")
    private List<Tutor> tutors = new ArrayList<>();

}

 

 

 

N:M 매핑의 문제점

  1. 중간테이블에 id값만 존재하는게 아니라 추가적인 데이터가 필요할 경우가 많다.
    • @ManyToMany 에서는 사용할 수 없다.
  2. 중간 테이블이 숨겨져 있어서 생각하지 못한 SQL Query가 실행된다.
  3. tutor_id, language_id 를 묶어서 PK로 설정된다.
    • PK가 종속적이면 사이드 이펙트가 생긴다.
    • PK 값은 비지니스적으로 의미없는 Long 값으로 설정하는 것이 유연성에 좋다.

 

위와 같은 문제를 해결하기 위해 중간테이블을 실제 Entity로 만들어서 관리하면 편리하다.

 

 

 

코드예시

@Entity
@Table(name = "tutor_language")
public class TutorLanguage {

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

    @ManyToOne
    @JoinColumn(name = "tutor_id")
    private Tutor tutor;

    @ManyToOne
    @JoinColumn(name = "language_id")
    private Language language;

    private Integer level;

    private String license;

}



@Entity
@Table(name = "language")
public class Language {

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

    private String name;

    @OneToMany(mappedBy = "language")
    private List<TutorLanguage> tutorLanguages = new ArrayList<>();

}



@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    @OneToMany(mappedBy = "tutor")
    private List<TutorLanguage> tutorLanguages = new ArrayList<>();

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

데이터베이스 설계 관점에서는 @ManyToMany 방식이 맞지만 실제 코드 구현에는 중간 테이블을 만드는 방법이 편리하다