API/JPA

프록시와 연관관계 관리

시뻘건 튼튼발자 2024. 8. 27. 00:06
반응형

1. 프록시

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아닐 것이다. 지금 내가 리딩하고 있는 토이프로젝트만 봐도 유저 엔티티에 피드 엔티티, 댓글 엔티티 등 많은 엔티티와 연관관계를 갖는데 항상 사용되지는 않는다. 유저의 정보를 조회할 때 이 사용되지 않는 엔티티까지 조회하는 것은 굉장한 부담이 되고 비효율적일 것이다.

JPA는 이런 문제를 해결하고자 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라고 한다. 이 지연 로딩을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이 것을 프록시 객체라 부른다.

1.1. 프록시 기초

[그림 1] 프록시 초기화

 

프록시는 JPA가 엔티티의 연관관계를 처리하는 핵심적인 기법 중 하나로, 실제 엔티티 객체를 대신해 데이터베이스 접근을 지연시킬 수 있는 가상의 객체다.

[그림 1]과 같이 프록시 객체(MemberProxy)의 메소드(getName())가 실행될 때 데이터베이스를 조회해서 실제 엔티티객체를 생성하는데 이것을 프록시 객체의 초기화라 부른다. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 엔티티 타겟(Member Target) 멤버 변수에 보관한다.

 

프록시의 특징은 아래와 같다.

  1. 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
  2. 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능하다. (참조)
  3. 프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크 시 주의해야 한다.
  4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환한다.
  5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생한다.

1.2. 프록시와 식별자

엔티티를 프록시로 조회할 때 식별자(Primary Key) 값을 파라미터로 전달하는 데 프록시 객체는 이 식별자 값을 보관한다. 그러므로 getId()를 호출해도 프록시를 초기화하지 않는다. 이와 관련된 어노테이션은 @Access를 살펴보자.

  • @Access(AccessType.PROPERTY): 초기화 되지 않는다.
  • @Access(AccessType.FIELD): 초기화한다.
Member member = em.getReference(Member.class, memberId); // 프록시 객체 반환
System.out.println("Member ID: " + member.getId()); // 프록시 객체로도 ID 접근 가능
System.out.println("Member Name: " + member.getName()); // 이 시점에서 초기화

1.3. 프록시 확인

프록시 객체는 실제 엔티티와 구분하기 어렵다. 따라서 JPA에서는 프록시 객체인지 실제 객체인지 확인할 수 있는 여러 방법을 제공한다.

Member member = em.getReference(Member.class, memberId);
System.out.println("Proxy? " + (member instanceof HibernateProxy));
System.out.println("Initialized? " + Hibernate.isInitialized(member));

2. 즉시 로딩과 지연 로딩

2.1. 즉시 로딩

즉시 로딩(Eager Loading)은 엔티티가 조회될 때 관련된 연관 엔티티를 즉시 함께 조회하는 방식이다. 즉시 로딩은 성능상 오버헤드가 있을 수 있으며, 연관된 엔티티가 많을수록 더 많은 쿼리가 발생하거나 복잡한 조인이 필요할 수 있다. 이로 인해 과도한 쿼리 발생이나 N+1 문제를 초래할 수 있으므로 주의가 필요하다. @ManyToOne(fetch = FetchType.EAGER)와 같이 설정할 수 있다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}

// 사용 코드
Member member = em.find(Member.class, memberId);
Team team = member.getTeam(); // 이 시점에서 team이 이미 로딩됨

2.2. NULL 제약조건과 JPA 조인 전략

JPA는 선택적 관계면 외부 조인을 사용하고 필수 관계면 내부 조인을 사용한다. 엔티티 간의 연관관계를 매핑할 때, 제약조건을 잘 보고 내부 조인을 사용할지 외부 조인을 사용할지 판단하여 사용해야한다. 아래 예제는 필수 관계로 설정하여 내부 조인을 사용할 수있도록하는 코드다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "team_id", nullable = false) // 외래 키 컬럼에 NULL 값 허용하지 않음
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    private String name;
}
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    private String name;
}

2.3. 지연 로딩

지연 로딩(Lazy Loading)은 엔티티가 실제로 필요할 때까지 연관된 데이터를 로딩하지 않는 방식이다. 지연 로딩은 프록시를 활용하여 필요한 시점에 데이터베이스를 조회함으로써 성능을 최적화할 수 있다. (@OneToMany(fetch = FetchType.LAZY)와 같이 설정할 수 있다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}

// 사용 코드
Member member = em.find(Member.class, memberId);
Team team = member.getTeam(); // 이 시점에서 team은 로딩되지 않음
String teamName = team.getName(); // 이 시점에서 team이 로딩됨

3. 영속성 전이 - CASCADE

3.1. 영속성 전이: 저장

영속성 전이(CASCADE)는 엔티티가 저장될 때 연관된 엔티티도 함께 저장되도록 하는 기능이다. JPA는 CascadeType.PERSIST를 통해 부모 엔티티를 저장할 때 자식 엔티티도 자동으로 저장되도록 지원한다.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST) // 저장 시 영속성 전이
    private List<Child> children = new ArrayList<>();
}

@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

// 사용 코드
Parent parent = new Parent();
parent.setName("Parent 1");

Child child1 = new Child();
child1.setName("Child 1");
child1.setParent(parent);

Child child2 = new Child();
child2.setName("Child 2");
child2.setParent(parent);

parent.getChildren().add(child1);
parent.getChildren().add(child2);

em.persist(parent); // parent와 함께 child1, child2도 저장됨

3.2. 영속성 전이: 삭제

영속성 전이는 저장뿐만 아니라 삭제에도 적용될 수 있다. CascadeType.REMOVE를 설정하면 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 자동으로 삭제된다. 이는 연관된 데이터의 일관성을 유지하는 데 매우 유용하다. 그러나 잘못된 사용은 데이터 손실로 이어질 수 있으므로 주의가 필요하다.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) // 삭제 시 영속성 전이
    private List<Child> children = new ArrayList<>();
}

// 사용 코드
Parent parent = em.find(Parent.class, parentId);
em.remove(parent); // parent와 함께 children도 삭제됨

3.3. CASCADE의 종류

JPA에서는 다양한 CASCADE 유형을 제공한다.

 

  • CascadeType.PERSIST: 저장
  • CascadeType.MERGE: 병합
  • CascadeType.REMOVE: 삭제
  • CascadeType.REFRESH
  • CascadeType.DETACH
  • CascadeType.ALL: 모두 적용
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.MERGE) // 부모 엔티티 병합 시 자식도 병합됨
    private List<Child> children = new ArrayList<>();
}

// 사용 코드
Parent parent = em.find(Parent.class, parentId);
parent.setName("Updated Parent Name");

for (Child child : parent.getChildren()) {
    child.setName("Updated Child Name");
}

em.merge(parent); // parent와 함께 자식 엔티티도 병합됨
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.REFRESH) // 부모 엔티티 새로고침 시 자식도 새로고침됨
    private List<Child> children = new ArrayList<>();
}

// 사용 코드
Parent parent = em.find(Parent.class, parentId);
parent.setName("Changed Name");

em.refresh(parent); // 변경된 엔티티가 데이터베이스의 최신 상태로 업데이트됨

 

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.DETACH) // 부모 엔티티 분리 시 자식도 분리됨
    private List<Child> children = new ArrayList<>();
}

// 사용 코드
Parent parent = em.find(Parent.class, parentId);
em.detach(parent); // parent와 함께 자식 엔티티도 분리됨
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // 모든 CASCADE 작업이 자식 엔티티에 전파됨
    private List<Child> children = new ArrayList<>();
}

// 사용 코드
Parent parent = new Parent();
parent.setName("Parent 1");

Child child1 = new Child();
child1.setName("Child 1");
child1.setParent(parent);

Child child2 = new Child();
child2.setName("Child 2");
child2.setParent(parent);

parent.getChildren().add(child1);
parent.getChildren().add(child2);

em.persist(parent); // 저장
em.merge(parent); // 병합
em.remove(parent); // 삭제
em.refresh(parent); // 새로고침
em.detach(parent); // 분리

 


5. 고아 객체

고아 객체(Orphan Removal)는 부모 엔티티와의 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다. @OneToMany(orphanRemoval = true)로 설정할 수 있다.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", orphanRemoval = true) // 고아 객체 제거
    private List<Child> children = new ArrayList<>();
}

// 사용 코드
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(0); // 연관관계가 끊어진 자식 엔티티가 자동으로 삭제됨

 

반응형