JPA 6강 - JPA 내부구조
- 영속성 컨텍스트
- 프록시와 즉시로딩, 지연로딩
JPA에서 가장 중요한 2가지
- 객체와 관계형 데이터베이스 매핑하기(Object Relation Mapping)
- 영속성 컨텍스트
영속성 컨텍스트를 알고 있으면 트러블슈팅을 제대로 할 수 있다.
엔티티 매니저 팩토리와 엔티티 매니저
JPA는 EntityManagerFactory에서 유저의 요청이 올 때마다 EntityManager를 별도로 생성한다. EntityManager는 내부적으로 데이터베이스 커넥션 풀에서 커넥션을 가져와 DB에 액세스 작업을 한다.
영속성 컨텍스트
- JPA를 이해하는데 가장 중요한 용어
- "엔티티를 영구 저장하는 환경"이라는 뜻
- EntityManager.pesist(entity);
엔티티 매니저? 영속성 컨텍스트?
- 영속성 컨텍스트는 논리적인 개념
- 눈에 보이지 않는다.
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
J2SE 환경
J2EE, 스프링 프레임워크같은 컨테이너 환경
엔티티 매니저와 영속성 컨텍스트가 N:1
스프링 프레임워크에서 EntityManager 객체를 의존주입 받을 수 있는 데 같은 트랜잭션 안에서 의존주입을 받으면 같은 객체를 참조하게 되고 이는 같은 영속성 컨텍스트에 접근하게 된다는 뜻이 된다.
엔티티의 생명주기
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
하단에 Managed는 remove라고 보면된다.
비영속
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId('member1");
member.setUsername("회원1");
우선 EntityManager는 영속성 컨텍스트다. 위 코드와 같이 member 객체를 생성만 하고 JPA 등록요청은 안했다. 이걸 바로 비영속상태라고 한다.
영속
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId('member1");
member.setUsername("회원1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member);
위 코드와 같이 member 객체를 생성하여 em.persist(member)로 영속성 컨텍스트에 member 객체를 집어넣으면 영속상태가 된다. 영속 상태란 영속성 컨텍스트 안에서 관리된다는 의미이다.
준영속, 삭제
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
//객체를 삭제한 상태(삭제), DB에서도 삭제함.
em.remove(member);
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
엔티티 조회, 1차 캐시
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId('member1");
member.setUsername("회원1");
//객체를 저장한 상태(영속)
em.persist(member);
1차 캐시는 new해서 생성해서 없어질 때까지만 존재한다. member 객체를 생성하고 EntityManager를 이용해서 em.persist(member) 코드로 영속성 컨텍스트에 member를 저장하면 방금 이용한 EntityManager(영속성 컨텍스트) 안에 키가 Member 클래스의 @Id 어노테이션으로 지정한 id 필드의 값으로 지정한 "member1" 값과 밸류는 객체 자체인 member 객체가 저장된다. 이렇게 내부적으로 영속성 컨텍스트에 1차캐시가 저장된다고 보면 된다. 그래서 객체비교를 하면 같은 것이다.
1차 캐시에서 조회
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId('member1");
member.setUsername("회원1");
//객체를 저장한 상태(영속), 1차 캐시에 저장됨.
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
em.find(Member.class, "member1") 코드로 Member 클래스를 조회하면 DB를 안 가고 먼저 1차캐시를 검색한다. 검색을 해서 존재하면 바로 반환한다. 1차캐시는 글로벌 캐시가 아니다. 쓰레드가 생성되고 끝날때까지 잠깐 사용하는 것이다. 즉, 요청이 100개가 오면 영속성 컨텍스트가 100개가 생기고 캐시도 100개가 생긴다. 요청이 끝나면 다 사라진다. 서로 공유하지 않는다. 트랜잭션이 시작하고 끝날 때까지만 유지된다.
데이터베이스에서 조회
Member findMember2 = em.find(Member.class, "member2");
영속 엔티티의 동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println( a == b ); // 동일성 비교 true
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
-> 똑같은 "member1"을 조회하면 객체비교시 true가 나온다. 내부에 1차캐시가 있어서 그렇다.
엔티티 등록 (트랜잭션을 지원하는 쓰기 지연)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); //트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); //트랜잭션 커밋
위에 코드가 어떻게 가능할까? 버퍼기능에 대해서 살펴보자.
em.persist(memberA);
em.persist(memberB);
em.persist(memberA) 코드를 실행하면 일단 1차캐시에 memberA를 저장하고 쓰기 지연 SQL 저장소에 INSERT SQL 문을 생성만 해놓는다. DB에 넣진 않는다. em.persist(memberB) 코드도 마찬가지이다. 그렇다면 언제 DB에 저장할까? 바로 transaction.commit() 코드를 실행하면 저장한다.
transaction.commit();
transaction.commit() 코드를 실행하면 "쓰기 지연 SQL 저장소"에서 동시에 INSERT A, INSERT B 쿼리를 동시에 DB에 보낸다.(동시에 보낼 수도 있고 하나씩 보낼 수도 있다. 옵션설정에 따라 다름) 이 것을 flush라고 한다. flush는 SQL문을 DB에 보내지 1차캐시의 데이터를 지우진 않는다. DB와 싱크를 맞추는 것이다. flush로 SQL을 보내고 commit(tx.commit())을 하면 flush와 commit(DB 커밋)을 동시에 수행한다. 정리하자면 영속성 컨텍스트에서 위와 같은 내부적인 기능(버퍼)을 통해서 쿼리를 모으기 때문에 내부적으로 한 방에 쿼리를 보낼 수 있는 것이다.(물론 하이버테이트에서 관련 옵션을 켜야한다.)
엔티티 수정(변경감지)
- EntityManager em = emf.createEntityManager();
- EntityTransaction transaction = em.getTransaction();
- transaction.begin(); //트랜잭션 시작
- //영속 엔티티 조회
- Member memberA = em.find(Member.class, "memberA");
- //영속 엔티티 데이터 수정
- memberA.setUsername("hi");
- memberA.setAge(10);
- //em.update(member) 이런 코드가 있어야 하지 않을까?
- transaction.commit(); // 트랜잭션 커밋
엔티티 수정시 라인 9, 10과 같이 값만 바꾸고 transaction.commit()과 같이 트랜잭션 커밋을 하면 자동으로 업데이트 쿼리가 나간다.
변경감지(Dirty Checking)
사실은 1차캐시가 생성되는 시점에 스냅샷이라는 항목을 하나 더 둔다. commit이나 flush를 하게 되면 1차캐시에서 엔티티와 스냅샷의 값을 비교한다. 비교해서 바뀐게 있으면 UPDATE 쿼리를 생성해서 DB에 보낸다. 그래서 위 코드와 같이 값만 변경해도 트랜잭션 커밋시 JPA에서 엔티티를 찾아온 다음(영속상태) 엔티티와 스냅샷의 값을 비교하여 변경된 값이 있으면 UPDATE 쿼리를 생성하여 DB에 보내서 값이 변경되는 것이다. 영속성 컨텍스트에서 하는 일은 크게 1. 영속상태에 있는 엔티티를 반환하는 것과 2.변경감지이다. 변경감지는 영속성 컨텍스트에서 어떤 값이 변경되었는지 아는 것이다. 영속성 컨텍스트에서 관리된는 객체는 뭐가 변경됬는지 다 안다. 필드 값 하나만 변경해도 안다. 그래서 트랜잭션 commit()을 하거나 엔티티매니저에서 flush를 하면 UPDATE 쿼리를 DB에 날린다.
그렇다면 왜 이런 변경감지 기능을 만들 것일까? 이 것은 사상때문에 그렇다. 예를 들어 자바 컬렉션에서 List에서 객체를 가져온 다음 그 객체의 값을 변경하고 다시 List.add()를 하지않아도 객체에 있는 값은 변경된다. JPA는 이 것과 똑같은 컨셉인 것이다. 마치 자바 컬렉션에서 값을 가져와서 변경한 것처럼 JPA도 변경할 수 있게 하기 위해서 위와 같이 한 것이다.
변경감지 실습
Main클래스를 아래와 같이 작성 후 실행해보자.
- try {
- Team team = new Team();
- team.setName("teamA");
- em.persist(team);
- Member member = new Member();
- member.setName("hello");
- em.persist(member);
- team.getMembers().add(member);
- em.flush();//DB에 쿼리를 다 보낸다.
- em.clear();//영속성 컨텍스트 안에 있는 캐시를 다 지워버린다.
- //캐시를 비웠기 때문에 깨끗한 상태에서 변경감지 실습을 진행해보자.
- Member findMember = em.find(Member.class, member.getId());
- findMember.setName("변경감지");
- tx.commit();
- } catch (Exception e) {
라인 11, 12에서 DB에 쿼리를 다 보내고 영속성 컨텍스트 안에 있는 캐시를 다 지웠다. 라인 15 em.find(Member.class, member.getId()) 코드로 member를 조회하고 라인 16에서 findMember.setName("t아카데미")로 1차캐시에 있는 findMember 객체의 값을 변경했다. 그리고 라인 17에서 트랜잭션 커밋을 했다. 아래 실행결과를 보자.
위와 같이 UPDATE 쿼리가 실행되는 것을 알 수 있다. H2 콘솔 결과를 보자.
USERNAME에 변경감지로 값이 변경된 것을 확인할 수 있다. 정리하자면 아래 코드처럼 member 객체의 값을 변경하고 바로 트랜잭션 커밋을 했을 뿐이다. 현업에서도 이렇게 쓰인다. 마치 자바 컬렉션에 있는 객체를 다루듯이 다룬다. 물론 처음 저장 시에는 persist를 해줘야 한다. 사실 스냅샷이 있어서 메모리를 2배로 먹는다. 그래서 데이터를 너무 많이 유지하면 고민거리가 된다. 하지만 이런 것도 최적화하는 방법이 있다.
- findMember.setName("변경감지");
- tx.commit();
엔티티 삭제
//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); //엔티티 삭제 (트랜잭션 커밋 시점에 DELETE 쿼리가 나가면서 삭제된다.
플러시
영속성 컨텍스트의 변경내용을 데이터베이스에 반영
플러시 발생
- 변경 감지(1차캐시의 스냅샷을 다 비교)
- 수정된 엔티티 "쓰기 지연 SQL 저장소"에 등록
- "쓰기 지연 SQL 저장소"의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법
- em.flush() - 직접 호출
- 트랜잭션 커밋 - 플러시 자동 호출
- JPQL 쿼리 실행 - 플러시 자동 호출
flush를 해야 쿼리가 나간다고 보면 된다.
JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유
- em.persist(memberA);
- em.persist(memberB);
- em.persist(memberC);
- //중간에 JPQL 실행
- query = em.createQuery("select m from Member m", Member.class);
- List<Member> members = query.getResultList();
위 코드를 보면 라인 1, 2, 3에서 member 데이터를 등록하고 있고 라인 6에서 쿼리를 만들어서 라인 7에서 조회를 하고 있다. 당연히 중간에 flush를 하지 않았기 때문에 결과값이 없을 것이다. 그래서 JPQL에서는 쿼리 실행 시 자동으로 flush가 되게했다. 따라서 라인 7 query.getResutList()를 실행하면 라인 1, 2, 3에서 member 데이터들이 DB에 반영이 되고(트랜잭션 커밋이 아닌 flush가 된다.) 그 다음에 select 문을 날려서 결과 3개가 나오게 된다.
위에는 JPQL이 제공해주는 것이다. 따라서 마이바티스나 JdbcTemplate 라이브러리와 같이 사용하면(JPQL 대신에) flush를 꼭해줘야 한다.
플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT);
- FlushModeType.AUTO(커밋이나 쿼리를 실행할 때 플러시(기본값)
- FlushModeType.COMMIT(커밋할 때만 플러시)
플러시는!
- 영속성 컨텍스트를 비우진 않음!
- 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화! 이게 목적!(비우는 건 clear())
- 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화하면 됨
준영속 상태
- 영속 -> 준영속
- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
- 영속성 컨텍스트가 제공하는 기능을 사용 못함
준영속 상태로 만드는 방법
- em.detach(entity) -> 특정 엔티티만 준영속 상태로 전환
- em.clear() -> 영속성 컨텍스트를 완전히 초기화
- em.close() -> 영속성 컨텍스트를 종료
em.clear()를 하면 영속성 컨텍스트를 완전히 초기화하는데 값을 수정해도 UPDATE 문이 안나간다.
실습을 해보자.
Main 클래스를 아래와 같이 변경하자. 라인 18 em.detach(findMember)를 추가했다. 실행하고 H2콘솔을 확인해보자.
- try {
- Team team = new Team();
- team.setName("teamA");
- em.persist(team);
- Member member = new Member();
- member.setName("hello");
- em.persist(member);
- team.getMembers().add(member);
- em.flush();//DB에 쿼리를 다 보낸다.
- em.clear();//영속성 컨텍스트 안에 있는 캐시를 다 지워버린다.
- //캐시를 비웠기 때문에 깨끗한 상태에서 변경감지 실습을 진행해보자.
- //다시 영속상태가 된다.
- Member findMember = em.find(Member.class, member.getId());
- //준영속 상태
- em.detach(findMember);
- findMember.setName("변경감지");
- tx.commit();
- } catch (Exception e) {
위와 같이 USERNAME이 변경되지 않았다. 이번에 em.detach(findMember) 대신에 em.clear() 코드를 넣어서 실행해보자. clear()메서드는 영속성 컨텍스트에 있는 것을 다 날려버리는 것이다. clear를 해도 위와 같이 값이 변경되지 않는 것을 확인할 수 있을 것이다.
Member를 조회할 때 Team도 함께 조회해야 할까?
단순히 member 정보만 사용하는 비지니스 로직
println(member.getName());
지연 로딩 LAZY을 사용해서 프록시로 조회
- @Entity
- public class Member {
- @Id
- @GeneratedValue
- private Long id;
- @Column(name = "USERNAME")
- private String name;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "TEAM_ID")
- private Team team;
- ...
- }
라인 11과 같이 @ManyToOne(fetch = FetchType.LAZY) 설정을 하면 member만 조회하면 member만 조회가 된다.
지연로딩 LAZY을 사용해서 프록시로 조회
어플리케이션 로딩 시점에 team은 프록시객체(가짜 객체)로 들어오게 된다(Member 클래스 team필드). Member member = em.find(Member.class, 1L) 로 member를 조회하게 되면 member안에 있는 team 필드는 프록시 객체라는 가짜 객체가 들어가게 된다. 그리고 Team team = member.getTeam(); 코드로 member 객체 안에 있는 team 객체를 가져와서 team.getName()으로 실제 team 객체 안에 있는 필드를 조회(사용)하는 시점에 team 필드를 초기화(DB조회)하는 것이다.
Member와 Team을 자주 함께 사용한다면?
즉시 로딩 EAGER를 사용해서 함께 조회
- @Entity
- public class Member {
- @Id
- @GeneratedValue
- private Long id;
- @Column(name = "USERNAME")
- private String name;
- @ManyToOne(fetch = FetchType.EAGER)
- @JoinColumn(name = "TEAM_ID")
- private Team team;
- ...
- }
현업에서는 EAGER를 쓰지 않고 LAZY로 걸고 fetchjoin이라는 게 있어서 조회하는 시점에 한 번에 가져올 수 있다.
프록시와 즉시로딩 주의
- 가급적 자연 로딩을 사용
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
- @OneToMany, @ManyToMany는 기본이 지연로딩
지연로딩을 하려면 프록시를 사용해야하는데 그럴려면 영속성 컨텍스트가 살아있어야 한다.
'IT공부 > JPA' 카테고리의 다른 글
JPA 8강 - Spring Data JPA와 QueryDSL 이해 (0) | 2020.05.23 |
---|---|
JPA 7강 - JPA 객체지향쿼리 (0) | 2020.05.22 |
JPA 5강 - 양방향 매핑 (0) | 2020.05.21 |
JPA 4강 - 연관관계 매핑 (0) | 2020.05.21 |
JPA 3강 - 필드와 컬럼 매핑 (0) | 2020.05.21 |