[JPA] JPA 프로그래밍 기본기 다지기 6강 - JPA내부 구조

업데이트:

JPA에서 가장 중요한 2가지

  • 객체와 관계형 데이터베이스 매핑하기(Object Relation Mapping)
  • 영속성 컨텍스트

영속성 컨텍스트를 잘 이해하면 트러블슈팅을 제대로 할 수 있다.

💡JPA내부 구조

✔엔티티 매니저 팩토리와 엔티티 매니저

jpaimage9
JPA는 EntityManagerFactory에서 유저의 요청이 올 때마다 EntityManager를 별도로 생성한다. EntityManager는 내부적으로 데이터베이스 커넥션 풀에서 커넥션을 가져와 DB에 액세스 작업을 한다.


✔영속성 컨텍스트

“엔티티를 영구 저장하는 환경”이라는 뜻

  • JPA를 이해하는데 가장 중요한 용어
  • EntityManager.pesist(entity);


✔엔티티 매니저? 영속성 컨텍스트?

  • 영속성 컨텍스트는 논리적인 개념(눈에 보이지 않는다)
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하게 된다(쉽게 엔티티매니저는 영속성 컨텐스트다 라고 생각해도 된다.) jpaimage10
  • J2EE, 스프링 프레임워크 같은 컨테이너 환경: 스프링 프레임워크에서 EntityManager 객체를 의존주입 받을 수 있는 데 같은 트랜잭션 안에서 의존주입을 받으면 같은 객체를 참조하게 되고 이는 같은 영속성 컨텍스트에 접근하게 된다는 뜻이 된다.


✔엔티티의 생명주기

jpaimage11

비영속(new/transient)

영속성 컨텍스트와 전혀 관계가 없는 상태

jpaimage12

객체를 생성한 상태

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
  • member객체를 생성만 하고 JPA등록 요청은 안한 상태

영속(managed)

영속성 컨텍스트에 저장된 상태

jpaimage13

//객체를 생성한 상태(비영속)

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 객체를 집어넣으면 영속상태가 된다. 영속 상태란 영속성 컨텍스트 안에서 관리된다는 의미이다.

준영속(detached)

영속성 컨텍스트에 저장되었다가 분리된 상태

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태  
em.detach(member);  

삭제(removed)

삭제된 상태, 지우고 DB에서 날려버림

//객체를 삭제한 상태(삭제)  
em.remove(member);


✔영속성 컨텍스트의 이점

그렇다면, 영속성 컨텍스트란 건 중간에 왜 필요한 걸까?

1차 캐시
동일성(identity) 보장
트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
변경 감지(Dirty Checking)
지연 로딩(Lazy Loading)

엔티티조회, 1차 캐시

jpaimage14

//객체를 생성한 상태(비영속)
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차 캐시에서 조회

jpaimage15

//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//객체를 저장한 상태(영속), 1차 캐시에 저장됨.
em.persist(member);

//DB에 안가고 먼저 1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
  • em.find(Member.class, “member1”) 코드로 Member 클래스를 조회하면 DB를 안 가고 먼저 1차캐시를 검색한다. 검색을 해서 존재하면 바로 반환한다. 1차캐시는 글로벌 캐시가 아니다. 쓰레드가 생성되고 끝날때까지 잠깐 사용하는 것이다. 즉, 요청이 100개가 오면 영속성 컨텍스트가 100개가 생기고 캐시도 100개가 생긴다. 요청이 끝나면 다 사라진다. 서로 공유하지 않는다. 트랜잭션이 시작하고 끝날 때까지만 유지된다.

데이터베이스에서 조회

jpaimage16

Member findMember2 = em.find(Member.class, "member2");
  • 1차 캐시에 member2가 없으면 DB를 조회하고 나서 가져온 member2를 1차 캐시에 넣고 반환한다.

영속 엔티티의 동일성 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member2");

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)
    jpaimage17
    1차 캐시에 저장하고 INSERT문을 말아서 쌓아둔다.

  • em.persist(memberB) jpaimage18
    역시나, 1차 캐시에 저장하고 INSERT문을 말아서 쌓아둔다.

  • transaction.commit() jpaimage19

    • em.flush(): 커밋을 하면 쓰기 지연 SQL 저장소에서 쌓아둔 INSERT문을 동시에 DB로 보낸다.
    • 자동으로 flush, commit 두가지의 일을 한다.

엔티티 수정(변경 감지)

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();  //트랜잭션 시작
 
//영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
 
//영속 엔티티 데이터 수정
//9
memberA.setUsername("hi");

//10
memberA.setAge(10);
 
//em.update(member) 이런 코드가 있어야 하지 않을까? => ㄴㄴ필요없음
 
transaction.commit(); // 트랜잭션 커밋
  • 엔티티 수정시 라인 9, 10과 같이 값만 바꾸고 transaction.commit()과 같이 트랜잭션 커밋을 하면 자동으로 업데이트 쿼리가 나간다.

변경감지(Dirty Checking)

jpaimage20

  • 사실은 1차캐시가 생성되는 시점에 스냅샷이라는 항목을 하나 더 둔다. commit이나 flush를 하게 되면 1차캐시에서 엔티티와 스냅샷의 값을 비교한다. 비교해서 바뀐게 있으면 UPDATE 쿼리를 생성해서 DB에 보낸다. 그래서 위 코드와 같이 값만 변경해도 트랜잭션 커밋시 JPA에서 엔티티를 찾아온 다음(영속상태) 엔티티와 스냅샷의 값을 비교하여 변경된 값이 있으면 UPDATE 쿼리를 생성하여 DB에 보내서 값이 변경되는 것이다. 영속성 컨텍스트에서 하는 일은 크게 1. 영속상태에 있는 엔티티를 반환하는 것과 2.변경감지이다. 변경감지는 영속성 컨텍스트에서 어떤 값이 변경되었는지 아는 것이다. 영속성 컨텍스트에서 관리된는 객체는 뭐가 변경됬는지 다 안다. 필드 값 하나만 변경해도 안다. 그래서 트랜잭션 commit()을 하거나 엔티티매니저에서 flush를 하면 UPDATE 쿼리를 DB에 날린다.
  • 그렇다면 왜 이런 변경감지 기능을 만들 것일까? 이 것은 사상때문에 그렇다. 예를 들어 자바 컬렉션에서 List에서 객체를 가져온 다음 그 객체의 값을 변경하고 다시 List.add()를 하지않아도 객체에 있는 값은 변경된다. JPA는 이 것과 똑같은 컨셉인 것이다. 마치 자바 컬렉션에서 값을 가져와서 변경한 것처럼 JPA도 변경할 수 있게 하기 위해서 위와 같이 한 것이다.

실습

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) {
...

엔티티 삭제

//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); //엔티티 삭제 (트랜잭션 커밋 시점에 DELETE 쿼리가 나가면서 삭제된다.  

플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영

플러시 발생

  • 변경 감지(1차캐시의 스냅샷을 다 비교)
  • 수정된 엔티티 “쓰기 지연 SQL 저장소”에 등록
  • “쓰기 지연 SQL 저장소”의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리가 일괄로 나간다.)

영속성 컨텍스트를 플러시하는 방법

  • em.flush() - 직접 호출
  • 트랜잭션 커밋 - 플러시 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동 호출


✔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();  
  • 중간에 flush를 하지 않았기 때문에 결과값이 없을 것이다. 그래서 JPQL에서는 쿼리 실행 시 자동으로 flush가 되게한다.
  • 마이바티스나 JdbcTemplate 라이브러리와 같이 사용하면(JPQL 대신에) flush를 꼭해줘야 한다.

플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT);

  • FlushModeType.AUTO(커밋이나 쿼리를 실행할 때 플러시(기본값)
  • FlushModeType.COMMIT(커밋할 때만 플러시)

플러시는!

  • 영속성 컨텍스트를 비우진 않음!
  • 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는것이 목적!(비우는 건 clear())
  • 트랜잭션이라는 작업 단위가 중요 → 커밋 직전에만 동기화하면 됨

준영속 상태

  • 영속 → 준영속
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

준영속 상태로 만드는 방법

  • em.detach(entity): 특정 엔티티만 준영속 상태로 전환
  • em.clear(): 영속성 컨텍스트를 완전히 초기화
  • em.close(): 영속성 컨텍스트를 종료
  • em.clear()를 하면 영속성 컨텍스트를 완전히 초기화하는데 값을 수정해도 UPDATE 문이 안나간다.

실습

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) {

Member를 조회할 때 Team도 함께 조회해야 할까?

단순히 member 정보만 사용할 경우 함께 조회할 필요가 없다.
이럴땐 LAZY를 걸어주면 된다.

지연 로딩 LAZY을 사용해서 프록시로 조회

jpaimage21

@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;
   ...
}
  • @ManyToOne(fetch = FetchType.LAZY) 설정을 하면 member만 조회하면 member만 조회가 된다.
  • 어플리케이션 로딩 시점에 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를 쓰지 않고 LAZY로 걸고 fetchjoin이라는 게 있어서 조회하는 시점에 한 번에 가져올 수 있다.

@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;
   ...
}

프록시와 즉시로딩 주의

  • 가급적 자연 로딩을 사용
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 → LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연로딩

지연로딩을 하려면 프록시를 사용해야하는데 그럴려면 영속성 컨텍스트가 살아있어야 한다.

태그:

카테고리:

업데이트: