빙응의 공부 블로그

[Spring]자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계 관리 본문

Spring/인프런_JPA

[Spring]자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계 관리

빙응이 2024. 2. 19. 16:56

📝프록시

프록시란? JPA에서는 em.getReference라는 메소드가 제공된다.

em.getReference는 데이터베이스 조회를 미루는 가짜 엔티티 객체(프록시)를 조회한다. 

 

이것을 사용하는 이유는 무엇일까?

객체처럼 조회로 인해 Member을 조회할때 Team도 조회해야한다.
이것은 성능적으로 좋지 않다.!

 

 

EntityManager em = // EntityManager 생성

// 프록시 획득
Member proxyMember = em.getReference(Member.class, 1L);

// 프록시 사용
System.out.println("프록시 클래스: " + proxyMember.getClass());

// 프록시를 통한 엔터티 접근
System.out.println("프록시를 통한 엔터티 접근 - ID: " + proxyMember.getId());
System.out.println("프록시를 통한 엔터티 접근 - Username: " + proxyMember.getUsername());

프록시의 특징

  • 실제 클래스를 상속받아 만들어진다.
  • 실제 클래스와 겉 모양이 같다.
  • 사용자 입장에서는 구분하지 않고 사용할 수 있게 JPA에서 지원해준다.
  • 프록시 객체는 실제 객체의 참조를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

위 그림처럼 프록시 객체가 호출 시 가진 타겟을 이용해 실제 객체를 호출한다.

 

🚩프록시 객체

실제 사용하는 방법에 대해 알아보자 

Member member = em.getReference(Member.class, “id1”);
member.getName();

프록시 객체는 처음 사용할 때 한 번만 초기화하며 다음과 같이 동작한다. 

 

프록시 객체의 특징은 다음과 같다. 

  • 프록시 객체는 사용할 때 한 번만 초기화한다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다, 초기화되면 프록시 객체를 통해 실제 엔티티를 접근한다.
  • 프록시 객체는 원본 엔티티를 상속받는다, 따라서 타입 체크시 주의해야 한다.(== 비교 대신, instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, em.getReference를 호출해도 프록시 객체가 아닌 실제 엔티티가 호출된다.
  • 준영속 상태에서 프록시를 초기화하면 문제가 발생한다. 

 

 

📝즉시 로딩과 지연 로딩

프록시처럼 객체 조회를 나중으로 미루는 것을 JPA에서는 간편히 지원한다. 

 

지연 로딩
@Entity
@Data
public class Member {
  @Id
  @GeneratedValue
  private Long id;
  @Column(name = "USERNAME")
  private String name;
  @ManyToOne(fetch = FetchType.LAZY) //**
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}
 @ManyToOne(fetch = FetchType.LAZY)

해당 문을 넣으면 이것은 지연 로딩 LAZY이며 프록시로 가져와 객체 생성을 늦춘다.

Team을 실제로 사용할 때 객체가 생성된다. 이렇게하면 성능 최적화가 가능하다.
즉시 로딩 

만약 항상 두 객체를 같이 쓴다면 굳이 지연로딩을 할 이유가 없다.

그러므로 즉시 로딩을 통해 한가지를 참조하면 나머지도 같이 조회될 수 있게 하면된다. 

@Entity
@Data
public class Member {
  @Id
  @GeneratedValue
  private Long id;
  @Column(name = "USERNAME")
  private String name;
  @ManyToOne(fetch = FetchType.EAGER) //**
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}
  @ManyToOne(fetch = FetchType.EAGER)

즉시로딩은 프록시를 사용하지 않는다. 어차피 프록시를 사용안하면 지연로딩이 안되기 때문이다.

 

즉시 로딩의 주의점!

즉시 로딩은 MEMBER 조회시 항상 Team도 조회한다. (조인)

그러므로 실무처럼 많은 테이블이 있으면 엄청난 쿼리가 나오기 때문에 안쓰는 것이 좋다.

  • 가급적 지연 로딩만 사용하자(특히 실무에서) -> 차라리 패치조인 사용
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본적으로 즉시 로딩 -> LAZY로 설정하자
  • @OneToMany, @ManyToMany는 기본이 지연로딩 

 

📝영속성 전이 : CASCADE

부모를 저장할 때 연관된 자식도 함께 저장하기 위해 사용한다.

즉 

 

특정 엔티티를 영속 상태로 만들 때 관련된 엔티티도 함께 영속상태로 만든다. 

 

다음 코드를 보자

@Entity
@Data
public class Parent {
  @Id
  @GeneratedValue
  private Long id;
  private String name;
  @OneToMany(mappedBy = "parent")
  private List<Child> childList = new ArrayList<>();
  public void addChild(Child child){
    childList.add(child);
    child.setParent(this);
  }
}
            Child child1 = new Child();
            Child child2 = new Child();
            
            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);
            
            em.persist(parent);
            em.persist(child1);
            em.persist(child2);

위 코드 처럼 부모는 기준으로 만들어서 저장을 하지만 자식들도 모두 저장해야 하는 번거로움이 있다. 

이러한 문제를 해결하는 것이 CASCADE이다. 

 

부모 엔티티 관계 애노테이션에 cascade 문을 추가하면 된다.

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  private List<Child> childList = new ArrayList<>();

이렇게 하면

            Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent);

이것처럼 부모만 저장해도 자식도 같이 저장된다. 

 

CASCADE 종류

  @OneToMany(mappedBy = "", cascade = CascadeType.ALL)

ALL : 모두 적용

  @OneToMany(mappedBy = "", cascade = CascadeType.PERSIST)

PERSIST : 영속 관계 적용

  @OneToMany(mappedBy = "", cascade = CascadeType.REMOVE)

REMOVE : 삭제 

 

 

주의사항:

  • 관리 범위 주의: Cascade를 사용할 때는 해당 엔티티 간의 관계와 생명주기를 잘 이해해야 한다. 특히, 부모가 자식을 모두 관리할 때만 사용하는 것이 좋다. 다른 관계가 있는 경우 예상치 못한 문제가 발생할 수 있기 때문이다.
  • 성능 고려: Cascade를 사용하면 연관된 모든 엔티티에 변경이 전파되므로 주의가 필요하다. 성능상의 이슈를 고려하여 적절한 Cascade 전략을 선택해야 한다.
    • 예를 들자면 CASCADE의 삭제기능은 다량의 데이터가 있을 시 전체 삭제를 못하고 다운될 수 있다.

🚩고아 객체

고아 객체란 부모 엔티티와 연관관계가 끊어진 자식 엔티티이다. 

 

orphanRemoval = true

해당 속성은 자식객체가 부모와 연관관계가 끊어지면 자동 삭제하는 기능이다.

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)

 

만약 부모가 자식 엔티티를 컬렉션에서 삭제하면 JPA에서 자동으로 SQL문을 이용해 삭제한다.

Parent parent1 = em.find(Parent.class, id); 
parent1.getChildren().remove(0);

 

주의점!  

  • CASCADE와 같이 참조하는 곳이 하나일 때만 사용해야한다. 자동으로 엔티티를 삭제하기 때문에 취급에 주의가 필요하다.
  • @OneToOne, @OneToMany만 가능하다. 
  • 특정 엔티티가 개인 소유할때만 사용하자!
  • 참고 : 개념적으로 부모를 제거하면 자식은 고아가 되어 자식도 모두 제거된다.

 

🚩영속성 전이 + 고아객체 

CascadeType.ALL + orphanRemoval=true를 같이 사용하면 부모 엔티티가 자식의 생명 주기를 관리할 수 있다.

도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다 .