데이터베이스를 기준으로 다중성을 결정합니다.
연관 관계는 대칭성을 갖습니다.
일대다 ↔ 다대일
일대일 ↔ 일대일
다대다 ↔ 다대다
게시판(Board)과 게시글(Post)의 관계로 예를 들겠습니다.
하나의 게시판(1)에는 여러 게시글(N)을 작성할 수 있습니다.
하나의 게시글은 하나의 게시판에만 작성할 수 있다.
게시글과 게시판은 다대일 관계를 갖습니다.
데이터베이스를 기준으로 다중성(게시글N : 게시판1)을 결정했습니다.
즉, 외래 키를 게시글(N)이 관리하는 일반적인 형태입니다. (참고로 데이터베이스는 무조건 다(N)쪽이 외래 키를 갖습니다.)
→ 다대일(N:1) 단방향
@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
}
다대일 단방향에서는 다 쪽인 Post에서 @ManyToOne 만 추가해준 것을 확인할 수 있습니다.
반대로 Board에서는 참조하지 않습니다. (단방향이기 때문)
→ 다대일(N:1) 양방향
@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) 쪽에 @OneToMany 를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy 로 지정해줍니다.
mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 됩니다. 여기서는 Post 객체(대상)의 board라는 이름의 변수이기 때문에 board로 지정했습니다.
어? 일대다는 다대일에서 반대 입장인데 정리할 필요가 있나? 생각할 수 있지만 앞서 다대일의 기준은 연관관계의 주인 다(N)쪽에 둔 것이고 이번에 언급할 일대다의 기준은 연관관계의 주인을 일(1)쪽에 둔 것입니다.
→ 일대다(1: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가 수정이 되지? 이런 생각을 하게 만듦.
업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
그렇기 때문에 TIP으로 일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월하기 때문에 이 방식을 채택하는 것을 추천합니다.
그런데 실무에서 사용을 금지하지 않는 이유는 되도록 피하는 게 좋지만, JPA 값 타입을 사용하는 것을 대신하여 사용할 때는 또 유용합니다. = 유용한 경우가 적게 나마 있음.
일대다 양방향은 공식적으로 존재하는 건 아니라서 생략하겠습니다.
키워드는 @JoinColumn(updatable = false, insertable = false) 이지만, 일대다 양방향을 사용해야할 때는 다대일 양방향 사용하도록 하는게 더 좋습니다.
일대일(1:1)
주 테이블에 외래키를 넣을 수도 있고, 대상 테이블에 외래키를 넣을 수도 있습니다.
※ 일대일(1:1)이기 때문에 테이블 A, B가 있을 때, A가 주 테이블이면 B가 대상 테이블이고, B가 주 테이블이면 A가 대상 테이블입니다.
→ 일대일(1:1) 단방향
외래 키를 주 테이블이 갖고 있다는 의미로 해석하겠습니다. (Post테이블(주 테이블)에서 외래키(FK)인 Attach 테이블(대상 테이블)의 PK를 갖고 있도록)
게시글(Post)에 첨부파일(Attach)을 반드시 1개만 첨부할 수 있다고 가정합니다.
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
//... getter,setter
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
//... getter, setter
}
특별할 게 없습니다.
→ 일대일(1:1) 양방향
단순하게 똑같이 @OneToOne 설정하고 mappedBy 설정만 해서 읽기 전용으로 만들어주면 양방향도 간단하게 됩니다.
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "attach")
private Post post;
//... getter, setter
}
아까 정리했는데 왜 또 나왔냐하면, 이번에는 Post테이블(주 테이블)이 아닌 Attach테이블(대상 테이블)에 외래 키(FK)를 갖고 있을 때를 생각해보려고 합니다.
그러나 이거는 JPA에서는 아예 지원을 하지 않습니다.
→ 일대일(1:1) 양방향
이럴 때는 어차피 양 쪽이 일대일이기 때문에 위에서 정의한 대로 처리하면 됩니다.
그러나 논란의 여지가 있습니다.
외래 키를 Post에서 관리하는 게 좋을 것인지, Attach에서 관리하는 게 좋을 것인지 생각을 해봐야합니다. 즉 테이블에 어디에 둘 것 인지를 생각해야합니다.
테이블은 한 번 생성되면 보통 굳어집니다. 변경이 어렵다는 얘기입니다.
그러나 비즈니스는 언제든 바뀔 수 있습니다.
게시글이 여러 개의 첨부파일을 첨부할 수 있도록 비즈니스가 변경되면 어떨까요?
그러면 다(N)쪽인 Attach테이블에 외래 키가 있는 것이 변경에 유연합니다.
그러면 다(N)가 될 확률이 높은 테이블에 외래 키를 놓는게 무조건 좋을까요?
그건 또 아닙니다.
객체 입장에서 Post쪽(1)에서 외래 키를 갖게되면 Post를 조회할 때마다 이미 Attach의 참조를 갖고 있기 때문에 성능상 이득이 있습니다.
※ 결론
종합적으로 판단하고 결정해야하는데 단순화해서, 보통 일대일이라고 정할 때도 아주 신중하게 정했다고 가정한다면 주 테이블(Post)에 외래 키를 두는 것이 더 낫습니다.
다시 말씀드리지만 논쟁이 있고 의견일 뿐입니다.
실무 사용 금지 ❌
중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문입니다.
다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높습니다. JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있습니다.
출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그]