섭섭한 개발일지

[필기용] 자바 ORM 표준 JPA 프로그래밍 Ch05 본문

프로그래밍/JPA

[필기용] 자바 ORM 표준 JPA 프로그래밍 Ch05

Seop 2023. 1. 30. 12:01

git : https://github.com/Dev-Chaee/Java_JPABook/tree/ch05-jpa

CHAPTER 05

[ 연관관계 매핑 기초 ]

  • 엔티티들은 대부분 다른 엔티티와 연관관계가 있다.
    • 주문 엔티티는 어떤 상품을 주문했는지 알기 위해 상품 엔티티와 연관관계가 있으며 이외에도 엔티티들은 서로 필요에 의한 연관관계가 있다.
      객체와 관계형 데이터베이스에서 이러한 엔티티를 관계를 짓는 부분에서 패러다임 불일치가 발생한다. 객체는 다른 객체를 참조로 관계형 데이터베이스는 외래 키를 사용하여 연관관계를 맺는다.
      이러한 패러다임 불일치를 매핑하는 것이 이번 장에서의 학습 목표다.
  • 핵심키워드
    • 방향
      • 단방향
        • 회원과 팀이 관계가 있을 때 회원만이 팀을 바라보는 것을 단방향 관계라고 한다.
          이때 회원이 어느 팀에 속한지 회원 객체에서는 확인이 가능하나 팀에서는 팀에 어떤 회원이 속해있는지 알 수 없다.
      • 양방향
        • 서로 관계를 맺고 있어 단방향과는 다르게 어디에서는 서로를 조회할 수 있는 관계이다.
    • 다중성
      • N:1, 1:N, 1:1, N:N 과 같은 관계가 있다.
      • 예로 회원과 팀 관계가 있을 떄 회원은 한개의 팀에만 속할 수 있고 팀은 여러명의 회원을 품을 수 있다면 1(팀):N(회원) 관계가 된다.
    • 연관관계의 주인
      • 객체를 양방향 연관관계로 만들면 연관관계의 주인을 지정해야 한다.
  • 단뱡향 연관관계
    • 연관관계 중에서는 다대일 단방향 관계를 가장 먼저 이해해야 한다.
      • 회원과 팀이 있다.
      • 회원은 하나의 팀에만 소속될 수 있다.
      • 회원과 팀은 다대일 관계다.
    • 객체는 단방향으로 설정을 하면 바라보는 쪽에서만 상대 객체를 조회할 수 있다.
      허나 DB에서는 양방향 관계가 되어 양쪽에서 서로 조회를 할 수 있다. 객체에서도 DB와 같이 양방향에서 조회가 가능하게 할 수도 있다.
      상대측 필드에도 객체를 추가하면 되는데 이는 양방향 관계처럼 보이지만 실제로는 서로 다른 단방향 관계 2개이다.
    • 객체는 참조(주소)로 연관관계를 맺는다.
      • 참조를 사용하는 객체의 연관관계는 단방향이다.
    • 테이블은 외래 키로 연관관계를 맺는다.
      • 외래 키를 사용하는 테이블의 연관관계는 양방향 관계이다.
    • 이 둘은 비슷해 보이지만 매우 다른 특징을 갖는다. 연관된 데이터를 찾을 때 객체는 get***을 사용하지만 DB는 JOIN을 이용한다.
      @ManyToOne
      @JoinColumn(name = "TEAM_ID")
      private Team team;
    • ManyToOne : 다대일 관계 매핑 정보다. 회원과 팀은 다대일 관계이므로 해당 어노테이션을 사용하였고 매핑 시 이러한 어노테이션은 필수다.
      • optional : false로 설정하면 연관된 엔티티가 항상 있어야한다.
      • fetch : 글로벌 패치 전략을 설정한다. (8장에서 추가 설명)
      • cascade : 영속성 전이 기능을 사용한다. (8장에서 추가 설명)
      • targetEntity : 연관된 엔티티의 타입 정보를 설정, 이 기능은 거의 사용하지 않음, 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
    • JoinColumn : 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. (이 어노테이션은 상황에 따라 생략이 가능하다.)
      • name : 매핑할 외래 키 이름 (기본값 : 참조하는 테이블의 기본 키 컬럼명)
      • referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
      • foreignKey : 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.
      • @Column의 unique, nullable 등의 속성과 동일한 속성들을 사용할 수 있다.
  • 조회
    • 연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지가 있다.
      • 객체 그래프 탐색
        • member에서 getTeam 을 통해 찾는 것
      • 객체지향 쿼리 사용 (JPQL)
        • jpql에서 join할 테이블을 객체에서 가져와 지정한다.
        • jpql에서 파라미터를 바인딩 받는 방법은 : 을 이용하는 것이다.
        • private static void queryLogicJoin(EntityManager entityManager) { String jpql = "select m from Member m join m.team t where " + "t.name=:teamName"; List<Member> result = entityManager.createQuery(jpql, Member.class) .setParameter("teamName", "팀1") .getResultList(); for (Member member : result) { System.out.println("member.username : " + member.getUsername()); } }
  • 수정
    • 엔티티 속성 값 수정은 조회한 후 set 혹은 별도로 속성을 수정해주면 트랜잭션을 커밋하는 순간에 변경 감지 기능이 작동하며 변경사항을 데이터베이스에 반영한다.
```
private static void queryLogicJoin(EntityManager entityManager) {
  String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";
  List<Member> result = entityManager.createQuery(jpql, Member.class)
          .setParameter("teamName", "팀1")
          .getResultList();

  for (Member member : result) {
      System.out.println("member.username : " + member.getUsername());
  }
}
```
```
private static void queryLogicJoin(EntityManager entityManager) {
  String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";
  List<Member> result = entityManager.createQuery(jpql, Member.class)
          .setParameter("teamName", "팀1")
          .getResultList();

  for (Member member : result) {
      System.out.println("member.username : " + member.getUsername());
  }
}
```
  • 연관관계 재거
    • 연관관계를 제거하려면 수정할 객체를 조회한 뒤 null 값을 주면 연관관계가 제거된다.
  • 연관된 엔티티 삭제
    • 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거한 뒤에 삭제해야 한다.
      • 예로 팀1을 삭제하려면 멤버 중 팀1을 참조하고 있는 객체들의 연관관계를 모두 제거한 뒤 팀을 삭제해야한다.
  • 양방향 연관관계
    • 팀과 회원의 관계를 양방향으로 변경한다고 하면 Team에 member 정보가 담길 List를 생성해주면 된다. DB는 애초에 양방향이기에 변경되는 사항은 없다.
    • @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>();
    • 연관관계의 주인 mappedBy
      • 우선 mappedBy는 양방향 매핑일 때 사용을 하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
      • mappedBy 속성은 양방향 연관관계에 필요한데, 객체에는 엄밀히 따졌을 때 양방향 연관관계라는 것이 없다. 서로 다른 단방향 관계 2개를 애플리케이션 로직에 잘 묶어 양방향처럼 보이게 하는 것이다.
        • 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키가 하나인 경우가 발생하며 둘 사이에 차이가 발생한다. 이러한 문제로 인하여 JPA에서는 두 객체 연관관계 중 하나를 정해 테이블의 외래키를 관리해야 하는 연관관계 주인이라는 것을 설정한다.
      • 양방향 연관관계 매핑 시 반드시 하나를 연관관계의 주인으로 지정해야한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리하여 값을 수정할 수 있고 그렇지 않은 쪽은 읽기 전용이라고 생각하면 된다.
      • 연관관계의 주인은 mappedBy 속성을 사용하지 않는다.
      • 주인이 아닌 엔티티는 mappedBy 속성을 사용하여 연관관계의 주인 쪽의 매핑되어 있는 필드명을 속성 값으로 줘야한다.
      • 연관관계의 주인을 정하는 것은 외래키가 가까운 엔티티를 주인으로 지정하면 된다.
        • 다대일, 일대다 상황에서는 항상 다 쪽이 외래키를 가지게 된다. 따라서 필드 어노테이션이 @ManyToOne은 항상 연관관계의 주인이 되어 mappedBy 속성이 없다.
  • 양방향 연관관계 주의점
    • 흔히 발생되는 실수 중 연관관계의 주인에 값을 입력하지 않고 주인이 아닌 곳에서 값을 설정하여 데이터베이스에 의도와는 다르게 값이 저장된다고 한다.
      • 반드시 연관관계의 주인만이 외래 키의 값을 변경할 수 있으므로 주의하자.
  • 순수한 객체까지 고려한 양방향 연관관계
    • 연관관계의 주인에만 값을 저장하도록 이야기를 했지만 객체 관점에서 봤을 때 양쪽 모두 값을 입력해 주는 것이 안전하다.
      • 그러므로 관계형 데이터베이스만 고려할게 아니라 객체 상태도 고려를 해야하기에 객체의 양방향 연관관계에서는 주인이 아닌 곳에도 값을 입력해주자
  • 연관관계 편의 메소드
    • 양방향 연관관계는 결국 양쪽 모두 신경 써야 한다.
      • 양쪽 중 주인쪽만 값을 입력하는 실수가 발생할 수 있다. 그러므로 두 코드는 하나인 것 처럼 사용하는 것이 더 안전하다.
      • setTeam() 메소드를 조금 변경하여 team에 값이 저장되도록 하는 것이 좋다.
      • public void setTeam(Team team) { this.team = team; team.getMembers().add(this); }
  • 오류
    • member의 팀을 변경하개 되면 데이터베이스에서의 문제점은 없다.다만 Team 객체의 members를 호출했을 때 변경 전 팀에서도 멤버가 조회되는 문제가 발생한다. 이 문제는 쉽게 해결이 가능하다.
    • if (this.team != null) { this.team.getMembers().remove(this); }
    • 데이터베이스에서는 문제가 생기지 않지만 영속성 컨텍스트에서 호출할 경우 발생되는 문제로 위 코드와 같이 주인이 아닌 객체의 연관관계도 변경을 해주는 것이 안전하다.
  • 정리
    • 양방향 매핑은 단방향 매핑보다 복잡하다. 연관관계의 주인도 정해야하고 두 개의 단방향을 양방향 관계와 같이 보이게 하기 위해 로직도 잘 관리를 해야한다.
      양방향의 장점은 반대방향으로 객체 그래프 탐색이 가능하다는 것이다.
    • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
    • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
    • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
    • 양방향 매핑은 복잡하다. 로직에 따라 다르겠지만 단방향으로 우선적으로 매핑을 하고 필요에 따라 반대 방향 객체 그래프 탐색을 추가하는 것도 괜찮다.
    • 연관관계의 주인을 정할때는 비즈니스의 중요도가 아닌 외래 키의 위치만으로 판단하여 정해야 한다.
    • 양방퍙 매핑 시 무한 루프에 빠지지 않도록 조심하자 Member.toString에 getTeam을 호출하고 Team.toString 에서 getMember를 호출하면 무한루프에 빠지게 된다.
      이런 문제는 엔티티를 JSON으로 변환할 때 자주 발생한다. 또한 Lombok을 사용할 때에도 많이 발생한다.
Comments