다양한 연관관계 매핑

뚝딱이·2022년 9월 4일
0

JPA

목록 보기
5/11

연관관계 매핑시에 고려해야할 사항은 아래의 세가지가 있다.

  • 다중성
  • 단방향, 양방향
  • 연관관계 주인

다중성

  • 다대일: @ManyToOne
  • 일대다: @OneToMany
  • 일대일: @OneToOne
  • 다대다: @ManyToMany

다중성은 약간 헷갈릴 수 있는데 jpa어노테이션은 db랑 매핑하기 위해서 있는것이다.
db관점에서의 다중성으로 보면된다.
한번씩 헷갈릴때 애매할 땐 대칭성이 있기 때문에 반대로 생각해보면 된다.

여기서 중요한 것은 다대다는 실무에서 쓰면 안된다. 절대 절대 ..

가장 많이 쓰는 건 다대일이고, 가끔 - 일대다, 일대일도 사용한다.

단방향, 양방향

테이블은 외래키만 있어도 양쪽으로 조인이 가능해 방향이라는 개념이 없다.
외래키를 양쪽에 두는게 아닌 한쪽에만 세팅하면 양쪽으로 join가능하기 때문이다.

객체는 참조용 필드가 있는 쪽으로만 참조가 가능하다.
그래서 참조가 있는 곳만 참조가 가능해서 한쪽만 있으면 단방향이고 양쪽이 서로 참조하면 양방향이다.

테이블은 외래 키 하나로 두 테이블이 연관관계를 맺는다.
객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데이다. 하지만 테이블은 하나이다.

객체 양방향 관계는 참조가 2군데 있다. 둘중 테이블의 외래 키를 관리할 곳을 지정해야한다.

• 연관관계의 주인: 외래 키를 관리하는 참조
• 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능

다대일

다대일 단방향

member가 다 쪽이므로 다에 외래키가 간다.. 따라서 member에 외래키가 있으므로 연관관계의 주인은 member이다.
member 테이블의 team_id도 team을 찾기 위함이고 member의 team도 team을 찾기 위함이다. 따라서 둘이 매핑한다.

@Entity
//어노테이션에 필요한 매핑을 할 수 있다.
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;
}

코드로 나타내자면 위와 같다.

다대일 양방향

양방향은 어떻게 되냐 반대쪽에도 넣어주면 된다.
주의 : 반대쪽에 추가한다고 해도 테이블엔 추가되지 않는다.
team에 list member가 객체에 추가 된다고 해서 테이블에 list member가 추가되진 않는다.

@Entity
//어노테이션에 필요한 매핑을 할 수 있다.
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")//일대 다 매핑에서 나는 뭐랑 연결되어있지 ? Member 에 team 나의 반대편 team과 매핑
    private List<Member> members = new ArrayList<>();//NPE안뜨기 위해서 NEW
}

외래키가 있는 쪽이 연관관계의 주인이다.
양쪽을 서로 참조하도록 개발해야한다.

일대다

여기선 일이 연관관계의 주인이다.

일대다 단방향

그림을 잘봐야된다. 이 모델은 권장하지 않는다.
표준 스펙에서 지원하기 때문에 설명은 하나 실무에선 거의 진짜 거의 쓰지 않는다.

team을 중심으로 뭘 해보고 싶을때 사용한다.
team에선 member를 알고 싶은데 member에선 team을 알고 싶지 않은것이다.
객체 입장에선 이런 설계가 나올 가능성이 높다.

team : member = 1 : N 이므로 당연히 member에 fk가 들어간다.
그러면 team에 있는 members값을 바꿨을 때 team_id라는 다른 테이블에 있는 외래키를 업데이트 해줘야한다.
연관관계의 주인이 memebers이기 때문이다.

@Entity
//어노테이션에 필요한 매핑을 할 수 있다.
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때
}

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name="TEAM_ID")
    private List<Member> members = new ArrayList<>();//NPE안뜨기 위해서 NEW

주의할 점
테이블은 그대로 이다.

try {

Member member = new Member();
member.setUsername("member1");

em.persist(member);
Team team = new Team();
team.setName("teamA")
team.getMembers().add(member);

em.persist(team);

team.getMembers().add(member);가 애매하다.
team테이블에 INSERT 되는 내용이 아닌 member테이블 업데이트 되는 내용이다.
insert 쿼리는 member와 team 두번 나가고 1대다 update 쿼리가 나간다.

따라서 쿼리가 많이 나간다.
team입장에서 볼 때 setName까지는 team 테이블의 것이기 때문에 insert로 가고 add member는 member테이블에 있기 때문에 업데이트를 해야된다.

업데이트가 한번 나감으로써 성능이 떨어진다.
근데 사실 업데이트 한번 더 나간다해서 성능이 그렇게 떨어지지 않는다.
실질적인 문제는 따로 있다.
team만 손을 댄 것 같은데 쿼리를 보면 member에 update가 나가는 걸 보고 혼란이 된다.
실무에선 테이블이 수십개가 돌아가는데 이렇게 되면 운영에서 힘들어진다.

다대일 양방향으로 가는게 낫다 -> 다 가 연관관계 주인

테이블 일대다 관계는 항상 다 쪽에 외래키가 있기 때문에 일쪽에 연관관계 주인을 주면 이상해진다.

join컬럼을 꼭 사용해야한다. joinColumn을 사용하지 않으면 조인 테이블 방식을 사용한다.
team_member 테이블이 생긴다. 중간 테이블은 team의 teamid와 member의 memberid를 가진다.

쓸 수도 있지만 운영하기 쉽지 않고 성능상 좋지 않다.

단점

엔티티가 관리하는 외래 키가 다른 테이블에 있음 -> 제일 문제다.
연관관계 관리를 위해 추가로 update sql실행 -> 당연히 내테이블이 아닌 다른 테이블에 넣는거니 update 쿼리가 따로 나간다.

일대다 양방향

굉장히 억지러운 면이 있다. 스펙상 되는 건 아니고 약간 야매로 하는 것이기 때문이다.
일대다로 하고 싶은데 역방향도 추가하고 싶을 때 사용한다.

@Entity
//어노테이션에 필요한 매핑을 할 수 있다.
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @ManyToOne
    @JoinColumn(name="TEAM_ID", insertable = false, updatable = false)
    private Team team;
}
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;

이렇게 하면 연관관계 주인이 둘이 된다. 한마디로 망하는거다.
그래서 insertable 옵션과 updatable 옵션을 통해 읽기 전용으로 만들어서 사용한다.

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name="TEAM_ID")
    private List<Member> members = new ArrayList<>();//NPE안뜨기 위해서 NEW

}

이러한 일대다 양방향 매핑은 공식적으로 존재하지 않는다.

일대일

외래키를 아무곳이나 다 넣을 수 있다. member에 넣어도 되고 team에 넣어도 된다. 둘 중에 한군데만 넣으면 된다.
외래키에 데이터베이스 유니크 제약조건이 추가되어야 일대일이된다.
안해도되는데 안하면 애플리케이션 상에서 관리를 엄청 잘 해야한다.

일대일 단방향 : 주 테이블에 외래키 단방향

회원이 사물함을 가지는데 하나만 가질 수 있고, 사물함 입장에서도 사물함 하나는 한사람만 사용한다고 하자.


@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

@Entity
public class Locker {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
}

다대일 단방향이랑 매우 유사하다.

일대일 양방향 : 주 테이블에 외래 키 양방향

양방향을 만들고 싶으면 locker에 member 추가하면 된다.


@Entity
public class Locker {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
}

일대일 단방향 : 대상 테이블에 외래키 단방향

member가 연관관계 주인을 하고 싶은데 외래키가 locker에 있는것이다. 이건 아예 지원자체가 안되고 방법도 없다.

일대일 양방향 : 대상 테이블에 외래 키 단방향 정리

주 테이블이 member라고 생각했을 때 locker에 fk가 있을 때 양방향은 문제가 없다.
locker의 member를 locker 테이블의 fk에 매핑하면 된다.

일대일 관계에서 fk가 member에 있는게 좋을까 locker에 있는게 좋을까
둘다 상관은 없다.
미래에 하나의 회원이 여러개의 locker를 가질 수있다고 요구사항이 바뀌었을 때
locker에 fk가 있을 경우 alter로 uni제약조건만 빼면 된다.
자연스럽게 일대다로 바꾸기 쉽다.

member에 fk가 있을 경우 변경 포인트가 많아진다. member의 fk를 지우고 locker에 fk를 추가하는 등 ..

하지만 비즈니스가 반대로 변경된다면, 또 다르게 얘기가 된다.
위엔 db입장에서 바라볼 때 이다.
개발자 입장에선 member에 locker가 있는게 성능도 그렇고 여러가지 방면에서 유리하다.

장점
예를 들어 member가 locker를 가지고 있는지 없는지 member 테이블을 많이 select한다고 가져왔을 때 member테이블에서 이미 select할 때 가져와서 본다.
locker에 값이 있으면 어떤 로직이 돌고 없으면 안돌고라는 조건문이 있을 때
member는 이미 조회를 해오니까 이미 locker값이 있음 join없이 memebr를 가져왔을 때 locker의 값이 있는지 없는지 쉽게 확인이 가능하다.

객체를 개발하는 입장에선 member에 fk가 있는게 더 좋다.

양방향을 걸어야하는 단점이 있다.

일대일 정리

주 테이블 : 많이 액세스 하는 것

주 테이블에 외래 키

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

단점이 조금 치명적이다.

대상 테이블에 외래 키

전통적인 데이터베이스 개발자 선호

  • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
  • 단점 : 반대쪽 테이블에 있는거 update 못하므로 양방향으로 만들어야 함, 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨

jpa입장에서는 프록시 객체를 만드려면 member를 로딩할때 locker에 값이 있는지 없는지 확인해야하는데 주테이블에 외래키가 있을 때는 확인이 쉽지만 대상테이블에 외래키는 member테이블만으로 해결이 안되고 locker 테이블까지 뒤져야 된다.
그래서 있는지 없는지 확인하려면 어차피 쿼리가 나가는데 그럼 프록시를 만드는 의미가 없다.

지연 로딩을 설정했을 때 연관된 엔티티가 있으면 프록시 객체가 대신 들어가면 되지만, 연관된 엔티티가 없으면 null이 들어가야한다.

강사님은 실무에서 거의 주테이블에 외래 키 단방향 방법을 사용

참고

Q&A

다대다

실무에서 사용하지 않는다.

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야한다.

회원은 여러개의 상품을 구매할 수 있고 상품은 여러 구매자가 구매할 수 있다.
이때 다대다 관계인데, 이 다대다 관계는 해소해서 사용해야한다.
중간테이블이 있어야하는데 이걸 일대다, 다대일 관계로 풀어야함

객체에선 조금 다르다.
member가 product list를 가지고 product도 memberlist를 가지면 된다.
따라서 객체는 컬렉션 두개로 다대다 관계가 가능하다

그래서 객체는 가능하기 때문에 jpa도 이걸 어떻게 테이블에서 해결을 해줘야한다.
하지만 다대다 테이블은 안되기 때문에 일대다 다대일로 매핑해준다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
}
@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

}

하면 MEMBER_PRODUCT 테이블이 생기고 FK,PK가 생성된다.

양방향으로 만들고 싶을 경우

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "products")0
    private List<Member> members = new ArrayList<>();
}

위와 같이 하면 된다.

한계

편리해보이지만 실무에서 사용하지 못한다.
연결 테이블이 단순히 연결만 하고 끝나는게 아니고 예를 들어 주문 시간이나 수량 같은 추가 데이터가 들어올 수 있는데 이게 안된다.
매핑 정보만 들어가고 테이블에 추가 데이터가 들어갈 수 없다.

그리고 쿼리가 이상하게 나간다. 중간테이블이 숨겨져 있기 때문에 예상하지 못한 쿼리가 나간다.

다대다 한계 극복

일대다와 다대일로 만들고 연결 테이블을 엔티티로 승격해서 사용한다.
테이블을 만들어서 사용하자

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @OneToMany(mappedBy = "member")
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

}

이렇게 하면 마음대로 데이터를 추가 할 수 있다.

@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private in count;
    private in price;
}

결론 연결 테이블 엔티티로 승격 해라
왠만하면 pk는 그냥 의미 없는 값을 쓰자
전통 적인 방식은 다대다 연결 테이블 처럼 pk를 fk두개 묶어서 사용하는데, pk는 아무 의미 없는 값을 사용하고 fk를 따로 두자.

pk로 제약을 거는게 그 순간에는 좋은데, id라는게 어딘가에 종속된다는게 시스템의 유연성이 떨어진다. 따라서 비즈니스적으로 의미 없는 id를 설정하는 것이 좋다.

참고

Q&A


출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
백엔드 개발자 지망생

0개의 댓글