티스토리 뷰

Spring/JPA

JPA - 연관관계 이해하기

땅속 디그다 2022. 8. 3. 16:16

🤔 연관관계 이해하기

객체 vs 테이블

🚀 테이블의 연관관계 표현

  • 외래키를 사용하여 다른 테이블 과의 pk로 연관 관계를 표현
  • 1:1 과 1:N 의 관계만 표현이 가능
  • N:N 은 2차원으로 쉽게 매핑이 불가능

🚀 객체의 연관관계 표현

  • 다른 객체간의 관계가 있다면 참조로 연관 관계를 표현
  • 방향성을 표현 가능

    예를 들자면 한쪽 객체에서만 참조를 표현 할 수 있다.
  • 1:1 과 1:N 말고도 N:N 관계가 표현 가능

🤔 객체와 테이블의 차이에서 오는 애매모호함

주목해야 할 점은 JPA 는 Table을 객체화 시키는 것 + java에서 손쉬운 db사용을 제공

  • 테이블은 다른 테이블의 정보를 가져올 때 pk와 fk로 join을 한다.
    즉, sql을 통해 정보를 가져와도 fk 값만 가져온다는 의미이다

    하지만 이런식으로 column을 가져오게 되면 객체의 참조 표현을 사용하지 못하게 된다 예를 들어보자
    🤦 Team 과 Member Table이 존재하고 Member 테이블의 정보를 가져온다고 가정

    🖥 코드
    @Entity
    public class Member {
      @Id
      private Long id;
      private String name;
      @JoinColumn
      @ManyToOne
      private Team team;
      }
  • 재밌는 점은 DB 기준으로 가져오게 되면 Team에는 fk 값으로 Team의 id가 들어가지 Team의 정보는 들어가지 않는다
    (말이 되는 소리는 아니지만 쉬운 설명을 위해)

  • 결론은 jpa는 테이블과 객체 사이의 패러다임의 불일치를 최대한 없애기 위해 수많은 것들을 지원해준다.
    (지연로딩, 조인패치, 등등... 사실상 이것만 마음에 새겨놓으면 된다.)

객체 사이의 연관관계 매핑

아까도 말했듯이 객체들은 db와 달리 N:N 관계 또한 맺을 수 있다. (거의 사용 X)
pk와 fk로 다른 테이블 간의 '참조'가 자유로운 db와는 달리 application에서 사용 할 객체는 참조에 자유롭지 못하다
위에서 말했듯이 member의 테이블을 java-application으로 가져오게되면 fk 값을 쥐고 있는 것이 당연하다.

고로 위의 코드에서와 같이 @ManyToOne을 사용해 JPA에게 Team과 member가 N:1의 관계에 있음을 알리고 Member에서 Team을 참조할 수 있는 fk를 JoinColumn으로 매핑해 Member 객체에서 Team 객체를 '참조' 가능하도록 돕는다.

🤔 결국 DB의 연관관계를 JPA 쪽으로 어떻게 연결지을 것인가?

기본적으로 db에는 1:N 1:1 관계뿐이다. 왜냐면 1 의 table에서 n을 표현할 방법이 없기 때문에 N:N 의 관계가 나올 수 없다.
db에는 방향성이 없다. (거의 양방향으로 생각이 가능하다) join으로 모든 참조가 해결이 되기 때문이다.

하지만 객체 쪽은 1쪽에서 N의 참조값을 가지고 있지 않다면 1쪽에서 N을 찾을 수 있는 방법이 없다.

 

🖥 1:N 관계에서의 단방향

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    @JoinColumn
    @ManyToOne
    private Team team;
}

@Entity
public class Team {
    @Id
    private Long id;
    private String teamName;
}

Team 쪽에서는 Member를 참조 할 수있는 방법이 없다.

참조를 하도록 양방향을 설정하면 다음과 같아진다.

 

🖥 1:N 양방향

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    @JoinColumn
    @ManyToOne
    private Team team;
}

@Entity
public class Team {
    @Id
    private Long id;
    private String teamName;
    @OneToMany(mappedBy = "team")
    private List<Member> members;
}

이제 Team에서도 Member를 참조할 수 있도록 바꾸었다.

방향성에서 나오는 연관관계의 주인 개념

단방향과 양방향을 살펴보았는데 DB와는 다르게 JPA는 참조에 대한 기준(FK)을 Member와 Team 모두에서 가지고 있고
JPA는 해당 내용으로 Member Table의 fk 값을 최신화 하고 식별할 수 있다.

그렇다면 다음과 같은 문제에 직면한다

  • Team class 의 List members로 Member 테이블의 fk 값 관리가 가능
  • Member class 의 Team team으로 Member 테이블의 fk 값 관리가 가능
  • 그렇다면 둘중 어느 것을 고쳤을 때 member의 fk 값이 변경되어야 하나?

    두개를 다 가져가려고 하면 말이 안된다.

🤒 결론적으로 어느 쪽을 기준으로 fk 값을 업데이트 할 것인지에 대한 기준이 필요하다.

위에서 살펴보았듯이 fk column을 매핑하려면 @JoinColumn을 사용해야 한다.

Team 쪽에서 Member 테이블의 fk를 관리 하려면 다음과 같다

🖥 Team에서 Member Table의 fk 관리

@Entity
public class Team {
    @Id
    private Long id;
    private String teamName;
    @OneToMany
    @JoinColumn(name = "TeamId")        // Member Table의 fk 관리
    private List<Member> members;
}

이제 Member class의 Team 멤버 변수는 fk를 관리하지 않는 연관관계의 주인이 아니므로 다음과 같이 변경한다.

🖥 1:N 양방향일 때 Member를 어떻게 풀어야하나?

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    @JoinColumn(name = "TeamId", insertable = false, updatable= false)
    @ManyToOne
    private Team team;
}

ManyToOne 에는 mappedby 속성이 없기 때문에 야매로 해당 column을 수정할 수 없도록 변경해 연관관계의 주인을
Team쪽으로 유지시킨다

🚀 JPA의 연관관계 정리

연관관계를 정리하게 될 경우 다음과 같다

N:1 단방향

🖥 코드

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    @JoinColumn(name = "TeamId")
    @ManyToOne
    private Team team;
}

@Entity
public class Team {
    @Id
    private Long id;
    private String teamName;
}

N:1 양방향

🖥 코드

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    @JoinColumn(name = "TeamId")
    @ManyToOne
    private Team team;
}

@Entity
public class Team {
    @Id
    private Long id;
    private String teamName;
    @OneToMany(mappedBy = "team")
    private List<Member> members;
}

mappedBy 속성을 통해 연관관계의 주인이 아님을 밝힌다.

1:N 단방향

🖥 코드

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
}

@Entity
public class Team {
    @Id
    private Long id;
    private String teamName;
    @OneToMany
    @JoinColumn(name = "TeamId")
    private List<Member> members;
}

N:1 양방향

🖥 코드

@Entity
public class Member {
  @Id
  private Long id;
  private String name;
  @JoinColumn(name = "TeamId", insertable = false, updatable= false)
  @ManyToOne
  private Team team;
}

@Entity
public class Team {
  @Id
  private Long id;
  private String teamName;
  @OneToMany
  @JoinColumn(name = "TeamId")
  private List<Member> members;
}

1:1 단방향

🖥 코드

@Entity
public class Member {
  @Id
  private Long id;
  private String name;
  @OneToOne
  @JoinColumn("lockId")
  private Lock lock;
}

@Entity
public class Lock {
  @Id
  private Long id;
  private String lockName;
}

1:1 양방향

🖥 코드

@Entity
public class Member {
  @Id
  private Long id;
  private String name;
  @OneToOne
  @JoinColumn("lockId")
  private Lock lock;
}

@Entity
public class Lock {
  @Id
  private Long id;
  private String lockName;
  @OneToOne(mappedBy = "lock")
  private Member member;
}

참고로 1대 1 관계에 있어서는 fk 값이 unique 해야므로 db에 unique속성을 넣어주어야 한다.

N:N 관계

🖥 코드

@Entity
public class Member {
  @Id
  private Long id;
  private String name;
  @ManyToMany
  @JoinColumn("lockId")
  private List<Lock> lockList;
}

@Entity
public class Lock {
  @Id
  private Long id;
  private String lockName;
  @ManyToMany(mappedBy = "lockList")
  private List<Member> memberList;
}

DB에는 N:N관계를 표현을 못하기 때문에 결국에 N:1, 1:N 으로 관계를 풀게 된다.

즉 JPA에서 자동으로 table을 생성하게 된다.

그렇게 될 경우 중간 테이블에 column을 생성해야할 일이 있을 때 꼬이게 된다. 그렇게 될 경우 다음과 같은 2가지 해결방안이 있다.

  1. @IdClass 로 관리 (복합키로 관리)
  2. @Id 로 관리

🖥 @IdClass (복합키로) 테이블 관리

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
  @Id
  @ManyToOne
  @JoinColumn
  private Member member;

  @Id
  @ManyToOne
  @JoinColumn
  private Product product;

  // 생략
}

public class MemberProductId implements Serializable {
  private Long member;
  private Long product;

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    MemberProductId member = (MemberProductId) o;
    // 생략
  }

  @Override
  public int hashCode() {
    // 생략
  }
}

결국에는 MemberProductId 클래스를 사용해서 테이블을 관리하기 위해 2개의 class가 추가적으로 나오게 된다.

🖥 2번 방법 (추천)

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
  @Id
  private Long id;

  @ManyToOne
  @JoinColumn
  private Member member;

  @ManyToOne
  @JoinColumn
  private Product product;

  // 생략
}

차라리 그럴 꺼면 그냥 식별자를 하나 두고 fk 매핑을 해서 테이블을 관리하는 편이 훨씬 효과적이다.

🤔 결론

ORM 은 데이터 베이스와 객체지향언어 와의 괴리감을 줄이기 위해 나온 것임을 생각하자

그렇게 될경우 N:1 관계에서 N쪽에서 fk를 관리하는 것이 당연해지고 그것이 편하다

(Member Table의 fk를 Member 엔티티에서 관리하게 되는 것이므로)

결론은 JPA라는 새로운 기술에 매달리지 말고 관계형 DB에 초점을 맞추고 JPA가 해당 괴리감을 어떻게 해결해 가는지 초점을
두어 공부하는 것이 중요하다.

'Spring > JPA' 카테고리의 다른 글

프록시와 연관관계 관리  (0) 2022.08.23
고급 매핑  (0) 2022.08.23
JPA - 엔티티 매핑  (0) 2022.08.03
JPA - 영속성 컨텍스트  (2) 2022.08.03
JPA 시작  (0) 2022.08.03
댓글
11-08 08:24
Total
Today
Yesterday
링크