[DB] 테이블을 설계하자! - 2

SeoYoung Jung·2022년 3월 18일
0

DataBase

목록 보기
2/6

다중성

데이터베이스를 기준으로 다중성을 결정합니다.

연관 관계는 대칭성을 갖습니다.
일대다 ↔ 다대일
일대일 ↔ 일대일
다대다 ↔ 다대다

다대일(N:1)

게시판(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로 지정했습니다.

일대다(1:N)

어? 일대다는 다대일에서 반대 입장인데 정리할 필요가 있나? 생각할 수 있지만 앞서 다대일의 기준은 연관관계의 주인 다(N)쪽에 둔 것이고 이번에 언급할 일대다의 기준은 연관관계의 주인을 일(1)쪽에 둔 것입니다.

※ 참고로 실무에서는 일대다(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가 수정이 되지? 이런 생각을 하게 만듦.
업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
그렇기 때문에 TIP으로 일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월하기 때문에 이 방식을 채택하는 것을 추천합니다.

그런데 실무에서 사용을 금지하지 않는 이유는 되도록 피하는 게 좋지만, JPA 값 타입을 사용하는 것을 대신하여 사용할 때는 또 유용합니다. = 유용한 경우가 적게 나마 있음.

→ 일대다(1:N) 양방향 (실무 사용 금지 ❌)

일대다 양방향은 공식적으로 존재하는 건 아니라서 생략하겠습니다.

키워드는 @JoinColumn(updatable = false, insertable = false) 이지만, 일대다 양방향을 사용해야할 때는 다대일 양방향 사용하도록 하는게 더 좋습니다.

🌈 결과적으로 일대다(1:N) 단방향, 양방향은 쓰지 말고 차라리 다대일(N:1) 양방향으로 쓰는 것이 맞다라고 단순화하여 결론 내리면 될 것 같습니다.

일대일(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
}

→ 일대일(1:1) 단방향 지원 안함 ❌

아까 정리했는데 왜 또 나왔냐하면, 이번에는 Post테이블(주 테이블)이 아닌 Attach테이블(대상 테이블)에 외래 키(FK)를 갖고 있을 때를 생각해보려고 합니다.

그러나 이거는 JPA에서는 아예 지원을 하지 않습니다.

→ 일대일(1:1) 양방향

이럴 때는 어차피 양 쪽이 일대일이기 때문에 위에서 정의한 대로 처리하면 됩니다.

그러나 논란의 여지가 있습니다.

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

테이블은 한 번 생성되면 보통 굳어집니다. 변경이 어렵다는 얘기입니다.

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

게시글이 여러 개의 첨부파일을 첨부할 수 있도록 비즈니스가 변경되면 어떨까요?

그러면 다(N)쪽인 Attach테이블에 외래 키가 있는 것이 변경에 유연합니다.

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

그건 또 아닙니다.

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

※ 결론

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

다시 말씀드리지만 논쟁이 있고 의견일 뿐입니다.

다대다(N:N)

실무 사용 금지 ❌
중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문입니다.
다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높습니다. JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있습니다.

출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그]

profile
뚱땅뚱땅개발자

0개의 댓글