양방향 매핑에서 N쪽이 주인인 경우.
연관관계 편의메서드를 N쪽에 배치해도 되지만, 보편적으로 연관관계 편의 메서드를 1쪽에 둔다고 하여 연관관게 편의 메서드 내에서 1쪽의 N객체를 설정하는 연관관계 편의 메서드도 추가함.
참고로 연관관계 편의 메서드를 작성하는 이유는 양방향 매핑이라고 해서 연관관계가 동기화되지는 않기 때문!
이 경우를 JPA 는 지원하지 않음.
즉, 양방향 다대일 관계일때 일때 1쪽을 주인으로 놓을 수 없음.
따라서 해당 매핑 관계에 대해서는 테스트 및 엔티티 설계를 진행하지 않음. 진행 자체가 불가능함. 컴파일되지가 않음.
다대일일때 N 쪽을 FK를 관리하는 주인으로 놓는 단방향 매핑.
이 방식이 가장 많이 사용되는 방식으로 정석인 매핑방식이라고 볼 수 있음.
하지만 언제나 케바케. 상황에 따라 OneToMany 단방향이 개발 편의성/가독성 면에서 좋을 수 도 있음. OneToMany 단방향이 가져오는 성능 면에서 단점에 대해서는 아래에 보충설명되어있음.
왜냐하면 N 쪽에서 1쪽을 공유하고 있을 가능성이 높기 때문이다! 이 상황에서 cascade를 사용하면, 하나의 N 객체가 1 객체를 삭제하거나 갱신할때 의도하지 않은 side-effect들이 발생할 수 있기 때문이다.
예를 들어, 여러 Member 객체가 하나의 Team을 참조 중인데, 그 중 하나의 Member를 remove 하면서 CascadeType.REMOVE가 전파되어 Team 까지 삭제되면, 나머지 Member들이 참조하고 있는 Team 객체에 대한 참조 오류가 발생해 NullPointerException이 터질 수 있다.
이렇게 CascadeType으로 인해 N쪽에서 1을 갱신하거나 삭제하면서 연관된 타 N들에도 영향을 미칠 수 있기 때문에 쓰지 않는 것이 권장됨.
@OneToMany 쪽에서 @JoinColumn을 명시하여, 1쪽이 외래키를 직접 관리하는 구조이다.
단방향 연관관계일때 상황에 따라 선택적으로 3번 대신 해당 매핑방식을 사용 가능하다.
N쪽을 언제나 1을 통해 참조하는 단방향 Flow 일때 해당 방식 고려 가능.
class Team {
...
@OneToMany
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
...
}- 실제 객체 그래프 탐색이 1쪽 중심이라면, 직관적인 코드 작성 가능
- 가독성이 좋아지고 코드가 객체지향적으로 깔끔해질 수 있음. (ORM과 객체지향에서 오는 간극을 해결할 수 있음)
- cascade나 orphanRemoval과 함께 쓰면 연관관계 관리가 코드상 명확해짐
하지만 이 방식은 일반적으로는 권장되지 않음. 왜냐하면 이런 경우에는 특정 1의 외래키를 업데이트하기 위해 N쪽의 기존 데이터를 전부 삭제하고 새로 삽입하는 방식으로 처리되기 때문이다.
즉, 1의 FK를 직접 수정하지 않고, 전체 컬렉션을 지우고 다시 삽입하는 방식으로 동작한다. 따라서 불필요한 DELETE문 → INSERT문의 반복으로 성능 저하로 이어질 수 있다.
-- 기존 멤버 모두 삭제
DELETE FROM member WHERE team_id = ?
-- 수정된 컬렉션 기준으로 멤버 모두 재삽입
INSERT INTO member (id, ..., team_id) VALUES (?, ..., ?)다대일 관계에서 N쪽을 FK를 관리하는 주인쪽으로 설정했을 경우, 1쪽에 CascadeType 과 OrphanRemoval 옵션을 설정해둠으로써 영속성 전이 확인 및 고아 객체 자동 삭제 현상을 확인 가능합니다.
CascadeType과 orphanRemoval 옵션은 양쪽에 둘 수 있지만, 실무에서는 One 쪽에 설정하는 것이 일반적입니다.
cascadeType 에 따라 부모의 영속성 변화를 자식에게도 전파하여 적용할 수 있습니다. orphanremoval = true 설정을 통해 부모와 연관관계가 끊어진 자식객체는 고아객체로 취급하여 자동 삭제가 됨을 테스트를 통해 확인할 수 있었습니다.
두 경우 모두 일반적으로 부모 → 자식 방향으로 전파되기를 의도하는 것이고 그 방향이 가장 자연스럽기 때문에, One → Many 방향으로 설정했습니다.
반대로 생각해봐도 Many 쪽에서 변경된게 One 쪽에 영향을 끼치면 해당 One 과 연관된 타 Many 쪽에도 영향을 미치는거임.
지연로딩 시 연관관계에 있는 객체는 모두 Hibernate:Proxy 객체로 찍히는 것을 확인할 수 있었음.
System.out.println(foundMember.getTeam().getClass());
-> class com.example.jpatest.BiManyToOne.Team.Team$HibernateProxy$TccwV5Dl추후 getTeam().getName() 과 같이 연관관계에 있는 객체의 멤버변수 조회 시 추가적인 쿼리가 나감을 확인할 수 있음.
즉시로딩을 적용하면 연관관계에 있는 객체들도 모두 실제 객체로 찍히는 것을 확인 가능합니다. 이미 연관관계에 있는 객체들을 모두 끌어왔으므로 연관관계에 있는 객체들의 멤버변수 조회 시 추가적인 쿼리가 찍히지 않고 바로 멤버변수를 참조할 수 있음을 확인할 수 있었음.
하지만 즉시로딩을 했을 시에도 지연로딩 후 조회와 같이 총 두 번의 쿼리가 나간 것을 확인할 수 있습니다.
결론적으로 지연로딩과 즉시로딩 모두 1+N 문제를 유발한다는 것을 쿼리 찍히는 것으로 확인 가능!
지연로딩과 즉시로딩 시 항상 연관관계에 있는 객체 조회 시 1+N 문제를 마주함.
지연로딩이 된 칼럼을 보유한 엔티티도 해당 엔티티만 불러올 시에는 연관관계에 있는 엔티티를 프록시 객체로 불러와 1+N 문제가 발생하지 않는 것처럼 보이지만, 실제로 연관관계에 있는 엔티티를 조회 시 추가적인 쿼리가 나감에 따라 1+N 문제가 발생함.
따라서 JPQL의 fetch join 이나 @EntityGraph를 활용해서 1+N 문제를 해결할 수 있음
추가적으로 JPQL의 특성 상 SELECT 문에 연관관계에 있는 엔티티 정보를 사용하지 않으면 쿼리문에 연관된 객체 테이블과의 명시적인 JOIN 을 해도 JOIN 쿼리가 나가지 않는 것을 확인할 수 있었음 -> 이건 JPQL의 내부 동작 방식과 연관되어 있음