[Spring Boot] 11. Spring Boot Project (Blog v2 - jpa)_1.User - Join

김미숙's avatar
Jul 21, 2025
[Spring Boot] 11. Spring Boot Project (Blog v2 - jpa)_1.User - Join

Persistence Context

notion image
 

📌 Persistence Context(영속성 컨텍스트)란?

  • 객체를 영속성(Persistence) 상태로 관리하는 환경
  • PA에서 엔티티 객체를 저장하고 변경 사항을 추적하는 일종의 캐시(cache) 역할
  • 엔티티 매니저(EntityManager)에 의해 관리되는 엔티티 객체들의 저장소
  • 특정 트랜잭션 범위 내에서 엔티티 객체를 조회, 저장, 수정, 삭제하는 작업을 관리
  • 1차 캐시 역할을 하여 같은 엔티티를 반복 조회할 때 DB 조회를 최소화
 

📌 JPA에서 엔티티 객체의 생명주기(Entity LifeCycle)

→ Persistence Context에서 엔티티 객체는 4가지 상태를 가짐
  1. 비영속(new, transient)
      • 아직 영속성 컨텍스트에 저장되지 않은 상태
      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()); } }
    1. 영속(managed, persistent)
      • EntityManager를 통해 persist() 호출 후 Persistence Context에 저장된 상태
      • 1차 캐시에 저장되며, 트랜잭션이 끝날 때 DB에 반영됨
      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차 캐시
      • 같은 엔티티를 반복 조회해도 DB 쿼리를 실행하지 않고 메모리에서 조회
      • 성능 최적화에 도움
        • Member member1 = em.find(Member.class, 1L); // DB에서 조회 후 1차 캐시에 저장 Member member2 = em.find(Member.class, 1L); // 1차 캐시에서 조회 (DB 조회 X)
      ✅ 변경 감지(Dirty Checking)
      • 엔티티 객체의 값을 변경하면 commit() 시 자동으로 UPDATE 쿼리가 실행됨
      • persist()를 다시 호출하지 않아도 자동으로 변경 사항을 반영
        • Member member = em.find(Member.class, 1L); member.setName("Alice"); // 변경 em.getTransaction().commit(); // 자동으로 UPDATE 쿼리 실행
      ✅ 트랜잭션을 통한 쓰기 지연(Write Behind)
      • persist()를 호출해도 즉시 DB에 반영되지 않고, 트랜잭션 commit() 시 한꺼번에 저장됨
      • 여러 개의 INSERT를 모아 두었다가 한 번에 실행 → 성능 최적화
        • em.persist(member1); em.persist(member2); em.getTransaction().commit(); // INSERT가 한꺼번에 실행됨
      ✅ 엔티티 동일성 보장
      • 같은 트랜잭션 안에서 같은 id의 엔티티는 항상 동일한 객체(instance)
      • 비교 연산(==)으로도 같은 객체인지 확인 가능
        • 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()
      영속성 컨텍스트 종료

      📌 정리

      • Persistence Context는 엔티티 객체를 관리하는 메모리 공간
      • 1차 캐시, 변경 감지, 동일성 보장, 쓰기 지연 등의 기능 제공
      • 트랜잭션 단위로 엔티티를 관리하며 성능 최적화에 도움
      • clear(), detach(), 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; } }
notion image

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}}
notion image
notion image
notion image

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

parangdajavous