Skip to content

Latest commit

 

History

History
315 lines (282 loc) · 14.3 KB

[민경] 8장.md

File metadata and controls

315 lines (282 loc) · 14.3 KB

프록시와 연관관계 관리

프록시

등장배경('프록시' 개념이 왜 필요한가?)

다음과 같이 연관된 객체, Member 객체와 Team 객체가 있다고 하자. 1

  • Member만 조회하고 싶은 경우, 굳이 Team 엔티티까지 데이터베이스에서 같이 조회하는 것을 비효율적
  • 어떤 엔티티를 조회할 때, 그 엔티티와 연관된 모든 앤티티를 조회함으로써 생기는 낭비 방지 위해 지연로딩 개념 사용
  • 지연로딩: 연관된 객체를 처음부터 DB에서 조회하지 않고, 실제 사용하는 시점에서 DB에서 조회
    • 회원 엔티티만 조회해서 사용 가능
    • team.getName()과 같이 Team 엔티티의 값을 실제 사용하는 시점에 DB에서 team 엔티티에 필요한 데이터를 조회 가능
      -> 지연로딩 개념 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체, '프록시 객체' 필요

프록시 기초

  • 식별자로 엔티티 하나를 조회할 때는 EntityManager.find() 사용
Member member = em.find(Member.class, "memeber1");
  • em.find()는 영속성 컨텍스트에 엔티티가 없으면 DB 조회
  • 이렇게 엔티티를 직접 조회하면 조회한 엔티티의 사용 여부에 상관없이 DB 조회
  • 엔티티를 실제 사용하는 시점까지 DB 조회를 미루고 싶으면 EntityManager.getReference() 사용
    • 이 메소드를 호출할 때는 DB를 조회하지 않고, 실제 엔티티 객체도 생성하지 않음
    • 대신, 데이터베이스 접근이 가능한 가짜 객체인 '프록시 객체'를 반환 2

프록시 특징 1

  • 실제 클래스를 상속 받아 만들어지기 때문에 똑같은 모양
    -> 사용하는 입장에서는 이것이 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용하면 됨
    3
  • 프록시 객체는 실제 객체에 대한 참조 target 보관
  • 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출
  • 처음에는 아직 DB 조회를 하지 않았기 때문에 target에 참조값이 없음

프록시 객체의 초기화

  • 객체의 초기화: 프록시 객체는 member.getName()처럼 실제 사용될 때 DB를 조회해서 실제 엔티티 객체의 생성을 요청하는 것
Member member = em.getReference(Member.class, "id1");
member.getName();

4

  • 동작 과정
  1. 처음에 member.getName() 호출
  2. 가져온 Member 프록시 객체의 target 값이 null(아직 DB 조회하기 전이므로)
  3. JPA가 영속성 컨텍스트에 초기화 요청
  4. 영속성 컨텍스트는 DB를 조회해서 실제 Entity 생성해서 반환
  5. 프록시 객체의 target에 반환된 실제 Entity 참조값 저장
  6. target.getName()이 호출되면서 결과 반환

프록시 특징 2

  • 프록시 객체는 처음 사용될 때 한번만 초기화
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
    -> 초기화되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있다는 것을 의미
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 함
    -> 즉, == 비교 대신 instance of를 사용해야 함.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없기 때문에 em.getReference() 호출해도 프록시가 아닌 실제 엔티티 반환
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능
    -> 준영속상태일 때, 프록시 초기화하면 문제 발생(LazyInitializationException) -> 엔티티가 준영속성 상태가 되는 대표적인 경우는 트랜잭션이 종료돼서 영속성 컨텍스트가 close 된 경우 -> 실무에서 트랜잭션이 끝나고 영속성 컨텍스트를 조회하는 경우에 많이 마주치는 오류

프록시와 식별자

  • 프록시 객체는 파라미터로 전달받은 식별자 값을 보관
Team team = em.getReference(Team.class, "team1"); //식별자 보관
team.getId(); // 초기화 되지 않습니다.
  1. 엔티티 접근 방식이 프로퍼티(@Access(AccessType.PROPERTY))일 경우
    프록시 객체는 식별자 값을 가지고 있기 때문에, 식별자 값을 조회하는 team.getId() 호출해도 프록시 초기화 x
  2. 엔티티 접근 방식이 필드(@Access(AccessType.FIELD))일 경우
    JPA는 getId() 메소드가 id만 조회하는 메소드인지 아니면 다른 필드까지 활용해서 다른 일을 하는 메소드인지 알 수 없기 때문에 프록시 객체를 초기화
  • 프록시는 연관관계 설정할 때 유용
    -> 연관관계 설정 시, 식별자 값만 사용하기 때문에 프록시를 사용하면 데이터베이스 접근 횟수 줄일 수 있음
    -> 연관관계 설정할 때는 엔티티 접근 방식을 필드로 설정해도 프록시 초기화되지 않음
    -> 멤버 엔티티와 팀 엔티티 연관관계 설정 시, 팀 엔티티를 DB에서 영속성 컨텍스트로 가져오지 않고 팀 엔티티의 식별자 값만 가지고 있는 프록시를 사용하면 DB 접근 횟수 줄일 수 있음
Member member = em.getReference(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); //식별자 보관
member.setTeam(team);

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    -> PersistenceUnitUtil.isLoaded(Object entity) -> true/false 반환

    • 예제
    boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);
    System.out.println("isLoad = " + isLoad)
  • 프록시 클래스 확인 방법(조회한 엔티티가 진짜 엔티티인지, 프록시 엔티티인지 확인)
    -> entity.getClass().getName()으로 클래스명 직접 출력 -> 클래스명 뒤에 ..javassist.. 붙으면 프록시 엔티티

    • 예제
    System.out.println("memberProxy =" + member.getClass().getName());
    // 결과 : memberProxy = jpabook.domain.Member_$$_javassist_0
  • 프록시 강제 초기화

    • 하이버네이트의 initialize() 메소드를 사용하면 프록시 강제 초기화 가능
    • JPA 표준은 강제 초기화 없음

즉시 로딩과 지연 로딩

  • 즉시 로딩 : 엔티티 조회할 때 연관된 엔티티도 함께 조회
  • 지연 로딩 : 연관된 엔티티 실제 사용할 때 조회
  • Member를 조회할 때 연관된 Team도 함께 조회해야 할까? -> 비즈니스 로직에 따라 달라짐

즉시 로딩

  • Member와 Team 을 자주 함께 사용한다면? -> 한번에 같이 조회하는 것이 좋음
  • @ManyToOne의 fetch 속성을 FetchType.EAGER로 지정
@Entity
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;
    ...
}

5

  • 즉시 로딩을 최적화하기 위해 조인 쿼리 사용 (쿼리 2번 실행)
SELECT 
	M.MEMBER_ID AS MEMBER_ID,
	M.TEAM_ID AS TEAM_ID,
	M.USERNAME AS USERNAME,
	T.TEAM_ID AS TEAM_ID,
	T.NAME AS NAME,
FROM
	MEMBER M LEFT OUTER JOIN TEAM 
		  ON M.TEAM_ID=T.TEAM_ID
WHERE
	M.MEMBER_ID='member1'

-> DB에서 Member 조회할 때 Team도 함께 조회
-> 2번 쿼리를 날리지 않고 join해서 한번에 가져옴
-> 한번에 엔티티 조회해서 가져오기 때문에 Team엔티티도 프록시가 아니라 실제 엔티티를 가져옴

지연로딩

  • member 정보만 사용
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    //지연로딩 전략, team 객체는 프록시 객체로 조회한다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team
    ...
}

6 -> member1 조회할 때 연관된 team 엔티티는 DB에서 조회하지 않고 프록시로 초기화
-> 실제로 team.getName()과 같이 team 엔티티를 사용하는 시점에 프록시가 실제 엔티티 가리키도록 초기화

  • JPA의 기본 패치 전략
    • @ManyToOne, @OneToOne : 즉시 로딩(default)
    • @OneToMany, @ManyToMany : 지연 로딩(default)
    • JPA의 기본 패치 전략은 연관된 엔티티가 하나면 즉시로딩을, 컬렉션이면 지연 로딩 사용
    • 실무에서는 가급적 지연 로딩만 사용 추천
    • 즉시 로딩 적용하면 예상치 못한 SQL 발생 가능
      -> 테이블에 연관관계 복잡하게 되어 있을 때 즉시 로딩 사용하면 join 여러번 발생 -> 성능 감소!!
      • 결론 : 무조건 다 지연 로딩 써라. 그 후, 꼭 필요한 곳에만 즉시 로딩을 최적화

영속성 전이 : CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
  • 쉽게 말해, 영속성 전이 사용하면 부모 엔티티 저장할 때 자식 엔티티도 함께 저장 가능
  • JPA에서 엔티티 저장할 때 연관된 모든 엔티티는 영속 상태여야 함
    이때, 영속성 전이 안해주면 자식 개수만큼 em.persist(child) 다 해줘야 함 -> 귀찮아!!

영속성 전이:저장

  • 영속성 전이 사용 X
@Entity
public class Parent {
   @Id @GeneratedValue
	 private Long id;

   @OneToMany(mappedBy="parent")
   private List<Child> children = new ArrayList<Child> ();
   ...
private static void saveNoCascade (EntityManager em) {
	//부모 저장 
	Parent parent = new Parent();
  em.perists(parent);

	//1번 자식 저장 
	Child child1 = new Child();
	child1.setParent(parent); // 자식과 부모 연관관계 설정 
  parent.getChildren().add(child1); // 부모 -> 자식 
	em.persist(child1);

	//2번 자식 저장 
	Child child2 = new Child();
	child2.setParent(parent); // 자식과 부모 연관관계 설정 
  parent.getChildren().add(child2);  // 부모 -> 자식 
	em.persist(child2);
  • 영속성 전이 사용 O
@Entity
public class Parent {
   ...
   @OneToMany(mappedBy="parent", cascade = CascadeType.PERSIST)
   private List<Child> children = new ArrayList<Child> ();
   ...
private static void saveWithCascade(EntityManager 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);

7

  • 주의
    • 영속성 전이는 연관관계 매핑하는 것과 아무 관련 없음
    • 그저 엔티티 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함만 제공
    • 그래서, 위의 코드에서 양방향 연관관계 추가한 다음 영속 상태로 만들어 줘야 함

영속성 전이: 삭제

  • 영속성 전이 사용 X
Parent findParent = em.find(Parent.class, 1L);
Parent findChild1 = em.find(Child.class, 1L);
Parent findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);
  • 영속성 전이 사용 O
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
  • 코드 실행하면 DELETE SQL 3번 실행되고 부모와 연관된 자식 모두 삭제
  • 이때, 삭제 순서는 외래 키 제약조건 고려해 자식 먼저 삭제하고 부모 삭제

CASCADE 의 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REPRESH : REPRESH
  • DETACH : DETACH
    → 여러 속성 같이 사용 가능 (cascade ={CascadeType.PERSIST, Cascade.REMOVE})
  • 참고 : PERSIST, REMOVE는 em.persist(), em.remove()를 실행할 때 바로 전이가 발생하지 않고, 플러시 호출할 때 전이 발생

고아 객체

  • 고아 객체 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티
  • 고아 객체 제거 : 고아 객체를 자동으로 삭제하는 기능 제공
@Entity
public class Parent {
	@Id @GeneratedValue
	private Loing id;

	@OneToMany (mappedBy = "parent", orphanRemoval= true)
	private List<Child> children = new ArrayList<Child> ();
  ...
}
  • orphanRemoval = true
Parent parent1 = em.find(Parent.class,id);
parent1.getChildren().remover(0); // 자식 엔티티를 컬렉션에서 제거 -> 부모 객체와의 연관관계 끊어짐
  • 실행 결과 DELETE 쿼리 발생
DELETE FROM CHILD WHERE ID = ?
  • 주의
    • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아로 보고 삭제
    • 참조하는 곳이 하나일 때 사용해야 한다!!
      • 즉, 특정 엔티티가 개인 소유하는 엔티티일때만 이 기능 적용해야 함
        -> @OneToMany, @OneToOne에만 orphanRemoval 사용 가능

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

  • CascadeType.ALL + orphanRemoval = true 를 동시에 사용하면?
  • 일반적으로 엔티티는 EntityManager.persist()를 통헤 영속화되고 EntityManager.remove() 통해 제거 -> 즉, 엔티티가 스스로 생명주기 관리
  • 두 옵션 모두 활성화하면 부모 엔티티 통해 자식 엔티티의 생명주기 관리 가능
  • 예제
    1. for 자식 저장 -> 부모에 등록
      Parent parent = em.find(Parent.class,parentId);
      parent.addChild(child1); 
    2. for 자식 삭제 -> 부모에서 삭제
      Parent parent = em.find(Parent.class,parentId);
      parent.getChildren().remove(removeObject);