JPA에서 가장 중요한 것 두 가지를 꼽으라고 하면, 첫 번째는 영속성 컨텍스트 두 번째는 연관 관계 매핑이다.
어떤 비즈니스 로직이냐에 따라서 적절한 관계를 정해야 하기 때문에 굉장히 중요하다.
다중성을 알아보기 전에 알아봐야 할 용어가 몇 가지 있다.
다중성 : 다대일, 일대다, 일대일, 다대다
방향성 : 단방향, 양방향이 있다. 테이블에는 존재하지 않는다.
연관관계의 주인 : 양방향에서 연관 관계에 대해 관리하는 주체 즉, 읽기전용이 아닌 쪽을 말한다.
그 전에 짚고 넘어가야 할 부분이 있다. 이 부분은 자주 헷갈릴 수 있는 부분인데, 관계를 파악하기 위한 내 개인적인 방법이다.
연관관계 파악하기
때때로 지금 내가 파악하고자 하는 관계가 다대다 관계인지 다대일 관계인지 헷갈리는 경우가 종종 있다.
가장 간단한 예시를 살펴보자.
하나의 팀에는 여러 명의 팀원이 속할 수 있고, 팀원은 하나의 팀에만 속해야 한다.
팀원은 하나의 팀에만 속할 수 있고, 하나의 팀에는 여러 명의 팀원이 속할 수 있다.
하나의 팀에는 여러 명의 팀원이 보인다. 연관관계는 한 쪽이 다대일 일대다라면 반대 쪽은 그 반대로 적용된다.
하지만 우리는 이런 상황에서 오해를 불러온다.
한 사람은 여러 개의 게시글을 쓸 수 있고, 게시글은 여러 명의 사람에 의해서 쓰여질 수 있고..? 이런 오해에서 뭐지? 게시글도 일대다인가? 일대다 일대다 니까 다대다인가? 라는 혼란이 일어나게 된다.
연관관계를 파악 할 때는 외래 키 기준으로 생각해보자. 객체의 관점에서 살펴보자.
한 사람은 분명히 여러 개의 게시글을 쓸 수 있다. 참조를 한다면 List로 하겠다.
이어서 게시글의 입장에서 살펴보자. 게시글은 분명 여러 명의 사람에 의해서 쓰여질 수 있다. 하지만 이렇게 바라보는 것이 아니다.
게시글의 입장에서 게시글 그 자체가 누구한테 쓰여졌는가? 당연히 한 사람에 의해서 쓰여졌을 것이다.
게시글 그 자체는 한 명의 사람을 Person 으로 참조한다.
맞는 비유인지는 모르겠지만, 테이블의 레코드 하나 하나 그 자체를 객체의 관점으로 봤을 때 그 객체가 컬렉션으로 받아야 하는지 아니면 단순히 구성으로 참조 변수만 가져도 되는지 객체 그 자체로 바라보니 판단하기가 수월했다.
예를 들어 다음 상황도 살펴보자.
한 사람은 여러 개의 물품을 구매 할 수 있다.
물품의 입장에서 보자.
물품을 한 사람이 독차지하다니 말도 안된다. 물품은 분명히 여러 사람에게 사질 수 있다.
둘 다 List<Goods>(산 물품), List<Person>(산 사람) 으로 구성 할 것 같지 않은가?
이 방법이 가장 좋고 비유 할 수 있는 방법인지는 모르겠다. 하지만 이러한 연관관계를 파악하는데 있어서 굉장히 고생했기에 적어봤다.
이어서 다중성 + 방향성을 합친 연관관계를 하나 하나 살펴보자.
다대일
단방향
@Entity
public class PointCard {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
Member member;
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
}
데이터베이스 기준에서는 외래키는 항상 다에 존재하고 있다.
실제로 테이블을 생각해봐도 PointCard 테이블에는 외래키가 회원으로 되어있을 것이다. 단방향이기 때문에 회원에서는 PointCard를 참조하지 않는 것을 볼 수 있다.
참고로 JoinColumn의 name은 외래키의 이름을 지정하는 것이다. 회원 엔티티의 기본키 이름과 동일하게 매핑하지 않아도 된다.
양방향
@Entity
public class PointCard {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
Member member;
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "member")
List<PointCard> pointCards = new ArrayList<>();
}
이번에는 양방향이기 때문에 회원 엔티티에서 pointCard 리스트를 참조하고 있는 것을 볼 수 있다.
@OneToMany의 mappedBy 속성에 주목하자. 해당 속성은 연관관계의 주인을 정하는 것이다.
이때 mappedBy 속성의 값은 연관관계 주인 엔티티에 있는 변수명입니다.
즉, 연관관계의 주인은 PointCard가 되고, 회원이 읽기 전용이 된다.
그런데 지금 같은 상황에서 보니 좀 이상하다. 회원이 주 테이블이 되어야 하고 포인트 카드가 대상테이블이 되어야 한다고 생각 할 것이다.
이런 경우에 일대다를 이용해서 주체를 바꿔 줄 순 있다.
일대다
단방향
@Entity
public class PointCard {
@Id
@GeneratedValue
private Long id;
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToMany
@JoinColumn(name = "pointcard_id")
List<PointCard> pointCards = new ArrayList<>();
}
이번에는 일대다 단방향으로 연관관계의 주인을 회원의 입장에서 지정했다.
이렇게 설정하는 것이 가능하다. 그러나 한 가지 문제가 발생한다. 바로 객체와 테이블간의 패러다임의 불일치성 때문이다.
이렇게 설정을 하더라도 결국에 외래키는 무조건 다 쪽에 존재를 하게 된다.
예를 들어서, 새로운 포인트 카드가 추가가 되었다고 가정하자.
PointCard card = new PointCard();
em.persist(card);
Member member = new Member();
member.getPointCards().add(card);
em.persist(member);
아무 문제가 없어보이지만 쿼리가 2개가 나간다. 바로 InsertQuery와 updateQuery이다.
영속성 컨텍스트 내부에서 쓰기지연 저장소에 있다가 보내지니까 문제 없지 않을까? 라고 생각 할 수 있다.
그러나 캐시의 사용 용도는 트랜잭션 내부에서 중요한 것이지, 트랜잭션 내부에서 어떠한 변경사항이 있고, 이를 체크하여 쓰기지연 저장소에서 나간다면 해당 쿼리가 수행되어야 할 것 이다.
따라서, 테이블 입장에서는 외래키가 다 쪽에 있는데, 외래키가 없는 다른 테이블에서 외래키가 있는 테이블을 조작하니 업데이트가 발생 할 수 밖에 없는 것이다.
만약 발생이 안했다고 가정을 해보자. 그럼 포인트 카드 테이블 입장에서 해당 포인트 카드가 누구 것인지에 대한 여부를 모를 것이다.
추가적으로 이런 일로 인한 업데이트 쿼리가 발생하다 보니 개발자 입장에서는 분명히 회원에 대해서 건드렸는데, 업데이트 쿼리가 나간다면 테이블이 많을 때 어디서 무엇이 잘못했는지 파악하기가 쉽지 않을 것이다.
양방향
일대다 양방향을 사용해야 될 상황이면 다대일 양방향을 사용하는 것이 옳다.
기본적으로 일대다 양방향을 JPA가 지원하지 않는다. @OneToMany 에는 mappedBy 라는 연관관계의 주인을 결정하는 속성이 있지만 @ManyToOne 에는 존재하지 않는다. 그럼에도 불구하고 연결을 하려면 다음과 같이 할 수는 있다.
@Entity
public class PointCard {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "member_id", insertable = false, updatable = false)
Member member;
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToMany
@JoinColumn(name = "pointcard_id")
List<PointCard> pointCards = new ArrayList<>();
}
강제로 읽기 전용 속성으로 만든 것을 볼 수 있다.
이를 이용해 아까와 달리 업데이트 쿼리가 나가지 않도록 다음과 같이 수정을 할 수도 있다.
PointCard card = new PointCard();
em.persist(card);
Member member = new Member();
card.setMember(member);
em.persist(member);
이렇게 하면 직접 외래키에 참조해서 추가를 하기 때문에, 업데이트 쿼리가 발생 할 일도 없다.
하지만, 사용하지말자. 이렇게 사용하는 것보다 더 좋은 다대일이 있는데 굳이 고집 할 이유가 있을까?
일대일
일대일은 조금 복잡 할 수 있다. 이때 한 가지 알아두고 가야하는 점은 주 테이블과 대상 테이블이다.
주테이블은 자주 참조하는 테이블, 대상 테이블은 자주 참조하지 않는 테이블에 대한 관점으로 바라보자.
일대일에서는 단순히 단방향 양방향이 아닌, 외래키를 누가 가지고 있는지에 따라 각 2가지 관점으로 나뉘어 총 4가지 관점으로 볼 수 있다.
주 테이블 외래키 단방향
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "fingerprint_id")
Fingerprint fingerprint;
}
@Entity
public class Fingerprint {
@Id
@GenenratedValue
private Long id;
}
단순한 예시이다. 세상에 고유한 사람마다 고유한 지문은 하나밖에 없어야 하며 사람 또한 하나의 고유한 지문을 가져야 하는 상황이다.
주 테이블에 외래키가 있는 이러한 상황은 마치 다대일 단방향과 비슷하다.
테이블 시점에서도 주 테이블에 외래키가 있기 때문에 상관없지만 일대일이기 때문에 하나 다르다면 외래키에 고유키가 추가 되어야 한다는 것이다.
주 테이블 외래키 양방향
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "fingerprint_id")
private Fingerprint fingerprint;
}
@Entity
public class Fingerprint {
@Id
@GenenratedValue
private Long id;
@OneToOne(mappedBy = "fingerprint")
private Member member;
}
다대일 양방향과 비슷합니다.
대상 테이블 외래키 단방향
이는 JPA에서 지원하지 않습니다. 객체 간의 참조를 생각했을 때도 생성되지 않은 상태에서 참조 자체가 불가능하다.
대상 테이블 외래키 양방향
대상 테이블 외래키 단방향과 달리 외래키 양방향은 가능하다. 참조 또한 멤버 -> 지문 -> 멤버로 건너가서 참조가 가능하다. 그러나 해당 방 식은 사실 주 테이블 양방향에서 단계를 한 단계 더 거치는 것 뿐이다. 성능 상의 저하도 있고, 더 좋은 주 테이블 외래키 양방향이 있으니 꼭 사용해야 한다면 주 테이블 외래키 양방향을 사용하는 것이 좋다.
일대일 양방향을 사용 할 때, DBA분들은 대상 테이블에 연관관계의 주인을 설정하기를 선호한다고 한다.
이유는 그럴 일은 없겠지만, 하나의 지문을 여러 명의 사람들이 쓸 수 있다고 가정하면 단순히 OneToMany로 바꿔주기만 하면 깔끔하게 해결되기 때문이다. 하지만 주 테이블이 연관관계의 주인이었다면 여러모로 수정해야 될 것들이 많아진다.
또한 주 테이블에 연관관계의 주인을 설정하게 되면 없을 경우에 null이 들어갈 수 있다는 문제점도 있기 때문에 비 선호하는 것으로 보여진다.
하지만 개발자의 시점에서는 주 테이블에 있는 것이 개발하기 더 수월하고 아키텍쳐도 잘 보인다는 장점이 있다.
이 부분은 DBA 분들과 원만한 대화를 통해 맞춰가는 것이 중요하다고 생각한다.
다대다
마지막으로 다대다이다.
다대다는 특히 실무에서는 절대로 사용하면 안되는 연관관계이다.
데이터베이스 관점으로 봤을 때, 각 테이블 2개만으로 연관관계를 형성 할 수가 없다. 따라서, 조인테이블을 생성해서 두 테이블의 연관관계를 관리한다. 그러나 여기서 또 테이블과 객체의 패러타임의 불일치성때문에 문제가 발생하게 된다.
객체는 각 객체 내에서 컬렉션으로 다대다 참조가 가능하다.
따라서, JPA는 @ManyToMany 애노테이션을 사용해서 연결을 할 수 있도록 해두었는데, 하지만 동일하게 @JoinTable을 이용해서 다대다 연관관계를 관리할 테이블을 하나 만들어주어야 함은 변하지 않는다. 추가적으로 여기서 연관관계의 주인은 중요하지 않고, 그냥 내키는 쪽에 연관관계의 주인을 잡아준다.
여기서 문제는 실제로 조인테이블에는 단순히 키값 뿐만이 아닌, 여러 개의 추가 컬럼들이 존재 할 수 있다는 것이다. 따라서, 다대다 관계라면 OneToMany <-> ManyToOne <-> OneToMany 로 변경해서 조인테이블을 새로운 엔티티로 만들고 사용하기를 권장한다고 한다.
해당 글은 김영한님의 인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편을 바탕으로 작성되었습니다.
'Backend > JPA' 카테고리의 다른 글
Spring Data JPA는 왜 @Repository를 사용하지 않아도 될까? (0) | 2023.05.14 |
---|---|
JPA 상속관계 매핑 (0) | 2023.05.01 |
영속성 컨텍스트 (0) | 2023.04.25 |