티스토리 뷰
프록시와 연관관계 관리
지연 로딩
@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 제약 조건 설정
- @JoinColum(nullable = false)
@Entity public class Member { @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "TEAM_ID", nullable = false) private Team team; }
- @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 사용시 주의
- 컬렉션을 하나 이상 즉시 로딩 하는 것은 권장하지 않음
- 컬렉션과 조인 == 데이터베이스 테이블로 보면 일대다 조인
- 결과가 다 쪽에 있는 수만큼 증가하게 됨
- A 테이블을 N, M 두 테이블과 일대다 조인하면 실행결과가 N * M 이 되고 JPA는 조회된 결과를 메모리에서 필터링해서 반환
- 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다
- 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
- 누출 버킷 알고리즘
- 이동 윈도우 카운터 알고리즘
- 처리율 제한 알고리즘
- 회고
- 이동 윈도우 로깅 알고리즘
- 알고리즘
- 글또
- 개발자
- 처리율제한
- 고정 윈도우 카운터 알고리즘
- 카카오프로젝트100
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |