1. 프록시
•
엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.
•
연관관계의 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.
// Member 엔티티
@Entity
public class Member{
private String userName;
@ManyToOne
private Team team;
...
}
// Team 엔티티
@Entity
public class Team{
private String teamName;
...
}
Java
복사
// CASE 1. Member, Team 객체 조회 필요
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
System.out.println("소식팀: " + team.getName()); // team 객체 조회
}
// CASE 2. Member 객체 조회 필요
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
}
Java
복사
•
JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공한다.
◦
이것을 지연 로딩이라 한다.
•
그런데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대상에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.
1.1. 프록시 기초
•
JPA에서 식별자로 엔티티 하나를 조회할 때 영속성 컨텍스트는 엔티티가 없으면 데이터 베이스를 조회한다.
Member member = em.find(Member.class,"member1");
Java
복사
•
직접 엔티티를 조회하면 실제 사용 유무와 무관하게 데이터베이스에서 조회하게 된다.
◦
실제 사용 시점까지 데이터베이 조회를 미루고싶다면 EntityManager.getReference() 메소드를 사용하면 된다.
•
1.1.1. 프록시의 특징
•
프록시는 실제 클래스를 상속받아 만들어지기 때문에 실제 클래스와 모양이 같다.
•
사용자 입장에서는 프록시인지 아닌지 구분하지 않고 사용해도 된다.
•
프록시 객체는 실제 객체에 대한 참조(target)를 보관한다.
◦
객체 메서드 호출 시 실제 객체의 메서드를 호출한다.
1.1.2. 프록시 객체의 초기화
•
프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
◦
이를 프록시 객체 초기화라 한다.
1.1.3. 프록시의 특징
•
프록시 객체는 처음 사용할 때 한 번만 초기화된다.
•
프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
•
프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
•
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
•
초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다. 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.
1.2. 프록시와 식별자
•
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출 해도 프록시를 초기화하지 않는다.
◦
단, @Access(AccessType.PROPERTY)로 설정한 경우에만 초기화하지 않는다.
•
엔티티 접근 방식을 필드 (@Access(AccessType.FIELD))로 설정하면 JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화한다.
•
프록시는 연관관계를 설정할때 DB 접근을 줄일 수 있다.
1.3. 프록시 확인
•
PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
•
하이버네이트의 initialize() 메소드를 사용하면 프록시를 강제로 초기화할 수 있다.
2. 즉시 로딩과 지연 로딩
•
Member 테이블에 Team 테이블을 외래키로 가지고 있다고 할때 Member 엔티티를 호출하면 연관 엔티티인 Team 엔티티도 DB에서 같이 가져와야할까?
◦
JPA는 엔티티의 조회 시점을 선택할수 있도록 해준다.
•
즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
◦
설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
•
지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
◦
설정 방법 : @ManyToOne(getch = FetchType.LAZY)
2.1. 즉시 로딩
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
// 즉시 로딩
Member member = em.find(Member.class,"member1");
Team team = member.getTeam(); // 객체 그래프 탐색
Java
복사
•
즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.
SELECT
M.MEMBER_ID AS MEMBER_ID,
M.TEAM_ID AS TEAM_ID,
M.USERNAME AS USERNAME,
T.TEAM_ID AS TEAM_ID,
T.NAME AS NAME
FROM
MEMBER M LEFT OUTER JOIN TEAM T
ON M.TEAM_ID=T.TEAM_ID
WHERE
M.MEBER_ID='member1'
SQL
복사
◦
여기서는 회원과 팀을 조인해서 쿼리 한번으로 두 엔티티를 모두 조회한다.
2.2. 지연 로딩
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
// 지연 로딩
Member member = em.find(Member.class,"member1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용
Java
복사
•
지연 로딩은 실제 객체가 사용되지 전까지 로딩을 미룬다.
•
실제 데이터가 필요한 순간이 되엇야 데이터베이스를 조회해서 프록시 객체를 초기화한다.
2.3. 즉시 로딩, 지연 로딩 정리
•
즉시 로딩을 사용하면 처음부터 연관된 모든 엔티티를 영속성 컨텍스트에 올려두므로 현실적이지 않다.
•
연관된 엔티티를 즉시/지연 로딩하는것이 좋을지는 상황에 따라 다르다.
•
즉시 로딩
◦
연관 엔티티가 적고 연관 엔티티들이 거의 같이 사용된다.
•
지연 로딩
◦
연관 엔티티가 굉장히 많고 거의 같이 사용되지 않는다.
3. 지연 로딩 활용
•
사내 주문관리 시스템을 개발한다고 가정해보자
•
얼마나 같이 사용되는지에 따라 로딩 전략을 구성한다.
◦
Member - Team : 거의 같이 사용, 즉시 로딩, N:1
◦
Member - Order : 서로 가끔 사용, 지연 로딩, 1:N
◦
Order - Product : 거의 같이 사용, 즉시 로딩, N:1
@Entity
public class Member{
@Id
private String id;
private String username;
private Integer age;
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
@OneToMany(mappedBy = "member",fetch = FetchType.LAZY)
private List<Order> orders;
...
}
Java
복사
회원 조회
•
회원과 팀은 즉시로딩으로 설정되어 조회시 함께 조인되어 쿼리가 발생한다.
•
반면 주문 내역은 지연 로딩이기 때문에 필요시 실제 DB에 내용을 질의한 뒤 초기화된다.
3.1. 프록시와 컬렉션 래퍼
주문내역 조회
Member member = em.find(Member.class,"member1");
List<Order> orders = member.getOreders();
System.out.println("orders = "+orders.getClass().getName());
// orders = org.hibernate.collection.internal.PersistentBag
Java
복사
•
하이버네이트는 엔티티를 영속 상태로 만들때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하고 이를 컬렉션 래퍼라고 한다.
•
엔티티를 지연 로딩하면 프록시 객체를 사용하는데 컬렉션(List orders)은 컬렉션 래퍼가 지연 로딩을 처리해준다.
3.2. JPA 기본 페치 전략
•
fetch 속성의 기본 설정값은 다음과 같다.
◦
@ManyToOne, @OneToOne : 즉시 로딩
◦
@ManyToMany, @OneToMany : 지연 로딩
•
JPA의 기본 페치 전략은 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다.
◦
하지만 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다.
3.3. 컬렉션에 FetchType.EAGER 사용 시 주의점
•
컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
◦
예를 들어 A 테이블을 N, M 두 테이블과 일대다 조인하면 SQL 실행 결과가 N 곱하기 M이 되면서 너무 많은 데이터를 반환할 수 있고 결과적으로 애플리케이션 성능이 저하될 수 있다.
▪
따라서 2개 이상의 컬렉션을 즉시 로딩으로 설정하는 것은 권장하지 않는다.
•
컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
◦
데이터베이스 제약조건으로 내부 조인으로 인해 검색이 되지 않는 상황을 막을 수는 없다.
▪
따라서 JPA는 일대다 관계를 즉시 로딩할 때 항상 외부 조인을 사용한다.
•
FetchType.EAGER 설정과 조인 전략
◦
@ManyToOne, @OneToOne
▪
optional=false : 내부 조인
▪
optional=true : 외부조인
◦
@ManyToMany, @OneToMany
▪
optional=false : 외부 조인
▪
optional=true : 외부조인
4. 영속성 전이: CASCADE
•
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다.
•
JPA는 CASCADE 옵션으로 영속성 전이를 제공한다.
4.1. 영속성 전이 : 저장
@ManyToOne(optional = false, cascade = CascadeType.PERSIST)
Java
복사
•
해당 옵션 사용 시 부모와 자식 엔티티를 한 번에 영속화할 수 있다.
4.2. 영속성 전이 : 삭제
@ManyToOne(optional = false, cascade = CascadeType.REMOVE)
Java
복사
•
해당 옵션 사용 하여 부모 엔티티를 삭제할 경우 연관된 자식 엔티티도 함께 삭제된다.
4.3. CASCADE의 종류
public enum CascadeType {
ALL, //모두 적용
PERSIST, //영속
MERGE, //병합
REMOVE, //삭제
REFRESH, //REFRESH
DETACH //DETACH
}
Java
복사
•
여러 속성을 같이 사용할 수 있다.
5. 고아 객체
•
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체(ORPHAN) 제거라 한다.
•
부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 가능하다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
...
}
Java
복사
•
고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행된다.
•
고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
•
고아 객체 제거에는 기능이 하나 더 있는데 개념적으로 볼때 부모를 제거하면 자식은 고아가 된다.
◦
부모를 제거하면 자식도 같이 제거할 수 있다.
◦
CadecadeType.REMOVE를 설정하면 가능하다.
6. 영속성 전이 + 고아 객체, 생명주기
•
CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 어떻게 될까?
◦
일반적으로 엔티티는 EntityManager.persist()를 통해 영속화되고 EntityManager.remove()를 통해 제거된다.
◦
이것은 엔티티 스스로 생명주기를 관리한다는 뜻이다.
◦
그런데 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
•
영속성 전이는 DDD의 Aggregate Root개념을 구현할 때 사용하면 편리하다.