Spring Boot 로그 시스템 정리 – SLF4J, Logback, Log4j2 차이는 뭘까?

Spring Boot 프로젝트를 하다 보면 @Slf4j 써서 로그 찍고, logback-spring.xml 만들어서 설정하고, 가끔 누군가는 @Log4j2를 쓰기도 한다. 나도 Spring을 처음 배울땐 아무 생각 없이 @Slf4j 를 사용하고 있었고, 블로그 글을 보면서 대충 넘겼다. 시간이 많이 지난 지금에서야 내 블로그에 정리해본다.


1. SLF4J: 그저 추상화일 뿐

SLF4J(Simple Logging Facade for Java)는 말 그대로 Facade다. 즉, 진짜 로그를 찍는 애가 아니라, "찍어줘~"라고 부탁만 하는 애다.

개발자는 log.info("hello")라고 쓰면 되고, 실제로 어디에 어떻게 출력할지는 SLF4J 뒤에 있는 **구현체(Logback, Log4j, Log4j2 등)**가 알아서 처리한다.

장점?

  • 코드에서는 SLF4J만 쓰면 되니까, 나중에 로깅 구현체를 바꿔도 코드 손댈 필요가 없다.
  • 유연하고 중립적이다.

2. Logback: Spring Boot 기본값

Spring Boot는 기본적으로 Logback을 로깅 구현체로 쓴다.
이건 명시적으로 선언하지 않아도 spring-boot-starter-web만 넣으면 자동으로 따라온다. 왜냐하면 이 안에 spring-boot-starter-logging이 포함돼 있고, 그 안에 Logback 관련 의존성이 들어있기 때문이다.

그리고 설정은 logback-spring.xml로 한다.
이 파일을 보면 <appender>나 <springProfile> 같은 태그가 있고, 로그 패턴이나 저장 경로, 파일 롤링 등을 세세하게 지정할 수 있다.

장점?

  • Spring Boot랑 찰떡궁합
  • 별도 설정 없이도 바로 동작
  • 대부분의 프로젝트에 무난하고 안정적임

3. Log4j2: 성능 괴물

Log4j의 후속 버전인 Log4j2는 성능에 진심이다. 아파치에 따르면 멀티쓰레드 환경에서 비동기 로거의 경우 처리량이 18배 높고 대기시간이 더 짧다.
GC 영향 적은 구조, 비동기 로깅, 커스텀 Appender 등 고급 기능이 많다. 로그양이 많거나 성능 튜닝이 필요한 경우엔 Logback보다 낫다.

근데 Spring Boot에서 기본으로 안 쓰기 때문에 Logback을 제거하고 Log4j2로 갈아끼워야 한다.

갈아끼우는 법은?

 

  1. spring-boot-starter-logging 제거
  2. spring-boot-starter-log4j2 추가
  3. 설정 파일도 log4j2-spring.xml로 바꿔야 함

설정 문법도 완전 다르다. <Configuration>부터 <Appenders>, <Loggers> 같은 구조로 되어 있어서, Logback 쓰던 감성으로는 안 된다.


4. @Slf4j vs @Log4j2

Lombok이 제공하는 이 로그 어노테이션들… 똑같아 보이지만 내부는 다르다.

어노테이션내부 Logger 타입
@Slf4j SLF4J API (org.slf4j.Logger)
@Log4j2 Log4j2 API (org.apache.logging.log4j.Logger)
 

즉, @Slf4j는 어떤 구현체가 뒤에 있든 상관없이 SLF4J 인터페이스만 쓴다.

유연하고 유지보수하기 좋다. 나중에 Logback → Log4j2로 갈아탈 때도 코드 안 고쳐도 된다. 설정만 바꾸면 된다.

@Log4j2는 아예 Log4j2 전용이다.


5. 정리

  1. SLF4J는 그냥 추상화. 실제로 로그는 안 찍는다.
  2. Logback은 Spring Boot의 기본 로깅 구현체. logback-spring.xml로 설정.
  3. @Slf4j는 SLF4J 기반이라 유연함. @Log4j2는 Log4j2 전용.
  4. Log4j2를 쓰면 될 듯 하다. 하지만 ThreadContext등 Log4j2 전용 기능을 사용하면 구현체(Log4j2)에 강하게 묶이게 될 수 있다.

덧붙임

“왜 로그 라이브러리 하나에 이렇게 복잡해?”라고 생각할 수도 있다.
근데 나중에 로그가 수천 줄씩 터질 때, 누가 메모리를 많이 잡아먹는지, 비동기로 돌릴 수는 있는지 등등이 중요해진다.
언젠가 꼭 필요한 개념이다.

JPA를 사용할 때, 엔티티의 데이터를 변경한 후 save() 메서드를 호출해야만 update 쿼리가 실행된다고 생각하는 경우가 많다. 그러나 이는 JPA의 핵심 기능 중 하나인 더티 체킹(dirty checking) 개념을 이해하지 못한 데서 비롯된 오해다.

이 글에서는 Spring Data JPA의 save()가 언제 쿼리를 날리는지, 그리고 더티 체킹과 어떤 차이가 있는지 예제와 함께 정리한다.


더티 체킹(DIRTY CHECKING)이란?

더티 체킹이란 JPA가 트랜잭션 내에서 영속 상태로 관리되는 엔티티의 변경 여부를 감지하고, 트랜잭션이 종료되는 시점에 자동으로 update 쿼리를 수행하는 기능이다. 이 기능 덕분에 개발자는 명시적으로 save()를 호출하지 않아도 변경된 내용을 데이터베이스에 반영할 수 있다.

 

더티 체킹이 실행되기 위해선 아래와 같은 조건이 있다.

 

  • 엔티티가 영속 상태(Persistent) 여야 한다
  • 트랜잭션 안에서 엔티티의 필드를 변경했을 때
  • flush 시점 (보통은 트랜잭션 commit 직전에 자동 flush됨)

 

 

예를 들어, 다음과 같은 코드가 있다고 가정해보자.

@Transactional
public void updateTodo(Long id, String newTitle) {
    Todo todo = todoRepository.findById(id).orElseThrow();
    todo.setTitle(newTitle); // setter만 호출
}

 

위 코드에서는 save() 메서드를 호출하지 않았지만, 트랜잭션이 종료될 때 Hibernate는 todo 객체의 변경사항을 감지하여 update 쿼리를 자동으로 생성한다(더티체킹) .


save()는 언제 필요한가?

Spring Data JPA의 save() 메서드 내부는 isNew()라는 판별 메소드가 있다.

isNew()는 다음 조건에 따라 동작한다.

 

    public boolean isNew(T entity) {
        ID id = this.getId(entity);
        Class<ID> idType = this.getIdType();
        if (!idType.isPrimitive()) {
            return id == null;
        } else if (id instanceof Number) {
            Number n = (Number)id;
            return n.longValue() == 0L;
        } else {
            throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
        }
    }

 

  • 신규 엔티티(id == null || id == 0L)인 경우: EntityManager.persist()를 호출하여 insert 쿼리를 실행한다.
  • 기존 엔티티(id != null  || id != 0L) 이 아닌 경우: EntityManager.merge()를 호출하여 병합 과정을 수행한다.

사실상, 객체를 영속화 시키는 과정이라고 볼 수 있다.

 

여기서 persist()와 merge()를 다시 짚어보자.

1. persist

  • 새로운(비영속) 엔티티를 영속 상태로 만듦
  • 즉, 아직 ID가 없는 "처음 만든 객체"를 영속성 컨텍스트에 등록한다
  • 이후 트랜잭션이 flush될 때 insert 쿼리가 실행된다
  • persist()는 이미 영속성 컨텍스트에 있는 엔티티에 대해 호출하면 예외가 발생한다.

2. merge

  • 비영속 또는 준영속(detached) 상태의 엔티티를 병합하여 영속 상태로 만듦
  • 영속성 컨텍스트(1차 캐시)에 동일한 ID가 존재하면 그 객체를 사용하고, 없으면 DB에서 select로 로딩한다
  • 이후 변경 사항은 더티 체킹 대상이 된다.

영속화를 persist로 하든 merge로 하든 영속화 된 객체에 대해선 더티체킹은 항상 실행될 수 있다.

persist는 insert 쿼리를 실행하기 때문에 더티체킹이 필요 없을 뿐이다.

 


 

save() 사용시 주의사항

 

그럼 id가 GeneratedValue(strategy = GenerationType.IDENTITY) 인 경우가 아니라, 수동으로 만들어 save() 한다면 어떻게 될까? 기존 엔티티로 취급하여 merge하기 때문에 select 될 것이다. 따라서 insert만 해도 되는데 불필요한 select 쿼리까지 실행되는 것이다. 이를 해결하기 위해서 객체에서 Persistable 인터페이스를 구현하고 isNew()를 오버라이드하는 방법이 있다.

 

아래는 예시다.

package com.mallapi.mall.domain;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.domain.Persistable;


@Entity
@ToString
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "tbl_todo")
public class Todo implements Persistable<Long> {

    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private Long id;
    
    // isNew()가 항상 true를 반환해 persist()로 동작
    @Override
    public boolean isNew() {
        return true;
    }
}

 

 


실습 예제: save()와 더티 체킹 비교

다음은 테스트 코드에서 findById()를 통해 조회한 엔티티에 값을 변경하고 save()를 호출한 예제다.

@Test
@Transactional
@Rollback(false)
public void testSaveCall() {
    Todo todo = todoRepository.findById(1L).orElseThrow();
    todo.setTitle("변경 - save 호출");

    todoRepository.save(todo); // 이미 영속 상태

    log.info("After Save: {}", todo);
}
 
Hibernate:
    select
        t1_0.id,
        t1_0.complete,
        t1_0.content,
        t1_0.due_date,
        t1_0.title
    from
        tbl_todo t1_0
    where
        t1_0.id=?

Hibernate:
    update
        tbl_todo
    set
        complete=?,
        content=?,
        due_date=?,
        title=?
    where
        id=?
 

여기서 주목할 점은 save() 호출 시 별도의 select 쿼리가 발생하지 않는다는 점이다. 이는 todo 객체가 이미 영속 상태이기 때문에, save()는 내부적으로 merge()를 호출하더라도 select 없이 1차캐시(영속성 컨텍스트)에서 조회되며, 이후 더티 체킹에 의한 update만 수행된다.

 

JPA의 영속성 컨텍스트(Persistence Context)는 트랜잭션 안에서만 유지된다. 따라서, 만약 @Transactional이 없다면 findById로 가져온 객체는 detached(준영속) 상태일 것이고, save(todo) 호출 시 select + update 쿼리가 실행된다. 영속성 컨텍스트가 없으므로 더티체킹도 되지 않는다. 한마디로 주옥될 수 있으니 트랜잭션을 잘 사용하자.


반대로 save()가 필요한 경우

save()가 실제로 의미를 가지는 경우는 다음과 같다.

 

1. 비영속(new) 객체를 저장할 때

Todo newTodo = new Todo();
newTodo.setTitle("새로운 할 일");
todoRepository.save(newTodo); // persist → insert 쿼리 발생

 

2. 준영속(detached) 객체를 다시 병합할 때

Todo detached = new Todo();
detached.setId(1L);
detached.setTitle("준영속 상태");

todoRepository.save(detached); // merge → select + update 발생
 

이 경우에는 JPA가 해당 ID를 가진 엔티티가 존재하는지 DB에서 조회한 뒤 병합을 수행하므로 select 쿼리가 발생한다.


정리: save() vs 더티 체킹


 

상황 쿼리 발생 save() 의미
영속 상태 객체 수정 후 save() 호출 update 1회 의미 없음 (더티 체킹)
영속 상태 객체 수정 후 save() 생략 update 1회 의미 없음 (더티 체킹)
준영속 상태 객체 save() 호출 select + update 병합(Merge) 수행
비영속(new) 객체 save() 호출 insert 새로운 데이터 저장
 

결론

  • findById()로 조회한 객체는 이미 영속 상태이므로, 값을 변경하기만 해도 트랜잭션 종료 시점에 자동으로 update된다.
  • 이 경우 save()를 호출하는 것은 불필요하며, 코드 가독성에도 혼동을 줄 수 있다.
  • save()는 신규 객체 저장 또는 준영속 객체 병합 시에만 필요하다.

JPA를 효과적으로 활용하기 위해서는 **엔티티의 생명주기(영속/준영속/비영속)**를 잘 이해하고, 불필요한 save() 호출을 피하는 것이 좋다.

Spring Boot는 @SpringBootTest 어노테이션을 통해 테스트에서 실제 애플리케이션 컨텍스트를 실행할 수 있는 강력한 기능을 제공한다. 이번 포스팅 에서는 아래 내용을 상세하게 설명하려고 한다.

  • @SpringBootTest의 역할과 사용 시점
  • 슬라이스 테스트(@WebMvcTest, @DataJpaTest, @DataJdbcTest 등)와의 비교
  • 컨텍스트 커스터마이징 방법
  • 테스트 시간을 어떻게 줄일 수 있는지

“테스트”란 무엇을 의미할까?

유닛 테스트(Unit Test)

  • 단일 클래스 또는 기능 단위를 테스트
  • 외부 의존성은 mock 처리
  • 빠르고 격리된 테스트
class UserServiceTest {
  @Test
  void testLogic() {
    // 단일 클래스 내 로직만 테스트
  }
}

통합 테스트(Integration Test)

여러 계층의 조합과 실제 흐름을 테스트

통합 테스트는 다음 중 하나일 수 있다:

  1. 여러 객체 간 상호작용 (예: 서비스 ↔ 레포지토리)
  2. 여러 계층 간 테스트 (컨트롤러 → 서비스 → DB)
  3. 전체 애플리케이션 플로우 (요청 → 응답 + DB 상태 확인)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
  @Autowired MockMvc mockMvc;
  @Autowired UserRepository userRepository;

  @Test
  void registrationWorks() throws Exception {
    mockMvc.perform(post("/register")
        .contentType("application/json")
        .content("{\"name\": \"jihun\"}"))
      .andExpect(status().isOk());

    assertTrue(userRepository.findByName("jihun").isPresent());
  }
}

@SpringBootTest 란?

@SpringBootTest는 다음을 제공:

  • 실제 운영 환경처럼 전체 컨텍스트를 로딩
  • 컨트롤러, 서비스, 리포지토리, 빈 모두 포함
  • 테스트 내에서 전체 요청/응답 흐름 시뮬레이션 가능

내부적으로

  • @SpringBootConfiguration을 찾기 위해 패키지 상단까지 탐색
  • 보통 @SpringBootApplication이 붙은 메인 클래스를 사용

다양한 슬라이스 테스트 어노테이션

Spring Boot는 특정 레이어만 테스트할 수 있도록 여러 "Test Slice" 어노테이션을 제공한다.

@WebMvcTest

  • 컨트롤러 계층만 로딩
  • @Controller, @ControllerAdvice, MockMvc만 포함
  • 서비스, 레포지토리는 mock 필요
@WebMvcTest(UserController.class)
class UserControllerTest {
  @Autowired MockMvc mockMvc;
  ...
}

@WebFluxTest

  • WebFlux 환경에서 사용하는 슬라이스 테스트
  • WebTestClient로 WebFlux 컨트롤러 테스트

@DataJpaTest

  • JPA 엔티티, Repository 테스트
  • H2 등 임베디드 DB 자동 설정
  • DB 쿼리, 매핑, 제약 조건 검증에 적합
@DataJpaTest
class UserRepositoryTest {
  @Autowired UserRepository userRepository;
  ...
}

@DataJdbcTest

  • Spring Data JDBC 사용 시 적합
  • 복잡한 매핑보다는 단순한 SQL 기반 테스트에 유용

@JsonTest

  • Jackson, Gson 직렬화/역직렬화 검증
  • ObjectMapper, JacksonTester 자동 구성

@RestClientTest

  • RestTemplate 기반 외부 API 호출 테스트 전용
  • MockRestServiceServer로 응답 모킹 가능

@SpringBootTest 커스터마이징 방법

1. MockMvc 사용: @AutoConfigureMockMvc

@SpringBootTest
@AutoConfigureMockMvc
class MyTest {
  @Autowired MockMvc mockMvc;
}

2. 특정 프로퍼티 설정

@SpringBootTest(properties = "feature.enabled=true")
class FeatureTest {
  @Value("${feature.enabled}")
  boolean enabled;
}

3. 프로필 지정

@SpringBootTest
@ActiveProfiles("test")
class ProfileTest {}

application-test.yml에서 설정된 값 자동 로딩

4. @TestPropertySource로 외부 파일 지정

@SpringBootTest
@TestPropertySource("classpath:test.properties")
class FilePropertyTest {}

5. @MockBean으로 빈 대체

@SpringBootTest
class MockTest {
  @MockBean UserRepository userRepository; // 진짜 빈을 mock으로 대체
}

6. @Import로 외부 빈 등록

@SpringBootTest
@Import(Foo.class)
class ImportTest {}

고급: @TestConfiguration, @SpringBootTest(classes = ...)

운영 클래스가 아닌 테스트 전용 애플리케이션 클래스를 사용할 수도 있다:

@SpringBootTest(classes = CustomTestApplication.class)
class CustomAppTest {}

혹은 @EnableScheduling 같은 설정을 켜고 끄는 조건부 빈을 작성할 수 있다:

@Configuration
@EnableScheduling
@ConditionalOnProperty(name = "scheduling.enabled", havingValue = "true")
public class SchedulingConfig {}

→ 테스트에서는 이렇게 끄기:

@SpringBootTest(properties = "scheduling.enabled=false")

왜 통합 테스트는 느릴까?

  • @SpringBootTest는 전체 컨텍스트를 매번 로딩 → 느림
  • 여러 커스터마이징이 다르면 캐시되지 않음
  • 가능하면 공통 설정 재사용이 테스트 속도 향상에 도움

참고: JUnit Insights로 테스트 시간 시각화 가능


결론

  • @SpringBootTest전체 경로(Controller → Service → Repository) 를 테스트할 때 가장 강력함
  • 단순 레이어 테스트라면 @WebMvcTest, @DataJpaTest슬라이스 테스트를 우선 고려
  • 컨텍스트 커스터마이징은 운영 환경과의 차이를 줄이는 선에서 최소화할 것

Spring Boot에서는 초기화 과정에서 컴포넌트를 주입할 때, 어플리케이션에 대한 Key/Value 형태의 설정을 클래스 내 변수에 값을 넣어주는 @Value Annotation이 존재한다. 이러한 설정은 application.properties 또는 application.yml 과 같은 파일에서 다음과 같은 형식으로 관리할 수 있다.

예) application.properties

application.version = v1.0.2

예) application.yml

application
    version: v1.0.2

이러한 방식을 사용하여 아마존 서비스와 같이 다른 3rd party 서비스를 사용할 때 Access Key 또는 Secret Key 같은 설정을 유용하게 할 수 있다. 또한, Spring Boot는 Profile 별로 설정 파일을 분리하여 관리할 수 있다. 이와 같이 설정 파일에 정의한 값을 사용하기 위하여 Spring Boot에서는 @Value annotation 을 제공하고 있다.

static 변수에 대하여 @Value Annotation 사용

서론에서 설명한 것과 같이 Spring Boot의 설정 파일에 등록되어 있는 값을 주입된 컴포넌트에서 사용하기 위하여 @Value annotation를 사용한다. 클래스 인스턴스 내에서 @Value annotation 으로 decorate 된 변수를 어렵지 않게 사용할 수 있다. 하지만, static 변수 에서 다음과 같이 @Value annotation 을 사용한다면 잘못된 결과를 초래할 수 있다.

예) EncryptionUtil.java

@Component
public class EncryptionUtil {

    @Value("${enc.key}"})
    public static String key;

}

위와 같이 작성된 EncryptionUtil 클래스 내 정의되어 있는 static 변수 key를 사용하려고 EncryptionUtil.key 로 접근을 하게 된다면 항상 null 이 반환 될 것이다. 이는 static 변수에 대하여 @Value annotation 이 동작하지 않는다.

이를 해결하기 위해서는 static 이 아닌 setter 메소드를 추가하여 static 변수에 직접적으로 값을 넣을 수 있도록 하면 된다.

예) 수정된 EncryptionUtil.java

@Component
public class EncryptionUtil {

    public static String key;

    @Value("${enc.key}"})
    public void setKey(String value) {
        key = value;
    }

}

 

 

출처 

 

https://jkpark.me/springboot/java/2020/06/04/Spring-Boot-static-%EB%B3%80%EC%88%98%EC%97%90%EC%84%9C-@value-annontation-%EC%82%AC%EC%9A%A9.html

JPA의 FlushMode, Flush, 변경 감지 개념 및 동작 원리

1. JPA에서 영속성 컨텍스트란?

JPA를 쓰면 엔티티를 데이터베이스에서 가져와 객체로 다루게 된다. 이때 객체를 단순히 메모리에 올리는 게 아니라, 영속성 컨텍스트라는 곳에서 관리하게 된다. 쉽게 말해, JPA가 엔티티를 보관하고 변경 사항을 추적하는 1차 캐시 같은 개념이다.

엔티티를 조회하면 JPA는 먼저 영속성 컨텍스트에서 찾고, 없으면 데이터베이스에서 가져와서 저장한다. 덕분에 같은 트랜잭션 내에서는 동일한 엔티티를 여러 번 조회해도 쿼리가 추가로 실행되지 않는다.

2. Flush란?

영속성 컨텍스트에 있는 엔티티의 변경 사항은 곧바로 데이터베이스에 반영되지 않는다. 대신 쓰기 지연 저장소에 모아뒀다가 특정 시점에 한꺼번에 반영하는데, 이 작업을 flush라고 한다.

하지만 flush는 데이터베이스에 커밋을 하는 게 아니라, SQL을 생성해서 데이터베이스와 동기화하는 것일 뿐이다. 실제 데이터 반영은 트랜잭션이 커밋될 때 이루어진다.

Flush가 발생하는 조건

  1. EntityManager.flush() 메서드를 직접 호출할 때
  2. 트랜잭션이 커밋될 때 (commit 시 자동 실행)
  3. JPQL 또는 네이티브 쿼리를 실행하며, 해당 쿼리가 변경된 엔티티가 포함된 테이블을 조회할 때

3. FlushMode란?

FlushMode언제 flush를 실행할지 결정하는 설정이다.

FlushMode의 종류

  • FlushModeType.AUTO (기본값)
    • 트랜잭션이 커밋될 때 자동 flush
    • JPQL 실행 시 변경된 엔티티가 포함된 테이블을 조회하면 flush
  • FlushModeType.COMMIT
    • JPQL 실행 시 flush가 발생하지 않음
    • 오직 트랜잭션이 커밋될 때만 flush 실행

4. 변경 감지(Dirty Checking)란?

변경 감지는 JPA가 영속성 컨텍스트 내 엔티티의 변경 사항을 자동으로 감지하고 반영하는 기능이다. 엔티티를 수정하면 JPA가 알아서 UPDATE 쿼리를 생성해준다.

변경 감지 동작 방식

  1. 엔티티 조회 (1차 캐시 또는 데이터베이스에서 불러옴)
  2. 엔티티 필드 변경 (entity.setXXX() 호출)
  3. 트랜잭션이 커밋되거나 flush가 실행될 때 1차캐시 엔티티와 스냅샷의 변경사항을 비교
  4. update 쿼리 생성 및 쓰기 지연 저장소에 저장 후 쿼리 실행

5. Flush, FlushMode, 변경 감지 동작 예제

// 1. PK로 엔티티 조회
Member member = MemberRepository.findById(id);
// 엔티티 필드 수정
member.setGrade(5);

// 2. PK가 아닌 컬럼으로 엔티티 조회
MemMember member = MemberRepository.findByPhoneNum(phoneNum);/ ㅇ 이후 쿼리 실행 로직 (생략)

실행되는 SQL 쿼리 순서

  1. SELECT (PK 기반 조회)
  2. UPDATE 실행
  3. SELECT (컬럼 기반 조회)

UPDATE 쿼리가 실행될까?

findByPhoneNum()은 PK가 아닌 컬럼을 기준으로 엔티티를 조회하는 메서드이므로, 1차 캐시가 아닌 데이터베이스 테이블을 직접 조회한다.

따라서 JPA는 **데이터베이스 일관성을 유지하기 위해 조회 전에 변경 사항을 flush**한다. flush 과정에서 변경 감지가 수행되어 UPDATE 쿼리가 생성되고, 쓰기 지연 저장소에 저장된 후 실행된다.

6. Flush가 발생하지 않는 경우

만약 findByPhoneNum()이 아니라 전혀 관련 없는 다른 테이블을 조회한다면, flush는 실행되지 않는다.

즉, flush는 영속성 컨텍스트에 보류 중인 변경이 포함된 테이블을 조회할 때만 발생한다. 다른 테이블을 조회할 경우, 변경 사항과 관련이 없으므로 flush가 실행되지 않는다.

7. 영속성 컨텍스트 - Flush - Commit 과정

이해하기 쉽게 그림으로 정리해보자.

 

이렇게 보면 감이 올 것이다. flush는 변경 사항을 데이터베이스에 밀어 넣지만, 트랜잭션이 커밋되기 전까지 확정되지 않는다.

8. 정리

  • 영속성 컨텍스트: JPA가 엔티티를 관리하는 메모리 공간 (1차 캐시 역할)
  • Flush: 변경 사항을 데이터베이스에 반영하는 과정 (트랜잭션 내에서만 적용됨)
  • Commit: 트랜잭션이 종료되며, 변경 사항이 데이터베이스에 확정됨
  • FlushMode: AUTO는 JPQL 실행 시 flush, COMMIT은 트랜잭션 커밋 시에만 flush
  • 변경 감지: JPA가 변경된 엔티티를 감지하여 UPDATE 쿼리를 생성함

이제 JPA의 flush가 언제, 왜 실행되는지 감이 좀 잡혔을 거다. 더 깊이 알고 싶다면 직접 실험해보는 게 최고다. JPA의 동작을 로그로 확인하면서 실행 순서를 보는 것도 추천한다!

+ Recent posts