프록시와 연관관계 관리

twocowsong·2023년 4월 30일
0

김영한_jpa

목록 보기
9/13

프록시

em.find한번으로 member와 team객체를 동시에 가져오고 싶은경우 아래처럼 사용할 수 있습니다.

Member findMember = em.find(Member.class, member.getId());
Team findMemberTeam = findMember.getTeam();
System.out.println("회원 이름: " + findMember.getUsername());
System.out.println("소속팀: " + findMemberTeam.getName());

당연히 아래처럼 연관관계 매핑이 되어있어야하죠.

	...for..Member
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
    ...
	...for...Team
    @OneToMany(mappedBy = "team")
	private List<Member> members = new ArrayList<>();
    ...

그런데, 만약 Member만 쓰고싶어하는경우에도 em.find(Member.class, member.getId()); 를 사용 하면 아래처럼 쿼리가 실행됩니다.

즉, 소 잡을 칼로 닭을 잡아버리는 그런 상황입니다.
이를 해결하기위해 JPA에서 지원해주는것이 프록시입니다.

em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
...
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("회원 아이디: " + findMember.getId());
System.out.println("회원 이름: " + findMember.getUsername());
...

em.getReference(Member.class, member.getId());로 회원을 검색하게되면 쿼리는 아래와 같이 실행되게됩니다.

회원 아이디의 경우 DB의 쿼리를 실행시킬 필요없이 알수있기때문에 조회 없이 가져오지만, 회원 이름은 가져올때 데이터가 없기때문에 DB에 쿼리를 실행시켜 가져오게 됩니다.

System.out.println("Member class Info : " + findMember.getClass());


이름이 Member가 아닌걸로 확인되어, 하이버네이트가 강제로 만든 프록시 객체입니다.

프록시 특징

실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같습니다.
사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됩니다.

프록시 객체는 실제 객체의 참조(target)를 보관하며, 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출합니다.

프록시 객체 초기화

Member member = em.getReference(Member.class, “id1”); // 프록시 객체 호출
member.getName(); // 그림으로 설명

  1. getName() 호출
  2. MemberTarget에도 getName() 값이 없기에 영속성 컨텍스트에 요청
  3. 영속성컨텍스트에서 DB에 조회
  4. 실제 Entity를 생성
  5. MemberTarget은 4번에서 생성한 Entity를 연결

프록시 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
    - 객체 초기화 단계에서 5단계를 딱 한번만 실행 후 더이상 진행하지 않음
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // false
System.out.println("m1 == Member : " + (m2 instanceof Member)); // true
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 class : " + m1.getClass()); // m1 class : class hello.Member

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference : " + reference.getClass()); // reference : class hello.Member

em.find(Member.class, member1.getId());로 영속성 컨텍스트에 Member정보를 조회한 경우 em.getReference(Member.class, member1.getId());를 사용하여 프록시를 사용해서 굳이 영속성컨텍스트에 있는 객체를 또 타겟으로 설정하여 만들어서 얻을 이점이 없습니다.

System.out.println("m1 == reference : " + (m1 == reference)); // true

em.getReference 사용하여 가져온 객체라도, 영속성컨텍스트에 존재하는 실제 객체를 가져왔기때문에 위와 같은 상황에서 true 출력되는것을 확인 할 수 있습니다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember : " + refMember.getClass()); // proxy 객체

em.detach(refMember); // 영속성 컨텍스트에서 해제

String username = refMember.getUsername(); // 에러 발생
System.out.println("username : " + username);


refMember를 영속성컨텍스트에서 해제 함으로, 더이상 영속성 컨텍스트의 도움을 받지 못합니다.
더이상 프록시 객체를 초기화 하지 못함을 의미하게됩니다.

프록시 확인

  • 프록시 객체 초기화 여부 확인
    - emf.getPersistenceUnitUtil().isLoaded(확인하고자하는 객체);
  • 프록시 클래스 확인 방법
    - 객체.getClass()
  • 프록시 강제 초기화
    - Hibernate.initialize(초기화 하고자하는 객체);

즉시 로딩과 지연 로딩

지연로딩

Member 객체를 조회할때 무조건 Team객체도 항상 조회해야할까요?
이를해결하기위해 JPA에서는 지연로딩을 지원합니다.

@ManyToOne(fetch = FetchType.LAZY) // 지연로딩 설정
@JoinColumn(name = "TEAM_ID")
private Team team;
	...
    Member findMember = em.find(Member.class, member1.getId());
	...


이처럼, 지연로딩 설정 후 em.find를 통하면 Team정보는 조회하지않는걸 확인 할 수 있습니다.

Member findMember = em.find(Member.class, member1.getId());
System.out.println("==============================");
String name = findMember.getTeam().getName(); // Query 호출


처음 Member를 조회후 Team에 getName()호출 시 Team Select 쿼리가 실행되는걸 확인 할 수 있습니다. (프록시 객체 초기화)

지연로딩으로 객체를 호출 시 에는 프록시 객체를 조회합니다.

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember : " + findMember.getTeam().getClass());


지연로딩으로 설정 후 find 조회 시 객체의 class가 프록시인걸 확인 할 수 있습니다.

단, Member와 Team을 자주 함께 사용한다면?

즉시로딩

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;


Member 조회 시 한번에 Team정보까지 조회하며, 프록시를 사용하지않습니다.
프로젝트 마다 설정에 따라 지연로딩을 할지 또는 즉시로딩을 할지 선택하면됩니다.

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
    - 추후 확장을 통한 예상치 못한 조인증가가 발생될수 있음
  • 즉시 로딩은 JPQL에서 N+1 문제 발생
... 
List<Member> memberList = em.createQuery("select m from Member m", Member.class).getResultList();
...

JPQL로 조회 시 아래와 같이 SELECT문이 2번 호출되게 됩니다.

JPQL은 개발자가 직접 적은 SQL로 호출되기 때문에,
일단 select m from Member m SQL을 실행하게 됩니다.
그 후, Member 엔티티에 설정된 Team이 즉시로딩 설정이기때문에 Team을 조회하는 쿼리가 또 실행되게 되어 위 사진처럼 2번에 호출이 되게됩니다.

테스트 용도로 Team엔티티 1개로만 했지, 만약 점점 많아지게된다면 SQL은 계속적으로 증가되게 됩니다.
이를 해결하기위해 모든 로딩은 지연로딩으로 설정 후 FetchJoin을 사용합니다.

	...
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "TEAM_ID")
	private Team team;
    ...
	...
	List<Member> memberList = em.createQuery(
					"select m from Member m join fetch m.team", Member.class).getResultList();
	...


지연로딩이지만, 즉시로딩처럼 SQL에 team정보가 조회된걸 확인 할 수 있습니다.

영속성 전이(CASCADE)와 고아 객체

영속성 전이 : CASEADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 사용합니다.
Parent와 Child 엔티티2개, 1:N 양방향으로 연관관계가 설정되있는 예제입니다.

@Entity
@Setter
@Getter
public class Parent {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "PARENT_ID")
	private Long id;

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

	@OneToMany(mappedBy = "parent")
	private List<Child> childList = new ArrayList<>();

	public void addChild(Child child) {
		this.childList.add(child);
		child.setParent(this);
	}

}

@Entity
@Getter
@Setter
public class Child {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "CHILD_ID")
	private Long id;

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

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

}
...
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);
...

저장로직을 보면, em.persist가 너무 빈번하게 일어납니다.
이때, em.persist(parent); 한번으로
em.persist(child2); em.persist(child1); 로직을 줄일수있는방법이 CASCADE입니다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // CASCADE 설정!
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);
...


위처럼 child에 2번 INSERT된걸 확인할수있습니다.
연관관계와는 상관이없으며, em.persist(parent); 할때 parent안에 childList 모두를 persist해주는 설정입니다.

영속성 전이 : CASCADE - 주의!

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함 을 제공할 뿐
  • 단일객체 일때만 사용
    • child는 Parent에서만 사용되고 관리되기 때문에 CASCADE 사용가능
    • 하지만, 다른곳에서 child를 사용한다면 CASCADE를 지양
    • 다른곳에서도 child를 사용한다면 Parent에서 CASCADE로 데이터를 바꾸는 상황에 문제가 발생

고아객체

고아 객체 제거, 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 설정
orphanRemoval = true

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // 연결 해제
// 자식 엔티티를 컬렉션에서 제거


Delete 쿼리가 실행된걸 확인할수 있습니다.

고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
    • 특정 엔티티가 개인 소유할 때 사용!!!!
    • 참조하는 곳이 하나일 때 사용해야함!!!!!!

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

CascadeType.ALL + orphanRemoval=true

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거

  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음

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

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

em.persist(parent);
em.flush();
em.clear();

// Child의 모든 생명주기는 Parent가 관리
Parent findParent = em.find(Parent.class, parent.getId());
// 실행 시 Child는 모두 삭제 됨
em.remove(findParent);
profile
생각하는 개발자

0개의 댓글