[JPA] 프록시와 연관관계 정리
Updated:
들어가며
해당 글은 자바 ORM 표준 프로그래밍을 정리한 글입니다.
JPA에 사용하는 프록시와 지연로딩과 즉시로딩에 대해서 알아보고 영속성 전이에 대한 개념과 영속성 전이를 이용해 고아객체를 제거하는 방법에 대해서 알아본다.
프록시
- 엔티티를 조회할 때 연관된 엔티티들을 항상 사용하는 것이 아닐 수 있기에 필요한 연관 엔티티를 사용할 때 조회할 수 있다.
- 지연로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.
- EntityManager.getReference() 메소드를 사용해 실제 사용하는 시점까지 데이터베이스 조회를 미룰 수 있다.
@Getter
@Setter
@Entity
public class Game {
@Id @GeneratedValue
private Long id;
private String name;
}
Game game = new Game();
game.setName("game");
em.persist(game);
em.flush(); em.clear();;
Game refGame = em.getEntityManager().getReference(Game.class, game.getId());
System.out.println(refGame.getClass().getName());
me.blog.jpa.model.proxy.Game$HibernateProxy$Quw3ynSC
프록시의 특징
프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다. 사용하는 입장에서는 해당 객체가 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
프록시 객체의 초기화
프록시 객체는 실제 사용 될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.
Game game = new Game();
game.setName("game");
em.persist(game);
em.flush(); em.clear();;
Game refGame = em.getEntityManager().getReference(Game.class, game.getId());
System.out.println(refGame.getClass().getName());
System.out.println(refGame.getName()); // 해당 시점에 SELECT 실행
System.out.println(refGame.getClass().getName()); // proxy 그대로 사용
me.blog.jpa.model.proxy.Game$HibernateProxy$WXYVKbhV
...
select
game0_.id as id1_1_0_,
game0_.name as name2_1_0_
from
game game0_
where
game0_.id=?
...
me.blog.jpa.model.proxy.Game$HibernateProxy$WXYVKbhV
프록시와 식별자
엔티티를 프록시로 조회할 때 식별자 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다. 따라서 식별자 값을 조회하는 경우 프록시를 초기화하지 않는다. 단 @Access(AccessType.FIELD)로 설정하는 경우에는 프록시 객체를 초기화 한다.
프록시 확인
PersistenceUnitUtil.isLoaded(Object Entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인 할 수 있다.
- 초기화되지 않은 프록시 인스턴스는 false를 반환
책에서는 EntityManager
에서 가져오게 되어 있지만 버전이 높아져서 가져오는 방법이 바뀌었다. 😭
즉시 로딩과 지연 로딩
즉시로딩
- 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- @ManyToOne(FetchType.EAGER)를 사용
- 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.
NULL 제약조건과 JPA 조인 전략
- 즉시 로딩 실행시에 JPA가 내부 조인(INNER JOIN)이 아닌 외부 조인(LEFT OUTER JOIN)을 사용한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
Hibernate:
select
member0_.member_id as member_i1_4_0_,
member0_.team_id as team_id2_4_0_,
team1_.team_id as team_id1_8_1_,
team1_.name as name2_8_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
- @JoinColumn(name = “TEAM_ID”, nullable = false)을 설정해서 해당 외래 키는 NULL 값을 허용하지 않는다고 알려주면 JPA는 외부 조인 대신 내부조인을 사용한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@ManyToOne
@JoinColumn(name = "team_id", nullable = false)
private Team team;
}
Hibernate:
select
member0_.member_id as member_i1_4_0_,
member0_.team_id as team_id2_4_0_,
team1_.team_id as team_id1_8_1_,
team1_.name as name2_8_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
- @ManyToOne(fetch = FetchType.EAGER, optional = false)로 설정해도 내부 조인을 사용한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "team_id", nullable = false)
private Team team;
}
Hibernate:
select
member0_.member_id as member_i1_4_0_,
member0_.team_id as team_id2_4_0_,
team1_.team_id as team_id1_8_1_,
team1_.name as name2_8_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
지연 로딩
- 실제 사용될 때까지 데이터 로딩을 미룬다.
- @ManyToOne(FetchType.LAZY)를 사용
지연 로딩 활용
프록시와 컬렉션 래퍼
- 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라 한다.
- team.getMembers()를 호출해도 컬렉션은 초기화되지 않고 컬렉션의 실제 데이터를 조회할 때 데이터베이스에서 조회해서 초기화한다.
@Test
void teamInsertAndSelect() {
Team team = new Team();
em.persist(team);
em.flush(); em.clear();
Team result = em.find(Team.class, team.getId());
System.out.println(result.getMembers().getClass().getName()); // JPA 컬렉션 래퍼
}
org.hibernate.collection.internal.PersistentBag
JPA 기본 페치 전략
- @ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)
- @OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)
- 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것
- 지연 로딩을 사용하다가 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하는게 좋음
컬렉션에 FetchType.EAGER 사용 시 주의점
- 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
- 일대다 조인의 결과는 다 쪽에 있는 수 만큼 증가하기 때문에 주의해야 한다.
- 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
- @ManyToOne, @OneToOne
- optional = false : 내부 조인
- optional = true : 외부 조인
- @OneToMany, @ManyToMany
- optional = false : 외부 조인
- optional = true : 외부 조인
영속성 전이: CASCADE
- JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
- 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.
- ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH 옵션이 존재 한다.
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@ManyToOne(optional = false, cascade = CascadeType.PERSIST)
@JoinColumn(name = "team_id")
private Team team;
}
@Test
void cascadeTest() {
Team team = new Team();
Member member = new Member();
member.setTeam(team);
em.persist(member); // team insert 후 member insert
em.flush(); em.clear();
Member m = em.find(Member.class, member.getId());
}
Hibernate:
insert
into
team
(name, team_id)
values
(?, ?)
Hibernate:
insert
into
member
(team_id, member_id)
values
(?, ?)
고아 객체
- 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제
- orphanRemoval = true를 추가
- 컬렉션에서 제거하면 데이터베이스의 데이터도 삭제된다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
...
@OneToMany(mappedBy="team", orphanRemoval = true)
private List<Member> members = new ArrayList<Member>();
}
@Test
void orphanRemovalTest() {
Team team = new Team();
Member member = new Member();
member.setTeam(team);
em.persist(member);
em.persist(team);
em.flush(); em.clear();
Team result = em.find(Team.class, team.getId()); // team의 id를 알기 위해 조회
em.remove(result);
em.flush(); em.clear();
Team teamRes = em.find(Team.class, team.getId());
Member memberRes = em.find(Member.class, member.getId());
assertThat(teamRes).isNull();
assertThat(memberRes).isNull();
}
Hibernate:
insert
into
member
(team_id, member_id)
values
(?, ?)
Hibernate:
insert
into
team
(name, team_id)
values
(?, ?)
// member insert 후 team insert 했기 때문에 다시 member의 team을 update 필요
Hibernate:
update
member
set
team_id=?
where
member_id=?
...
// 고아 객체 제거
Hibernate:
delete
from
member
where
member_id=?
Hibernate:
delete
from
team
where
team_id=?
마치며
JPA에서 엔티티를 로딩할 때 프록시를 사용해서 로딩한다는 부분과 설정에 따라서 지연로딩, 즉시로딩 되는 것을
예제를 통해서 확인하였다. cascade
를 통해서 연관된 엔티티도 한꺼번에 처리할 수 있으나
단순히 편하게 사용하기 위해서 cascade
에 ALL
을 셋팅하는 것은 부담이 될 수 있으니 주의 해야 한다.
- JPA 리뷰 글 보러가기
Leave a comment