JPA 학습정리 - 연관관계 매핑

DragonTiger·2021년 12월 28일
1
post-custom-banner

연관 관계 정의

연관 관계를 매핑할 때, 생각해야 할 것은 방향, 연관관계 주인, 다중성 있습니다.

  1. 방향 : 단방향, 양방향 (객체 참조)
  2. 연관 관계의 주인 : 양방향일 때, 연관 관계에서 관리 주체
  3. 다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)

단방향, 양방향

테이블

  • 외래키 하나로 양쪽 조인 가능.
  • 방향이란 개념이 없다.

객체

  • 참조용 필드가 있는 쪽으로만 참조 가능
  • 한쪽만 참조하면 단방향
  • 양쪽이 서로 참조하면 양방향

테이블은 조인을 할때 외래키 하나로 모든 테이블을 양쪽으로 조인 할 수 있는반면에,
객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능합니다.

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

여기서 객체의 양방향 관계라는 말은 사실 두 객체가 각각 단방향을 가지고 있어서 양방향이라고 하는거지 사실은 서로 단방향 관계이다.


JPA를 사용하여 데이터베이스와 패러다임을 맞추기 위해서 객체는 단방향 연관 관계를 가질지, 양방향 연관 관계를 가질지 선택해야합니다.

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

비즈니스 로직에 맞게 선택했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관 관계가 되는 것입니다.

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키
    를 관리할 곳을 지정해야함
  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용X
  • 주인이 아니면 mappedBy 속성으로 주인 지정

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

비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
연관관계의 주인은 외래 키의 위치를 기준으로 정해야함.

다중성

다대일: @ManyToOne // 제일 많이 쓰임
일대다: @OneToMany
일대일: @OneToOne
다대다: @ManyToMany

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

연관 관계는 대칭성을 갖습니다.
일대다 ↔ 다대일
일대일 ↔ 일대일
다대다 ↔ 다대다

Member와 Team의 관계로 예를 들겠습니다.(멤버는 하나의 팀만 가집니다.)

하나의 팀에 여러멤버가 소속될수있고, 많은팀에는 하나의 멤버가 소속되지못함
여러멤버는 하나의 팀을 가질수있고, 하나의 멤버는 여러팀에 소속되지못함.

데이터베이스를 기준으로 다중성(Member 'N' : Team '1')을 결정했습니다.

즉, 외래 키를 Member 'N'이 관리하는 일반적인 형태입니다. (참고로 데이터베이스는 무조건 다'N'쪽이 외래 키를 갖습니다.)

다대일: @ManyToOne 단방향

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

다대일 단방향에서는 다 쪽인 Member에서 @ManyToOne 만 추가해준 것을 확인할 수 있습니다.

반대로 Team에서는 참조하지 않습니다. (단방향이기 때문)

다대일: @ManyToOne 양방향

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

다대일 양방향으로 만드려면 일(1) 쪽에 @OneToMany 를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy 로 지정해줍니다.

mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 됩니다. 여기서는 Member 객체의 team라는 이름의 변수이기 때문에 team로 지정했습니다.

일대다 @OneToMany 단방향

앞서 다대일의 기준은 연관관계의 주인 N 쪽에 둔 것이고 이번에 언급할 일대다의 기준은 연관관계의 주인을 1 쪽에 둔 것입니다.

참고로 실무에서는 일대다(1:N) 단방향은 잘 쓰지않는다.

→ 일대다(1:N) 단방향

데이터베이스 입장에서는 무조건 다(N)쪽에서 외래키를 관리합니다.

근데 일(1)쪽 객체에서 다(N) 쪽 객체를 조작(생성,수정,삭제)하는 방법입니다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
}
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToMany
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();
}

@OneToMany에 mappedBy가 없어집니다. 양방향이 아니기 때문입니다.

여기선 좋지않은 단점들이 나옵니다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member = new Member();
            member.setName("DragonTiger");
            em.persist(member);
            Team team = new Team();
            team.setName("DragonTigerTeam");
            team.getMembers().add(member);
            em.persist(team);
            
            tx.commit();
        }catch (Exception e){
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();

Team 클래스가 연관관계의 주인인데 Member 테이블에 FK가 있기때문이다.

즉, team.getMembers().add(member); 이 코드는 자바세상에서만 적용가능하지 데이터베이스의 FK는 Member 테이블에 있기때문에 아래에 보면 팀테이블이 인서트 되고 업데이트 쿼리를 Member 테이블에 보낸다.

일만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생하는 것.
Team를 저장했는데 왜 Member가 수정이 되지? 이런 생각을 하게 만듦.
(위 문제는 규모가 작을땐 상관없는데 큰규모로 가게되면 매우 큰 단점이된다 직관적으로 파악하기 쉽지 않다. 이 문제가 가장 치명적이다.)
업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
그렇기 때문에 TIP으로 일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월하기 때문에 이 방식을 채택하는 것을 추천한다고 한다.

그런데 실무에서 사용을 금지하지 않는 이유는 되도록 피하는 게 좋지만, JPA 값 타입을 사용하는 것을 대신하여 사용할 때는 또 유용하다고 한다.

일대다 @OneToMany 양방향 (실무 사용 금지 ❌)

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

키워드는 @JoinColumn(updatable = false, insertable = false) 이지만, 일대다 양방향을 사용해야할 때는 다대일 양방향 사용하도록 하는게 더 좋습니다.

결과적으로 일대다(1:N) 단방향, 양방향은 쓰지 말고 차라리 다대일(N:1) 양방향으로 쓰는 것이 맞다라고 단순화하여 결론 내리면 될 것 같습니다.

일대일 관계

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
  • 주 테이블에 외래 키
  • 대상 테이블에 외래 키
  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

일대일 @OneToOne 단방향

JPA에서는 아예 지원을 하지 않습니다.

일대일 @OneToOne 양방향

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToOne
    @JoinColumn(name = "Locker_id")
    private Locker locker;
}
@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToOne(mappedBy="locker")
    private Member member;
    
}

여기선 외래 키를 Member에서 관리하는 게 좋을 것인지, Locker에서 관리하는 게 좋을 것인지 생각을 해봐야합니다. 즉 테이블에 어디에 둘 것 인지를 생각해야합니다.

테이블은 한 번 생성되면 보통 굳어집니다. 변경이 어렵다는 얘기입니다.
그러나 비즈니스는 언제든 바뀔 수 있습니다.
하나의 멤버가 여러개의 Locker를 가진다고 변경이되면?
그러면 다(N)쪽인 Locker테이블에 외래 키가 있는 것이 변경에 유연합니다.
그러면 다(N)가 될 확률이 높은 테이블에 외래 키를 놓는게 무조건 좋을까? 그건 또 아니라고한다.
객체 입장에서 Member쪽(1)에서 외래 키를 갖게되면 Member를 조회할 때마다 이미 Locker의 참조를 갖고 있기 때문에 성능상 이득이 있습니다.

일대일 정리

주 테이블에 외래 키

  • 주 객체가 대상 객체의 참조를 가지는 것 처럼
    주 테이블에 외래 키를 두고 대상 테이블을 찾음
  • 객체지향 개발자 선호
  • JPA 매핑 편리
  • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
  • 단점: 값이 없으면 외래 키에 null 허용

대상 테이블에 외래 키

  • 대상 테이블에 외래 키가 존재
  • 전통적인 데이터베이스 개발자 선호
  • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
  • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨

다대다 @ManyToMany

사용 금지!!

중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문입니다.

다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높습니다. JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있습니다.

그러면 어떻게 사용하면 되나 ?

위 그림처럼 엔티티를 하나 생성해서 서로 다대일을 걸어주면 된다.

참고
자바 ORM 표준 JPA 프로그래밍 - 기본편
기본기를 쌓는 정아마추어 코딩블로그

profile
take the bull by the horns
post-custom-banner

0개의 댓글