엔티티의 연관관계를 매핑할 때는 3가지를 고려해야한다.
- 다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
- 단방향, 양방향 : (객체 참조)
- 연관관계의 주인 : 양방향일 때, 연관 관계에서 관리 주체
연관관계 매핑 : 비즈니스 로직, 비즈니스 요구사항에 따라 개발자가 더 적절한 관계 설정 방법을 선택
다중성
다대일(@ManyToOne)
일대다(@OneToMany)
일대일(@OneToOne)
다대다(@ManyToMany)
※ 보통 다대일과 일대다 관계를 많이 사용하고 다대다 관계는 실무에서 거의 사용하지 않는다.
단방향, 양방향
- 데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다.
- 데이터베이스는 양방향으로 쿼리가 가능하기 때문에 단방향이니 양방향이니 나눌 필요가 없다.
- 반면, 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
- 그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다.
- 사실, 엄밀하게는 양방향 관계↔️는 없고 두 객체가 단방향 참조를 각각 가져서 양방향 관계처럼 사용하고 말하는 것
연관관계 주인
- 두 객체(A, B)가 양방향 관계, 다시 말해 단방향 관계 2개(A→B, B→A)를 맺을 때, 양방향 참조가 존재하기 때문에 어느 쪽에서 외래키를 관리할지 정해야한다.
- 외래 키를 가진 테이블을 매핑한 엔티티에서 외래 키를 관리하는게 효율적이다.
- 따라서 이곳을 연관관계의 주인으로 선택한다. 외래 키를 가진 엔티티가 주인이라고 생각하면 쉽다.
- 일대다, 다대일 관계에서 항상 '다'쪽이 외래키를 가진다. 주인이 아닌 쪽은 외래 키를 변경할 수 없고 읽기만 가능하다.
다대일(N : 1) 단방향
게시판(Board)과 게시글(Post)의 관계로 이해해 보기!
- 요구 사항
- 하나의 게시판(1)에는 여러 게시글(N)을 작성할 수 있습니다.
- 하나의 게시글은 하나의 게시판에만 작성할 수 있다.
- 게시글과 게시판은 다대일 관계를 갖습니다.
데이터베이스를 기준으로 다중성(게시글N : 게시판1)을 결정했다. 즉, 외래 키를 게시글(N)이 관리하는 일반적인 형태.
다대일 단방향에서는 다 쪽인 Post에서 @ManyToOne 만 추가해준 것을 확인할 수 있다. 반대로 Board에서는 참조하지 않는다. (단방향이기 때문)
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
//... getter, setter
다대일(N : 1) 양방향
다대일 양방향으로 만드려면 일(1) 쪽에 @OneToMany 를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy 로 지정해준다. mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 된다. 여기서는 Post 객체(대상)의 board라는 이름의 변수이기 때문에 board로 지정
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "board")
List<Post> posts = new ArrayList<>();
//... getter, setter
}
일대다(1 : N) 단방향
(참고로 실무에서는 일대다(1:N) 단방향은 거의 쓰지 않는다.)
데이터베이스 입장에서는 무조건 다(N)쪽에서 외래키를 관리한다. 일(1)쪽 객체에서 다(N) 쪽 객체를 조작(생성,수정,삭제)하는 방법!
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany
@JoinColumn(name = "POST_ID") //일대다 단방향을 @JoinColumn필수
List<Post> posts = new ArrayList<>();
//... getter, setter
}
@OneToMany에 mappedBy가 없어진다. 양방향이 아니기 때문. 대신 @JoinColumn을 이용해서 조인을 한다.
실제 사용은 아래와 같다.
//...
Post post = new Post();
post.setTitle("가입인사");
entityManager.persist(post); // post 저장
Board board = new Board();
board.setTitle("자유게시판");
board.getPosts().add(post);
entityManager.persist(board); // board 저장
//...
위와 같은 시나리오로 동작을 살펴보면, post를 저장할 때는 멀쩡하게 insert 쿼리가 나가지만 그 다음이 문제이다.
board를 저장할 때는 Board를 insert하는 쿼리가 나간 후에 post를 update하는 쿼리가 나간다.
왜냐하면 board.getPosts().add(post); 부분 때문!
Board 엔티티는 Board 테이블에 매핑되기 때문에 Board 테이블에 직접 지정할 수 있으나, Post 테이블의 FK(BOARD_ID)를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 있다.
치명적인 단점
- 일만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생하는 것.
- Board를 저장했는데 왜 Post가 수정이 되지? 이런 생각을 하게 만듦.
- 업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
정리 : 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자. 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리한다는 것은 성능 문제도 있지만 관리도 부담스럽다. 해결법은 다대일 양방향 매핑을 사용하는 것이다.
일대다(1 : N) 양방향
(실무 사용 금지 )
일대다 양방향 매핑은 존재하지 않는다. 대신 다대일 양방향 매핑을 사용해야한다. (다대일 양방향과 일대다 양방향은 사실 똑같은 말이다) 양방향 관계에서 @OneToMany는 주인이 될 수 없다. 관계형 데이터베이스 특성상 일대다, 다대일 관계는 항상 '다'쪽에 외래 키가 있다. 따라서 연관관계의 주인은 항상 @ManyToOne이다.
그렇다고 일대다 양방향 매핑이 완전히 불가능하지는 않다. 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방행 매핑을 읽기 전용으로 추가하면 된다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany
@JoinColumn(name = "TEAM_ID") //MEMBER테이블의 TEAM_ID(FK)
private List<Member> members = new ArrayList<Member>();
private String name;
...
}
둘 다 같은 키를 관리하므로 문제가 될 수 있지만 Member의 JoinColumn속성을 보면 읽기만 가능하게 했다. 이 방법은 다대일 양방향처럼 보이게하지만 일대다 단방향 매핑이 가지는 단점을 그대로 가진다. 웬만하면 다대일 양방향 매핑을 사용하자.
[ 참고 자료 ]
https://jeong-pro.tistory.com/231
'Coding > Spring' 카테고리의 다른 글
[27] Spring AOP (0) | 2022.12.14 |
---|---|
[26] OAuth / 소셜 로그인 (0) | 2022.12.14 |
[24] ERD(Entity Relationship Diagram) (0) | 2022.12.13 |
[23] Spring Security (0) | 2022.12.13 |
[22] 항해99 주특기 숙련주차 시험 Spring (0) | 2022.12.08 |