개발일지/SPRINGBOOT

Spring Boot JPA 영속성 완벽 가이드: 엔티티 생명주기 이해하기

recording or reCoding 2025. 5. 17. 22:29

오늘은 Spring Boot에서의 핵심 개념인 영속성에 대해서 알아보려고 합니다. JPA의 영속성 관리, 엔티티 생명주기, 그리고 영속성 컨텍스트에 대해 자세히 알아보겠습니다.

영속성이란 무엇인가?

JPA의 영속성은 엔티티 객체를 영속성 컨텍스트(Persistence Context) 라는 논리적인 저장 공간에서 관리하는 것을 의미합니다. 영속성 컨텍스트는 엔티티의 상태를 추적하고, 데이터베이스와의 동기화를 담당합니다.

 

하나의 트랜잭션 내에서 영속성이 유지된다는 것은, 트랜잭션이 시작될 때 영속성 컨텍스트가 생성되고, 트랜잭션이 종료될 때 영속성 컨텍스트도 함께 종료됨을 의미합니다. 트랜잭션 안에서 엔티티의 변경 사항은 영속성 컨텍스트에 의해 추적되고, 트랜잭션이 커밋되는 시점에 데이터베이스에 반영됩니다.

 

하나의 데이터베이스를 사용한다고 가정하면, 하나의 EntityMangerFactory가 존재하는데요. 이 EntityMangerFactory가 EntityManger를 생성합니다. 하나의 요청이 하나의 EntityManger와 매핑되어 사용 됩니다.
하나의 트랜잭션 내에서 영속성이 유지됩니다.

 

영속성 컨텐스트 - 엔티티를 영구 저장하는 환경

엔티티 생명주기

JPA에서 관리되는 엔티티는 다음과 같은 네 가지 생명주기를 가집니다.

  • 1. 비영속 (Transient): 단순히 new 연산자로 생성된 객체. 영속성 컨텍스트와 관계없음.
  • 2. 영속 (Persistent): 영속성 컨텍스트에 의해 관리되는 객체. EntityManager.persist() 또는 EntityManager.find() 등을 통해 영속 상태가 됨.
//상단 세줄은 비영속 상태.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); //db transaction 시작

//객체를 저장한 상태 (영속)
em.persist(member);
//이지점에서 바로 쿼리가 날라가지 않음
  • 3. 준영속 (Detached): 영속성 컨텍스트에서 분리된 객체. EntityManager.detach()를 호출하거나 트랜잭션이 종료되면 준영속 상태가 됨.
  • 4. 삭제 (Removed): EntityManager.remove()를 호출하여 삭제된 객체. 트랜잭션 커밋 시 실제 데이터베이스에서 삭제.

영속성에 대해서 간단하게 정리를 하자면, 예를 들어 하나의 트랜잭션 안에서 동일한 데이터에 대해서 조회를 5번 하게 될때

 DB의 쿼리 로그나 JPA가 기록한 로그를 보면, SELECT문이 1번만 실행됩니다. 이는 영속성 컨텍스트 내의 1차 캐시(First-Level Cache)와 동일성 보장(Identity Guarantee) 의 개념 때문에 1번만 수행됩니다.

@Service
@Transactional
public class MemberService {

    @PersistenceContext
    private EntityManager em;

    public void findMemberMultipleTimes(Long memberId) {
        Member member1 = em.find(Member.class, memberId); // SELECT 쿼리 실행
        Member member2 = em.find(Member.class, memberId); // 캐시에서 조회 (SELECT 쿼리 없음)
        Member member3 = em.find(Member.class, memberId); // 캐시에서 조회 (SELECT 쿼리 없음)
        // ...

        // member1, member2, member3는 모두 동일한 객체를 참조합니다.
        System.out.println(member1 == member2); // true
        System.out.println(member2 == member3); // true
    }
}

 

더티 체킹(Dirty Check) - 영속성 컨텍스트 변화 감지.

JPA는 엔티티를 조회할 때 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어 놓는다. 그리고 트랜잭션이 끝나는 시점(commit)에 이 스냅샷과 비교하여 다른 점이 있으면 UPDATE 쿼리를 DB에 전달한다.이러한 상태 변경 확인의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용된다. 

플러시(flush)

영속성 컨텍스트의 변경 내용을 데이터베이스에 반영 하는 의미로  3가지 방식에서 플러시 행위가 일어난다.

  • EntityManager.flush() 를 직접 호출
  • 트랜잭션의 commit을 통해 자동으로
  • JPQL 쿼리 실행을 통해 자동으로

따라서  더티 체킹 과 플러시 때문에 엔터티와 Dto를 분리하여 사용합니다.

 

DTO 를 이용한 request/response 활용.

JPA에서는 Entity를 대변하는 dto를 만들고 resposneDto,requestDto를 따로 사용 합니다. DTO는 말 그대로 "데이터 전송을 위한 객체"입니다. 이를 활용하여 Entity의 영속성 문제를 해결합니다.

 

그렇기 때문에 dto를 통해서 reqeust 값과 response 를 처리하며, 중간에서 Entity에 적용하는 일은 'DB에 반영해줘' 와 같은 의미 입니다.

 

* 엔터티를 바로 적용 할 경우 발생하는 LazyInitializationException

FetchType.LAZY로 설정된 연관 관계를 가진 엔티티를 서비스 계층에서 조회하여 컨트롤러로 넘긴 후, 컨트롤러나 뷰(View) 계층에서 해당 LAZY 연관 관계에 접근하려 할 때, 이미 트랜잭션이 종료되어 영속성 컨텍스트가 닫혀있다면 예외가 발생합니다.

 

 

@Query 방식과 Querydsl 에서 DTO를 이용하여 엔터티로 변환하여 사용

@Data
@NoArgsConstructor
public class TodoDTO {

 private Long mno;
 
 private String title;
 
 private String writer;
 
 public TodoDTO(TodoEntity todoEntity){
        this.mno = todoEntity.getMno();
        this.title = todoEntity.getTitle();
        this.writer = todoEntity.getWriter();
    }
}
//@Query 방식 예시 
public interface TodoRepository extends JpaRepository<TodoEntity,Long>, TodoSearch {

    @Query(" SELECT t from TodoEntity t where t.mno =:mno")
    Optional<TodoDTO> getDTO(@Param("mno")Long mno);

}

//Querydsl 에서 의 예시

public class ProductSearchImpl extends QuerydslRepositorySupport implements ProductSearch {
    public ProductSearchImpl(){
        super(ProductEntity.class);
    }

...
    @Override
    public Page<ProductListDTO> list(Pageable pageable) {

        QProductEntity productEntity = QProductEntity.productEntity;
        QProductImage productImage = QProductImage.productImage;

        JPQLQuery<ProductEntity> query = from(productEntity);
        query.leftJoin(productEntity.images,productImage);

        query.where(productImage.idx.eq(0));

        JPQLQuery<ProductListDTO> dtojpqlQuery = query.select(Projections.bean(
                ProductListDTO.class, productEntity.pno,
                productEntity.pname,
                productEntity.price,
                productEntity.writer,
                productImage.fileName.as("productImage")));


        this.getQuerydsl().applyPagination(pageable,dtojpqlQuery);

        List<ProductListDTO> dtoList = dtojpqlQuery.fetch();

        long count = dtojpqlQuery.fetchCount();

        return new PageImpl<>(dtoList,pageable,count);

    }
..
}

 

결론

SpirngBoot 에서의 '영속성'에 대한 개념을 잘 이해하고 엔터티를 활용하지 않으면 원치 않은 데이터가 업데이트 되고 반영될수 있다는 생각이 들었고, 코드 개발시 dto 를 적절하게  활용하고 Projection을 잘 이용해야 한다.