[JPA] 연관 관계 정리

윤재열·2022년 5월 9일
0

JPA

목록 보기
11/21
post-custom-banner

JPA에서 가장 중요한 것.

JPA에서 가장 중요한 것을 생각해 보았는데, "객체"와 관계형 데이터베이스 테이블이 어떻게 매핑되는지를 이해하는것" 이 라고 생각합니다.
왜냐하면 JPA의 목적인 "객체 지향 프로그래밍과 데이터베이스 사이의 패러다임 불일치를 해결" 이라는 것과 가장 직접적으로 연관되어 있기 때문입니다.

  • 객체와 테이블 매핑에 대한 내용을 더 구체적으로 나누면 칼럼, 타입, 테이블 등에 대한 1차원적인 매핑과 테이블 간의 연관 관계 매핑으로 나눌 수 있습니다.

  • 1차원적인 매핑의 경우에는 @Entity,@Column,@Id,@GeneratedValue, @Enumerated 등의 말 그대로 객체와 데이터베이스 사이의 일대일로 대응되는 것으로써 기본적인 어노테이션을 숙지하고 필요한 경우에 찾아보는게 효율적입니다.

  • 연관 관계 매핑은 필요할 때 찾아보기 보다는 비즈니스 로직, 비즈니스 요구사항에 따라 개발자가 더 적절한 관계 설정 방법을 선택해야 하는 주제이기 때문에 공부해 보았습니다.

연관 관계 정의 규칙

  • 연관 관계를 매핑할 때, 생각해야 할 것 3가지
    • 방향 : 단방향,양방향(객체참조)
    • 연관 관계의 주인 : 양방향 일 때, 연관관계에서 관리 주체
    • 다중성: 다대일(N:1),일대다(1:N),일대일(1:1), 다대다(N:M)

단방향, 양방향

  • 데이터베이스 테이블은 외래 키(FK) 하나로 양 쪽 테이블 조인이 가능합니다.

  • 따라서 데이터베이스는 단방향이니 양방향이니 나눌 필요가 없습니다.

  • 그러나 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능합니다.

  • 그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 합니다.

  • 엄밀하게는 양방향 관계라는 것은 없고 두 객체가 단방향 참조를 각각 가져서 양방향 관계처럼 사용하는 것입니다.

  • JPA를 사용하여 데이터베이스와 패러다임을 맞추기 위해서 객체는 단방향 연관 관계를 가질지, 양방향 연관 관계를 가질지 선택합니다.

  • 선택은 비즈니스 로직에서 두 객체가 참조가 필요한지 여부를 고민해보면 됩니다.

    • Board.getPost()처럼 참조가 필요하다면 Board -> Post 단방향 참조
      • 만약 참조가 굳이 필요없다면 하지 않아도됩니다.
    • Post.getBoard()처럼 참조가 필요하다면 Post -> Board 단방향 참조
      • 만약 참조가 굳이 필요없다면 하지 않아도 됩니다.
  • 이렇게 비지니스 로직에 맞게 선택했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관 관계가 되는 것입니다.

무조건 양방향 관계를 하면 쉽지 않을까?

  • 객체 입장에서 양방향 매핑을 했을 때 오히려 복잡해 질 수 있습니다.
  • 예를 들어 일반적인 비지니스 애플리케이션에서 사용자(User)엔티티는 많은 엔티티와 연관 관계를 갖습니다.
  • 이런 경우에 모든 엔티티를 양방향 관계로 설정하게 되면 사용자(User)엔티티는 엄청나게 많은 테이블과 연관 관계를 맺게 되고 User 클래스를 보면 엄청나게 복잡해집니다.
  • 그리고 다른 엔티티들도 불필요한 연관관계 매핑으로 인해 복잡성이 증가할 수 있습니다.
  • 그래서 양방향으로 할지 단방향으로 할지 필히 구분해 줘야합니다.
  • 구분하기 좋은 기준은 기본적으로 단방향 매핑으로 하고 나중에 역방향으로 객체 탐색이 꼭 필요하다고 느낄 때 추가하는 것으로 잡으면 됩니다.

연관 관계의 주인

  • 두 객체 (A,B)가 양방향 관계, 다시 말해 단방향 관계 2개(A->B,B->A)를 맺을 때, 연관 관계의 주인을 지정해 주어야 합니다.

  • 연관 관계의 주인을 지정하는 것은 두 단방향 관계 중에서 제어의 권한(외래 키를 비롯한 테이블 레코드를 저장,수정,삭제 처리)를 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려준다고 생각하면 됩니다.

  • 연관 관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회,저장,수정,삭제를 할 수 있지만, 연관 관계의 주인이 아니면 조회만 가능합니다.

  • 연관 관계의 주인이 아닌 객체에서 mappedBy 속성을 사용해서 주인을 지정해줘야 합니다.

  • 외래 키가 있는 곳을 연관관계의 주인으로 정하면 됩니다!!!!!!!중요x100

연관 관계 주인을 지정하는 이유

  • 두 객체 (Board,Post)가 있고 양방향 연관 관계를 갖는다고 생각해 봅니다.
  • 게시글(Post)의 게시판을 다른 게시판(Board)으로 수정하려고 할 때, Post 객체에서 setBoard(...)같은 메서드를 이용해서 수정하는게 맞는지, Board객체에서 getPost()같은 메서드를 이용해서 List의 게시글을 수정하는게 맞는지 헷갈릴 수 있습니다.
  • 물론 두 객체 입장에서는 두 방법 모두 맞는 방법입니다.
  • 그러나 이렇게 객체에서 양방향 연관 관계 관리 포인트가 두 개일 때는 테이블과 매핑을 담당하는 JPA입장에서는 혼란을 주게 됩니다.
  • 즉, Post에서 Board를 수정할 때 FK(Foreign Key)를 수정할 지,
    Board에서 Post를 수정할 때 FK(Foreign Key)를 수정할 지를 결정하기가 어렵습니다.
  • 그렇기 때문에 두 객체 사이의 연관 관계의 주인을 정해서 명확하게 Post에서 Board를 수정할 때만 FK를 수정하겠다! 라고 정하는 것입니다.

연관 관계의 주인만 제어하면 될까?

  • 데이터베이스의 외래 키가 있는 테이블을 수정하려면 연관 관계의 주인만 변경할 수 있습니다.
  • 하지만 그것은 데이터베이스만 생각했을 때고, 객체를 생각해 보면 사실 둘 다 변경해 주는 것이 좋습니다.
    (연관 관계의 주인이 아닌 곳에서도 변경!)
  • 왜냐하면 두 참조를 사용하는 순수한 두 객체는 데이터 동기화를 해줘야 하기 때문입니다.

다중성

  • 데이터베이스를 기준으로 다중성을 결정합니다.
  • JPA는 JPQL도 그렇고 보통 객체를 기준으로 하는게 일반적인데 다중성을 기준은 데이터베이스 입니다.
  • 연관 관계는 대칭성을 갖습니다.
    • 일대다 & 다대일
    • 일대일 & 일대일
    • 다대다 & 다대다

다대일(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 를 추가해준것을 확인할 수 있습니다.

다대일 (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) 단방향

  • 전과 다르게 연관관계의 주인을 일(1)쪽에 둘 수 있습니다.

  • 참고로 실무에서는 일대다(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")
    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) 단방향,양방향은 쓰지 말고 차라리 다대일(N: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에서 관리하는게 좋을지 생각을 해 봐야합니다.
  • 즉 테이블에 어디에 둘 것 인지를 생각해야 합니다.
  • 테이블은 한번 생성되면 변경이 어렵습니다.
  • 하지만 비지니스는 언제든 바뀔 수 있습니다.
  • 게시글이 여러 개의 첨부파일을 첨부할 수 있도록 비즈니스가 변경되었다면??
    • 그렇다면 다(N)가 될 확률이 높은 테이블에 외래 키를 좋을 것 같지만 그것은 또한 아닙니다.
    • 객체 입장에서 Post쪽(1)에서 외래 키를 갖게 되면 Post를 조회할 때마다 이미 Attach를 참조를 갖고 있기 떄문에 성능상 좋을 수 있습니다.
  • 결론 : 종합적으로 판단하고 결정해야 하는데 단순화하여 보통 일대일이라고 정할 때도 아주 신중하게 정했다고 가정한다면 주 테이블(Post)에 외래 키를 두는 것이 더 낫습니다.
profile
블로그 이전합니다! https://jyyoun1022.tistory.com/
post-custom-banner

0개의 댓글