✏️ JPA 연관 관계 (2)

박상민·2023년 9월 30일
0

JPA

목록 보기
10/24
post-thumbnail

이번 글은 ✏️ JPA 연관 관계 (1)에 이어서 JPA 연관관계 중 다중성에 작성하겠습니다.

⭐️ 다중성(Multiplicity)

다중성은 데이터베이스를 기준으로 결정된다.

연관 관계는 대칭성을 갖는다.

  • 일대다(1:N) ↔ 다대일(N:1)
  • 일대일(1:1) ↔ 일대일(1:1)
  • 다대다(N:N) ↔ 다대다(N:N)
  • 다대일: @ManyToOne
  • 일대다: @OneToMany
  • 일대일: @OneToOne
  • 다대다: @ManyToMany

📌 N:1 관계 (다대일)

회원(Member)과 소속(Team)의 관계를 예로 들어보자.

요구 사항

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다,
  • 회원과 팀은 다대일(N:1) 관계다.
    • 다대일 관계?
      쉽게 생각하면 하나의 팀에 여러 회원이 소속될 수 있다.

데이터베이스를 기준으로 다중성(N(회원):1(소속))을 결정했다.
외래 키(FK)를 회원이 관리하는 형태다.

데이터베이스는 무조건 N쪽이 외래 키를 갖는다.

N:1 단방향

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")//Fk, 외래키
    private Team team; //연관관계의 주인
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
} 

다대일 단방향에서는 N에 해당하는 Member에만 @ManyToOne을 추가해준 것을 알 수 있다.
단방향이기 때문에 Team에서는 Member을 참조하지 않았다.

다대일 단방향은 가장 많이 사용하는 연관관계다.

N:1 양방향

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")//Fk, 외래키
    private Team team; //연관관계의 주인
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>(); 
} 

다대일 양방향은 1에 해당하는 쪽에 @OneToMany를 추가해주고 연관 관계의 주인을 mappedBy로 지정한다.

mappedBy로 지정할 때 값은 대상이 되는 연관관계 주인의 변수명을 지정하면된다.

📌 1:N 관계 (일대다)

"일대다 관계는 다대일 관계의 반대인데 정리할 필요가 있나?" 라고 생각할 수 있다.
차이점은 연관 관계의 주인을 어디에 두냐에 있다. 다대일은 N쪽에 연관관계 주인을 줬지만 일대다는 1쪽에 준다.

1에 연관 관계의 주인을 두는 것은 좋지 않다. 실무에서는 거의 사용하지 않는 것이 좋다고 한다

1:N 단방향

  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하 는 특이한 구조
@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String username;
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    
    
    @OneToMany
    @JoinColumn(name = "MEMBER_ID")//Fk, 외래키
    private List<Member> members = new ArrayList<>(); //연관관계의 주인
} 

실제 사용

//...
Member member = new Member();
Member.setUserName("Hello");

entityManager.persist(member); // member 저장

Team team = new Team();
Team.setName("JPA");
team.getMembers().add(member);

entityManager.persist(team); // team 저장
//...

멤버를 저장할 때는 정상적으로 Insert 쿼리가 나간다.
그 다음이 문제다.
team를 저장할 때는 Team를 insert하는 쿼리가 나간 후에 team을 update하는 쿼리가 나간다.
왜냐하면 board.getPosts().add(post); 부분 때문이다.

Team 엔티티는 Team 테이블에 매핑되기 때문에 Team 테이블에 직접 지정할 수 있으나, Member 테이블의 FK(BOARD_ID)를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 있다.

치명적인 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
    • 일만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생하는 것.

그렇기 때문에 일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월하기 때문에 다대일을 사용하자.

1:N 양방향

  • 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
  • 공식적으로 존재 X, 생략

결과적으로 일대다(1:N) 단방향, 양방향은 쓰지 말고 차라리 다대일(N:1) 양방향으로 쓰는 것이 맞다.

📌 1:1 관계 (일대일)

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블에 외래키를 넣을 수도 있고, 대상 테이블에 외래 키를 넣을 수도 있다.

    1:1 관계이기 때문에 테이블 A, B가 있을 때, A가 주 테이블이면 B가 대상 테이블, B가 주 테이블이면 A가 대상 테이블

  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

1:1 단방향

  • Member 테이블(주 테이블)에서 FK인 Team 테이블 (대상 테이블)의 PK를 갖도록

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String username;
    
    @OneToOne
    @JoinColumn(name = "TEAM_ID")//Fk, 외래키
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
} 

다대일(@ManyToOne) 단방향 매핑과 유사하다.

1:1 양방향

  • 주 테이블에 외래 키

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")//Fk, 외래키
    private Locker locker;
}

@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    private String name;
    
    @OneToOne(mappedBy = "locker") //읽기 전용
    private Member member;
} 

1:1 양방향은 논란의 여지가 있다.

외래 키를 Member에서 관리하는 게 좋을 것인지, Locker에서 관리하는 게 좋을 것인지 생각을 해봐야한다. 즉, 테이블에 어디에 둘 것 인지를 생각해야한다.

테이블은 한 번 생성되면 보통 굳어지고 변경이 어렵다.

그러나 비즈니스는 언제든 바뀔 수 있다.

Locker를 여러 명의 회원이 쓸 수 있도록 비즈니스가 변경된다면?
그러면 다(N)쪽인 Member 테이블에 외래 키가 있는 것이 변경에 유연하다.

그러면 다(N)가 될 확률이 높은 테이블에 외래 키를 놓는게 무조건 좋을까?

아니다. 객체 입장에서 Locker(1)쪽에서 외래 키를 갖게되면 Locker를 조회할 때마다 이미 Member의 참조를 갖고 있기 때문에 성능상 이득이다.

종합적으로 판단하고 결정해야하는데 단순화해서, 보통 일대일이라고 정할 때도 아주 신중하게 정했다고 가정한다면 주 테이블(Member)에 외래 키를 두는 것이 더 낫다.

다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인이다.

📌 N:M 관계 (다대다)

  • 실무 사용 금지 ❌

    • 중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있다.

    • 다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높다.

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
    • 연결 테이블용 엔티티를 추가하는 것이 더 좋음

JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(연결 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있다.

@ManyToMany -> @OneToMany, @ManyToOne


출처
자바 ORM 표준 JPA 프로그래밍 강의
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글