티스토리 뷰
이전 글: https://gojs.tistory.com/47
JPA 연관관계 매핑 (2)
JPA에서의 기본적인 연관관계 매핑하는 방법에 대해서 알아보았다.https://gojs.tistory.com/46 연관관계 매핑관계형 데이터베이스의 테이블간의 관계를 객체간의 연관관계로 표현할 수 있다.크게 단
gojs.tistory.com
특정 엔티티를 조회할 때 연관된 엔티티를 항상 같이 조회하는 것은 아니다.
한번에 모든 연관관계를 로딩하게 되면 너무 많은 데이터를 조회하게 되어서 OOM이 발생할 수도 있다.
그렇다면 JPA에서 연관 엔티티를 조회하기 위한 방법들에 대해서 알아보자.
프록시 객체
@Entity
public class Member {
...
private String username;
@ManyToOne
private Team team;
...
}
@Entity
public class Team {
...
private String name;
...
}
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원이름: " + member.getUsername() + ", 팀이름: " + team.getName());
}
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
System.out.println("회원이름: " + member.getUsername());
}
위 메서드 중 printUser(String) 메서드는 Member 엔티티에만 의존성을 가지고 있기 때문에 굳이 Member 엔티티를 조회할 때 Team 엔티티를 같이 조회할 필요는 없다.
JPA에서는 이와 같은 비효율을 줄이기 위해 실제 사용시점 전까지 데이터베이스에서 조회하지 않는 지연 로딩 기능을 제공한다.
지연 로딩을 사용하기 위해서는 실제 엔티티 객체 대신 가짜 객체가 필요한데 이를 프록시 객체라고 한다.
EntityManager.find(...) 메서드는 실제로 엔티티를 사용하지 않더라도 데이터베이스에서 데이터를 조회한다.
그러나 실제 사용 시점까지 데이터베이스 조회를 미루고 싶은 경우에는 EntityManger.getReference() 메서드를 사용하면 된다.
getRefererence() 를 사용하면 실제 엔티티 객체가 아닌 프록시 객체를 반환한다.
프록시 객체는 Entity 클래스를 상속받아 구현하기 때문에 겉모양이 같다.
프록시 객체를 사용하게되면 그 시점에 데이터베이스를 조회해서 Entity 인스턴스가 생성되어 Entity 객체의 메서드를 호출하여 로직을 처리한다.
class MemberProxy extends Member {
Member target = null;
public String getName() {
if (target == null) {
// DB 조회
this.target = ...;
}
return target.getName();
}
}
Member member = em.getReference(Member.class, "id1");
String memberName = member.getName();
위 코드의 흐름을 순차적으로 표현하면 아래와 같다.
- 프록시 객체에 member.getName() 메서드를 호출해서 데이터를 조회
- 프록시 객체는 Entity 인스턴스가 생성되어있지 않으면 영속성 컨텍스트에 엔티티 생성 요청
- 영속성 컨텍스트는 데이터베이스를 조회해서 엔티티 생성
- 프록시 객체는 생성된 엔티티 객체를 멤버변수에 보관
- 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과 반환
프록시 객체는 아래와 같은 특징을 가지고 있다.
- 처음 사용할 때 한번만 초기화
- 프록시 객체는 엔티티 객체를 상속받았으므로 타입 체크 시 유의해야한다
- 영속성 컨텍스트에 이미 엔티티가 있는 경우 em.getReference()호출하면 실제 엔티티 반환
- 준영속 상태 프록시 초기화는 예외발생 (LazyInitializationException, 영속성 컨텍스트 도움 못받음)
// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
transaction.commit();
em.close(); // memberProxy 준영속 상태
member.getName(); // 예외 발생
프록시와 식별자
프록시 객체를 생성할 때 PK를 파라미터로 전달하게되어 생성된 프록시 객체는 PK를 내부에 저장하고 있다.
따라서 @Access(AccessType.PROPERTY)로 설정한 경우, 식별자 필드 getter 메서드를 호출하더라도 프록시를 초기화하지 않는다.
반대로 @Access(AccessType.FIELD)로 설정한 경우, getter 메서드가 식별자 필드만 조회하는지 다른 작업을 하는지 알 수 없기 때문에 초기화 한다.
Team team = em.getReference(Team.class, "team1");
team.getId();
즉시 로딩과 지연 로딩
연관관계 엔티티를 조회하는 방법은 크게 두 가지로 분류된다.
- 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회
- 지연 로딩 : 연관된 엔티티는 실제 사용할 때 조회
지금까지 지연 로딩에 대한 내용을 알아보았다. 어떤 방법으로 연관관계마다 페치 정책을 설정할 수 있는지도 알아보자.
즉시 로딩 (EAGER LOADING)
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Member member = em.find(Member.class, "id1");
Team team = member.getTeam();
@ManyToOne(fetch = FetchType.EAGER)로 지정해 Member -> Team 연관관계를 즉시 로딩으로 설정했다.
따라서 em.find(Member.class, “id1”)를 수행하며 Member와 Team이 같이 조회된다.
이 경우 최적화를 위해 쿼리를 두 번 실행하지 않고 조인 쿼리를 사용한다.
NULL 제약조건에 따른 JPA 조인 전략
em.find(Member.class, “id1”)를 수행하며 발생하는 조인 쿼리는 OUTER JOIN이 걸릴 것이다.
그 이유는 Member가 Team을 가지지 않는 (team_id가 nullable) 상황이 발생할 수 있기 때문이다.
이에 따라서 team field에 @JoinColumn(nullable = false) 또는 @ManyToOne(optional = false) 옵션을 넣어주어서 team field가 null인 경우를 배제하게 된다면 이에 따라 JPA는 조인 쿼리에 INNER JOIN 을 사용할 것이다. → 성능 최적화
지연 로딩 (LAZY LOADING)
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Member member = em.find(Member.class, "member1");
Team team = member.getTeam();
team.getName();
@ManyToOne(fetch = FetchType.LAZY) 로 지정해 Member -> Team 연관관계를 지연 로딩으로 설정했다.
따라서 em.find(Member.class, "member1") 코드를 실행할 때는 Member만 조회한다. 이 때 member 인스턴스의 team field에는 Team의 Proxy 객체가 세팅되어 있다.
이 프록시 객체를 사용하면 (team.getName()) 그 시점에 team을 초기화해서 조회한다.
즉시 로딩 vs 지연 로딩
둘 중에 정답은 없다.
회원과 팀이 왠만하면 같이 쓰이는 경우에는 즉시 로딩을 쓰는 것이 좋다.
그렇지 않다면 쓸데없이 영속성 컨텍스트의 공간을 낭비하지 않고 지연 로딩을 쓰는 것이 좋다.
JPA 기본 페치 전략
fetch 속성의 기본 설정값은 아래와 같다.
- @ManyToOne, @OneToOne : 즉시 로딩
- @OneToMany, @ManyToMany : 지연 로딩
즉 연관관계 엔티티가 하나이면 즉시 로딩이 기본 값이고 컬렉션이면 지연 로딩이 기본값이다.
추천하는 방법은 모든 연관관계에 지연로딩을 사용하고, 필요한 지점에만 즉시로딩으로 최적화해주는 것이 좋다.
즉시 로딩 주의점
즉시 로딩을 활용하여 개발하는 경우 주의해야할 점들이 있다.
- 컬렉션을 하나 이상 즉시 로딩하지 않는다.
- 하나의 엔티티에 일대다 연관관계 즉시로딩으로 설정되어있고, 반대편 엔티티에 또 다른 엔티티와 일대다 연관관계 즉시로딩으로 설정되어 있다고 가정해보자.
- 이 경우 하나의 엔티티를 조회하더라도 NxM 연산이 발생하여 너무 많은 데이터를 반환하게 될 수 있다.
- 컬렉션 즉시 로딩은 항상 외부 조인 사용
- Team 엔티티의 members를 즉시 로딩으로 조인 쿼리를 발생시킬 때, inner join을 사용하게 된다면 members.size()가 0인 경우에 team이 조회가 안되는 경우가 발생한다.
- 이를 방지하기 위해 항상 outer join을 사용하는 것이 좋다.
영속성 전이 (CASCADE)
특정 엔티티를 영속 상태로 만들 때 다른 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
...
}
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
...
}
Parent parent = new Parent();
em.persist(parent);
Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1);
Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2);
1개의 Parent와 2개의 Child를 순서대로 영속 상태로 만드는 코드이다.
이럴 때 영속성 전이를 사용하면 Parent만 영속 상태로 만들면 연관된 자식도 같이 영속 상태로 만들 수 있다.
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
...
}
Parent parent = new Parent();
Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(parent);
Child를 영속 상태로 만드는 em.persist(child) 코드가 없더라도 Child 인스턴스들도 모두 영속 상태가 되었다.
삭제의 경우에는 @OneToMany(cascade = CascadeType.REMOVE) 옵션을 이용해 영속성 전이를 설정하면 2개의 자식을 삭제하고 그 이후 1개의 부모를 삭제한다. (외래 키 제약조건 고려)
고아 객체
부모 엔티티와 연관관계가 끊어진 자식 객체를 고아 객체라고 한다.
JPA는 고아 객체를 자동으로 삭제해주는 기능을 제공한다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
...
}
Parent parent = em.find(Parent.class, id);
parent1.getChildren().remove(0);
위의 코드와 같이 orphanRemoval 옵션을 true로 주고 해당 필드의 하나의 요소에 대해 참조를 끊어버리면 자동으로 삭제 쿼리가 발생한다.
DELETE FROM CHILD WHERE ID = ?
이 기능은 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 것으로 간주하고 삭제하는 기능이다.
따라서 참조하는 곳이 하나일 때만 사용해야한다. (완벽하게 하위(포함) 관계인 엔티티)
만약 다른 곳에서 참조하고 있는 엔티티였다면 데이터 정합성이 깨진다.
다음 글: https://gojs.tistory.com/49
JPA 값 타입
JPA는 Java 엔티티와 DB 테이블 간의 매핑을 도와주는 프레임워크이다.따라서 엔티티의 필드 타입과 테이블의 컬럼 타입도 매핑해준다.엔티티의 각 필드에 정의할 수 있는 타입들에 대해서 알아
gojs.tistory.com
'공부 > JPA' 카테고리의 다른 글
객체지향 쿼리 언어 알아보기 (0) | 2024.08.26 |
---|---|
JPA 값 타입 구현하기 (0) | 2024.07.27 |
JPA Entity 간 연관관계 매핑 (2) (0) | 2024.07.27 |
JPA Entity 간 연관관계 매핑 (1) (0) | 2024.07.26 |
JPA Entity 구성하기 (0) | 2024.07.24 |