Engineering Note

[JPA] 영속성 컨텍스트 생명주기와 @Transacional의 동작 원리: 영속성 컨텍스트 유지 본문

Server/JPA ORM

[JPA] 영속성 컨텍스트 생명주기와 @Transacional의 동작 원리: 영속성 컨텍스트 유지

Software Engineer Kim 2025. 10. 5. 00:55

1. 영속성 컨텍스트 생명주기

JPA를 사용하는 환경(주로 Spring Boot의 Service Layer)에서 @Transactional 어노테이션이 붙은 메서드가 호출될 때 영속성 컨텍스트는 다음과 같은 생명주기를 가집니다.
 
1-1. 트랜잭션 시작 (생성)

  • @Transactional 메서드가 호출되면 데이터베이스 트랜잭션이 시작됩니다.
  • 이와 동시에 JPA의 영속성 컨텍스트가 생성되어 현재 실행 중인 쓰레드의 트랜잭션에 바인딩됩니다.
  • 이 영속성 컨텍스트가 1차 캐시 역할을 하며, 트랜잭션 내에서 조회되거나 저장된 모든 엔티티 객체들을 관리합니다.

 
1-2. 비즈니스 로직 실행 (유지)

  • 메서드 내에서 JPA 관련 작업(조회, 저장, 수정 등)이 실행되는 동안 영속성 컨텍스트는 유지됩니다.
  • 엔티티를 수정하더라도, 이 변경 사항은 당장 DB에 반영되지 않고(쓰기 지연) 영속성 컨텍스트 내에서 관리됩니다.
  • 지연로딩(Lay Loading)으로 설정된 연관 객체에 접근할 때도, 살아있는 영속성 컨텍스트를 통해 DB 접근이 가능해집니다.

 
1-3. 트랜잭션 종료 (소멸)

  • commit: 메서드가 정상적으로 완료되면 트랜잭션이 commit됩니다. commit 직전에 영속성 컨텍스트의 변경 내용이 DB에 반영(flush)되고, 영속성 컨텍스트는 소멸됩니다.
  • rollback: 예외가 발생하면 트랜잭션이 rollbacke되며, 영속성 컨텐스트는 DB 변경 없이 소멸됩니다.

 
 

2. 영속성 컨텍스트는 왜 트랜잭션 단위로 생성되는가?

영속성 컨텍스트를 트랜잭션 단위로 관리하는 것은 JPA의 설계 원칙과 ACID 원칙을 지키는 데 필수적입니다.
 
1-1. 데이터 일관성 및 원자성 보장(ACID)
트랜잭션은 하나의 논리적안 작업 단위입니다. 영속성 컨텍스트는 이 작업 단위 내에서 엔티티의 모든 변경을 관리하고, 트랜잭션이 commit될 때만 DB에 일괄적으로 반영하여 원자성을 보장합니다. 만약 트랜잭션이 실패하면, 영속성 컨텍스트만 버려지므로 DB는 변경되지않아 일관성이 유지됩니다.
 
1-2. 격리성 보장(Isolation)
멀티쓰레드 환경에서 여러 사용자가 동시에 요청을 처리할 때, 각 요청은 독립적인 트랜잭션과 영속성 컨텍스트를 가집니다. 이를 통해 각 요청은 서로의 작업에 영향을 주지 않고 격리성을 유지할 수 있습니다.
 
1-3. 성능 최적화 (1차 캐시)
트랜잭션 내에서 같은 엔티티를 여러 번 조회하더라도, 1차 캐시에 이미 존재하면 DB 재접근을 하지 않아 성능을 향상시킵니다. 트랜잭션이 종료되면 캐시도 사라지므로 메모리를 효율적으로 관리할 수 있습니다.
 
 
 

2. @Transacional의 동작 원리: 영속성 컨텍스트 유지

 
2-1. 트랜잭션과 영속성 컨텍스트의 생명 주기 일치
Spring Data JPA 환경에서 @Transactional이 붙은 메서드가 호출되면, Spring은 다음과 같이 동작합니다.

단계동작설명
시작트랜잭션 시작 & 영속성 컨텍스트 생성메서드 호출 전, Spring AOP 프록시가 데이터베이스 트랜잭션을 시작합니다. 동시에 JPA의 영속성 컨텍스트가 생성되어 이 트랜잭션에 바인딩 됩니다.
실행DB 접근 가능메서드 내부의 모든 DB작업(엔티티 조회, 변경 등)은 이 영속성 컨텍스트 내에서 처리됩니다. 지연 로딩으로 설정된 엔티티에 접근하는 코드도 이 영속성 컨텍스트를 통해 DB에 접근하여 데이터를 로드합니다.
종료트랜잭션 종료 & 영속성 컨텍스트 종료메서드가 성공적으로 완료되면 트랜잭션이 commit되고, 영속성 컨텍스트도 flush 및 종료됩니다.

 
 
2-2. 지연로딩(Lay Loading)의 동작 보장
 
BoardEntity와 CommentEntity로 @Transactional이 지연 로딩을 어떻게 보장하는지 살펴보겠습니다.
 
게시글 댓글, CommentEntity

@Entity
@Getter
@Setter
@Table(name = "comment_table")
public class CommentEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20, nullable = false)
    private String commentWriter;

    @Column
    private String commentContent;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    private BoardEntity boardEntity;
    
}

 
 
게시글 BoadEntity

@Entity
@Getter
@Setter
@Table(name = "board_table")
public class BoardEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //mysql 기준으로 auto_increment
    private Long id;

    @Column(length = 20, nullable = false) //크기는 20, not null
    private String boardWriter;

    @Column //default : 크기255, null가능
    private String boardPass;

    @Column
    private String boardTitle;

    @Column(length = 500)
    private String boardContent;

    @Column
    private int boardHits;

    @Column
    private int fileAttached; // 1 or 0

    @OneToMany(mappedBy = "boardEntity", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<BoardFileEntity> boardEntityList = new ArrayList<>();

    @OneToMany(mappedBy = "boardEntity", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<CommentEntity> commentEntityList = new ArrayList<>();
    
}

 
commentEntity에서 BoardEntity를 호출하는 상황
 

@Transactional(readOnly = True)
public List<CommentDto> findAll(Long boardId) {
    BoardEntity boardEntity = boardRepository.findById(boardId).get();
    List<CommentEntity> commentEntityList = commentRepository.findAllByBoardEntityOrderByIdDesc(boardEntity);

    List<CommentDto> commentDtoList = commentEntityList.stream()
            .map(commentEntity -> CommentDto.toCommentDTO(commentEntity))
            .collect(Collectors.toList());

    return commentDtoList;
}

 
toCommentDto 메서드 내부 코드

commentDto.setBoardId(commentEntity.getBoardEntity().getId()); //Service 메서드에 @Transactional 이 붙어야 동작가능


@Transactional 덕분에 메서드가 실행되는 동안 영속성 컨텍스트가 살아있습니다.

  1. commentEntity가 DB에서 로드 됩니다. (이때 BoardEntity는 프록시 상태)
  2. commentEntity.getBoardEntity().getId() 코드가 실행됩니다.
  3. 프록시 객체가 .getId()를 호출하는 순간, 영속성 컨텍스트가 살아있으므로 DB접근 권한(커넥션)을 얻어 Board 정보를 로드합니다.(지연로딩)

 
이처럼 @Transactional은 메서드 실행 범위를 하나의 "DB 작업 단위로" 묶어주고, 그 범위내에서 JPA가 필요할 때마다 DB에 접근할 수 있도록 영속성 컨텍스트를 유지해주는 역할을 합니다. 만약 @Transactional이 없다면 영속성 컨텍스트는 이미 종료되어 DB 접근 시도 자체가 불가능해집니다.
 
 
 

2-3. @Transactional이 없는 경우의 동작 흐름

@Transactional 어노테이션이 없다고 가정할 때의 동작 흐름입니다.

public List<CommentDto> findAll(Long boardId) {

    BoardEntity boardEntity = boardRepository.findById(boardId).get();
    List<CommentEntity> commentEntityList = commentRepository.findAllByBoardEntityOrderByIdDesc(boardEntity);

    List<CommentDto> commentDtoList = commentEntityList.stream()

    .map(commentEntity -> CommentDto.toCommentDTO(commentEntity)

    .collect(Collectors.toList());

    return commentDtoList;

}

List<CommentEntity> commentEntityList = commentRepository.findAllByBoardEntityOrderByIdDesc(boardEntity);  호출 시 동작 과정

  • 단기적인 DB 작업 수행
    • 1.1(내부)DB 커넥션 획득 및 짧은 영속성 컨텍스트 생성.
    • 1.2(내부)SELECT 쿼리 실행. CommentEntity들을 영속성 컨텍스트에 로드.
    • 1.3(내부)쿼리 결과를 반환한 후, 영속성 컨텍스트가 즉시 종료되고 커넥션 반환.

 

3. 지연 로딩된 연관 객체 접근 과정

지연 로딩으로 설정된 연관 객체(BoardEntity 등)는 실제 사용되기 전까지는 프록시 객체 상태로 영속성 컨텍스트에 보관되어 있습니다.
 
3-1. 프록시 객체 접근(트리거)

  • 코드 실행: @Transactional 내부에서 commentEntity.getBoardEntity().getId()와 같이 연관 객체의 실체 필드에 접근하는 코드가 실행됩니다. 이것이 실제 데이터를 로드하라는 트리거가 됩니다.
  • 프록시 확인: JPA는 getBoardEntity()가  반환한 객체가 프록시 객체임을 인식합니다.

 
3-2. 영속성 컨텍스트 확인(1차캐시)

  • 1차 캐시 조회: 프록시 객체는 자신이 관리해야 할 실제 엔티니(예: BoardEntity)가 현재 살아있는 영속성 컨텍스트의 1차 캐시에 존재하는지 확인합니다.
  • 있다면: 이미 트랜잭션 내에서 해당 엔티티가 되어되어 캐시에 있다면, JPA는 DB 접근 없이 캐시 있는 실제 엔티티를 프록시에 연결하고 바로 값을 반환합니다.
  • 없다면: 캐시에 없다면, 이제 DB로 가서 조회합니다.

 
3-3. 데이터베이스 조회(로딩)

  • DB 쿼리 실행: 1차 캐시에 엔티티가 없는 경우, JPA는 데이터베이스에 필요한 SELECT 쿼리를 날려 해당 엔티티의 데이터를 가져옵니다.
  • 프록시 초기화: DB에서 가져온 데이터로 실제 엔티티 객체를 생성하여 1차 캐시에 저장하고, 동시에 프록시 객체를 이 실제 엔티티로 초기화합니다.

 
3-4. 값 반환

  • 초기화된 프록시를 통해 요청된(.getId())을 반환합니다.

 
이 모든 과정이 @Transctional의 범위 내에서 영속성 컨텍스트가 유지되고 있기 때문에 LazyInitializationException 없이 안전하게 발생하는 것입니다. 트랜잭션이 끝나면 영속성 컨텍스트도 사라지므로, 그 후에는 지연 로딩을 지도하면 예외가 발생합니다.
 
 
 
참고사항 
@Transactional(readOnly = True)의 중요성
JPA 환경에서 조회 기능만 하는 메서드에서는 이 옵션을 사용하는 것이 권장됩니다.

  • 영속성 컨텍스트 최적화: 변경 감지(dirty checking)를 위한 스냅샷을 저장하지 않아 메모리 사용량을 줄이고 성능을 향상시킵니다.
  • flush 모드: 데이터 변경이 없으므로, 쓰기 지연 저장소(write-behind buffer)를 비우는 flush 작업을 수행하지 않도록 최적화될 수 있습니다.
Comments