코드 훔쳐보는 변태 코더
춤 좋아하는 백엔드 개발자(였으면 좋겠다)
JPA (1)
JPA - 프록시객체와 즉시로딩/지연로딩

프록시 ?

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.

예제 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 #개발자취업부트캠프 #내일배움카드
  Comments,     Trackbacks