토이 프로젝트 배우게 된 것들 & 오류 해결🐰

JPA 연관관계 총 정리

j_estory 2022. 11. 17. 23:11

🏷 JPA에서 가장 중요한 것

 

JPA에서 가장 중요한 것을 뽑자면 객체와 관계형 데이터베이스 테이블을 어떻게 매핑되는지 이해하는 것이다.

왜냐하면 JPA의 목적인 "객체 지향 프로그래밍과 데이터베이스 사이의 패러다임 불일치를 해결" 이라는 것과 가장 직접적으로 연관되어 있기 때문이다. 

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

 

  • 1차적인 매핑
    • @Entity, @Column, @Id, @GeneratedValue, @Enumerated .. 등의 말 그대로 객체와 데이터 베이스 사이의 1:1로 대응되는 것으로써, 기본적인 annotation을 숙지하고 필요한 경우에 찾아보는게 효율적이다. 
  • 연관 관계 매핑
    • 그때 그때 찾아보기보다는 비즈니스 로직, 비즈니스 요구사항에 따라 개발자가 더 적절한 관계 설정 방법을 선택해야 하는 주제이기때문에 학습하기 위해서 아래에 정리를 했다.

 

🏷 연관 관계 정의 규칙

 

연관 관계를 매핑할 때, 생각해야 할 것은 크게 3가지가 있다.

  • 방향 : 단방향, 양방향
  • 연관 관계의 주인: 양방향일 때, 연관 관계에서 관리 주체
  • 다중성: 다대일(n:1), 일대다(1:n), 일대일(1:1), 다대다(n:m)

하나 하나 생각해보자!

 

✔️ 단방향, 양방향

 

데이터베이스 테이블은 외래 키 하나로 양 쪽의 테이블 조인이 가능하다.

따라서, 데이터베이스는 단방향, 양방향 등을 나눌 필요가 없다. 

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

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

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

 

JPA를 사용하면 데이터베이스와 패러다임을 맞추기 위해서 객체는 

단방향 연관 관계를 가질지, 양방향 연관 관계를 가질지 선택해야 한다.

 

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

  • Board.getPost() 처럼 참조가 필요하면 Board 🔜 Post : 단방향 참조
  • Post.getBoard() 처럼 참조가 필요하면 Post 🔜 Board : 단방향 참조

이렇게 비즈니스 로직에 맞게 선택했는데 두 객체가 서로 단방향 참조를 했다면

양방향 연관 관계가 되는 것이다.

 

❓ 무조건 양방향 관계를 하면 쉽지 않나

 

객체 입장에서는 양방향 매핑을 했을 때 오히려 복잡해질 수 있다.

무조건 양방향 맵핑으로 구현할 때, 굉장히 많은 엔티티와 연관 관계를 가지게 된다. 

이런 경우, 해당 클래스의 복잡성이 증가할 수 있다. 

그래서, 양방향으로 할지 단방향으로 할지는 필수적으로 구분해줘야 한다. 

 

구분하기 좋은 기준은 !! 

기본적으로 단방향 매핑으로 하고 나중에 양방향 매핑이 꼭 필요하다고 느낄 때 추가하는 것으로 잡으면 된다. 

 

🏷 연관 관계의 주인

 

두 객체(A,B) 가 양방향 관계, 다시 말해 단방향 관계 2개(A > B , B> A)를 맺을 때,

연관 관계의 주인을 지정해줘야 한다.

 

연관 관계의 주인을 지정하는 것은 두 단방향 관계 중, 제어의 권한을 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려주는 것이다.

더보기

제어의 권한 

- 외래 키를 비롯한 테이블 레코드를 저장, 수정, 삭제 처리

연관 관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만,

연관 관계의 주인이 아니면 조회만 가능하다. 

 

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

 

📌 TIP.

외래 키가 있는 곳을 연관 관계의 주인으로 정하면 된다. (무조권?)

 

❓ 왜 연관 관계의 주인을 지정해 줘야 할까?

 

두 객체 (Board, Post) 가 있고, 양방향 연관 관계를 갖는다고 생각해보자.

그 상황에서 게시글의 게시판을 다른 게시판으로 수정을 한다고 할 때,

 

Post 객체에서 setBoard() 와 같은 메소드를 이용해서 수정하는게 맞는지, 

Board 객체에서 getPosts() 같은 메소드를 이용해서 List의 게시글을 수정하는 게 맞는지!

 

두 객체 입장에서는 두 방법 모두 맞는 방법이다.

 

그러나, 이렇게 객체에서 양방향 연관 관계로, 관리 포인트가 2개가 될 때

테이블과 매핑을 담당하는 JPA 입장에서는 혼란을 주게 된다. 

그렇게 때문에 두 객체 사이의 연관 관계의 주인을 정해서 Post에서 Board를 수정할 때에만 FK를 수정하겠다! 라고 정하는 것이다.

 

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

 

데이터베이스에 외래 키가 있는 테이블을 수정하려면 연관 관계의 주인만 변경하는 것이 맞나?

맞습니다 !

 

하지만, 그것은 데이터베이스 기준이고, 객체를 생각해보면 사실 둘 다 변경해주는 것이 좋다. 

(연관 관계의 주인이 아닌 곳에서도 변경!)

 

왜냐면, 두 참조를 사용하는 순수한 두 객체는 데이터를 동기화 해줘야 하기 때문이다.

 

🏷 다중성

 

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

 

연관 관계는 다음과 같은 대칭성을 갖습니다.

  • 일대다 🔛 다대일
  • 일대일 🔛 일대일
  • 다대다 🔛 다대다

 

🟠 다대일 (N:1)

 

게시판과 게시글의 관계를 예로 들겠습니다. 

  • 요구 사항
    • 하나의 게시판에는 여러 게시글을 작성할 수 있습니다. 
    • 하나의 게시글은 하나의 게시판에만 작성할 수 있습니다.
    • 게시글과 게시판은 다대일의 관계를 갖습니다. 

데이터베이스를 기준으로 다중성을 결정했습니다. (게시글 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;
}

@Entity
public class Board {
    @Id @GeneratedValue
    @Column(name="board_id")
    private Long id;
    
    @Column(name="title")
    private String title;
}
  • 다대일 단방향에서는 다 쪽인 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;
}

@Entity
public class Board {
    @Id @GeneratedValue
    @Column(name="board_id")
    private Long id;
    
    @Column(name="title")
    private String title;
    
    @OneToMany(mappedBy="board")
    List<Post> posts = new ArrayList<>();
}

 

다대일 양방향으로 만드려면 일(1) 쪽에 @OneToMany 를 추가하고, 

양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy로 지정해준다. 

 

mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 된다. 

여기서는 Post 객체의 board라는 이름의 변수이기 때문에 board로 지정해주면 된다. 

 

 

🟢 일대다(1:N)

 

어? 일대다는 다대일에서 반대 입장인데 정리할 필요가 있나? 할 수 있겠지만, 

앞서 다대일의 기준은 연관관계의 주인 다(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;
}

@Entity
public class Board {
    @Id @GeneratedValue
    @Column(name="board_id")
    private Long id;
    
    @Column(name="title")
    private String title;
    
    @OneToMany
    @JoinColumn(name="post_id")
    List<Post> posts = new ArrayList<>();
}
  • @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 테이블과 매핑이 되기 때문에 직접 저장할 수 있으나,

Post 테이블의 FK를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 생긴다.

 

🚫 치명적인 단점

  • Board를 저장했는데 왜 Post가 수정이 되지? 이러한 생각을 하게 된다. 
  • 업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않다. 

이러한 단점이 존재하기 때문에, 일대다 단방향 연관 관계 매핑이 필요한 경우는 다대일 양방향 연관 관계를 매핑 하는것을 추천한다. 

실무 사용은 되도록 피하는게 좋지만, JPA 값 타입을 사용하는 것을 대신하여 사용할 때에는 적게나마 있다.

 

✔️ 일대다(1:N) 양방향

 

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

위의 해당하는 키워드

  • @JoinColumn(updateable = false, insertable = false) 이지만, 일대일 양방향을 사용할 때에는 다대일 양방향을 사용하는 것이 더 좋다. 

 

✅ 결과적으로 !!

 

일대다 단방향, 양방향은 쓰지 말고, 다대일 양방향으로 쓰는 것으로 결론을 내리면 될 것 같다. 

 

 

🔵 일대일 (1:1)

 

주 테이블에 외래키를 넣을 수도 있고, 대상 테이블에 외래키를 넣을 수도 있다. 

 

✔️ 일대일(1:1) 단뱡향

 

외래키를 주 테이블이 갖고 있다는 의미로 해석하겠다.

게시글에 첨부파일을 반드시 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;
}

@Entity
public class Attach {

    @Id @GeneratedValue
    @Column(name = "ATTACH_ID")
    private Long id;
    
    ...
}
  • OneToOne 일대일 단방향에서는 위의 어노테이션과 JoinColumn을 통해 대상이 되는 컬럼을 지정해주면 된다.

 

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

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

위의 1:1 단방향은 주 테이블이 아닌 대상 테이블에 외래키를 갖고 있을 때를 생각해보려고 합니다. 

더보기

굼금한게 .. 주테이블과 대상 테이블의 기준은 사용자가 정하기 나름 아닌가..?

아하..! db 설계 시, 외래키가 있는 쪽이 post 테이블이니깐 이게 주테이블로 하는건가?

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

그러면 외래키를 post에서 관리하면 좋을 지, attach에서 관리하면 좋을지 생각을 해봐야 합니다. 

 

테이블은 한번 생성되면 변경이 거의 어렵지만 비즈니스는 언제든 바뀔 수 있습니다. 

 

게시글이 여러개의 첨부파일을 첨부할 수 있도록 변경..? 

다(N) 쪽인 Attach 에서 외래키를 관리하는게 변경에 유연해진다. 

그렇다면, 다 쪽이 될 확률이 높은 테이블에 외래키를 두는 것이 좋은 것일까?

그건 또 아니다 ..!! 

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

 

✅ 결론

 

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

 

🟡 다대다(N:N)

 

실무 사용 금지 !!!

 

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

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

 

JPA를 해보면 중간 테이블에 외래키 외, 다른 정보가 들어가는 경우가 많기 때문에,

다대다를 일대다, 다대일로 풀어서 만드는 것이 추후 변경에도 유연하게 대처할 수 있습니다.