본문 바로가기

Spring/JPA

JPA 내부 구조. 영속성 컨텍스트

JPA에서 가장 중요한 2가지

ORM. 객체와 관계형 데이터베이스 매핑 영속성 컨텍스트

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

엔티티 매니저 팩토리에서 유저에서 요청 올 때 마다 엔티티 매니저를 별도로 만들어준다. 이 엔티티 매니저는 JDBC에서 제공하는 커넥션 풀을 사용해서 DB와 연결된다.

영속성 컨텍스트

"엔티티를 연구 저장하는 환경"이라는 뜻

EntityManager.persist(entity);

영속성 컨텍스트는 논리적인 개념! 엔티티 매니저를 통해 영속성 컨텍스트에 접근한다.

엔티티의 생명주기

비영속, 영속, 준영속, 삭제

비영속(new/transient)

member 객체를 생성했어. 생성만 하고 JPA에 안 집어넣었어. 이 상태를 비영속 상태라고 한다.

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

영속(managed)

객체 생성해서 엔티티 매니저 내에 집어넣었어. 안에서 관리되는 상태다.

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

EntityManager em = emf.creteEntityManager();
em.getTransaction().begin();

//객체를 저장한 상태(영속)
em.persist(member);

준영속, 삭제

준영속 : 엔티티 들어왔다가 다시 내보냄. 관리 안 할게(em.detached())

Member findMember = em.find(Member.class, member.getId());
em.detach(findMember);
findMember.setName("t아카데미");//업데이트 안 되겠지?

삭제 : 영속성 컨텍스트 완전 초기화.(em.clear())

Member findMember = em.find(Member.class, member.getId());
em.clear();
findMember.setName("t아카데미"); //업데이트 안 되겠지?

종료 : 영속성 컨텍스트 종료(em.close())

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

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

영속성 컨텍스트의 이점!

-1차 캐시

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

//엔티티를 영속
em.persist(member);

연속성 컨텍스트가 db에서 생성되고 없어질 때까지만 빤짝 존재함. 메모리에 영역을 가지고 있다고 생각하면 됨. 마치 캐시처럼

key는 @id, value는 객체 자체.

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");을 하면? db에 가서 찾는 게 아니라 1차 캐시를 먼저 찾아봅니다. 어 있네? 바로 사용!

*global캐시 아니다. 잠깐 생기는 거다. 100개 요청? 100개 만들어진다잉! 굉장히 짧은 캐시다.

1차 캐시에 없다? 그러면 DB에서 조회해서 1차 캐시에 넣고 다시 반환해주는 방식으로 함. (영속성 컨텍스트)

Member findMember2 = em.find(Member.class, "member2");

-동일성 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member2");
System.out.println(a == b); // 동일성 비교 true

-트랜잭션을 지원하는 쓰기 지연

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

Transaction!

em.persist(memberA), em.persist(memberB)했을 때 논리적인 그림

그리고 transaction.commit();를 요청하면! → 다음 그림처럼 flush()하고 commit 이루어짐.

buff처럼 쓰여서 flush()로 메서드 구현해뒀음. (1차 캐시 내용 지우지 않음) 1차 캐시 다 날리는 거? em.clear();

-변경 감지 (UPDATE, Dirty Checking)

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();

//영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

//영속 엔티티 데이터 수정
memberA.setName("hi");
memberA.setAge(10);

//em.update(member) 이런 코드가 있어야 하지 않을까? 없다.

transaction.commit();

값만 바꾸고 커밋하면 자동으로 update쿼리가 나갑니다....

1차 캐시 생성되는 시점에 스냅샷이라는 게 생깁니다. (...ㅎㅎ 근데 메모리 더 먹음) 여기서 transaction.commit(); 명령 내리면, Entity랑 스냅샷 다 비교합니다.(와우...) 그래서 바꼈다? update 쿼리문 써서 보냅니다..

영속성 컨텍스트에서 다 관리되니까... 뭐 바뀐 건지 알 수 있습니다.. 왕...

왜 em.update(member)이런 걸 안 만들었나? 사상 때문입니다. 자바 Collection 떠올려보세요! 우리가 list에서 값 가져와서 변경하고 다시 담나요? 아니죠. 그냥 가져와서 바꾸면 자동으로 바뀌죠. 마치 자바에서 쓰는 것 처럼!

EntityManagerFactory emf = Persistence.createEntityManagerFactory("helloJPA");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

Member findMember = em.find(Member.class, member.getId());
findMember.setName("t아카데미");
tx.commit();

업데이트 쿼리가 나간다!

삭제?

// 삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

em.remove(memberA); //엔티티 삭제
tx.commit();

-즉시 로딩, 지연 로딩

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

단순히 member 정보만 사용하는 비즈니스 로직 println(member.getName());

Member만 쓰고 싶은데 Team을 굳이 같이 조회 해야 할까? 지연 로딩 LAZY를 사용해서 프록시로 조회 @ManyToOne(fetch=FetchType.LAZY)

@Entity
public class Member{
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

  @Column(name = "USERNAME")
  private String name;

	@ManyToOne(fetch= FetchType.LAZY) //**
  @JoinColumn(name="TEAM_ID")
  private Team team;
	...
}

프록시 객체. 가짜 객체로 조회를 해둠.

Member member = em.find(Member.class, 1L);

실제로 Member 조회할 때 프록시 객체, 즉 가짜 객체로 조회 함 그리고 실제 사용하는 시점에 Team을 조회

Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB조회)

Member 조회할 때 Team도 같이 조회하고 싶다면? 즉시 로딩 EAGER를 사용한다.

@Entity
public class Member{
	@Id
	@GeneratedValue
	private Long id;
	
	@Column(name="USERNAME")
	private String name;
	
	@ManyToOne(fetch=FetchType.EAGER) // 즉시 로딩할 경우. fetch=FetchType.EAGER)
	@JoinColumn(name="TEAM_ID")
	Private Team team;
	..
}

-가급적으로 지연 로딩을 사용하세요!

-즉시 로딩을 적용하면 예상하지 못한 SQL이 발생 Member조회, Team 즉시 로딩. Team이 또 다른 객체 즉시 로딩 걸려있으면?? 연쇄 작업이 일어남. 그래서 기본으로는 지연 로딩으로 다 쓰고 필요할 경우만 즉시 로딩을 쓰는 것을 추천!

-즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

-@ManyToOne, @OneToOne은 기본이 즉시 로딩 → LAZY로 설정

-@OneToMany, @ManyToMany는 기본이 지연 로딩

추가

+영속성 컨텍스트를 flush()하는 법.

em.flush()-직접 호출, 트랜잭션 커밋/JPQL 쿼리 실행-자동 호출

JPQL 쿼리 실행시 플러시 자동 호출되는 이유?

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//중간에 JPQL 실행
query = em.createQuery("select m from Memeber m", Member.class);
List<Member> members = query.getResultList();

아직 DB에 persist안 됐는데 쿼리문을 날렸다? SELECT 안 돼... 영속성 컨텍스트에만 쌓여 있는 거잖아. 그러면? flush()해주고 쿼리문 날려서 데이터 읽어와야 해. 무조건이지 아니면 읽을 게 없는데!

그래서 JPQL쿼리 실행되면 flush 자동으로 호출되도록 했음. 중간에 조회하는 경우가 생길 수 있으니까. Mybatis나 그냥 SQL 쓰면 그냥 flush()해줘야겠지.

해당 포스팅은 T아카데미에서 진행한 김영한 강사님의 JPA 유튜브 강의를 듣고 정리한 것입니다 : )

https://www.youtube.com/watch?v=WfrSN9Z7MiA&list=PL9mhQYIlKEhfpMVndI23RwWTL9-VL-B7U

반응형

'Spring > JPA' 카테고리의 다른 글

JPA 기반 프로젝트  (0) 2021.04.12
JPA 객체지향쿼리 - JPQL  (0) 2021.04.12
JPA 연관관계 매핑 -양방향 매핑  (0) 2021.04.11
JPA 연관관계 매핑-단방향 매핑  (0) 2021.04.11
JPA 필드와 칼럼 매핑  (0) 2021.04.11