08장. 프록시와 연관관계 관리
이번 장에서는 다음의 개념을 배우게 될 것입니다. 짧게 정리해볼게요.
프록시와 즉시로딩, 지연로딩
- 프록시라는 기술을 사용하게 되면 실제 사용되는 시점에 데이터베이스를 조회할 수 있습니다(지연로딩).
- 하지만 자주 사용되는 것은 미리 조인해서 함께 가지고 데이터 정보를 가지고 있는 것이 좋습니다(즉시로딩).
영속성 전이와 고아 객체
- 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 기능을 제공합니다.
프록시
예를들어 Member
객체와 Team
객체가 있고 N:1 관계로 연결되어 있다고 가정해봅시다. 그리고 회원 엔티티만 조회하는 상황일 경우, Team
속성을 사용하지 않는다면 굳이 회원 엔티티 가져올 때 연관된 Team
엔티티까지 가져올 필요가 없습니다. 이와 같이 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 지연 로딩이라 합니다.
이러한 지연로딩이 가능한 이유는 프록시 객체가 있기 때문인데요. 프록시 객체는 실제 엔티티인 척하는 가짜 객체를 말합니다. 이 프록시 객체는 해당 엔티티 객체를 상속하고 있기 때문에 속은 비워져있지만 겉에 보기엔 똑같아보이고 기능을 제공할 수 있는 척할 수 있는 것입니다.
Member member = em.gerReference(Member.class, "member1");
EntityManager.gerReference()
메서드를 사용하면 해당 엔티티를 사용하기 전까지 데이터베이스 조회를 최대한 미룰 수 있습니다. 이 프록시 객체는 실제 객체에 대한 참조(target)를 보관합니다. 그리고 프록시 객체의 메소드를 호출하면 실제 객체의 메서드를 호출하게 됩니다.
프록시 객체의 초기화
이 초기화 과정이 데이터베이스를 조회하여 실제 엔티티 객체를 생성하는 작업입니다. 해당 엔티티가 직접적으로 사용되는 시점인 것이죠. 초기화는 영속성 컨텍스트의 도움을 받아야 가능하므로 영속성 컨텍스트가 준영속 상태인 경우 오류가 발생합니다.
프록시의 특징
그런데 만약 영속성 컨텍스트 내에 엔티티가 이미 있는 상태라면 em.gerReference()
를 호출해도 프록시 생성이 아닌 실제 엔티티를 반환합니다.
프록시와 식별자
재미있는 점은 다음과 같은 경우 프록시 객체의 초기화가 진행되지 않습니다.
Team team = em.gerReference(Team.class, "team1");
team.getId(); // 초기화되지 않음
그 이유는 프록시 객체가 생성되면서 식별자 값은 이미 들고있기 때문입니다. 물론 엔티티 접근 방식(@Access(AccessType.FIELD)
) 로 바꾸어서 초기화 시킬 수도 있습니다.
즉시 로딩과 지연 로딩
- 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회합니다.
- 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회합니다.
대부분의 JPA구현체는 즉시 로딩을 최적화하기 위해 조인 쿼리를 많이 사용합니다.
NULL 제약조건과 JPA 조인 전략
조인 쿼리 중에서도 보통은 외부 조인을 많이 사용하는데요. 그 이유는 팀과 멤버의 예를 들어서 설명해볼게요. 멤버 안의 TEAM_ID 외래키는 NULL값을 허용하고 있을 경우 팀에 소속되지 않는 멤버가 있을 수 있습니다. 이 경우 팀에 소속되지 않은 멤버과 팀을 내부 조인하면 팀은 물론 멤버을 조회할 수 없는 경우가 발생할 수 있는 것이죠.
하지만 외부 조인보다는 내부 조인이 성능과 최적화에서 더 유리합니다. 그렇기 때문에 사실 위의 예의 경우 NULL값이 허용하지 않는 경우라면 차라리 JPA로 하여금 내부 조인을 할 수 있도록 하는 것이 성능측면에서 더 좋은 것이죠. 이는 @JoinColumn
에서 nullable 속성을 이용하면 됩니다.
@JoinColumn(nullable = true) // NULL 허용(기본값), 외부 조인 사용
@JoinColumn(nullable = false) // NULL 허용하지 않음, 내부 조인 사용
또는 @ManyToOne.optional = false
를 사용해도 내부 조인을 사용합니다. NULL값이 없다면 굳이 외부조인을 사용할 필요는 없겠죠!
위의 팀과 멤버가 있고 멤버의 엔티티만 찾는다고 했을 때, 지연로딩을 사용하면 팀 멤버변수에 프록시 객체를 넣어둡니다.
지연 로딩 활용
프록시와 컬렉션 래퍼
컬렉션 래퍼는 하이버네이트가 엔티티 내에 컬렉션이 있으면 컬렉션을 추적, 관리의 목적으로 하이버네이트의 내장 컬렉션으로 변경하는 작업을 말합니다. 재미있는 점은 컬렉션이 있는 엔티티에서 컬렉션을 그냥 가져올 때는 초기화가 되지 않고, 실제 데이터를 get()
메서드를 이용해 조회할 때 초기화한다는 것입니다.
JPA 기본 페치 전략
fetch 속성의 기본 설정값은 다음과 같습니다.
@ManyToOne
,@OneToOne
: 즉시 로딩(FetchType.EAGER
)@OneToMany
,@ManyToMany
: 지연 로딩(FetchType.LAZY
)
위 기본 설정값을 한마디로 요약하면 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다는 것입니다. 그 이유는 컬렉션을 로딩한다는 것은 많은 비용이 들기 때문입니다. 예를들어 특정 회원이 연관된 컬렉션에 데이터가 수천만건이 있었는데 즉시 로딩하게 되면 회원을 로딩하는 순간 수천만건의 데이터가 함께 로딩이 될 것입니다.
저자가 추천하는 방법은 우선 모든 연관관계에 지연 로딩을 사용하고 개발이 어느 정도 완료단계에 왔을 때 사용되는 상황을 보면서 꼭 필요한 곳에만 즉시 로딩을 사용하는 것입니다. 참고로 만약 SQL로 이러한 작업을 했더라면 지연 로딩에서 즉시 로딩으로 바꾸는 최적화가 참 힘들었을 것입니다.
컬렉션에 FetchType.EAGER 사용 시 주의점
컬렉션을 하나 이상 즉시 로딩하는 것은 권하지 않습니다.
- 조인을 하는 것은 각 테이블의 칼럼수를 곱하는 수만큼 데이터를 반환하기 때문에 여러 개의 컬렉션이 사용되면 즉시 로딩을 피하는 것이 좋습니다.
컬렉션 즉시 로딩은 항상 외부 조인을 사용합니다.
- 예를들어 다대일 관계인 회원, 팀 테이블에서 회원 테이블의 외래 키에 not null 제약조건을 걸어두면 내부조인을 사용해도 됩니다.
- 하지만 반대로 팀 테이블에서 회원을 일대다 관계를 조인할 때 회원이 한 명도 없는 팀을 내부 조인하면 팀까지 조회되지 않는 문제가 발생합니다. 그렇기 때문에 JPA에서는 일대다 관계에서 즉시로딩할 때 항상 외부조인만 사용합니다.
@OneToOne
,@ManyToOne
- (optional = false) : 내부 조인
- (optional = true) : 외부조인
@OneToMany
,@ManyToMany
- (optional = false) : 외부 조인
- (optional = true) : 외부조인
영속성 전이: CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속성을 만들고 싶을 때 영속성 전이(transitive persistence) 기능을 사용합니다. JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 하는데요. 이런 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있습니다.
영속성 전이: 저장
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
영속성 전이는 연관관계를 매핑하는 것과는 관계가 없습니다. 다만 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함만 제공할 뿐입니다.
영속성 전이: 삭제
CascadeType.REMOVE로 설정하면 해당 엔티티가 삭제하면 연관된 엔티티도 함께 삭제됩니다.
CASCADE의 종류
public enum CascadeType {
ALL, // 모두 적용
PERSIST, // 영속
MERGE, // 병합
REMOVE, // 삭제
REFRESH, // REFRESH
DETACH // DETACH
}
PERSIST, REMOVE의 경우 실행할 때 바로 전이가 발생하지 않고 플러시를 호출할 때 전이가 발생합니다.
고아 객체
부모 엔티티와 연관관계가 끊어지면 자식 엔티티를 자동으로 삭제하는 기능이 있는데요. 이를 고아 객체(ORPHAN) 제거라 합니다. 부모 엔티티 컬렉션에서 자식 엔티티 참조만 제거해도 자식 엔티티가 삭제되는 것이죠.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
주의할 점은 이 기능은 참조하는 곳이 하나일 때만 사용할 수 있다는 것입니다. 만약 삭제한 엔티티를 다른 곳에서도 참조하고 있었다면 문제가 발생하기 때문입니다. 그렇기 때문에 @OneToOne
또는 @OneToMany
에서만 사용될 수 있는 것입니다.
Q & A
외래키의 주인, 종은 어떻게 구별할까요?
가장 쉬운 것은
mappedBy
속성을 통해 구별하는 것이고 이 속성을 가지고 있는 쪽이 '종'입니다.Question
과Answer
의 관계에서 주인과 종?Question
내에Answer
의 콜렉션이 있고mappedBy
속성이 있기 때문에 외래키 관점에서Question
이 종이 됩니다.- 개념상의 관점에서는 와닿지 않을 수 있습니다. 질문 내에 답변이 있는 것이 맞는 것 같거든요.
- 하지만
Answer
가 종이 되고Question
이 주인이 되었더라면 외래키 이외에 다른 중복되는 데이터를 여러 개 가지고 있는Question
객체가 존재하게 될 것입니다. - 그렇기 때문에 관계 내에서 다(N)이 있는 쪽이 주인이 되는 것이 좋은 설계인 것이고,
Question
과Answer
의 1:N 관계에서 N쪽인Answer
가 외래키의 주인이 되는 것입니다.
orphanRemovel = true
와CascadeType.REMOVE
의 차이점- 부모 엔티티가 삭제되었을 때 자식 엔티티도 삭제되는 것은 두 개의 공통된 속성입니다.
- 다만 다른 점은
orphanRemovel = true
의 경우 부모 엔티티와의 관계만 끊어줘도 자식 엔티티가 삭제된다는 점입니다.CascadeType.REMOVE
보다 좀 더 부가적인 기능으로 볼 수 있겠네요!
'Book > programming' 카테고리의 다른 글
웹을 지탱하는 기술2 - URI (0) | 2019.02.15 |
---|---|
웹을 지탱하는 기술1 - 웹 개론 (0) | 2019.02.07 |
[자바 ORM 표준 JPA 프로그래밍] 05장. 연관관계 매핑 기초3 (0) | 2018.12.27 |
[자바 ORM 표준 JPA 프로그래밍] 05장. 연관관계 매핑 기초2 (0) | 2018.12.26 |
[자바 ORM 표준 JPA 프로그래밍] 05장. 연관관계 매핑 기초 (0) | 2018.12.26 |