[JPA] JPA 연관관계 어노테이션

Intro

레거시를 사용할 때는 DB의 연관관계를 설정할 때 Foreign Key를 직접 설정하여 DB 관계를 설정하였고, JOIN을 할 때도 Mybatis를 사용했기에 직접 쿼리문을 작성하였다. 하지만, JPA에서 연관관계를 설정할 때 Entity 파일에 연관관계 어노테이션을 사용하여 설정할 수 있음을 배웠으며 연관관계를 단번에 이해하기 어려운 내용이므로 정리하고자 한다.

연관 관계

연관 관계 매핑을 하기 앞서, 연관 관계란 객체와 객체, 테이블과 테이블 간의 관계가 있는 것을 의미한다. 예를 들어 Member 객체(테이블)와 Board 객체(테이블)이 연관 관계를 가진다. 회원이 게시글을 작성하기에 서로의 관계를 맺을 필요가 있기에 이러한 객체(테이블)들을 연결하면 연관관계가 있다고 표현한다.

연관관계 고려 사항

첫 번째로 방향이다. 단방향인 지 양방향인 지 알아야 한다.
두 번째로는 연관관계의 주인이다. 양방향일 경우의 관리 주체가 누구인 지 고려해야 한다.
세 번째로는 다중성이다. 다대다, 다대일, 일대일, 일대다 인지 정해야 한다.

1. 방향

  • 방향에는 단방향, 양방향이 존재한다. DB에서는 Foreign Key를 설정하면 양 쪽 테이블 조인이 가능했기에 단방향 양방향을 구분할 필요가 없었다. 하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
    • 예를 들어 설명하자면, Board(게시판) 객체가 작성자 정보가 필요하기에 Member의 Id 컬럼을 Board에 추가한다. 이 때 Member의 Id 필드가 참조용 컬럼이 되므로 Board가 Member와 연관관계를 맺었다고 할 수 있다. 반대로 Member에는 Board에 대한 컬럼을 추가하지 않았기에 Member가 Board를 조회할 수 없다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    @Entity
    public class Member {
        private Long id;
        private String MemberName;
        //Member는 자신과 연관된 Board를 알 수 없다.
    }
    
    @Entity
    public class Board {
        private Long id;
        //Board는 자신과 연관된 Member를 알고 있다.
        private Member member;
        private String title;
    }
    
  • 위 예시에서 Member에 Board를 추가하면 양방향이 되는 것인가?
    • 이를 정확하게 말하면 객체에서는 양방향 연관관계라는 것이 없다. 단지, 두 객체를 서로 단방향으로 설정하여 양방향인 것처럼 보이게 하는 것이다.
  • 모두 다 양방향 관계로 사용하면 안되는 것인가?
    • 비즈니스 로직을 고려하여 참조가 필요한 경우에만 참조 컬럼을 추가하는 것이 좋다.
    • User 객체의 경우는 대부분의 테이블과 연관관계를 가진다. 만약, 모두 양방향으로 연관관계를 걸게되면 굉장히 복잡도가 올라가게 된다. 그렇기에 기본적으로 단방향 매핑을 한 후 역방향으로 참조가 반드시 필요할 때만 추가하는 것이 좋다.

2. 연관관계 주인(feat. mappedBy)

  • 두 객체의 관계 설정 시 연관관계 주인을 설정해야 한다.
  • 두 객체를 서로 단방향으로 연결했을 때 외래키는 1개이므로 둘 중 하나의 객체에서 외래키를 관리해야 하는데, 이것을 연관관계의 주인이라 한다. 즉, 주인이 외래키를 관리한다고 생각하면 된다.
  • 연관관계 주인이 될 경우 주인만 데이터베이스와 매핑되고 외래 키를 등록, 수정, 삭제를 할 수 있다.
  • 반면, 주인이 아닌 다른 객체의 경우는 읽기만 할 수 있게 된다.
  • 주인을 구별할 수 있게 mappedBy를 사용할 수 있다. mappedBy는 “이 컬럼은 연관관계의 주인이 아니다”를 선언하는 것으로 이제 다중성에서 나올 @OneToMany에서 사용할 수 있다.
    • ex) @OneToMany(mappedBy = "board")
  • mappedBy의 중요한 점은 주인에 mappedBy를 사용하는 것이 아니라, 주인이 아닌 객체에 mappedBy속성을 사용하여 속성의 값으로 연관관계의 주인을 지정해야 한다.

3. 다중성

3-1. 다대일(N:1, @ManyToOne)

  • 요구사항 예시 : Board(N) & User(1)의 관계를 가진다.
  • Board 객체(N)에서 User(1)를 참조할 때 Board객체에서 @ManyToOne을 사용한다.
    • 다대일 단방향 기준(N쪽인 Board에서만 @ManyToOne을 추가한다.)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      // Board 객체 (단방향 기준)
      public class Board {
      @ManyToOne(fetch = FetchType.EAGER) 
      @JoinColumn(name = "userId") 
      // ManyToOne과 JoinColumn을 사용하여 FK로 설정할 컬럼을 name속성으로 지정해야 함.
      private User user; 
            
      }
      
      // User 객체
      public class User {
      @Id
      private int id; 
      
      
    • 다대일 양방향 기준(1쪽인 User에 @OneToMany를 추가한다.)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
       // Board 객체 (양방향 기준)
      @Entity
      public class Board {
      @ManyToOne(fetch = FetchType.EAGER) 
      @JoinColumn(name = "userId") 
      // ManyToOne과 JoinColumn을 사용하여 FK로 설정할 컬럼을 name속성으로 지정해야 함.
      private User user; 
            
      }
      
      // User 객체
      @Entity
      public class User {
      @Id
      @GeneratedValue
      private int id; 
      
      @OneToMany(mappedBy = "board")
      //mappedBy를 사용하여 board가 주인임을 표시함.
      private Board board;
      
  • @ManyToOne 속성

    속성 기능 기본값
    optional not null 제약조건 여부 지정하는 기능 optional=true
    (true일 때 nullable, false는 not null 제약조건이 걸리게 된다.)
    fetch 패치 전략 설정하는 기능.(따로 정리할 예정) @ManyToOne = FetechType.EAGER
    @OneToMany = FetchType.LAZY
    cascade 영속성 전이 기능 사용 여부(따로 정리할 예정)  
    targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않음  
  • @JoinColumn 속성(@JoinColumn은 외래키를 매핑할 때 사용하는 어노테이션.)
    • name, foreignKey, referencedColumnName 등이 있다.

3-2. 일대다(1:N, @OneToMany)

  • 다대일에서 일대다를 다뤘지만 별도로 다루는 이유는 위에서 다대일의 경우 연관관계 주인을 다(N)에 둔 것이며 여기서 설명하는 일대다는 일(1)에 연관관계 주인을 설정한 것이다.
    • 즉, 연관관계 주인을 기준으로 다대일인지, 일대다인지 결정한다.
  • 일반적으로 권장되지 않는 방법으로, 다대일 방법을 권장한다.
    • 왜냐하면, DB에서 기본적으로 다(N)쪽에서 외래키를 관리하는데 일대다의 경우 일(1)이 주인임에도 불구하고 다(N)쪽에서 외래키를 관리하기 때문에 일반적으로 사용되지 않음.
  • 일대다 단방향은 존재하긴하지만, 일대다 양방향은 존재하지 않는다.

3-3. 일대일(1:1, @OneToOne)

  • 일대일 관계의 경우에는 주 테이블과, 대상 테이블을 정해야 한다.(왜냐하면 외래키가 2개 존재하기 때문이다.)
  • 단방향일 경우, 주 테이블에 @OneToOne과 @JoinColumn을 설정해주면 된다.
  • 양방향일 경우, 주 테이블에는 단방향과 동일하게 설정하며 대상 테이블에 @OneToOne과 mappedBy를 설정하여 읽기 전용으로 만들어주면 가능하다.

3-4. 다대다(N:N, @ManyToMany)

  • DB에서 정규화된 테이블 2개로 다대다를 표현할 수 없다.
  • 보통 다대다 관계를 다대일, 일대다 관계로 풀어내는 연결 테이블을 사용한다.
  • 실무에서는 사용하지 않음.

결론

  1. 연관관계의 주인(외래키 관리 객체 및 테이블)은 다(N)이다.
  2. 객체에서 양방향은 존재하지 않지만, 단방향을 서로 연결하면 양방향처럼 보인다.
  3. 실무에서 다중성은 @ManyToOne & @OneToMany가 사용될 것 같다.

참고 사이트

JPA 연관관계 총 정리 게시물
JPA 연관관계 참고 게시물
JPA 연관관계 참고 게시물2