// 회원 엔티티
@Entity
public class Member {
private String username;
@ManyToOne
private Team team;
public Team getTeam() {
return tream;
}
@public String getUsername() {
return username;
}
...
}
//팀 엔티티
@Entity
public class Team {
private String name;
public String getName() {
return name;
}
...
}
// 회원과 팀 정보를 출력하는 비즈니스 로직
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름:" + member.getUsername());
System.out.println("소속팀: " + team.gretName());
}
// 회원 정보만 출력하는 비즈니스 로직
public String printUser(String memberId) {
Member member = em.find(Member.class, memberId);
System.out.println("회원 이름:" + member.getUsername());
}
프록시랑 지연로딩에 대해 설명
printUserAndTeam() 메소드는 memberId로 회원 엔티티를 찾아서 회원과 연관된 팀의 이름도 출력한다.
printUser() 메소드는 회원 엔티티만 출력하는데 사용하고 회원과 연관된 팀엔티티는 전혀 사용하지 않는다.
printUser() 메소드는 회원 엔티티만 사용하므로 em.find()로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티까지 데이터베이스에서 함께 조회 해 두는 것은 효율적이지 않은데 JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연로딩 이라 한다.
지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체 라 한다.
하이버네이트는 지연 로딩을 지원하기 위해 프록시를 사용하는 방법과 바이트코드를 수정하는 두 가지 방법을 제공한다.
1.1 프록시 기초
JPA에서 식별자로 엔티티를 조회할 때는 EntityManager.find()를 사용한다.
엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManger.getReference() 메소드를 사용하면 된다.
이 메소드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다.
대신 데이터베이스 접근을 위임한 프록시 객체를 반환한다.
프록시의 특징
프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 클래스와 겉 모양이 같다. 따라서 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용할 수 있다.
프록시 객체는 실제 객체에 대한 참조(target)을 보관한다. 그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화
프록시 객체는 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.
프록시 초기화 과정 분석해보면
프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.
프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
프록시의 특징
프록시 객체는 처음 사용할때 한번만 초기화된다.
프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.(무슨 말일까..)
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
초기화는 영속성 컨텍스트의 도움을 받아야 가능하다.
준영속 상태(영속성 컨텍스트의 도움을 받을수 없는 상태)의 프록시를 초기화하면 문제가 발생한다.(하이버네이트는 LazyInitializationException 예외를 발생시킴)
1.2 프록시와 식별자(잘 이해가 가지 않음)
엔티티를 프록시로 조회할 때 PK 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출해도 프록시를 초기화하지 않는다.
@Access(AccessType.PROPERTY)로 설정한 경우에만 초기화하지 않는다.
@Access(AccessType.FIELD)로 설정하면 JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화한다.
Member member = em.find(Member.class, "member1");
Team team = em.getReference(Team.class, "team1");
member.setTeam(team);
연관관계를 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근횟수를 줄일수 있다.
JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
클래스명을 직접 출력해보면 클래스 명 뒤에 ..javaassist..라 되어 있는데 이것으로 조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인할 수 있다.
하이버네이트의 initialize() 메소드를 사용하면 프록시를 강제로 초기화할 수 있다.(JPA 표준에는 강제 초기화 메서드가 존재하지 않음)
2. 즉시 로딩과 지연 로딩
프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.
JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 즉시 로딩과 지연로딩 하는 방법을 제공한다.
즉시로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회 (@ManyToOne(fetch = FetchType.EAGER))
지연로딩 : 연관된 엔티티를 실제 사용할 때 조회 (@ManyToOne(fetch = FetchType.LAZY))
2.1 즉시 로딩
// 즉시로딩 설정
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
// 즉시로딩 실행 코드
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
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 OUTTER JOIN TEAM T
ON M.TEAM_ID=T.TEAM_ID
WHERE
M.MEMBER_ID='member1'
회원과 팀을 즉시 로딩으로 설정하면 em.find(Member.class, "member1")로 회원을 조회하는 순간 팀도 함꼐 조회한다.
회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행할 것 같지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인쿼리를 사용한다.
이후 member.getTeam()을 호출하면 이미 로딩된 팀1 엔티티를 반환한다.
(참고)NULL 제약조건과 JPA 조인전략
위의 실행 sql을 보면 내부조인이 아닌 외부조인을 사용했다.
회원 테이블의 TEAM_ID 외래 키는 NULL 값을 허용하고 있어 ㅊ팀에 소속되지 않은 회원이 있을 가능성이 있다.
팀에 소속되지 않은 회원을 내부조인하면 팀은 물론 회원데이터도 조회할 수 없다.
JPA는 이러한 상황을 고려 외부조인을 사용한다. 하지만 외부보단 내부조인이 성능 최적화에 유리하다.
외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장하므로 내부 조인만 사용해도 된다.
이때 JPA에도 이러한 사실을 알려줘야 하는데 @JoinColumn(nullable=false)를 설정하면 JPA는 외부대신 내부조인을 사용한다.
2.2 지연 로딩
// 지연로딩 설정
@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()l // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용
-- em.find(Member.class, "member1") 호출시
SELECT * FROM MEMBER
WHERE MEMBER_ID = "member1"
-- team.getName() 호출로 프록시 객체가 초기화 되면
SELECT * FROM TEAM
WHERE TEAM_ID = "team1"
회원과 팀을 지연 로딩으로 설정하면 em.find(Member.class, "member1")를 호출하면 회원만 조회되고 팀은 조회하지 않는다.
대신 조회한 회원의 team 멤버변수에 프록시 객체를 넣어둔다.
이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미루는데 이것을 지연로딩이라 한다.
조회 대상이 영속성 컨텍스트에 이미 있으면 프록시 객체를 사용할 이유가 없으므로 실제 객체를 사용한다.
2.3 즉시 로딩, 지연 로딩 정리
처음부터 연관된 엔티티를 모두 영속성 컨텍스트에 올려두는 것은 현실적이지 않다.
또 필요할 때마다 SQL을 실행해서 연관된 엔티티를 지연 로딩하는 것도 최적화 관점에서 보면 꼭 좋은 것만은 아니다.
결국 연관된 엔티티를 즉시 로딩하는 것이 좋은지 아닌면 실제 사용할 떄까지 지연해서 로딩하는 것이 좋은지는 상황에 따라 다르다.(어떤것이 지연로딩 할때 좋을지 생각해보면 좋을꺼같다. 내생각엔 연관관계에서 로딩할때 자주 함께 사용되어지면 즉시, 연관관계에서 가끔 같이 사용되면 지연로딩을 하는게 좋을꺼 같음)
3. 지연 로딩 활용
3.1 프록시와 컬렉션 래퍼
하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라 한다.
getClass().getName()을 호출해보면 org.hibernate.collection.internal.PersistentBag을 반환하는 것을 볼 수 있다.
컬렉션 또한 컬렉션에서 실제 데이터를 조회할때 데이터베이스를 조회해서 초기화한다.(프록시 역할을 한다.)
3.2 JPA 기본패치 전략
JPA의 기본패치 전략
연관된 엔티티가 하나면 즉시로딩
연관된 엔티티가 컬렉션이면 지연로딩(컬렉션을 로딩하는 것은 너무 많은 비용이 들수 있기 때문)
추천하는 방법은 모든 연관관계에 지연로딩을 사용한뒤 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 하도록 최적화 하는 방법이다.
3.3 컬렉션에서 FetchType.EAGER 사용 시 주의점
컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
서로 다른 컬렉션을 2개 이상 조인하면 너무 많은 데이터를 반환할 수 있고 JPA는 이렇게 조회된 결과를 메모리에서 필터링해서 반환하는데 결과적으로 에플리케이션 성능이 저하될 수 있다.
컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
다대일 관계에서 외래 키에 not null 제약 조건을 걸어두면 항상 내부조인을 사용해도 된다.
반대로 예를 들어 팀 테이블에서 회원 테이블로 일대다 관계를 조인할 때 회원이 한명도 없는 팀을 내부조인하면 팀이 조회되지 않는 문제가 발생한다.
데이터베이스 제약조건으로 이런 상황을 막을 수는 없으므로 일대다 관계를 즉시 로딩할 때는 항상 외부조인을 사용한다.
(잘 이해가 가지 않음 p.306)
FetchType.EAGER 설정 조인 전략
@ManyToOne, @OneToMany
(optional = false): 내부 조인
(optional = true): 외부 조인
@OneToMany, @ManyToOne
(optional = false): 외부 조인
(optional = true): 내부 조인
4. 영속성 전이: 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;
...
}
// 부모 자식 저장
private static void saveNoCascade(EntityManager em) {
// 부모 저장
Parent parent = new Parent();
em.persist(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);
}
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만드는데 이럴 때 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.
4.1 영속성 전이: 저장
// CASCADE 옵션 적용
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
...
}
// CASCADE 저장 코드
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);
}
부모를 영속화할 때 연관된 자식들도 함께 영속화하라고 CascadeType.PERSIST 옵션을 설정했다.
영속성 전이를 사용할때 CascadeType.REMOVE로 설정하고 부모 엔티티만 삭제하면 자식 엔티티도 함께 삭제 된다.
코드를 실행하면 DELETE SQL을 3번 실행하고 부모는 물론 연관된 자식도 모두 삭제한다.
삭제 순서는 외래키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제한다.
당연히 CascadeType.REMOVE를 설정하지 않고 이 코드를 실행한다면 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해 데이터베이스에서 외래키 무결성 예외가 발생한다.
4.3 CASCADE의 종류
// CascadeType 코드
public enum CascadeType {
ALL, // 모두
PERSIST, // 영속
MERGE, // 병합
REMOVE, // 삭제
REFRESH, // REFRESH
DETACH // DETACH
}
PERSIST, REMOVE는 em.persist(), em.remove()를 실행할 때 바로 전이가 발생하지 않고 플러시를 호출할 때 전이가 발생한다.
5. 고아 객체
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체제거라 한다.
부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제 되도록 하려면 (orphanRemoval = true)를 설정하면 컬렉션에서 제거한 엔티티는 자동으로 삭제된다.
// 고아 객체 제거 기능 설정
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
...
}
// 사용 코드
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 첫번째 자식 엔티티를 컬렉션에서 제거
8. 프록시와 연관관계 관리
1. 프록시
프록시랑 지연로딩에 대해 설명
1.1 프록시 기초
프록시의 특징
프록시 객체의 초기화
프록시의 특징
1.2 프록시와 식별자(잘 이해가 가지 않음)
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출해도 프록시를 초기화하지 않는다.
@Access(AccessType.PROPERTY)
로 설정한 경우에만 초기화하지 않는다.@Access(AccessType.FIELD)
로 설정하면 JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화한다.1.3 프록시 확인
2. 즉시 로딩과 지연 로딩
@ManyToOne(fetch = FetchType.EAGER
))@ManyToOne(fetch = FetchType.LAZY
))2.1 즉시 로딩
(참고)NULL 제약조건과 JPA 조인전략
@JoinColumn(nullable=false)
를 설정하면 JPA는 외부대신 내부조인을 사용한다.2.2 지연 로딩
2.3 즉시 로딩, 지연 로딩 정리
3. 지연 로딩 활용
3.1 프록시와 컬렉션 래퍼
3.2 JPA 기본패치 전략
3.3 컬렉션에서 FetchType.EAGER 사용 시 주의점
컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
FetchType.EAGER 설정 조인 전략
@ManyToOne
,@OneToMany
@OneToMany
,@ManyToOne
4. 영속성 전이: CASCADE
4.1 영속성 전이: 저장
4.2 영속성 전이: 삭제
4.3 CASCADE의 종류
5. 고아 객체
@OneToMany
,@OneToOne
에만 사용할 수 있다.8.6 영속성 전이 + 고아 객체, 생명주기