반응형

단방향 연관관계

  • 예) 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.
  • 객체를 테이블에 맞추어 데이터 중심으로 모델링하면 객체 연관관계에서 알 수 있듯이 객체들 간의 연관관계가 나타나지 않는다.
    • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
    • 객체는 참조를 사용해서 연관된 객체를 찾는다
    • 관계형 데이터베이스와 객체의 패러다임의 불일치에서 나오는 현상이다.

 

  • 객체지향적인 엔티티는 아래의 형태를 지닌다.
    • Member 객체가 연관된 객체의 식별자를 가지고 있는 형태가 아니라 연관된 객체에 대한 참조를 가지고있다.
    • 이를 보고 객체간의 관계를 쉽게 파악할 수 있다.

 

양방향 연관관계와 연관관계의 주인

  • 테이블 연관관계는 외래키 하나로 연관된 테이블 모두 조회가 모두 가능하다.(양방향 조회)
  • 객체 관계에서는 연관된 객체의 참조를 가지고 있어야 조회가 가능하다. 즉, 연관된 객체들이 서로 참조를 가지고 있는 경우 양방향 객체 연관관계로 볼 수 있다.
    • 이제 반대 방향으로도 객체 그래프를 탐색할 수 있다.

 

객체와 테이블이 관계를 맺는 차이

  • 양방향 연관관계를 이해하기 위해서 우선 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
  • 객체 연관관계 = 2개
    • 회원 → 팀 연관관계 1개(단방향)
    • 팀 → 회원 연관관계 1개(단방향)
    • 참조용 필드가 있는 쪽으로만 참조가 가능하다.
    • 한쪽만 참조하면 단방향, 양쪽이 서로 참조하면 양방향 관계이다.
    • 객체의 양방향 관계는 서로 다른 단방향 관계 2개이다. 따라서 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
  • 테이블 연관관계 = 1개
    • 회원 ↔ 팀의 연관관계 1개(양방향)
    • 외래키 하나로 두 테이블의 연관관계를 관리한다.
      • 연관된 두 테이블 모두 외래키 하나로 조인할 수 있다
      • 테이블은 사실상 방향이라는 개념이 없다.

 

  • 객체와 테이블이 관계를 맺는 차이로 인해 아래와 같은 상황이 발생한다.
    • 예) Member 테이블의 값을 수정하는 경우
    • 테이블 관점에서는 외래키 값만 변경되면 된다.
    • 그러나 객체 관점에서는 Member 객체에 속하는 team의 값을 수정했을 때 외래키 값이 변경되어야하는지 Team 객체에 속하는 members의 값이 변경되었을 때 외래키 값이 변경되어야하는지 알 수 없다.
      • 이러한 상황 때문에 두 관계 중 하나로 외래 키를 관리해야 한다는 규칙이 생긴다.
      • 외래키를 관리하는 관계를 연관관계의 주인(Owner)이라고 칭한다.
      • 연관관계의 주인(Owner)이라는 개념은 양방향 매핑에서 나온다.
      • mappedBy 속성은 해당 관계가 연관관계의 주인인지 아닌지 표현할 때 사용한다.

 

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만이 외래 키를 관리한다.(등록, 수정)
  • 연관관계의 주인이 아닌 관계는 읽기(조회)만 가능하다.
  • 연관관계의 주인은 mappedBy 속성을 사용하지 않는다.
  • 연관관계의 주인이 아닌 관계에서는 mappedBy 속성으로 연관관계의 주인을 지정한다.

 

연관관계의 주인을 정하는 기준

  • 외래 키가 있는 곳을 주인으로 정해야 한다. (N:1에서 N에 해당하는 관계)
    • Member 객체가 연관관계의 주인이 된다.
  • 이런 기준으로 관계를 정하면 애플리케이션을 좀 더 이해하기 쉽다.
    • 외래 키가 없는 곳을 연관관계의 주인으로 설정한 경우 다른 테이블에서 수정했는데 외래 키가 존재하는 테이블이 수정되어서 굉장히 찾기힘든 상황이 벌어질 수 있다.
    • 예) Team의 값을 수정했는데 Member 테이블이 수정되는 경우
  • 또한 성능적으로도 좋다.
    • 외래 키가 없는 곳은 직접적으로 외래 키를 관리할 수 없다. 따라서 외래 키 값을 변경하기 위한 update 쿼리가 추가적으로 발생한다.

 

양방향 매핑시 가장 많이 하는 실수

  • 연관관계의 주인에 값을 넣지 않고 주인이 아닌 관계에 값을 넣는 경우
    • 외래키 값에 null이 들어감
  • 연관관계의 주인에만 값을 넣는 경우
    • 1차캐시에서 연관관계 객체 조회 안되는 문제 발생
    • 예) Team 객체에 존재하는 관계에는 값을 넣지 않고 Member 객체에만 값을 넣었다고 가정해보자.
      • 영속성 컨텍스트를 초기화 한 경우에는 Team 객체를 조회할 때, 1차캐시가 모두 비워져 있어서 Team에 소속된 Member를 찾을 때 DB에서 조회해온다.
      • 그러나 영속성 컨텍스트가 초기화 되지않고 1차캐시에 Team 객체가 남아있는 상태에서 조회하면 1차캐시의 Team 객체는 member 객체가 들어있지 않기 때문에 조회되지 않는다.
  • 따라서 순수한 객체 관계를 고려하면 항상 양쪽 관계 모두 값을 입력해야 한다.
    • 양쪽 관계 모두에 객체를 설정하는 메서드(연관관계 편의 메서드)를 만들어 이를 활용하면 실수가 줄어든다.
    • 연관관계 편의 메서드가 양쪽 모두 존재하면 문제가 생길 수 있다. 한쪽에서만 만들어 사용하도록 하자
  • 양방향 매핑 시에 무한 루프를 조심해야 한다.
    • toString() 또는 lombok을 사용할 때 toString()을 사용하면 안된다.
      • 예) toString(), lombok,
        • 각 클래스마다 참조객체를 서로 가지기 때문에 양쪽의 toString이 무한으로 호출된다.
    • 컨트롤러에서는 엔티티를 절대 반환하면 안된다. 컨트롤러에서 엔티티를 반환하고자 할 때는 DTO(Data Transper Object)로 변환을해서 반환해야 한다.
      • 컨트롤러에서 엔티티를 반환하는 경우에서는 엔티티가 변경되면 API 스펙이 변경되는것과 마찬가지이다. 이는 기존 API 스펙으로 알고 사용하던 사용자는 엔티티의 변경으로 인해서 파급효과가 발생할 수 있는 문제가 발생한다.
      • JSON 변환 시 무한 루프 문제가 발생한다.
      • 예) JSON 생성 라이브러리
        • 엔티티를 컨트롤러에서 직접 응답(Response) 할 때, 엔티티에 걸린 연관관계가 양방향이라면 엔티티를 JSON으로 바꿀 때 무한루프에 걸릴 수 있다.
        • Member 클래스를 JSON 형태로 만들다가 Team 클래스를 JSON 형태로 만들다가 Member 클래스를 JSON 형태로 만드는 등 무한 루프 발생

 

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다.
    • 단방향 매핑으로 일단 설계를 완료하고 추후에 필요하면 양방향을 고려하는 것이 좋다.
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
    • JPQL에서 역방향으로 탐색할 일이 많다. 이 때 양방향 매핑이 필요하다.
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다.
    • 테이블에는 영향을 주지 않는다. 단지 객체 코드를 변경하면 된다.

 

다양한 연관관계 매핑

  • 연관관계 매핑시 고려사항
    • 방향(Direction): 단방향, 양방향
    • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
    • 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 필요

 

@JoinColumn

  • 외래 키를 매핑할 때 사용한다.
  • name
    • 매핑할 외래 키 이름을 의미한다.
    • 기본 값은 필드명_참조하는 테이블의 기본 키 컬럼명이다.
  • referencedColumnName
    • 외래 키가 참조하는 대상 테이블의 컬럼명
    • 기본 값은 참조하는 테이블의 기본 키 컬럼명이다.
  • (DDL) foreignKey
    • 외래 키 제약조건을 직접 지정할 수 있다. 테이블 생성 시에만 사용한다.
  • 나머지는 @Column 속성과 동일한 기능을 가진다.

 

@ManyToOne (N : 1 관계, 다대일 관계)

  • optional
    • false로 설정하면 연관된 엔티티가 항상 있어야 한다.
    • 기본 값은 true 이다.
  • fetch
    • 글로벌 페치 전략을 설정한다.
    • 기본값은 FetchType.EAGER 이다.
  • cascade
    • 영속성 전이 기능을 사용한다.
  • targetEntity
    • 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.

 

  • 가장 많이 사용하는 연관관계이다. 외래키가 있는 곳에 참조를 만들고 연관관계 매핑을 하면 된다.
  • 객체 양방향 관계에서는 단지 참조만 추가하면 된다. 테이블에는 영향을 주지 않는다.
  • 예) N : 1 단방향 관계
  • 예) N : 1 양방향 관계
    • 외래 키가 있는 쪽이 연관관계의 주인이다.

 

@OneToMany (1 : N 관계, 일대다 관계)

  • mappedBy
    • 연관관계의 주인 필드를 선택한다.
  • fetch
    • 글로벌 페치 전략을 설정한다.
    • 기본 값은 FetchType.LAZY 이다.
  • cascade
    • 영속성 전이 기능을 사용한다.
  • targetEntity
    • 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.

 

  • 예) 1 : N 단방향 관계
    • 객체와 테이블의 차이때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조이다.
    • @JoinColumn을 꼭 사용해야 한다. 그렇지 않다면 조인 테이블 방식을 사용한다.
      • 조인 테이블 방식은 테이블 사이에 중간에 테이블을 하나 추가해서 사용한다.
      • 관리하기가 쉽지 않다.
    • 1 : N 단방향 매핑은 권장하지 않는 모델이다. 이러한 경우 객체적으로 의미없어도 양방향으로 설계하여 DB의 형태에 맞게 설계하는 것이 좋다.
      • 즉, 1 : N 단방향 매핑보다는 N : 1 양방향 매핑을 사용하는 것이 좋다.
      • 예) 객체적으로 의미가 없는 경우
        • 실제로 Member 에서 Team에 대한 참조가 필요없다.
        • 그러나 Member에 Team에 대한 참조를 추가하는 행위가 이에 해당된다.
        • Member에 Team에 대한 참조를 추가함으로써 양방향 관계가 되며 DB의 모델과 유사하게 형태가 형성된다. 이는 ORM보다 DB에 설계의 초점을 맞추는 것이다.
        • 이에 대한 장점은 Member를 변경했을 때 Member가 변경되도록 할 수 있고 우리가 더 쉽게 현상을 파악할 수 있도록 해준다.
    • 1 : N 단방향 관계에서는 1 이 연관관계의 주인이다.
      • Team은 1 에 해당하고 Member는 N 에 해당한다.
      • Member 에 대한 관리를 Team 객체에서 하고 있다.
      • Team 객체를 수정하면 Member 테이블이 수정된다.
    • 1 : N 단방향 매핑의 단점
      • 엔티티가 관리하는 외래 키가 다른 테이블에 있다. 즉, Member 테이블이 어디서 수정되는지 파악하기가 어렵다.
      • 테이블 형태를 보면 Member는 TEAM 테이블에서 관리할 수 없다. 따라서 Member 테이블의 값을 변경해주는 update 쿼리가 추가로 만들어져서 실행된다.

 

  • 예) 1 : N 양방향 관계
    • JPA 공식적으로는 존재하지 않는 매핑이다.
    • 그러나 읽기 전용 필드(Member의 team)에 @JoinColumn(name = "TEAM_ID", insertable=false, updatable=false) 를 선언해서 양방향 처럼 사용할 수는 있다.
      • 읽기 전용의 필드도 연관관계의 주인처럼 설정한 후, 읽기만 가능하도록 생성, 수정을 못하도록 속성을 설정한다.
    • 1 : N 양방향 관계를 사용하기보다는 N : 1 양방향을 사용하는 것이 좋다.

 

@OneToOne (1 : 1 관계, 일대일 관계)

  • 외래 키가 주 테이블이나 대상 테이블 중에 어디에 위치해도 무관없다. 그러나 외래 키를 위치할 때 다음을 고려해서 정해야한다.
    • 주 테이블에 외래 키
      • 객체지향 개발자 선호
      • JPA 매핑이 편리하다.
      • 장점은 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
        • 추가적인 조회 쿼리없이 조건 처리 등을 쉽게 할 수 있다.
      • 단점은 값이 없으면 외래 키에 null 을 허용한다.
    • 대상 테이블에 외래 키
      • 데이터베이스 개발자 선호
      • 장점은 주 테이블과 대상 테이블을 1 : 1 에서 1 : N 관계로 변경할 때 테이블 구조가 유지된다.
        • 제약사항과 같은 세부 조건들만 수정하면 관계 형태를 바꿀 수 있다.
      • 단점은 JPA의 한계로 양방향 관계로 만들어야 한다. 또한 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다.

 

  • 외래 키에 데이터베이스 유니크 제약조건 추가가 되면 1 : 1 관계라고 볼 수 있다.
  • 1 : 1 관계에서는 자신의 위치에 있는 외래 키만 관리 가능하다.
  • 예) 주 테이블에 외래 키가 위치하고 단방향 관계인 경우
    • N : 1 단방향 매핑과 유사하게 설정할 수 있다.
  • 예) 주 테이블에 외래 키가 존재하고 양방향 관계인 경우
    • N : 1 양방향에서 보았듯이 외래 키가 있는 곳이 연관관계의 주인이 된다.
  • 예) 대상 테이블에 외래 키가 존재하고 단방향 관계인 경우
    • 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.

 

  • 예) 대상 테이블에 외래 키가 존재하고 양방향 관계인 경우
    • 주 테이블에 외래 키가 존재하고 양방향 관계인 경우를 뒤집은 것과 같다.

 

@ManyToMany (N : M 관계, 다대다 관계)

  • 관계형 데이터베이스는 정규화된 테이블 2개로 N : M 관계를 표현할 수 없다.
    • N : M 관계를 중간 테이블을 만들어 1 : N, N : 1 관계로 풀어내야 한다.
  • 객체는 컬렉션을 사용해서 객체 2개로 N : M 관계를 표현할 수 있다.
  • @JoinTable로 중간에 연결될 테이블을 지정할 수 있다.
  • 실무에서 사용하면 안되는 관계이다.
    • 연결 테이블이 단순히 연결만 하지 않고 추가적인 속성을 가질 수도 있다.
    • @JoinTable 은 추가적인 속성을 가지는 것이 불가능하다. 또한 쿼리도 파악하기 어렵다.

 


[참고자료]

자바 ORM 표준 JPA 프로그래밍 - 기본편, 김영한

반응형

+ Recent posts