스프링 핵심 원리 - 기본편 - 인프런
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Spring 객체지향 온
www.inflearn.com
강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
프록시
엔티티를 조회할 때 영속성 컨텍스트에 찾고자 하는 엔티티가 없다면 DB에서 직접 조회하는데 이 때, 연관된 객체들을 실제 사용하든 사용하지 않든 조인해서 가져온다. 그러나 막상 연관된 객체를 사용할 일이 없다면 조인하지 않고 가져오고 연관된 객체가 실제 사용될 때 조회하는 것이 더 유리하다. JPA는 이런 문제를 해결하기 위해 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다.
지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.
프록시 기초
JPA에서 식별자로 엔티티 하나를 조회할 때 em.find를 사용한다. 이 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 통해서 실제 엔티티 객체를 조회한다. 이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다. 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 em.getReference 메서드를 사용하면 된다. 이 메서드는 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
em.getReference(Member.class, member.getId());
-> 조회 쿼리가 나가지 않는다.
예시를 보자.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass()); // 하이버네이트가 만든 가짜 프록시 클래스(쿼리 없음)
System.out.println("findMember.getId() = " + findMember.getId()); // id는 getReference할 때 인자로 넣었기 때문에 쿼리 없음
System.out.println("findMember.getUsername() = " + findMember.getUsername()); // 이 때, select 쿼리 발생(진짜 사용할 때)
프록시의 특징
프록시 객체 초기화
프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다. 사용자 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용할 수 있다.
그림처럼 프록시 객체는 실제 객체와 모양이 같다. 다만, 프록시 객체는 실제 객체의 참조를 보관한다. 프록시 객체를 호출하면 프록시 객체는 참조를 통해 실제 객체의 메서드를 호출한다.
그렇다면 실제 객체는 언제 조회될까? : 프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다. 과정은 다음과 같다.
- 프록시 객체에 getName()을 호출해서 실제 데이터를 조회한다.
- 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성(초기화)을 요청한다.
- 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
- 프록시 객체는 생성된 실제 엔티티 객체의 참조를 보관한다.
- 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
프록시 객체는 처음 사용할 때 한 번만 초기화되고 이후에 메서드 호출 시 프록시 객체가 참조하는 실제 객체의 진짜 메서드를 호출한다.
동일성 보장
프록시 객체는 실제 객체를 상속받은 객체이기 때문에 실제 객체와 클래스 타입이 다르다. 따라서 타입 체크 시 주의해야한다. 예를 들어, '==' 비교는 실패하기 때문에 instanceof를 사용하는 것이 좋다.
그러나 JPA가 어느정도 동일성을 보장해주는 데 몇 가지 case를 알아보자.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
- em.getReference() 뒤에 em.find()를 하면 동일성을 보장해주기 위해 프록시 객체를 반환한다. 프록시를 그대로 반환하되, 내부에서 프록시를 한번 초기화해준다고 이해하면 된다.
준영속
프록시 객체가 준영속 상태일 때 초기화를 하면 문제가 발생한다.
em.detach(entity), em.clear(), em.close()를 통해 준영속 상태로 만들면 이후 초기화를 할 때, LazyInitializationException이 발생한다.(하이버네이트 기준)
+) 추가로 하이버네이트 5.4.10 Final 버전부터 em.close()에 대해서는 트랜잭션이 살아있으면 완전히 닫히지 않는 읽기 가능 상태가 된다.
프록시 확인
프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity) 메서드를 사용해 프록시 인스턴스의 초기화 여부를 확인할 수 있다. 아직 초기화되지 않은 프록시 인스턴스는 false를 반환하고, 이미 초기화되었거나 프록시 인스턴스가 아니면 true를 반환한다.
프록시 클래스 확인 방법
조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하려면 entity.getClass() 메서드를 통해 클래스명을 직접 출력해보면 된다.
프록시 강제 초기화
하이버네이트의 org.hibernate.Hibernate.initialize(entity); 메서드를 사용하면 프록시를 강제로 초기화할 수 있다.
+) 참고: JPA 표준은 강제 초기화 없음 강제 호출: member.getName() -> 메서드 직접 호출
+) getReference를 실무에서 많이 쓰지는 않지만 프록시 개념을 이해해야 즉시, 지연 로딩을 이해할 수 있다.
즉시 로딩과 지연 로딩
멤버<->팀이 연관되어 있을 때, 멤버 엔티티를 조회할 때 팀 엔티티도 함께 조회하는 것이 좋을까? 그것은 상황마다 다를 것이다. JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 즉시 로딩과 지연 로딩 두 가지 방법을 제공한다.
지연 로딩
팀이 아닌 멤버만 사용하는 경우라면 지연 로딩이 유리하다.
지연 로딩을 사용하려면 @ManyToOne(fetch = FetchType.LAZY)와 같이 연관관계 애노테이션에 fetch 옵션을 LAZY로 설정한다.
멤버 클래스의 team fetch 타입을 LAZY로 설정했다면 그림과 같이 team 멤버 변수에 프록시 객체를 넣어둔다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다.
+) 다만, 조회 대상이 영속성 컨텍스트에 이미 있으면 프록시 객체를 사용할 이유가 없다. 따라서 지연 로딩이라 하더라도 프록시가 아닌 실제 객체를 사용한다.
즉시 로딩
팀과 멤버를 함께 사용하는 경우라면 즉시 로딩이 유리하다.
즉시 로딩을 사용하려면 @ManyToOne(fetch = FetchType.EAGER)와 같이 연관관계 애노테이션에 fetch 옵션을 EAGER로 설정한다.
멤버 클래스의 team fetch 타입을 EAGER로 설정했다면 그림과 같이 멤버를 조회하는 순간 팀도 함께 조회한다. JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.
+) 다만, 즉시로딩도 지연로딩과 같이 먼저 영속성 컨텍스트에서 찾고 없으면 데이터베이스에 쿼리를 남긴다.
실무에서는 가급적 지연로딩을 사용하는 것이 좋다. 즉시 로딩일 때, 데이터베이스 입장에서 테이블이 매우 많고 그에 따라 join이 너무 많아지면 조회할 때 부하가 심해진다. 또한 JPQL에서 문제가 생긴다. 다음 코드를 보자.
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
JPQL은 우선 쿼리 내용을 우선적으로 데이터베이스에 보낸다. 그래서 멤버를 select하는데, 이 때 team 멤버 변수에 fetch 타입이 즉시 로딩으로 되어 있다면 select 쿼리를 한번 더 보내 team을 가져온다. 이렇게 연관된 객체가 많아지면 그만큼 쿼리를 계속 추가로 보내게되는데 이것을 N+1 문제라고 한다.(쿼리가 하나 나가는데 추가 쿼리가 N개 나간다.)
차라리 필요한 객체에 따라서 fetch join을 사용하는 것이 더 났다.
따라서, 모든 연관관계를 LAZY로 하고 상황에 따라 fetch join하는 것을 추천한다.
+)
@ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
@OneToMany, @ManyToMany는 기본이 지연 로딩
영속성 전이(CASCADE)와 고아 객체
영속성 전이
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.
다음 예시를 보자.
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //영속성 전이
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
//Getter, Setter
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
위처럼 부모-자식 클래스가 1:N으로 매핑되어 있을 때, 부모를 영속성 컨텍스트에 저장한다면 자식들 역시 같이 저장할 수 있다.
CascadeType은 ALL, PERSIST, REMOVE 등이 있는데 부모에 대해 각각 (모두적용, 영속, 삭제)의 기능을 제공한다.
주의할 점은 영속성 전이는 자식이 부모(단일 엔티티)에 완전히 종속되었을 때만 사용해야 한다. 만약 자식이 다른 곳에서도 연관이 되어 있다면 cascade는 사용하면 안 된다.
고아 객체
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라 한다. 이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 할 수 있다.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
위 컬렉션에서 remove 메서드를 사용하는 등 자식 엔티티를 제거하면 제거된 엔티티는 자동으로 삭제한다. (delete 쿼리가 나간다.) 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 따라서 이 기능 역시 참조하는 곳이 하나일 때만 사용해야 한다.
고아 객체 제거에는 기능이 하나 더 있는데 부모를 제거하면 자식도 같이 제거된다. (CascadeType.REMOVE와 같다.)
+) 만약 cascade = CascadeType.ALL, orphanRemoval = true을 같이 사용하면 어떨까? 두 옵션을 모두 활성화하면 부모 엔티티를 통해 자식 엔티티의 생명주기를 관리할 수 있다.
-> 부모를 등록하면 자식도 같이 등록이 되고, 부모를 삭제하면 자식도 같이 삭제가 된다.
실전 예제 5 - 연관관계 관리
공부한 예제를 깃허브에 등록한다.
'java > jpa' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어1 - 기본 문법 (0) | 2021.02.03 |
---|---|
[JPA] 값 타입 (0) | 2021.02.02 |
[JPA] 고급 매핑 (0) | 2021.01.27 |
[JPA] 다양한 연관관계 매핑 (0) | 2021.01.27 |
[JPA] 연관관계 매핑 기초 (0) | 2021.01.26 |