*김영한님의 JPA 기본 강의를 기반으로 작성하였습니다.
프록시: 실제 엔티티가 아닌 가짜 클래스. 실제 클래스를 상속받아 만들어진다. 껍데기는 똑같지만, id, name 등 기본정보만 보유하고 있다. 프록시 객체는 실제 객체의 참조를 보관하고, 프록시 객체 호출 시 프록시 객체는 실제 객체의 메소드 호출. ex) 프록시에서 A 메소드를 호출 -> 실제 객체의 A메소드 호출
member를 조회하는 상황에서 꼭 Team도 불려야 할까? (즉시 로딩)
필요할 때 불러 쓰자( 지연 로딩 )
member 생성시점에서는 member만 select하고, team은 select 하지 않는다.
member.getTeam() 등으로 team이 필요한 시점에 DB에 쿼리를 넣어서 가져온다.
이걸 프록시로 처리할 수있다.
member 생성시, 쓰이지 않는 Team에 대한 가짜 객체( 프록시 ) 를 생성하여 리턴하고, 실제 사용 시점에서 Team의 프록시에서 진짜 객체를 불러오는 작업을 수행하므로써 적절한 시점에 불러오는게 가능해진다. (프록시가 진짜 객체로 교체되는 것이 아니다, Target이 채워지는 것)
em.find() : 데이터 베이스에서 실제 엔티티 객체 조회
em.getReference(): 데이터베이스 조회를 미루는 가짜 프록시 엔티티 객체 조회
member에 getReference로 조회한 가짜 프록시 클래스를 넣고, getName() 이라는 실제 메소드 호출 시, member Target을 확인한다. 이때, 아직 Target(실제 객체) 가 없기에 영속성 컨텍스트에 실제 객체를 요청하고, 영속성 컨텍스트가 DB를 조회하여 실제 엔티티 객체를 Target에 연결해준다.
아직 실제 객체와 연결되지 않은 프록시가 영속성 컨텍스트에게 요청하여, 실제 객체를 가져오는 작업을 '프록시 객체의 초기화' 라고 한다.
(영속성 컨텍스트에서 프록시 객체를 통해 실제 객체를 가져오는 작업)
프록시의 특징
- 프록시객체는 처음 사용할 때 한번만 초기화
- 프록시 객체를 초기화할때,프록시 객체가 실제 엔티티로 바뀌는 것은 아님,초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림 -> 엄청 자주나오는 에러! 기억하자)
- 프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크시 주의해야함 (== 비교는 불가, instance of 사용해야한다. JPA에서는 em.find(), em.Reference() 를 통해 얻은 실제 엔티티, 프록시 객체간에 == 비교가 true가 나오게 맞추려고 노력한다. 따라서 실제 엔티티가 넘어올지, 프록시로 넘어올지 알 수 없기에 instanceof로 비교해야 정확한 타입 체크가 가능하다.)member1 instanceof Member 이런식으로 사용
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환 ( 1차 캐시에 있기에 굳이 프록시 반환할 필요없다. + 영속성 컨텍스트에서 각 엔티티를 구분하는 식별자가 같기에, 영속성 컨텍스트에 있다고 판단하고 반환한다.반대로 프록시가 영속성 컨텍스트에 있을 때, 실제 엔티티를 조회해도 프록시가 반환된다. 이는 JPA가 프록시와 실제 엔티티간의 == 연산이 true가 되게 유도하기 때문)
따라서, 타입 체크에서 instanceof를 사용하고, 프록시 객체가 들어오더라도 실제 엔티티처럼 사용가능하도록 구현하는 것이 중요하다.
프록시 확인
프록시 인스턴스의 초기화 여부 확인
emf.PersistenceUnitUtil.isLoaded(Object entity)
프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist.. or HibernateProxy...) sout으로 찍어서 proxy인지 확인.
프록시 강제 초기화
Hibernate.initialize(entity); (org.hibernate.): getName()등 실제 요소 호출하여 강제 초기화가능하지만, readable 코드를 위해 사용.
• 참고: JPA 표준은 강제 초기화 없음 강제 호출: member.getName()
[즉시 로딩과 지연 로딩]
@ManyToOne(fetch = FetchType.LAZY)//프록시 객체로 조회
@JoinColumn(name="TEAM_ID")
private Team team;
지연 로딩: 객체를 프록시로 조회해서, 실제 사용될 때 (getTeam()에서도 가져오지 않음. Team내의 attribute를 가져올 때) 가져오게 한다.
즉시 로딩
Team team= member.getTeam() // 프록시 초기화 발생 x
team.getName() // 프록시 초기화 발생
만약 member와 Team이 대부분 같이 불려진다면? 즉시 로딩을 사용한다.
즉시 로딩: 객체 조회시, 연관 객체를 한번에 join해서 전부 가져온다. (프록시 사용 x) -> 하지만 실무에선 거의 사용하지 않는다.
즉시 로딩을 적용시, 예상치 못한 SQL이 발생한다.( 연관된 모든 엔티티를 가져오기에)
(테이블 수가 많아지고 관계가 많아질 때, member 하나 호출했는데 연관된 모든 테이블이 가져와져서 직관성이 안좋아진다.)
FetchType.Eager 으로 사용.
@ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 따로 설정해줘야한다.
@OneToMany, @ManyToMany는 기본이 지연 로딩 ( XToOne)
실무에서 만약 member, Team이 같이 필요하면?: Lazy로 설정해 놓고cascade=ALL, fetch join, 엔티티 그래프 기능을 통해 같이 가져온다.fetch join: ORM 프레임워크에서 엔티티와 관련된 컬렉션을 로드하는데 사용. (JOIN은 SQL에서 사용)
그러나, 자식 엔티티가 다른 엔티티와도 연관된 경우에는 부작용(side effect)을 유발할 수 있습니다. 이러한 문제를 해결하기 위해, 엔티티 그래프를 사용할 수 있다.
엔티티 그래프:
JPA(Entity Manager) 엔티티 그래프 기능은 객체 그래프를 통해 엔티티들 간의 연관관계를 미리 정의하고
이를 사용하여 데이터베이스에서 필요한 데이터를 한 번에 가져오는 기능.
일반적으로 JPA에서 엔티티를 로딩할 때, 기본적으로 지연로딩(lazy loading) 방식을 사용합니다. 이 때문에 하나의 엔티티를 조회할 때, 연관된 엔티티들을 여러 번에 걸쳐서 조회하게 되고, 이로 인해 성능이 저하될 수 있자.
하지만 엔티티 그래프 기능을 사용하면 연관된 엔티티들을 미리 정의하고, 그래프를 통해 한 번에 모든 연관된 엔티티를 로딩할 수 있다. 이를 통해 데이터베이스에서 필요한 모든 데이터를 한 번에 조회할 수 있으므로, 성능이 향상된다.
예를 들어, 주문(Order) 엔티티와 주문 상품(OrderItem) 엔티티가 있고, Order와 OrderItem은 일대다 관계를 갖습니다. 이 때, 엔티티 그래프를 사용하여 Order와 함께 OrderItem을 로딩할 수 있습니다.
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems;
// ...
}
@Entity
public class OrderItem {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
// ...
}
위와 같은 엔티티 관계가 있다면
@EntityGraph(attributePaths = "orderItems")
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems;
// ...
}
위의 코드에서 @EntityGraph 애노테이션을 사용하여 orderItems 속성을 정의한 엔티티 그래프를 정의했습니다. 그리고 이를 사용하여 다음과 같이 주문을 로딩할 수 있습니다. (주인 x 엔티티에서 주인 객체를 표기)
@EntityManager em = ...;
EntityGraph<?> graph = em.getEntityGraph("graph.Order.orderItems");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, 1L, hints);
위 코드에서 em.getEntityGraph("graph.Order.orderItems")를 사용하여 정의한 그래프를 가져오고, 이를 javax.persistence.fetchgraph 힌트 값으로 사용하여 em.find() 메서드를 호출합니다. 이렇게 함으로써 Order와 함께 연관된 OrderItem을 로딩할 수 있습니다.
![](https://blog.kakaocdn.net/dn/xEZe6/btr21BXKGdZ/AIH1uFgi64gxFJYDigfTtk/img.png)
(위의 사용은 "이론적" 사항, 실무에선 지연 로딩으로 통일하고 그때 그때 fetch join, 엔티티 그래프로 같이 불러오는 식으로 구현해야한다)
CaseCade: 영속성 전이.
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 사용한다.
ex) 부모가 영속상태가 되면, 자식도 영속시키고 싶을 때자식 엔티티의 해당 필드에 아래와 같은 어노테이션 추가
@OneToMany (mappedBy="parent", cascade=CascadeType. PERSIST)
ALL: 모두 적용 (전체적으로 라이프 사이클을 맞춰야할 때)
PERSIST: 영속 (저장할 때만 라이프 사이클 맞추면 될 때)
REMOVE: 삭제
MERGE: 병합
REFRESH: REFRESH
DETACH: DETACH
게시판-첨부파일 경로 같이 1개의 부모 엔티티가 자식들을 관리할 시(단일 엔티티에 종속적일 때는 라이프 사이클이 유사하게됨)
유용하게 쓰인다.
하지만 만약 어떤 엔티티가 여러 부모와 연관관계 가질때는, 함부로 사용하면 안된다.
고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티 를 자동으로 삭제
orphanRemoval = true
부모 엔티티가 삭제되는 등, 만약 자식엔티티가 부모 엔티티와 연관관계가 끊어졌다면 해당 자식 엔티티를 제거한다.
부모: 자식 1 대 다 관계를 가지고 있는 상황에서, 만약 아래와 같이 설정을 부여하면 연관관계가 끊긴 child를 list에서 삭제한다.
![](https://blog.kakaocdn.net/dn/ecLNEG/btr291nGVaA/8ZDyD2UabXIst6bJZVvGIk/img.png)
정리하면 Casecade 옵션으로 영속 주기 관리, orphanRemoval로 종료 주기를 관리할 수 있다.
이 두가지 옵션을 통해 자식 엔티티의 생명주기를 제어할 수 있다.
CascadeType.ALL + orphanRemoval=true
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
-> Parent가 생명주기를 관리하기에, DAO나 리포지토리 구현이 없어도 가능하다.
도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
'Spring boot' 카테고리의 다른 글
[JPA] Part 5 (객채지향 쿼리 언어 pt.1) (0) | 2023.03.13 |
---|---|
[JPA] Part 4 (값 타입) (0) | 2023.03.10 |
[JPA] Part2 (심화 매핑) (0) | 2023.03.10 |
[JPA 기본] Part1 (영속성 컨텍스트, 기본 매핑 ) (0) | 2023.03.08 |
API 서버 구성과 JWT를 통한 인증, 인가 (1) | 2023.01.30 |