Persistence Context

📌 Persistence Context(영속성 컨텍스트)란?
- 객체를 영속성(Persistence) 상태로 관리하는 환경
- PA에서 엔티티 객체를 저장하고 변경 사항을 추적하는 일종의 캐시(cache) 역할
- 엔티티 매니저(EntityManager)에 의해 관리되는 엔티티 객체들의 저장소
- 특정 트랜잭션 범위 내에서 엔티티 객체를 조회, 저장, 수정, 삭제하는 작업을 관리
- 1차 캐시 역할을 하여 같은 엔티티를 반복 조회할 때 DB 조회를 최소화
📌 JPA에서 엔티티 객체의 생명주기(Entity LifeCycle)
→ Persistence Context에서 엔티티 객체는 4가지 상태를 가짐
- 비영속(new, transient)
- 아직 영속성 컨텍스트에 저장되지 않은 상태
- 영속(managed, persistent)
EntityManager
를 통해persist()
호출 후 Persistence Context에 저장된 상태- 1차 캐시에 저장되며, 트랜잭션이 끝날 때 DB에 반영됨
- 같은 엔티티를 반복 조회해도 DB 쿼리를 실행하지 않고 메모리에서 조회
- 성능 최적화에 도움
- 엔티티 객체의 값을 변경하면
commit()
시 자동으로UPDATE
쿼리가 실행됨 persist()
를 다시 호출하지 않아도 자동으로 변경 사항을 반영persist()
를 호출해도 즉시 DB에 반영되지 않고, 트랜잭션commit()
시 한꺼번에 저장됨- 여러 개의
INSERT
를 모아 두었다가 한 번에 실행 → 성능 최적화 - 같은 트랜잭션 안에서 같은
id
의 엔티티는 항상 동일한 객체(instance) - 비교 연산(
==
)으로도 같은 객체인지 확인 가능 - Persistence Context는 엔티티 객체를 관리하는 메모리 공간
- 1차 캐시, 변경 감지, 동일성 보장, 쓰기 지연 등의 기능 제공
- 트랜잭션 단위로 엔티티를 관리하며 성능 최적화에 도움
clear()
,detach()
,close()
를 사용하면 영속성 컨텍스트에서 제거 가능
package shop.mtcoding.blog.user;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
User user = joinDTO.toEntity(); // 1. 비영속객체
System.out.println("비영속 user: " + user.getId());
// 회원가입
userRepository.save(user);
// user 객체
System.out.println("영속/동기화 user: " + user.getId());
}
}
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
/*
1. createNativeQuery -> 기본 쿼리
2. createQuery -> JPA가 제공해주는 객체지향 쿼리
3. NamedQuery -> QueryMethod는 함수 이름으로 쿼리 생성 - 사용X
4. EntityGraph -> 지금 이해못함
*/
public void save(User user) {
em.persist(user); // insert query 발동 // 2. user 영속객체
// 3. user는 DataBase와 동기화됨
}
}
package shop.mtcoding.blog.user;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
User user = joinDTO.toEntity(); // 1. 비영속객체
System.out.println("비영속 user: " + user.getId());
// 회원가입
userRepository.save(user);
// user 객체
System.out.println("영속/동기화 user: " + user.getId());
}
}
📌 Persistence Context의 주요 기능
✅ 1차 캐시
Member member1 = em.find(Member.class, 1L); // DB에서 조회 후 1차 캐시에 저장
Member member2 = em.find(Member.class, 1L); // 1차 캐시에서 조회 (DB 조회 X)
✅ 변경 감지(Dirty Checking)
Member member = em.find(Member.class, 1L);
member.setName("Alice"); // 변경
em.getTransaction().commit(); // 자동으로 UPDATE 쿼리 실행
✅ 트랜잭션을 통한 쓰기 지연(Write Behind)
em.persist(member1);
em.persist(member2);
em.getTransaction().commit(); // INSERT가 한꺼번에 실행됨
✅ 엔티티 동일성 보장
Member a = em.find(Member.class, 1L);
Member b = em.find(Member.class, 1L);
System.out.println(a == b); // true (같은 인스턴스)
📌 영속성 컨텍스트 관련 메서드
메서드 | 설명 |
persist(entity) | 엔티티를 영속 상태로 변경 (1차 캐시에 저장) |
find(entityClass, id) | 1차 캐시 → DB 순으로 조회 |
remove(entity) | 엔티티 삭제 상태로 변경 |
detach(entity) | 특정 엔티티를 영속성 컨텍스트에서 분리 (변경 감지 X) |
clear() | 영속성 컨텍스트 초기화 (모든 엔티티 준영속 상태로 변경) |
close() | 영속성 컨텍스트 종료 |
📌 정리
JPA를 사용할 때 영속성 컨텍스트를 잘 이해하면 효율적인 데이터베이스 관리와 성능 최적화가 가능! 🚀
UserRepository
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
/*
1. createNativeQuery -> 기본 쿼리
2. createQuery -> JPA가 제공해주는 객체지향 쿼리
3. NamedQuery -> QueryMethod는 함수 이름으로 쿼리 생성 - 사용X
4. EntityGraph -> 지금 이해못함
*/
public User findByUsername(String username) {
return em.createQuery("select u from User u where u.username = :username", User.class) //객체지향 쿼리
.setParameter("username", username)
.getSingleResult();
}
public void save(User user) {
em.persist(user); // insert query 발동 // 2. user 영속객체
// 3. user는 DataBase와 동기화됨
}
}
UserService
package shop.mtcoding.blog.user;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
// 동일회원 있는지 검사
// User user = userRepository.findByUsername(joinDTO.getUsername());
// if (user != null) {
// throw new RuntimeException("동일한 username이 존재합니다.");
// }
User user = joinDTO.toEntity(); // 1. 비영속객체
System.out.println("비영속 user: " + user.getId());
// 회원가입
userRepository.save(user);
// user 객체
System.out.println("영속/동기화 user: " + user.getId());
}
public User 로그인(UserRequest.LoginDTO loginDTO) {
// username,password 검사
User user = userRepository.findByUsername(loginDTO.getUsername());
if (user == null) {
throw new RuntimeException("해당 username이 없습니다");
}
if (!(user.getPassword().equals(loginDTO.getPassword()))) {
throw new RuntimeException("해당 passward가 일치하지 않습니다");
}
// 로그인
return user;
}
}

UserRequest
package shop.mtcoding.blog.user;
import lombok.Data;
public class UserRequest {
//insert 용도의 dto에는 toEntity 메서드를 만든다
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
public User toEntity() {
return User.builder()
.username(username)
.password(password)
.email(email)
.build();
}
}
}
UserController
package shop.mtcoding.blog.user;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import shop.mtcoding.blog._core.Resp;
import java.util.Map;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
private final HttpSession session;
@GetMapping("/join-form")
public String joinForm() {
return "user/join-form";
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
@GetMapping("/check-username-available/{username}")
public @ResponseBody Resp<?> checkUsernameAvailable(@PathVariable("username") String username) {
Map<String, Object> dto = userService.유저네임중복체크(username);
return Resp.ok(dto);
}
@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
}
UserService
package shop.mtcoding.blog.user;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
// 비즈니스 로직, 트랜잭션 처리, DTO 완료
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
userRepository.save(joinDTO.toEntity());
}
}
UserRepository
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
/*
1. createNativeQuery -> 기본 쿼리
2. createQuery -> JPA가 제공해주는 객체지향 쿼리
3. NamedQuery -> QueryMethod는 함수 이름으로 쿼리 생성 - 사용X
4. EntityGraph -> 지금 이해못함
*/
public User findByUsername(String username) {
try {
return em.createQuery("select u from User u where u.username = :username", User.class) //객체지향 쿼리
.setParameter("username", username)
.getSingleResult();
} catch (Exception e) {
return null;
}
}
public void save(User user) {
em.persist(user); // insert query 발동
}
}
join-form
{{> layout/header}}
<div class="container p-5">
<!-- 요청을 하면 localhost:8080/join POST로 요청됨
username=사용자입력값&password=사용자값&email=사용자입력값 -->
<div class="card">
<div class="card-header"><b>회원가입을 해주세요</b></div>
<div class="card-body">
<form action="/join" method="post" enctype="application/x-www-form-urlencoded" onsubmit="return valid()">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username" id="username">
<button type="button" class="btn btn-warning" onclick="checkUsernameAvailable()">중복확인</button>
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password"
id="password">
</div>
<div class="mb-3">
<input type="email" class="form-control" placeholder="Enter email" name="email">
</div>
<button type="submit" class="btn btn-primary form-control">회원가입</button>
</form>
</div>
</div>
</div>
<script>
let isUsernameAvailable = false;
// 1. username 변경 감지
let usernameDom = document.querySelector("#username");
usernameDom.addEventListener("keyup", () => {
isUsernameAvailable = false;
})
// 2. username 중복체크
async function checkUsernameAvailable() {
let username = document.querySelector("#username").value;
let response = await fetch("/check-username-available/" + username);
let responseBody = await response.json();
// status = 200, msg = 성공, body: { "available" : true }
isUsernameAvailable = responseBody.body.available;
if (isUsernameAvailable) {
alert("사용 가능한 아이디입니다.");
} else {
alert("이미 사용 중인 아이디입니다.");
}
}
// 3. 최종 유효성 검사
function valid() {
if (!isUsernameAvailable) {
alert("아이디 중복체크를 해주세요");
return false;
}
return true;
}
</script>
{{> layout/footer}}



UserRequest
package shop.mtcoding.blog.user;
import lombok.Data;
public class UserRequest {
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
}
}
UserController
package shop.mtcoding.blog.user;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
private final HttpSession session;
@GetMapping("/join-form")
public String joinForm() {
return "user/join-form";
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO) {
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
return "redirect:/";
}
}
UserService
package shop.mtcoding.blog.user;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
// 동일회원 있는지 검사
// User user = userRepository.findByUsername(joinDTO.getUsername());
// if (user != null) {
// throw new RuntimeException("동일한 username이 존재합니다.");
// }
User user = joinDTO.toEntity(); // 1. 비영속객체
System.out.println("비영속 user: " + user.getId());
// 회원가입
userRepository.save(user);
// user 객체
System.out.println("영속/동기화 user: " + user.getId());
}
}
UserRepository
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
/*
1. createNativeQuery -> 기본 쿼리
2. createQuery -> JPA가 제공해주는 객체지향 쿼리
3. NamedQuery -> QueryMethod는 함수 이름으로 쿼리 생성 - 사용X
4. EntityGraph -> 지금 이해못함
*/
public User findByUsername(String username) {
return em.createQuery("select u from User u where u.username = :username", User.class) //객체지향 쿼리
.setParameter("username", username)
.getSingleResult();
}
public void save(User user) {
em.persist(user); // insert query 발동 // 2. user 영속객체
// 3. user는 DataBase와 동기화됨
}
}
Share article