Backend/JPA

[JPA/Java Persistence API] 프록시와 연관관계

냠냠:) 2021. 6. 3. 23:27

[우아한형제들 김영환님의 인프런 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 수강하고 정리한 내용입니다]


학습목표

  • 프록시
  • 즉시 로딩과 지연 로딩
  • 지연 로딩 활용
  • 영속성 전이 : CASCADE
  • 고아 객체
  • 영속성 전이 + 고아 객체, 생명주기

 

프록시

A엔티티와 B엔티티 간 연관관계가 있다고 가정하자. 만약 A엔티티를 조회할 때, B엔티티의 값을 같이 조회해야 할까?

아래의 Member와 Team이 있다. 이 둘은 다대일 연관관계가 있는 상태다.

 

Member

@Entity
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;

	private String name;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}

 

Team

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;

	private String name;

	@OneToMany(mappedBy = "team")
	private List<Member> members = new ArrayList<>();
}

 

여기서 맴버를 조회한다면?

 

@ManyToOne 특성상 EAGER(즉시 로딩) 연산을 통해 Team까지 같이 가져오게 된다.

Member findMember = em.find(Member.class, 1L);

EAGER 연산

 

이렇게 가져온 Team 데이터가 만약 해당 로직에서 사용되지 않는다면 낭비일 수밖에 없다.

 

JPA는 이런 낭비를 최소화하기 위해 프록시 지연 로딩 전략을 지원한다.

 

EntityManager는 테이블에서 Entity와 연관되어 있는 데이터를 찾는 find() 외에도 getReference()라는 메서드를 지원하는데 , 이 메서드는 가짜(프록시) 엔티티 객체를 조회한다.

getReference()

조회 해온 프록시 객체는 아래 그림처럼 실제 조회하려 했던 Entity를 상속받아서 만들어지며 실제 클래스와 겉모양이 같다

프록시와 엔티티와의 관계

  • 실제 클래스를 상속받아서 만듦
  • 실제 클래스와 겉모양이 같음
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지않고 사용할 수 있음

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

 

조금 자세히 풀면, 

 

Member를 조회해올 땐 Team 프록시 객체를 준비만 해두고 실제 Member내에 있는 Team의 데이터에 접근했을 때 Team에 대한 데이터를 가져온다고 생각하면 된다.

 

프록시 객체의 초기화

public static void main(String[] args) {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-application");
	EntityManager em = emf.createEntityManager();

	EntityTransaction tx = em.getTransaction();
	tx.begin();
	try {
		Member member = new Member();
		member.setName("test");
		em.persist(member);

		Team team = new Team();
		team.setName("teamA");
		em.persist(team);

		em.flush();
		em.clear();

		Member findMember = em.getReference(Member.class, 1L);
		System.out.println("=======LAZZZZZZYYYYYY=========");
		System.out.println(findMember.getName());

		tx.commit();
	}catch (Exception e) {
		tx.rollback();
		e.printStackTrace();
		System.out.println("ROLLBACK!!");
	}

	em.close();
	emf.close();
}

바로 값을 가져오는 것이 아닌, findMember.getName()시 값을 가져오는 것을 확인 할 수 있다


프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크 시 주의해야 함(==비교 실패, instanceof 키워드 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환(JPA의 Repeatable Read보장)
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생

 

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법 :entity.getClass().getName() 출력(..javasist.. or HibernaeProxy..)
  • 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity)

* JPA 표준은 강제 초기화 없음.

 

 


즉시 로딩과 지연 로딩

실무에서는 즉시 로딩 세팅을 안 한다고 한다. 무조건 지연 로딩을 사용하라고 권장한다.

@Entity
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;

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

	@ManyToOne(fetch = LAZY)
	@JoinColumn(name = "TEAM_ID")
	private Team team;
    
}

여기서 static import를 통해 LAZY로면 표시됐지만 @ManyToOne(fetch = FetchType.LAZY)로 표현되는 게 일반적이다

 

정리

  • 가급적 지연 로딩만 사용(실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> 지연 로딩으로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • JPQL fetch조인이나 엔티티 그래프 기능을 사용해라(LAZY 설정 후 EAGER로 값을 가져올 땐)

 

영속성 전이 - CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
  • 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

 

설정 방법

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;

	private String name;

	@OneToMany(mappedBy = "team" , cascade = CascadeType.ALL)
	private List<Member> members = new ArrayList<>();

	public void addMember(Member member) {
		member.setTeam(this);
		members.add(member);
	}
}
public static void main(String[] args) {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-application");
	EntityManager em = emf.createEntityManager();

	EntityTransaction tx = em.getTransaction();
	tx.begin();
	try {
		Team team = new Team();
		team.setName("teamA");
		em.persist(team);

		em.flush();

		Member member = new Member();
		member.setName("test");
        	member.setTeam(team);
		em.persist(member);

		em.flush();

		Member newMember = new Member();
		newMember.setName("난 새로운 친구");

		team.addMember(newMember);		//팀에만 새로운맴버를 추가했음

		tx.commit();
	}catch (Exception e) {
		tx.rollback();
		e.printStackTrace();
		System.out.println("ROLLBACK!!");
	}

	em.close();
	emf.close();
}

Insert 문 - 마지막에 Team에 추가 된 newMember도 같이 Insert
H2 DB 결과

정리

  • 영속성 전이는 연관관계 매핑과는 아무 관련 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
  • 영속성 컨텍스트가 clear 돼있을 때는 기능이 동작하지 않음
  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH
  • DETACH

고아 객체

고아 객체란?

부모 엔티티와 연관관계가 끊어진 자식 엔티티(ex. Team삭제 시 members에 있는 Member 객체들 삭제)

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;

	private String name;

	@OneToMany(mappedBy = "team", orphanRemoval = true)
	private List<Member> members = new ArrayList<>();
}
public static void main(String[] args) {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-application");
	EntityManager em = emf.createEntityManager();

	EntityTransaction tx = em.getTransaction();
	tx.begin();
	try {
		Team team = new Team();
		team.setName("teamA");
		em.persist(team);

		em.flush();

		Member member = new Member();
		member.setName("test");
		member.setTeam(team);
		em.persist(member);

		em.flush();
		em.clear();
		
		Team findTeam = em.find(Team.class, 1L);
		em.remove(findTeam);

		tx.commit();
	}catch (Exception e) {
		tx.rollback();
		e.printStackTrace();
		System.out.println("ROLLBACK!!");
	}

	em.close();
	emf.close();
}

Member 삭제 후 Team 삭제

 

주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 해당 엔티티 하나일 때 사용해야 함
  • @OneToOne, @OneToMany만 가능

*참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 마치 CascadeType.REMOVE처럼 동작한다.

 

 


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

CascadeType.ALL + orphanRemovel=true 조합

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;

	private String name;

	@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Member> members = new ArrayList<>();
}

members의 값을 직접 삭제해도 Member가 삭제 (cascadeType.REMOVE)

Team 객체를 삭제해도 members에 포함된 Member들이 삭제 (orphanRemoval = true)

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용
반응형