티스토리 뷰

프록시와 연관관계 관리

지연 로딩

@Entity
@Getter
public class Member {
	private String username;

	@ManyToOne
	private Team team;
}

@Entity
@Getter
public class Team {
	private String name;
}

// 회원 엔티티를 조회시 회원과 연관된 팀 엔티티도 조회 후 출력
public void printUserAndTeam(String memberId) {
	Member member = em.find(Member.class, memberId);
	Team team = member.getTeam();
}

// 회원 엔티티만 사용하므로 팀 엔티티까지 데이터베이스에서 함께 조회하는 것은 효율적이지 않음
public String printUser(String memberId) {
	Member member = em.find(Member.class, memberId);
}

엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법 제공

프록시?

  • 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체

 

프록시 기초

EntityManager.find()

Member member = em.find(Member.class, "member1");

식별자로 엔티티 하나 조회시 사용하며 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회

EntityManager.getReference();

Member member = em.getReference(Member.class, "member1");

엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶을때 사용

→ 실제 엔티티 객체도 생성하지 않고 대신 데이터베이스 접근을 위임한 프록시 객체를 반환

 

특징

프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다.

프록시 객체는 실제 객체에 대한 참조(target)을 보관 한다.

그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

 

프록시 객체의 초기화

실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체 생성

//MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
member.getName(); //1. 프록시 객체에 메소드 호출해서 실제 데이터 조회
class MemberProxy extends Member {
	Member target = null; //실제 엔티티 참조

	public String getName() {
		if(target == null) {
			//2. 초기화 요청 == 영속성 컨텍스트에 실제 엔티티 생성 요청
			//3. DB 조회 -> 실제 엔티티 객체 생성 
			//4. 실제 엔티티 생성 및 참조를 Member target 멤버변수에 보관 
		}

		// 5. 엔티티 반환 
	}
}

특징

  • 프록시 객체는 처음 사용시 한번만 초기화
  • 프록시 객체를 초기화 한다고 실제 엔티티로 바뀌는 건 아니고 프록시 객체를 통해 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 주의
  • 영속성 컨텍스트에 찾는 엔티티가 있으면 데이터베이스를 조회 할 필요가 없으므로 em.getReference() 호출시 프록시가 아닌 실제 엔티티 반환
  • 초기화는 영속성 컨텍스트의 도움을 받아야함
    • 준영속 상태의 프록시 초기화시 LazyInitializationException 발생

 

프록시와 식별자

Team team = em.getReference(Team.class, "team1"); // 식별자 보관
team.getId(); // 식별자 조회

엔티티는 프록시로 조회시 식별자(PK)값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관함

엔티티 접근 방식에 따른 차이

@Access(AccessType.PROPERTY)

  • 필드 접근 시에 프록지 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회해도 프록시를 초기화 하지 않음

@Access(AccessType.FIELD)

  • getter 접근시 JPA는 getId()가 id만 조회하는지 다른 필드까지 활용하는지 모름으로 객체를 초기화함
  • 연관관계 설정시에는 프록시를 초기화 하지 않음

연관관계 설정시 활용

Member member = em.find(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); //SQL을 실행하지 않음
member.setTeam(team);

연관관계 설정시 식별자 값만 사용 → 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있음

 

프록시 확인

boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);

true : 초기화 됨, 프록시 인스턴스가 아님

false : 초기화 되지 않은 프록시 인스턴스

member.getClass().getName()
  • 클래스명 출력시 뒤에 ..javasist..라고 되어 있으면 프록시

 

즉시 로딩과 지연 로딩

즉시 로딩 : @ManyToOne(fetch = FetchType.EAGER)

엔티티를 조회시 연관된 엔티티도 함께 조회

@Entity
public class Member {
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	//... 
}
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색

대부분의 JPA 구현체는 즉시 로딩을 최적화 하기 위해 가능하면 조인 쿼리 사용함

select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdBy as createdB2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.lastModifiedBy as lastModi4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        member0_.team_TEAM_ID as team_TEA7_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.createdBy as createdB2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.lastModifiedBy as lastModi4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.team_TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
m = class hellojpa.Team
[출처] [JPA] 프록시와 연관관계 관리 : 즉시 로딩과 지연 로딩|작성자 조정현

→ 쿼리 조회 이후 member.getTeam() 호출시 이미 로딩된 팀1 엔티티를 반환함

😲 외부 조인을 사용했다?!

  • 현재 회원 테이블의 TEAM_ID 외래키는 NULL값을 허용
    • 내부 조인시 소속되지 않은 회원이 있다면 팀은 물론이고 회원 데이터도 조회 안됨
  • 외부 조인보다 내부 조인이 성능과 최적화에 더 유리 ⇒ 외래키에 NOT NULL 제약 조건 설정
    1. @JoinColum(nullable = false)
    @Entity
    public class Member {
    	@ManyToOne(fetch = FetchType.EAGER)
    	@JoinColumn(name = "TEAM_ID", nullable = false)
    	private Team team;
    }
    
    1. @ManyToOne.optional = false
    @Entity
    public class Member {
    	@ManyToOne(fetch = FetchType.EAGER, optional = false)
    	@JoinColumn(name = "TEAM_ID")
    	private Team team;
    }
    

Inner Join

두 테이블에서 일치하는 로우만 반환

Outer Join

1번 테이블의 모든 로우와 일치하는 2번 테이블의 로우가 포함

2번 테이블의 값과 일치하지 않는 1번 테이블의 로우는 2번 테이블의 컬럼에 NULL 반환

지연 로딩 : @ManyToOne(fetch = FetchType.LAZY)

연관된 엔티티를 실제 사용하는 시점에서 JPA가 SQL을 호출해서 조회

@Entity
public class Member {
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	//... 
}
Member member = em.find(Member.class, "member1"); //(1)
Team team = member.getTeam(); // 객체 그래프 탐색을 통해 프록시 객체를 team 멤버 변수에 넣음
team.getName(); // 팀 객체 실제 사용 (2)

프록시 객체를 언제 사용하는지

(1)

SELECT * FROM MEMBER WHERE MEMBER_ID = 'member1'

(2) 단, 이미 조회 대상이 영속성 컨텍스트에 있으면 프록시 객체가 아닌 실제 조회 대상 엔티티 사용

SELECT * FROM TEAM WHERE TEAM_ID = 'team1' 

컬렉션 래퍼

하이버네이트는 엔티티를 영속 상태로 만들때 엔티티에 컬렉션이 있으면 이를 추적, 관리 할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경

  • 컬렉션 래퍼는 컬렉션에 대한 프록시 역할을 하며 지연 로딩 처리를 해줌
  • 컬렉션에서 실제 데이터 조회시 데이터베이스를 조회해서 초기화 함, 이때 연관된 엔티티도 로딩
Member member = em.find(Member.class, "member1"); List<Order> orders = member.getOrders(); // 컬렉션 초기화 되지 않음 
member.getOrders().get(0); // 이때, 초기화

컬렉션에 FetchType.EAGER 사용시 주의

  1. 컬렉션을 하나 이상 즉시 로딩 하는 것은 권장하지 않음
    1. 컬렉션과 조인 == 데이터베이스 테이블로 보면 일대다 조인
    2. 결과가 다 쪽에 있는 수만큼 증가하게 됨
    3. A 테이블을 N, M 두 테이블과 일대다 조인하면 실행결과가 N * M 이 되고 JPA는 조회된 결과를 메모리에서 필터링해서 반환
  2. 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다
    1. JPA는 일대다 관계를 즉시 로딩시 항상 외부 조인

@ManyToOne, @OneToOne

  • optional = false : 내부조인 / true : 외부조인

@OneToMany, @ManyToMany

  • optional = false : 외부조인 / true : 외부조인

 

JPA 기본 패치 전략

연관된 엔티티가 하나면 즉시 로딩, 컬렉션이면 지연 로딩

(사유 : 컬렉션을 로딩하는 것은 비용이 많이 들고 잘못하면 너무 많은 데이터를 로딩할 수 있음)

@ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)

@OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)

모든 연관관계에 지연 로딩을 사용하고 개발 완료 단계에서 꼭 필요한 곳만 즉시 로딩하자

 

영속성 전이 : CASCADE

특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속 상태로 만들고 싶을때 사용

JPA는 CASCADE 옵션 제공

연관 관계를 매핑하는 것과는 어떠한 관계도 없음

부모 엔티티를 저장 할 때 자식 엔티티도 함께 저장 가능

@Entity
public class Parent {
	@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
	private List<Child> children = new ArrayList<Child>();
}
private static void saveWithCascade(EntityManger em) {
    
    Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    child1.setParent(parent);         // 연관관계 추가
    child2.setParent(parent);         // 연관관계 추가
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);
    
    // 부모 저장, 연관된 자식들 저장
    em.persist(parent);
}

 

public enum CascadeType {
    ALL,       // 모두 적용
    PERSIST,   // 영속
    MERGE,     // 병합
    REMOVE,    // 삭제
    REFRESH,   // REFRESH
    DETACH     // DETACH
}

PERSIST, REMOVE는 em.persis(), em.remove() 실행시 전이되지 않고 플러시를 호출시 전이

  • CascadeType.ALL: 모든 Cascade를 적용
  • CascadeType.PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지
  • CascadeType.MERGE: 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합
  • CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거
  • CascadeType.DETACH: 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()상태가 되어 변경 사항 반영 X
  • CascadeType.REFRESH: 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침

출처 : https://zzang9ha.tistory.com/350 (TransientPropertyValueException)

 

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능

참조가 제거된 엔티티는 다른 곳에서 참조하지 않은 고아 객체로 보고 삭제

참조하는 곳이 하나일 때 사용 (@OneToOne, @OneToMany 에만 사용)

부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제됨

@Entity
public class Parent {
	@Id @GenerateValue
	private Long Int;

	@OneToMany(mappedBy = "parent", orphanRemoval = true)
	private List<Child> children = new ArrayList<Child>();
}
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거
parent1.getChildren().clear(); // 모든 자식 엔티티 제거
DELETE FROM CHILD WHERE ID = ?

ㄴ 플러시 시점에서 SQL 실행

부모를 제거하면 자식은 고아가 됨(= CascadeType.REMOVE)

 

영속성 전이 + 고아 객체, 생명주기

CascadeType.ALL + orphanRemoval = true 를 동시에 사용시?

일반적인 엔티티는 EntityManager.persist()를 통한 영속화, EntityManager.remove()를 통해 제거 됨

= 엔티티 스스로 생명주기를 관리

두 옵션 모두 활성화시 부모 엔티티를 통해 자식의 생명주기 관리 가능

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함