인프런에서 에서 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 듣고 쓴 정리 글입니다.
https://www.inflearn.com/course/ORM-JPA-Basic
평소에 Spring Data JPA 를 썼는데, 김영한님은 JPA 자체를 강의하시더라구요.
김영한님 강의 바탕으로 Spring Data JPA로 강의 소스를 테스트해보고 개념을 기록하기 위해 포스팅을 하게되었습니다.
프록시와 연관관계 관리
1. 프록시
Member를 조회할 때 Team도 함께 조회해야 할까?
- Member 가져올 때 Team도 함께 출력
- jpa에서 member가져올 때 team도 가져오면 좋다.
- Member 가져올때 오로지 member만!
- jpa에서 member가져올 때 team도 가져오면 안좋다!
1-1. 프록시 기초
- em.find() : DB를 통해서 실제 엔티티 객체조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
DB의 쿼리가 안나가는데 조회가 되는 것
@Test
public void 멤버와조회할때_팀도함께_조회() {
Member findMember = entityManager.find(Member.class, 1L);
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
}
select
member0_.member_id as member_i1_0_0_,
member0_.team_id as team_id3_0_0_,
member0_.username as username2_0_0_,
team1_.member_id as member_i1_1_1_,
team1_.name as name2_1_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.member_id
where member0_.member_id=?
자동적으로 Member를 조회하는데 Team도 join이 되서 같이 조회가된다.
@Test
public void 멤버만_조회() {
Member findMember = entityManager.getReference(Member.class, 1L);
}
이 경우 select 쿼리가 안나간다!!
@Test
public void 멤버만_조회() {
Member findMember = entityManager.getReference(Member.class, 1L);
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
}
이 경우에는 select 쿼리가 나간다!
getReference() 를 호출하는 시점에는 DB에 Query를 호출하지 않는다.
이 값이 실제 사용되는 시점 (username)에 DB에 Query를 호출한다.
System.out.println("findMember = " + findMember.getClass());
findMember = class com.jyami.jpalab.domain.Member$HibernateProxy$injSwDL2
이름이 Member가 아니다! HibernateProxy : 강제로 만든 가짜클래스이다 : 프록시 클래스
1-2. 프록시 특징
- 실제 클래스를 상속 받아서 만들어짐
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지는 구분하지 않고 사용하면 됨 (이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
em.getReference(Member.class, 1L); //프록시객체 가져온다.
getName() > Member target에 값이 없다 > 영속성 컨텍스트에 실제 값 가져오라 요청 > db가 그 값을 가져오고, Proxy객체에 진짜 객체를 연결시켜준다. 그래서 target.getName()으로 name을 가져온다.
영속성 컨텍스트에 초기화 요청 : 프록시에 값이 없을 때 DB에서 진짜 값을 달라.
1-3. 프록시 객체 매커니즘
- 프록시 객체는 처음 사용할 때 한 번만 초기화
@Test
public void 프록시_테스트() {
Member findMember = entityManager.getReference(Member.class, 1L);
System.out.println("1st = " + findMember.getUsername());
//1st에서는 query가 나간다.
System.out.println("2nd = " + findMember.getUsername());
//2nd에서는 query가 나가지 않는다.
}
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
@Test
public void 프록시_테스트() {
Member findMember = entityManager.getReference(Member.class, 1L);
System.out.println("before findMember = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("after findMember = " + findMember.getClass());
}
before findMember = class com.jyami.jpalab.domain.Member$HibernateProxy$EYDMo7wU
Hibernate: [select query]
findMember.username = member1
after findMember = class com.jyami.jpalab.domain.Member$HibernateProxy$EYDMo7wU
- 프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, instance of 사용) => 프록시로 넘어올지, 원래 객체 타입으로 넘어올지 모른다
@Test
public void 프록시_엔티티상속_테스트() {
Member member1 = Member.builder()
.username("member1")
.build();
memberRepository.save(member1);
Member member2 = Member.builder()
.username("member2")
.build();
memberRepository.save(member1);
entityManager.clear();
Member m1 = entityManager.find(Member.class, member1.getId());
Member m2 = entityManager.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // false
System.out.println("m1 instanceof : " + (m1 instanceof Member)); // true
System.out.println("m2 instanceof : " + (m2 instanceof Member)); // true
}
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
@Test
public void 프록시_영속성_테스트() {
Member member1 = Member.builder()
.username("member1")
.build();
memberRepository.save(member1);
entityManager.clear();
Member m1 = entityManager.find(Member.class, member1.getId()); //영속성 상태
System.out.println("m1 = " + m1.getClass());
Member references = entityManager.getReference(Member.class, member1.getId());
System.out.println("reference = " + references.getClass());
}
m1 = class com.jyami.jpalab.domain.Member
reference = class com.jyami.jpalab.domain.Member
멤버를 이미 1차 캐싱했는데 굳이 proxy로 가져오는게 의미가 없다.
JPA는 한 트랜잭션에서 같은거를 보장해준다.
한 영속성 컨텍스트에서 가져온거면 true.System.out.println(m==reference) // true로 무조껀 만들어 줘야한다 : proxy가 아닌 실 값 가져옴
Member reference1 = entityManager.getReference(Member.class, member1.getId());
System.out.println("reference1 = " + reference1.getClass());
Member reference2 = entityManager.getReference(Member.class, member1.getId());
System.out.println("reference2 = " + reference2.getClass());
System.out.println("a == a" + (reference1 == reference2)); //true
reference1 = class com.jyami.jpalab.domain.Member$HibernateProxy$Xr6pfd5T
reference2 = class com.jyami.jpalab.domain.Member$HibernateProxy$Xr6pfd5T
같은 프록시 객체를 가져온다. a == a 를 보장해주어야 하기 때문이다.
Member refMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
Member findMember = entityManager.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("a == a" + (refMember == findMember));
refMember = class com.jyami.jpalab.domain.Member$HibernateProxy$HbLZp8PQ
Hibernate: [select 쿼리]
findMember = class com.jyami.jpalab.domain.Member$HibernateProxy$HbLZp8PQ
find() 에서도 proxy가 반환된다!!
proxy를 한번 조회되면 em.find()에서 proxy를 반환해버린다! == 비교를 완료하려고
"프록시든 아니든 개발에 문제가 없게 하는게 중요하다."
- 영속성 컨텍스트의 도움을 받을 수 있는 준영속 상태일 때, 프록시를 초기화
Member refMember = entityManager.getReference(Member.class, member1.getId()); //영속성 상태
System.out.println("refMember = " + refMember.getClass());
entityManager.detach(refMember); //영속성 컨텍스트 관리 안한다.
entityManager.close();
assertThatThrownBy(() -> {
refMember.getUsername();
}).isInstanceOf(org.hibernate.LazyInitializationException.class);
에러 : could not initialize proxy - no Session
영속성 컨텍스트의 도움을 받지 못해서 proxy에 연결되었던 객체에 대한 target이 없어지는 듯
그래서 transaction 설정과 proxy 설정을 같게 하려고 한다~
1-4. 프록시 확인
-
프록시 인스턴스의 초기화 여부 확인
persistenceUnitUtil.isLoaded(Object entity)
System.out.println("isLoaded = " + entityManagerFactory.getPersistenceUnitUtil().isLoaded(refMember));
-
프록시 클래스 확인 방법
entity.getClass().getName() 출력 (..javasist.. or HibernateProxy..)
System.out.println("refMember = " + refMember.getClass()); //클래스 확인 System.out.println(refMember.getUsername()); //강제 호출
-
프록시 강제 초기화
System.out.println("refMember = " + refMember.getClass()); Hibernate.initialize(refMember); // 강제 초기화
-
참고: JPA 표준은 강제 초기화 없음
강제 호출: member.getName()
2. 즉시로딩과 지연로딩
2-1. 지연로딩 LAZY를 사용해서 프록시로 조회
멤버 클래스만 DB에서 조회한다.
@ManyToOne(fetch = FetchType.LAZY) ///fecth 설정을 해준다.
@JoinColumn(name = "TEAM_ID")
private Team team;
@Test
public void 지연로딩() {
Member member = memberRepository.findById(1L).get();
assertThat(member.getUsername()).isEqualTo("MemberDefault");
System.out.println("m = " + member.getTeam().getClass());
//getTeam()은 프록시 가져오는 것
}
Hibernate: select
member0_.member_id as member_i1_0_0_,
member0_.team_id as team_id3_0_0_,
member0_.username as username2_0_0_
from
member member0_
where
member0_.member_id=?
m = class com.jyami.jpalab.domain.Team$HibernateProxy$gs0vf0Qv
멤버만 나가는 걸 알 수 있다!
그리고 Team은 proxy 객체를 가져온다.
System.out.println("team.name = " + member.getTeam().getName());
select
team0_.member_id as member_i1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.member_id=?
그래서 위와 같이 영속성 컨텍스트 초기화를 하게 될 때 그때 쿼리가 나간다.
- Member에서 Team을 가져올 때 Lazy로 설정해두었기 때문에,
Team 객체 안에는 프록시 객체를 넣어둔다.
실제 team을 사용하는 시점에 영속성 컨텍스트 초기화를 한다. - BM 상에서 Member조회시 Team을 같이 조회하지 않을 때 LAZY를 사용하면!
2-2. 즉시로딩 EAGER를 사용해서 함께 조회
@ManyToOne(fetch = FetchType.EAGER) ///fecth 설정을 해준다.
@JoinColumn(name = "TEAM_ID")
private Team team;
Hibernate: insert into team (member_id, name) values (null, ?)
Hibernate: insert into member (member_id, team_id, username) values (null, ?, ?)
Hibernate: select
member0_.member_id as member_i1_0_0_,
member0_.team_id as team_id3_0_0_,
member0_.username as username2_0_0_,
team1_.member_id as member_i1_1_1_,
team1_.name as name2_1_1_
from
member member0_
left outer join
team team1_ on member0_.team_id=team1_.member_id
where
member0_.member_id=?
m = class com.jyami.jpalab.domain.Team
즉시 로딩이기 때문에 Proxy를 가져올 필요가 없어서
getClass() 를 했을 때 실제 객체가 나온다!
proxy를 가져오지 않으니까 영속성 컨텍스트 초기화를 해줄 필요가 없다.
BM 상에서 Mebmer를 쓸때 항상 Team도 조회할 경우!
JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회
2-3. 프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용(특히 실무에서)
만약 관련 링크객체가 N개면 N개만큼 Join이 발생해서 나간다. - 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
- 즉시로딩을 JPQL에서 N+1 문제를 일으킨다.
- @ManyToOne, @OneToOne은 기본이 즉시로딩 -> LAZY로 설정 (X To One 시리즈)
- @OneToMany, @ManyToMany는 기본이 지연 로딩
2-3-1. JPQL N+1 문제 preview
@Test
public void JPQL의_N_플러스_1_문제() {
List<Member> members = entityManager.createQuery("select m from Member m", Member.class)
.getResultList();
}
Hibernate: select
member0_.member_id as member_i1_0_,
member0_.team_id as team_id3_0_,
member0_.username as username2_0_
from
member member0_
Hibernate: select
team0_.member_id as member_i1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.member_id=?
- 쿼리가 두번나간다!!
- JPQL : 1번째 파라미터가 sql query로 그대로 읽힌다. 따라서 쿼리대로 Member를 가져온다. 근데 Team이 즉시로딩이 되어있음! 즉시로딩이라 무조껀 그안에 값이 들어가 있어야 하기 때문에 Team도 가져온다. 따라서 Team 쿼리를 또 따로 보낸다.
-
쿼리가 N+1 나간다
- 1 : 처음에 내보낸 쿼리 (N개의 Member 리턴)
- N : EAGER 설정이 되어있어 참조 객체를 가져오기 위한 추가 쿼리 (N개의 Member 각각의 Team 값을 채우기 위해 각 Team을 찾기위해 N개의 쿼리가 나간다.)
- 이걸 LAZY로 잡으면 그냥 Member만 가져오고, Team은 proxy 객체라서 쿼리가 1개만 나가게된다.
-
해결 기본은 fetchJoin : runtime에 동적으로 내가 원하는애들만 선택해서 가져온다.
application안에서도 member만 가져올 때 / member + team 가져올때가 구분되기 때문에
이 한방 쿼리에 모든게 들어가 있다.List<Member> members = entityManager.createQuery("select m from Member m join fecth m.team", Member.class).getResultList();
2-4. 지연 로딩 활용
지금은 굉장히 이론적이고, 실무에서는 그냥 다 LAZY로 해야한다.
- Member와 Team은 자주 함께 사용 : 즉시로딩
- Member와 Order는 가끔 사용 : 지연로딩
- Order와 Product는 자주 함께 사용 : 즉시로딩
3. 영속성 전이: CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속성 상태로 만들고 싶을 때
- 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
- 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없음
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
@Entity
@NoArgsConstructor
@Getter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
//cascade 옵션 : Parent를 저장할 때 child도 같이 저장하고 싶다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
@Builder
public Parent(String name) {
this.name = name;
}
}
@Entity
@Getter
@NoArgsConstructor
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn
private Parent parent;
@Builder
public Child(String name, Parent parent) {
this.name = name;
this.parent = parent;
parent.getChildList().add(this); //양방향 위해 추가함!
}
}
[테스트 코드]
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ParentTest {
@Autowired
ParentRepository parentRepository;
@Autowired
ChildRepository childRepository;
@Autowired
EntityManager entityManager;
@Before
public void setUp() throws Exception {
Parent parent = Parent.builder()
.name("parent")
.build();
Child child1 = Child.builder()
.parent(parent)
.name("child1")
.build();
Child child2 = Child.builder()
.parent(parent)
.name("child2")
.build();
parentRepository.save(parent);
entityManager.clear(); //영속성 컨텍스트 제거
}
@Test
public void Parent만_저장해도_Child_저장되는지_확인(){
Parent parent = parentRepository.findById(1L).get();
for(Child child: parent.getChildList()){
assertThat(child.getName()).startsWith("child");
}
}
}
Hibernate: insert into parent (id, name) values (null, ?)
Hibernate: insert into child (id, name, parent_id) values (null, ?, ?)
Hibernate: insert into child (id, name, parent_id) values (null, ?, ?)
---
Hibernate: select parent0_.id as id1_2_0_, parent0_.name as name2_2_0_ from parent parent0_ where parent0_.id=?
Hibernate: select childlist0_.parent_id as parent_i3_0_0_, childlist0_.id as id1_0_0_, childlist0_.id as id1_0_1_, childlist0_.name as name2_0_1_, childlist0_.parent_id as parent_i3_0_1_ from child childlist0_ where childlist0_.parent_id=?
심플하게 Parent를 저장할 때, Parent안에 있는 객체인 Child도 같이 저장할 때
3-1. CASCADE의 종류
- ALL : 모두 적용
- PERSIST : 영속 - 저장할 때만 lifecycle을 맞출래
- REMOVE : 삭제
- MERGE : 병합
- REFERESH : refresh
- DETACH : detach
하나의 부모가 자식들을 관리할 때는 의미가 있다.
ex ) 게시판에 댓글, 첨부파일의 경로 등이 들어갈 때 : 의미 있음그러나 여러 엔티티에서 관리한다면 쓰면 안된다.
소유자가 하나일 때는 써도 된다.
단일 엔티티에 완전히 종속적일 때 사용하자
Child와 Parent의 lifecycle이 완전히 비슷할 때 사용하자
4. 고아객체
- 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제 JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라 한다. 이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제 된다.
-
orphanRemoval = true
Parent parent1 = em.find(Parent.class, id); parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거
연관관계가 끊어져버린 상태 > delete가 나간다.DELETE FROM CHILD WHERE ID = ?
public class Parent{ @OneToMany(mappedBy = "parent", orphanRemoval = true) // orphanRemoval 옵션 추가 private List<Child> childList = new ArrayList<>(); }
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함!!
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
-
참고 : 개념적으로 부모를 제거하면 자식은 고아가된다.
따라서 고아 객체 기능을 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다.
이것은 CascadeType.REMOVE 처럼 동작한다.
흠 근데 왜 난 안되지ㅠㅠ 물어봐야겠다.
5. 영속성 전이 + 고아 객체, 생명주기
public class Parent{
@OneToMany(mappedBy = "parent", cascade = CascadeType=ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
}
- CasecadeType.ALL + orphanRemovel = true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다. 자식 repository가 필요 없어진다.
- 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용
'Develop > Springboot' 카테고리의 다른 글
springboot - application.yml 설정 (0) | 2019.11.20 |
---|---|
[JPA] 고급매핑 - 상속 관계 , 매핑 정보 상속 (4) | 2019.10.19 |
[JPA] 다양한 연관관계 매핑 - @OneToMany @ManyToOne @OneToOne @ManyToOne (1) | 2019.10.18 |
[JPA] 연관관계 매핑 (0) | 2019.10.18 |
[inflearn] 스프링 부트 개념과 활용 2.스프링 부트 시작하기 (0) | 2019.10.12 |