프록시 ?
엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.
예제 8.3 회원과 팀 정보를 출력하는 비즈니스 로직
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());
}
-알라딘 eBook <자바 ORM 표준 JPA 프로그래밍> (김영한 지음) 중에서
해당 코드에서는 멤버 아이디로 엔티티를 호출하면 member 엔티티와 연관된 team 엔티티까지 같이 가져온다.
하지만 이 코드에서 팀에대한 출력은 없이 회원 정보만 출력한다면 ? ⇒ 팀 데이터베이스까지 함께 조회해버리니 효율적이지 않다.
해당 문제를 해결하기 위해 엔티티가 실제 사용될 때 까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연로딩 이라고 한다. → 말그대로 해당 코드에서 member.getTeam() 메소드가 호출되지 않는다면 팀에대한 데이터베이스 조회는 하지 않는다.
지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.
‘Proxy. 대리(행위)나 대리권, 대리 투표, 대리인 등 을 뜻한다.’
프록시 기초 🤔
엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 getReference() 메소드를 사용하면 된다.
해당 메소드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 안히는다. 대신 접근을 위임한 프록시 객체를 반환한다.
- 프록시의 특징
- 프록시는 실제 클래스와 겉 모양이 같다. 사용하는 입장에서는 구분하지 않고 사용하면 된다.
- 프록시 객체는 실체 객체에 대한 참조를 보관한다. 그리고 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
- 객체는 처음 사용할 때 한 번만 초기화된다.
- 객체를 초기화 한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 실제 엔티티에 접근 할 수 있는것
- 원본 엔티티를 상속받은 객체이므로 타입 체크시에 주의해야한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면 DB를 조회하지 않고 실제 엔티티를 반환한다.
- 준영속 상태의 프록시를 초기화 하면 문제가 발생한다.
- 프록시 객체의 초기화
- 프록시 객체는 실제로 사용될 때 데이터베이스를 조회해 실제 엔티티 객체를 생성하는데, 이것을 프록시 객체의 초기화 라고 한다. ⇒ 사용될때 실제 객체를 생성하기 때문
- 메소드 호출→ 초기화 요청→ 영속성 컨텍스트에 엔티티 생성 요청 → DB조회→ 영속성 컨텍스트가 DB를 조회해서 실제 엔티티 생성 / 해당 순서로 프록시 객체의 초기화가 이뤄진다.
- 프록시와 식별자
- 프록시 객체는 식별자 값을 보관하기 때문에 식별자 값을 조회하는 메소드를 호출해도 프록시를 초기화하지 않는다.
- 프록시는 연관관계를 설정할 때 유용하게 사용할 수 있다. → 식별자 값만 사용하기 때문에 데이터베이스 접근 횟수를 줄일 수 있다.
- 프록시 확인
- JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 인스턴스의 초기화 여부를 확인할 수 있다. ⇒ 인스턴스의 초기화 여부가 분명해야 하는 경우가 있을까 ? 🤔
- 극각의 성능을 내려면 초기화 여부까지 따져가며 데이터베이스 조회 횟수를 줄이는 방법도 있을거같다.. 🤔
즉시 로딩과 지연 로딩 🤔
JPA는 개발자가 조회 시점을 선택할 수 있도록 두가지 방법을 제공한다.
- 즉시 로딩
- 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 설정방법 : @ManyToOne (fetch = FetchType.EAGER)
- 즉시 로딩을 최적화 하기 위해서 조인 쿼리를 사용하기도 한다. → 조인쿼리를 사용하면 쿼리 한번으로 두 엔티티를 모두 조회한다. (외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는것을 보장하기 때문에 내부 조인을 사용할 수 있다. nullable=false 를 설정하면 기본으로 설정되있는 외부조인 대신에 내부조인을 사용한다.)
- 선택적 관계 → 외부 조인, 필수 관계 → 내부 조인
- 지연 로딩
- 연관 엔티티를 실제 사용하는 시점에 DB를 조회한다.
- 설정 방법 : @ManyToOne (fetch = FetchType.LAZY)
- 정리
- 대부분의 애플리케이션 로직에서 연관관계가 맺어져있는 엔티티를 같이 사용한다면 join 을 이용해서 한번에 조회하는것이 더 효율적이다.
지연 로딩 활용 🤔
지연 로딩을 어떻게 활용하면 좋을까? 본인이 기존에 진행하던 프로젝트로 예를 들어보았다.
@Entity
public class Article extends AuditingFields{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 프라이머리 키
private Long id;
//@setter 가 붙은 값이 입력값, 없으면 자동
@Setter @Column(nullable = false) String title; //n
// ull 이 아닌 값을 컬럼에 저장 함
@Setter
@JoinColumn(name = "userId")
@ManyToOne(optional = false,fetch = FetchType.EAGER)
private UserAccount userAccount; // 유저 정보 (ID)
@Setter @Column(nullable = false,length = 10000) private String content;
@OrderBy("createdAt") //id 순서
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL, fetch = FetchType.LAZY) //양방향 관계 (article이 주체)
@ToString.Exclude //과부하 발생 예방
private final Set<ArticleComment> articleComments = new LinkedHashSet<>();
@OneToMany(mappedBy = "article")
@ToString.Exclude
@Setter
private Set<ArticleHashtag> hashtags = new HashSet<>();
해당 테이블에서 연관관계는 UserAccount ,ArticleComment 와 ArticleHashtag 에 맺어져있다.
해시태그 엔티티는 단독으로 관리되지 않기 때문에 Article 엔티티에 연관관계를 직접 맺어주었고, 마찬가지로 게시글을 작성하거나 댓글을 작성할때 무조건 계정 정보가 들어가고, 댓글 또한 단독으로 관리되지 않기때문에 맺어주었다.
- UserAccount
- 게시글이 불러와질때마다 단순히 게시글에 nickname 컬럼이 생성되어있거나 하지 않고 외래 키로 UserAccount 엔티티의 정보를 바로 참조해 출력하기 때문에 EAGER 로 설정해주었다.
- ArticleComment
- 댓글은 단독으로 관리되지 않지만, 게시글과 댓글을 한번에 가져오는게 아닌 따로 비동기 통신으로 댓글을 관리한다. 하지만 연관관계는 맺을 수 밖에 없는 상황이기 때문에 키만 참조하고 한번에 조회할 필요가 없기때문에 LAZY로 설정해주었다.
- ArticleHashtag
- ArticleHashtag 는 중간 테이블 엔티티가 따로 존재한다. 해당 부분에 지연로딩 즉시로딩을 명시해줬는데, Articled은 LAZY, Hashtag도 LAZY를 명시해주었다.
- 어차피 게시글을 조회하면 영속성 컨텍스트에서 관리되기때문에 db를 조회해오지 않는다.
- hashtag 를 즉시로딩 해버리면 해당 해시태그가 등록된 게시글까지 다 불러올것 같다는 생각에 LAZY를 주었다.
이런식으로 각각 엔티티를 조회하는 부분이 달라서 데이터베이스가 무조건 따로 조회되는 경우 말고는 조회 횟수를 신경써가며 즉시로딩, 지연로딩을 활용해주면 좋다.
프록시와 컬렉션 래퍼 🤔
- 하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션이 있으면 해당 컬렉션을 추척하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데, 이것을 컬렉션 래퍼라고 한다.
- 컬렉션을 조회하는 메소드를 호출해도 컬렉션은 초기화 되지 않고, 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화한다.
JPA 기본 Fetch 전략 🤔
- fetch 속성의 기본 설정값은 다음과 같다.
- @ManyToOne, @OneToOne : 즉시로딩
- @OneToMany, @ManyToMany : 지연로딩
- 추천되는 방법은 모든 연관관계에 지연 로딩을 사용하는것이다.
- 위에 적용한 로딩 방식을 죄다 지연로딩으로 변경해야겠다. (ㅡ,.ㅡ)
- 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하면 된다.
영속성 전이 : CASCADE 🤔
- 연관된 엔티티도 함께 영속 상태로 만들고 싶다면 영속성 전이 기능을 사용하면 된다.
- 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.
- cascade = CascadeType.PERSIST 옵션을 적용하면 부모와 자식 엔티티를 한 번에 영속화 할 수 있다.
- 영속성 전이는 엔티티를 삭제할때도 부모엔티티만 삭제했을때 자식 엔티티까지 함께 삭제해준다.
고아 객체 🤔
- JPA는 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라고 한다.
- 고아 객체 제거는 참조가 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
- orphanRemoval 은 @OneToOne, @OneToMany.에만 사용할 수 있다. (삭제한 엔티티를 다른곳에서도 참조하면 문제가 생기기 때문이다.)
마무리
느낀점 🤔
- 그동안 fetch = FetchType.LAZY 이게 뭔지 전혀 모르고 사용했었는데 지연로딩이라는걸 드디어 알게되었습니다….
- 중간에 즉시 로딩과 지연로딩을 각각 맞는 상황에 명시하는걸 보고 제 프로젝트에 적용해보았으나 결국엔 확실한 상황 말고는 지연로딩이 좋다는걸 뒤늦게 보고 말았습니다.
- 지연 로딩을 지원한다는게 결국에는 한번 데이터를 가져올때 최대한 알짜배기만 골라온다는 느낌이라고 단번에 이해가 되었습니다.
- 양방향 관계에 있어서 영속성 전이를 관리할 수 있으면 좋을텐데, 아직까지는 완전히 이해가 되지는 않은 느낌입니다.
- 즉시로딩과 지연로딩을 공부한 후 중간 엔티티가 각 엔티티의 부모 엔티티라고 봐야하는것인지.. 헷갈렸습니다.
- 생각해보니 제 프로젝트에서는 중간 엔티티를 삭제해버리면 각각의 엔티티도 삭제되도록 되있던거 같은데, 해당 부분을 다시 생각하고 구조를 바꿔도 될 것 같습니다 😀
- 프록시 객체에 대한 개념을 여기서 완벽히 이해한것 같습니다.
- 여기서 한번 더 getReferenceById 와 findById 의 차이를 짚고 넘어가야 할 것 같습니다.
- #패스트캠퍼스 #국비지원교육 #메가바이트스쿨 #MegabyteSchool #개발자취업부트캠프 #내일배움카드