Jackson 을 이용한 object serializing, deserializing 에서, 프로퍼티의 값을 원하는대로 바꿔 사용할 수 있다.

1. 원본 프로퍼티에는 @JsonIgnore 를 지정한다.

2. serializing 에 사용될 값에는 @JsonProperty fun 을 사용한다.

3. deserializing 에 사용될 값에는 @set:JsonProperty 를 사용한다.

 

예:

Item 객체를 포함하는 MySomething 객체 인스턴스를 Json <-> Object 변환 시, Item 을 그대로 내보내기보다는 item 의 id 만을 내보내고, 받고 싶은 경우:

class MySomething {
	
    @JsonIgnore
    lateinit var item: Item
    
    @JsonProperty("itemRef")
    fun itemId() = item.id
    
    @set:JsonProperty("itemRef")
    lateinit var itemRef: String
}

 

* item: Item 프로퍼티에 lateinit 을 사용하는 이유:

serializing 에서는 val 를 사용해도 상관 없겠지만, deserializing 에서는 val 를 사용할 수 없다. Json 에서 이 프로퍼티는 다음과 같이 변환될 것인데, deserializing 에서 이걸 가지고 다시 Item 으로 변환할 수 없다.(따로 deserializer 를 정의하면 가능하겠지만 이는 논외로 한다.)

"itemRef":"<item_id>"

위의 이유로 deserializing 시에 item 프로퍼티는 초기화되지 못하여(위의 "itemRef" 는 itemRef: String 프로퍼티에 세팅된다.) val 사용 시 deserializing 에서의 인스턴스 생성 시점에 예외가 발생한다.

 

* itemRef: String 프로퍼티에 lateinit 을 사용하는 이유:

위와 비슷하다. itemRef 는 deserializing 을 위한 것으로, 이외의 상황에서 사용되진 않는다고 상정했기 때문에 평소에는 굳이 초기화할 필요 없도록 남겨두고 위해서이다. (val 를 사용하면 반드시 초기화해야 하고, json 변환이 필요하지 않은 상황에도 영향을 주게 된다.)

 

* fun itemId() 를 사용하는 이유:

아래와 같이 사용해도 serializing 시에 같은 결과가 나온다:

@get:JsonProperty("xxxRef")
val itemId = item.id

 

하지만 이렇게하면 deserializing 시에 item property not initialized exception 을 만나게 된다. 이는 코틀린에 관한 것인데, val 프로퍼티는 인스턴스 생성 과정에서 시점에 초기화가 이루어진다. val itemId 프로퍼티 초기화 시 item.id 를 참조하게 되는데, item 프로퍼티는 아직 초기화되지 못한 상태이다. 때문에 예외가 발생한다. 이를 방지하고자 fun 을 사용한다.

'기술 일반 > 일반' 카테고리의 다른 글

CLOB 컬럼 핸들링 중 인코딩 문제  (0) 2019.03.08
유니코드와 UTF-8, UTF-16  (0) 2017.04.07

또 자꾸 헷갈려서 정리

 

JPA 에서, 관계를 맺는 데이터가 존재하는지 여부를 알 수 있는 방법이 직접 조회(select) 외엔 없다면, lazy loading 이 작동하지 못한다. (이 이유는 null 데이터에 대해 프록시를 생성할 수 없기 때문이다. 자세한 내용은 인터넷에 많으므로 생략.)

 

@OneToOne 에서, 다음의 조건을 만족한다면 lazy loading 이 작동한다.

1. (단방향/양방향에 관계 없이) FK 를 가진 쪽(관계의 주인)에서 조회한다.

- FK 가 null 이면 데이터가 없다고 판단할 수 있으므로, lazy loading 이 작동한다. 

 

2. FK 를 가지진 않았지만 optional = false 이다.

- FK 를 가지고 있지는 않지만 optional = false, 즉 이 관계는 필수임이 명시되어 있어 JPA 는 반드시 대상 데이터가 존재한다고 판단할 수 있다. 

 

OneToOne 이 아닌 다른 관계에서도 어쨌든 핵심은 관계하는 대상 데이터의 존재 여부를 미리 알 수 있느냐 이다. 알 수 있다면 lazy loading 은 작동할 것이고, 아니라면 작동하지 못할 것이다.

 

레퍼런스:

https://kwonnam.pe.kr/wiki/java/jpa/one-to-one

https://1-7171771.tistory.com/143

'Java' 카테고리의 다른 글

test] @Testcontainers, @Container  (0) 2022.08.03
JPA] Hibernate 1차 캐시  (0) 2022.06.27

메서드 파라미터로 객체를 전달할 것이냐? 객체의 프로퍼티를 전달할 것이냐?

아직 명확한 결론에 도달하지 못했다. 양쪽에 장단점이 있고, 컨텍스트에 따라 다르기 때문에 어느 하나의 정답이 존재하는 물음이 아니다.

 

일단 단순한 케이스에서 눈에 보이는 장단점을 정리하면,

아래와 같은 두 함수가 있다고 하자

fun doSomethingWithEachProperties(String arg1, Int arg2, Long arg3) {
	...
}

fun doSomethingWithWholeObject(ParamObject arg) {
	... // arg.getString(), arg.getInt(), arg.getLong() 을 필요할 때 알아서 호출하여 꺼내 씀
}

첫 번째 방법의 장점

- 정확히 필요한 인자만을 취하기에 유리하다. 함수를 호출하는 쪽에서도 어떤 값이 필요한지 정확히 알 수 있으며, 함수 코드의 응집성이 높아진다.

- 보통 첫 번째 접근법을 따를 때는, 일반적으로 메서드는 기본형(String 포함) 인자를 취한다. 때문에 하나의 객체 정의에 묶이지 않고, 보다 공통적으로 사용될 수 있는 함수를 정의하기에 유리하다.

 

첫 번째 방법의 단점

- 함수를 호출하는 쪽에서 어떤 값이 필요한지 정확히 안다는 사실은, 달리 말하면 함수가 '어떻게' 작동하는지 노출한다는 뜻이 될 수 있다. (함수가 private 이라면 이야기가 달라진다.)

- 함수의 작동 방법이 달라져 다른 인자를 추가로 필요로 하게되면 함수 시그니처를 변경해야 할 것이고, 그렇게 되면 이 메서드를 호출하는 모든 코드를 변경해야 한다.

 

 

두 번째 방법의 장점

- 일단 코드가 깔끔해진다. 함수 시그니처, 함수 호출 코드 모두 간결해진다.

- 함수 작동 방법이 달라져 다른 인자가 추가로 필요해져도 함수 시그니처 변경 없이 ParamObject 만을 변경해서 처리할 수 있다. 즉 함수 호출 코드를 변경하지 않을 수 있다. (언제나 그렇지는 않다.)

- 함수가 구체적인 값을 받지 않으므로, 함수의 클라이언트에게 함수가 어떻게 작동하는지에 관하여 '덜 노출하게 되는' 성질이 있다. (역시 함수가 private 이라면 이야기가 달라진다.)

 

두 번째 방법의 단점

- ParamObject 가 이 함수가 필요로 하는 값 외에 다른 값을 가지고 있다면, 이는 함수의 응집성이 낮아진다고 볼 수... 있을까?

- 함수 호출만을 위해 새로운 객체를 정의해야 할 수 있다. 이러면 오히려 더 많은 코드를 작성해야 한다. (물론 이는 추천하지 않는다.)

 

그래서?

예전에는 '코드(함수)가 꼭 필요한 것만을 알도록 한다' 에 중점을 두었었다. 응집성에 더 비중을 둔 것이었는데, 갈수록 생각을 달리하게 되는 케이스를 만난다. 클린 코드에서는 함수의 인자가 3개를 넘기지 않도록 하라는 이야기도 있고(물론 절대적인 것은 아니지만), 구현 캡슐화의 관점에서 보면 이것이 과연 옳은가 하는 생각이 든다.

코드 디자인은 '변경' 을 보다 잘 다루기 위한 일이라는 관점에서 보면, 인자가 추가될 경우를 고려하면 객체를 넘기는 편이 바람직해 보인다. 함수의 변경이 다른 코드의 변경을 유발하는 일은 기본적으로 안티 패턴으로 취급된다. (Shotgun Surgery)

 

 

자꾸 헷갈려서 정리

 

어플리케이션 시작 시 호출되는 순서

1. ApplicationStartingEvent

2. ApplicationEnvironmentPreparedEvent

3. ApplicationContextInitializedEvent

4. ApplicationPreparedEvent

5. ContextRefreshedEvent

6. ApplicationStartedEvent

7. ApplicationReadyEvent

 

여기서, 1-3 번 까지는 ApplicationContext 초기화 전에 시작된다. 때문에, 이 이벤트들은 @EventListener 를 통한 이벤트 등록으로 처리할 수 없다. 왜냐하면 @EventListener 는 해당 리스너 메서드를 가진 클래스가 스프링 빈으로 먼저 스캔되어야 작동하는데, 1-3번 이벤트들은 ApplicationContext 시작 전이기 때문에 이벤트 등록보다 실제 이벤트가 더 먼저 발생한다.

 

때문에 1-3 번 이벤트들이 작동하기 위해서는 아래와 같이 스프링 어플리케이션 시작 전에 리스너를 선등록해야 한다.

class MyApplicationStartingEventListener : ApplicationListener<ApplicationStaringEvent> {
	override fun onApplicationEvent(event: ApplicationStartingEvent) {
    	...
    }
}

...

fun main(args: Array<String>) {
    SpringApplicationBuilder(MyApplication::class.java)
        .listeners(
                MyApplicationStartingEventListener(),
                MyApplicationContextInitializedEventListener(),
                MyApplicationEnvironmentPreparedEventListener(),
                MyApplicationPreparedEventListener(),
            )
            .run(*args)
}

 

또, 이벤트는 등록한 순서대로 호출된다. 위의 방법으로 등록되는 이벤트는 ApplicationContext 초기화 시 스캐닝에 의해 등록된 이벤트보다 먼저 등록되기 때문에, 동일한 이벤트가 호출될 때, @EventListener 리스너보다 무조건 먼저 호출된다.

@Testcontainers 는 JUnit 5 (주피터) 의 테스트컨테이너 어노테이션이다.

 

이 어노테이션은 클래스에 지정하며, 이 어노테이션이 지정된 클래스는 테스트 실행 시 자신 안에서 @Container 가 지정된 필드를 찾아 컨테이너의 라이프싸이클을 관리한다.

 

@Container 필드가 static 이면 이 컨테이너는 첫 번째 테스트 메서드 시작 전에 한 번만 시작되어 마지막 테스트 메서드 종료 시 정지된다. 즉 모든 테스트 케이스에 대해 하나의 컨테이너를 공유해서 사용한다. (이 때 jdbc:tc:.. 를 사용하면 안 된다.)

오해하지 말아야 할 것이, '첫 번째 테스트 메서드' 는 하나의 테스트 클래스 안에서의 이야기이다. 프로젝트의 모든 테스트를 수행할 때는 여러 테스트 클래스를 실행하게 되는데, 컨테이너는 한 테스트 클래스의 시작과 함께 종료되고, 다른 테스트 클래스가 있으면 다시 시작된다.

그리고 컨테이너의 라이프싸이클(시작-종료) 는 알아서 관리되기 때문에, 사용자가 직접 start() 등을 호출할 필요가 없다.

 

또, MySQL 등의 컨테이너는 기본적으로 랜덤 포트를 사용하기 때문에 이 포트를 알기 위해서는 스프링 어플리케이션 컨텍스트 시작 이벤트를 받아서 getJdbcUrl() 등을 호출하게 되는데, 스프링 어플리케이션 컨텍스트는 테스트 시 기본적으로 한 번 로드되고 재사용된다.

이렇게되면 컨테이너는 테스트 클래스마다 다시 시작되어 포트가 변경되는데, jdbc url 은 처음 테스트에서 얻은 것을 그대로 사용하게 되어 첫 테스트 클래스만이 db 커넥션 연결에 성공하고 나머지는 실패하게 된다. 이를 방지하기 위한 방법 중 하나로, @DirtiesContext 를 사용하여 매 테스트 클래스마다 새로운 어플리케이션 컨텍스트를 로드하게 하여 매번 새로운 컨테이너의 jdbc url 을 받아서 사용하는 것이 있다.

 

@Container 필드가 인스턴스 필드이면 이 컨테이너는 각 테스트 메서드 시작 전에 시작되고, 메서드 종료 시 정지된다. 모든 테스트 케이스에 대해 각각의 컨테이너가 할당되는 셈이다.

 

예 (주피터 javadoc 에서 가져옴): 

  @Testcontainers
  class MyTestcontainersTests {
 
      // will be shared between test methods
      @Container
      private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();
 
      // will be started before and stopped after each test method
      @Container
      private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
              .withDatabaseName("foo")
              .withUsername("foo")
              .withPassword("secret");
 
      @Test
      void test() {
          assertTrue(MY_SQL_CONTAINER.isRunning());
          assertTrue(postgresqlContainer.isRunning());
      }
  }

 

@Testcontainers 를 슈퍼클래스에 지정하고 테스트 클래스는 이 클래스를 상속받아 사용하는 것이 가능하다. (@Inherited)

 

출처: https://javadoc.io/doc/org.testcontainers/junit-jupiter/latest/org/testcontainers/junit/jupiter/Testcontainers.html

'Java' 카테고리의 다른 글

JPA] @OneToOne lazy loading  (0) 2022.08.22
JPA] Hibernate 1차 캐시  (0) 2022.06.27

사용법

ApplicationContext 전체를 초기화하지 않고 일부 Bean 만을 테스트할 때

@ExtendWith(SpringExtension::class) + @ContextConfiguration + @TestConfiguration

=> @TestConfiguration 에서 원하는 빈을 초기화한다. (@Bean 을 쓰거나 @ComponentScan을 쓰거나..)

=> 프로파일 등 수동 프로퍼티 지정이 필요할 때는 @ContextConfiguration 의 initializers 에 ApplicationContextInitializer 구현체를 지정한다.

예(코틀린): 

    class PropertyOverrideContextInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
        override fun initialize(applicationContext: ConfigurableApplicationContext) {
            TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
                applicationContext,
                "spring.config.activate.on-profile=production",
            )
        }
    }

=> application.properties(or yml) 자동 로딩을 사용하고 싶으면 initializers 에 ConfigDataApplicationContextInitializer 클래스를 추가한다. 왜냐하면, 설정파일 자동 로딩을 통한 어플리케이션 자동 설정은 스프링 부트의 기능이다. SpringExtension::class 은 스프링 부트가 아닌 전통 방식으로 스프링을 가동하기 때문에, 별도 설정이 없다면 비어있는 프로퍼티 값을 보게 될 것이다.

 

 

ApplicationContext 전체를 초기화하지 않고 프로퍼티 설정을 테스트할 때

@ExtendWith(SpringExtension::class) + @ContextConfiguration with ConfigDataApplicationContextInitializer + @TestConfiguration + @EnableConfigurationProperties(optional)

=> 여기서 @TestConfiguration 은 초기화 대상 빈을 지정하는 역할을 한다. 만약 정말 프로퍼티 미러링 빈만을 필요로 한다면 아래와 같이 사용할 수 있다. (다른 빈들도 필요하다면 그냥 @ComponentScan 으로 프로퍼티 미러링 빈과 다른 빈들을 모두 함께 초기화하는 편이 낫다)

@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
public class MongoPropertiesTest {
    
    @Autowired
    private MongoProperties properties;

    @Configuration
    @EnableConfigurationProperties({MongoProperties.class})
    public static class Config {

    }

}

 

 

Jpa 테스트 수행 시

@DataJpaTest 를 사용한다. 추가로 다른 빈이 필요한 경우 @TestConfiguration + @Import 를 사용한다.

 

웹 MVC 테스트 수행 시

@WebMvcTest 를 사용한다. 추가로 다른 빈이 필요한 경우 @TestConfiguration + @Import 를 사용한다.

 

@Service 등 비즈니스 로직 테스트 수행 시

1. 데이터 저장 애뮬레이션이 필요하다면 아래와 같이 사용한다.

@ExtendWith(SpringExtension.class)  @SpringBootTest + @TestConfiguration(선택) + @Import 

2. 데이터 저장 애뮬레이션이 필요하지 않다면 아래와 같이 사용한다.

@ExtendWith(SpringExtension.class) + @SpringBootTest + @TestConfiguration(선택) + @Import + @MockBean(to repositories) 

 

통합 테스트 수행 시

@SpringBootTest 를 사용한다.

 

어노테이션 설명

@RunWith

JUnit 4 의 어노테이션으로, JUnit 의 Test Runner 구현체를 인자로 받아 테스트 메서드 실행에 관한 동작을 확장할 수 있다. 주로 아래와 같이 사용된다.

@RunWith(SpringJUnit4ClassRunner.class)

Runner 에 대한 자세한 설명은 아래를 참조

https://eminentstar.github.io/2017/07/23/about-junit-and-test.html

 

 

@RunWith(SpringJUnit4ClassRunner.class) vs @RunWith(SpringRunner.class)

같은 녀석들이다. SpringJUnit4ClassRunner 의 이름을 줄인 버전이 SpringRunner 라고 생각하면 된다.

 

 

@ExtendWith

JUnit 5 의 어노테이션으로, @RunWith 을 대체한다. @RunWith 에서 Runner 의 구현체를 인자로 받았던 것처럼, 여기서는 Extension 인터페이스의 구현체를 인자로 받는다. Extension 은 주로 test 메서드의 전/후 동작, 조건부 테스트 실행, 테스트 메서드 라이프싸이클 콜백, 파라미터 리솔루션, 예외 처리 등 테스트에 영향을 미치는 동작에 대한 확장을 제공한다. 대표적인 구현체로 SpringExtension 이 있다.

SpringExtension 은 어플리케이션 컨텍스트를 로드한다. 아래 @SpringBootTest 또한 어플리케이션 컨텍스트를 로드하는데, 둘의 차이는 SpringExtension 은 어플리케이션 컨텍스트를 '전통적인(tranditional) 스프링 방식' 으로 로드하고, @SpringBootTest 는 '스프링 부트 방식' 으로 로드한다는 점이다. 때문에 SpringExtension 는 스프링 부트에서 제공하는 '외부 설정 로딩' 을 사용하지 못하여 application.properties, 환경변수, cli 아규먼트 등으로 어플리케이션 옵션을 주입할 수 없다. 프로퍼티를 사용하려면 ConfigDataApplicationContextInitializer, @EnableConfigurationProperties 등을 사용해서 직접 프로퍼티 설정 로딩 코드를 작성해야 한다.

 

 

@SpringBootTest

스프링 어플리케이션 컨텍스트를 로드하는 어노테이션으로, 운영 환경에 가장 가까운 환경을 애뮬레이션한다. 필요한 모든 빈을 초기화하기 때문에 무겁게 작동한다. 내장 톰캣을 사용한 웹 환경 애뮬레이션을 지원하며, 통합 테스트에 적절하다. 이 어노테이션의 실체는 @BootStrapWith(SpringBootTestContextBootstrapper.class) 이다. SpringBootTestContextBootstrapper 에서 어플리케이션 컨텍스트를 로드하고 클래스패스에서 @SpringBootConfiguration(@SpringBootApplication) 을 찾아 초기화한다.

스프링 부트 버전 2.1 부터 이 어노테이션에 @ExtendWith(SpringExtension.class) 가 붙었다. 즉 버전 2.1 부터는 @ExtendWith 을 함께 사용할 필요가 없다.

위 SpringExtension 에서 설명하였듯, 이 어노테이션은 어플리케이션 컨텍스트를 '스프링 부트 방식' 으로 로드한다. 때문에 프로퍼티, 환경변수, cli 아규먼트를 사용하는 일이 가능하다.

@Configuration 또는 classes 가 함께 지정되지 않으면, 이 어노테이션이 지정된 클래스의 상위, 하위에 존재하는 @SpringBootConfiguration 을 모두 찾는다.

@ContextConfiguration(loader=...) 를 지정하지 않으면 기본적으로 SpringBootContextLoader 를 사용한다.

 

 

@MockBean

목 인스턴스를 주입한다. 테스트 클래스가 다른 빈에 의존할 때, 논리적으로는 필요하지만 실제 동작 테스트까지는 필요하지 않은 빈이 있을 수 있다. 그럴 때 @Autowired 를 대신해서 이 어노테이션을 사용한다. 목 인스턴스는 실제로 작동하지는 않고 '작동하는 척' 한다. 아래와 같이 given 등 메서드를 사용해서 마치 실제로 호출해서 어떤 결과를 반환받은 것처럼 사용한다.

given(mockRepository.save(any(MyEntity.class))).willReturn(mySavedEntity())

 

 

@DataJpaTest

data jpa 에 관련한 테스트를 지원하기 위한 어노테이션으로, jpa 리포지토리 테스트에 사용하기 적절하다. data jpa 작동에 필요한 빈만을 초기화하기 때문에 @SpringBootTest 보다 훨씬 가볍게 작동한다. 하지만 @Service 등 다른 빈을 초기화하지 않기 때문에, 테스트가 퍼시스턴스 레이어 위쪽에 의존성을 가진 경우 이 어노테이션만으로는 테스트 불가능하다.

이 어노테이션은 기본 설정으로 테스트 시 인메모리 데이터베이스를 사용하도록 하며, 테스트 케이스마다 @Transactional 을 적용하며 이 트랜잭션은 한 테스트 케이스가 종료되면 자동으로 롤백을 수행한다.

 

 

@AutoConfigureTestDatabase

어플리케이션에 설정된 데이터베이스를 테스트용 데이터베이스로 대체하는 어노테이션이다. replace 라는 인자값이 중요한데, 아래와 같은 값들이 있다.

Replace.ANY: 기본값으로, 명시적 or auto-configured 된 테스트용 데이터베이스로 대체한다. 

Replace.AUTO_CONFIGURED: 오직 auto-configured 된 테스트용 데이터베이스로만 대체한다.

Replace.NONE: 데이터베이스를 대체하지 않는다.

@DataJpaTest 는 이 어노테이션을 ANY 로 사용하며 인메모리 데이터베이스로 대체한다. 직접 데이터베이스를 구동하여 테스트 결과를 조회하고 싶거나 하는, 이런 동작을 원치 않는 경우에는 Replace.NONE 을 사용하면 된다.

 

 

@WebMvcTest

data jpa 전용 테스트를 지원하기 위해 @DataJpaTest 가 있듯, 웹 MVC를 테스트하기 위해 이 어노테이션이 있다. 아래와 같은 대상만 스캔하여 빈으로 초기화한다.

@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor

주로 컨트롤러-인터셉터-필터를 테스트하기 위해 사용한다.

 

 

@TestConfiguration

테스트 설정을 위한 어노테이션으로, @Configuration 을 테스트 환경에 맞게 확장한 것이라 생각할 수 있다. 사실 그 동작은 @Configuration 과 다르지 않다. (실제로 @TestComponent 가 붙어있을 뿐이다.) 차이점이 있다면 이 어노테이션은 기본적으로 @SpringBootConfiguration 에서 스캔하지 않는다. 때문에 직접 @Import or @ContextConfiguration 을 명시해야 한다. (대신, @SpringBootTest(classes=... ) 를 사용하거나, @ComponentScan을 직접 사용한 경우 스캔 대상이 되므로 이 때 이 어노테이션이 지정된 클래스를 제외하기를 원한다면 TypeExcludeFilter 를 사용해야 한다.)

 

이 어노테이션의 사용법은 대표적으로 두 가지가 있다.

1. @SpringBootTest 지정 클래스의 내부클래스에 지정하기

이 때는 @Import 를 사용하지 않아도 자동으로 설정이 로드된다. 특정 @SpringBootTest 테스트 클래스 안에서만 사용할 설정을 정의한다.

 

2. 독립된 외부 클래스에 지정하고 @Import 하기

여러 테스트에서 공유해서 사용할 설정을 정의한다.

 

이 어노테이션의 주 용도는:

1. 테스트 환경에서 필요한 빈들만을 초기화하는 데에 있다. @DataJpaTest, @WebMvcTest 와 같이 특정 레이어에 대한, 통합테스트보다 가벼운 환경으로 테스트하는 경우 초기화 대상에서 벗어난 빈이 필요할 수 있다. 이를 위해 @SpringBootTest 를 사용한다면 불필요하게 무거운 동작이 추가될 것이다. 이 때 @TestConfiguration 을 사용해서 원하는 빈들을 초기화하는 클래스를 작성하고, 테스트 클래스에서 이 클래스를 @Import 하면 여기의 빈들만을 초기화한다.

https://csy7792.tistory.com/335

 

2. 테스트 환경에서 특정 빈을 오버라이딩할 수 있다. 어떤 빈들은 테스트 시 다른 구현체를 사용하고 싶다면, 이 어노테이션을 사용하여 같은 타입, 이름의 빈 초기화 코드를 넣고 @Import 하면 테스트 시 이 빈을 사용한다.

빈 오버라이딩 시에는 spring.main.allow-bean-definition-overriding=true 를 설정하거나, 빈 이름을 따로 지정해서 기존 빈 이름과 겹치지 않게 하여야 한다. 그렇지 않으면 빈 중복으로 에러가 발생한다.

https://www.logicbig.com/tutorials/spring-framework/spring-boot/test-configuration.html

 

 

 

 

 

 

yaml

version: "3.8"

volumes:
  mongo-db-local: { }

services:
  mongo-1:
    image: mongo
    container_name: my-mongo-1
    ports:
      - "30000:30000"
    volumes:
      - mongo-db-local:/data1/db
    command: mongod --replSet anna-replSet --port 30000 --bind_ip_all

  mongo-2:
    image: mongo
    container_name: my-mongo-2
    ports:
      - "30001:30001"
    volumes:
      - mongo-db-local:/data2/db
    command: mongod --replSet anna-replSet --port 30001 --bind_ip_all

  mongo-3:
    image: mongo
    container_name: my-mongo-2
    ports:
      - "30002:30002"
    volumes:
      - mongo-db-local:/data3/db
    command: mongod --replSet anna-replSet --port 30002 --bind_ip_all

  mongo-setup:
    image: mongo
    depends_on:
      - mongo-1
      - mongo-2
      - mongo-3
    restart: "no"
    entrypoint: ["bash", "-c", "sleep 5 && mongo --host my-mongo-1:30000 --eval 'rs.initiate({\"_id\":\"anna-replSet\",\"members\":[{\"_id\":0,\"host\":\"my-mongo-1:30000\"},{\"_id\":1,\"host\":\"my-mongo-2:30001\"},{\"_id\":2,\"host\":\"my-mongo-3:30002\"}]})'"]

 

커맨드

docker-compose -f local-mongo.yaml -p local-mongo up -d

 

레플리카셋 구성까지 잘 되지만 완전하지 않다. MongoDB Compass로 레플리카셋 접속을 하기 위해서는 로컬의 /etc/hosts 파일에 아래 내용을 추가해주어야 한다.

my-mongo-1 127.0.0.1
my-mongo-2 127.0.0.1
my-mongo-3 127.0.0.1

 

 

이 작업을 하지 않으면 my-mongo-{n} 호스트를 찾을 수 없다는 따위의 오류가 발생한다. 커넥션 url에 "localhost" 를 사용해도 이 오류가 표시된다. 오류 내용으로 추측건대, MongoDB Compass는 먼저 지정된 노드로 접근하여 레플리카셋 정보를 쿼리하고 그 안의 호스트 이름으로 접근을 시도하는 것 같다. 몽고디비가 가진 레플리카셋 정보에는 rs.initiate()에 인자로 넘긴 호스트 정보가 담겨있기에, "my-mongo-1" 를 반환하는데, 이게 MongoDB Compass는 이게 로컬호스트인지  알 수 없다. 때문에 위 작업이 필요한 것이다.

반면 스프링부트를 사용한 몽고디비 연결 시에는 localhost 로 지정해도 연결이 잘 된다. 아마 둘의 연결 방식에 차이가 있는 것 같다.

 

====

아래 내용을 몰랐다가 굉장히 고생했다. 역시 쉽게쉽게 가려고 하면 더 힘들어진다....

 

- Docker Compose는 기본적으로 하나의 default network를 생성하고, 각 서비스(컨테이너)는 이 네트워크에 조인한다. 때문에 별도 설정 없이도 컨테이너끼리 자신의 이름을 통해 서로를 식별하고 접근할 수 있다.

 

- network_mode: "host" 를 사용하면 컨테이너끼리는 "localhost"로 통신 가능하지만, 브라우저 등 컨테이너 외부로부터의 localhost 호출은 불가능하다. 알아본 바에 의하면, mac에서의 도커는 리눅스 버추얼머신을 사용하며, 컨테이너는 이 안에 위치한다. network_mode: "host" 설정은 이 버추얼머신 안에서만 유효하기 때문에, 함께 버추얼머신 안에 위치한 컨테이너끼리는 서로 접근 가능하지만 외부에서는 접근이 불가능하다. 이 network_mode: "host"의 사용 목적에 완전히 반하는 동작으로 보인다. (다른 os는 확인하지 않음)

 

 

'DB > MongoDB' 카테고리의 다른 글

도커로 레플리카셋 구성하기  (0) 2022.07.14
트랜잭션에 관하여  (0) 2022.07.14

1. 도커 네트워크 생성

docker network create {network_name}

 

2. 컨테이너 생성

docker run -p {exposing_port1}:27017 --name mongo1 -d --net {network_name} mongo mongod --replSet mongo-set
docker run -p {exposing_port2}:27017 --name mongo2 -d --net {network_name} mongo mongod --replSet mongo-set
docker run -p {exposing_port3}:27017 --name mongo3 -d --net {network_name} mongo mongod --replSet mongo-set

 

3. 레플리카셋 생성

docker exec -it mongo1 mongo

접속 후 몽고쉘에서

config = {
	"_id": "mongo-set",
    "members": [
		{"_id":0, "host":"mongo1:27017"},
		{"_id":1, "host":"mongo2:27017"},
		{"_id":2, "host":"mongo3:27017"}
	]
}

rs.initiate(config)
// output: { "ok" : 1 }

 

4. 확인

아무 컨테이너에 접속해서 아래 커맨드 입력하면 primary, secondaries 등 레플리카 맴버들의 상태, 셋 이름 및 기타 정보가 표시됨.

db.runCommand("ismaster")

 

 

하지만 이 방법으로 하면 MongoDB Compass로 레플리카셋 접근이 되지 않는다. 추측건대 MongoDB Compass는 첫 노드에 접속하고 다른 레플리카 맴버의 정보를 노드를 통해 받아서 접속하는 것 같다. 때문에 mongo{N} 호스트를 알 수 없다는 오류가 뜬다. /etc/hosts 에 등록하면 호스트는 인식하지만 27017 포트에 접근할 수 없다는 오류가 뜬다. 때문에, MongoDB Compass를 사용하려면 몽고디비 실행 시 기본 포트를 사용하지 않고 직접 포트를 지정하되, 레플리카 맴버간 겹치지 않는 포트를 사용하도록 한다. 그리고 도커 포트 매핑을 이 포트로 사용하여 레플리카 맴버간 포트가 겹치지 않게 설정하면 해결할 수 있다.

 

 

 

'DB > MongoDB' 카테고리의 다른 글

Docker Compose 를 이용한 one-command 레플리카셋 구성  (0) 2022.07.15
트랜잭션에 관하여  (0) 2022.07.14

몽고디비의 트랜잭션은 트랜잭션 안에서 연산이 되는 document의 수에 따라 single-document / multi-document 로 나눌 수 있다. 

multi-document 트랜잭션은 여러 documents와 컬렉션에 대한 nomalizing 작업이 필요한데 반하여, single-document 트랜잭션은 embedded documents와 array를 사용하여 데이터간의 관계를 캡처하는 일이 가능하기 때문에 기본적으로 atomic하다. 당연히 multi-document 트랜잭션이 훨씬 어려운 일이고, 비용이 비싼 작업이다.

 

몽고디비 버전 4.0 에서는 레플리카셋에 대한 multi-document 트랜잭션을 지원한다.

버전 4.2 부터는 여기에 더하여 distributed transactions(분산 트랜잭션)이 등장했는데, 샤딩된 클러스터들에 대한 multi-document 트랜잭션을 지원한다.

 

multi-document 트랜잭션은 atomic 하며, 다음과 같은 성질을 지닌다.

  • 트랜잭션 안에서 커밋되지 않은 상태의 변경은 트랜잭션 밖에서 볼 수 없다. 변경된 데이터는 오직 커밋 후에만 보여진다.
  • 여러 샤드에 대한 트랜잭션일 때, 트랜잭션 안에서 변경 중인 데이터라고 해서 트랜잭션 밖에서 무조건 볼 수 없는 것은 아니다. "local" 모드의 read 연산을 통해 아직 모든 샤드에 대한 트랜잭션이 완료되지는 않았어도 특정 샤드에는 커밋된 트랜잭션 데이터는 볼 수 있다. 예를 들어 샤드 A, B 에 각각 데이터 1, 2 를 입력하는 트랜잭션이 있다면, 샤드 A 트랜잭션이 커밋되면 샤드 B 트랜잭션이 완료되지 않았어도 샤드 A 에서의 local read 연산은 데이터 1을 볼 수 있다.
  • 트랜잭션이 취소되면 모든 변경은 롤백된다.

 

WiredTiger 스토리지 엔진의 부담

몽고비디의 write는 먼저 그 내용을 WiredTiger의 공유 캐시에 적용하고 나중에 디스크에 write하는 방식이다. 트랜잭션이 작동하는 중에는 트랜잭션이 시작한 시점의 스냅샷에 모든 write를 보관하고 있어야 한다. 트랜잭션 안에서 데이터는 일관성을 보장해야 하기 때문에, 트랜잭션 중간에 발생하는 write들은 바로 적용되지 못하고 캐시에 쌓이게 된다. 이 write들은 트랜잭션이 커밋 혹은 롤백될 때까지 flush되지 못하고 남아 있다가, 트랜잭션이 종료되면 락이 풀리고 WiredTiger는 스냅샷을 폐기할 수 있게 된다. 이런 작동 방식으로 인해 장시간 유지되는 트랜잭션 혹은 한 트랜잭션 안에서 너무 많은 양의 데이터 변경이 발생하면 WiredTiger에게 부담이 된다.

 

트랜잭션 타임아웃

위의 이유로, 몽고디비는 multi-document 트랜잭션에 대해 기본적으로 60초의 타임아웃이 적용된다. 60초를 초과하는 트랜잭션은 실패로 처리한다. 당연히 이 시간은 조정 가능하다. 하지만 되도록 이 시간을 조정하기보다는 트랜잭션을 가능한대로 잘게 분리하여 실행하는 편이 바람직하다.

 

한 트랜잭션 안에서의 연산 수

몽고디비에 한 트랜잭션 안에서 연산의 수를 제한하지는 않는다. 하지만 best practice 제안으로는 한 트랜잭션 안에서 변경되는 documents는 1000개를 넘지 않는 것이 좋다. 만약 1000개의 documents를 초과하는 트랜잭션이 필요하다면, 역시 마찬가지로 가능한대로 이를 나누어 처리하는 편이 바람직하다.

 

분산 트랜잭션

여러 샤드에 대한 트랜잭션은 다른 여러 노드와의 네트워크 통신을 사용하는 코디네이션을 필요하기 때문에 훨씬 큰 비용이 필요하다. 여러 샤드에 대한 데이터 일관성을 보장하는 read concern은 Snapshot 뿐이다. 만약 여러 샤드에 대한 읽기 일관성이 중요하지 않은 상황이라면, 트레이드오프로 read concern 으로 local 을 취하는 것도 방법이다.

 

예외 핸들링

당연하게도 몽고디비 트랜잭션 실패 시 모든 변경을 롤백된다. 실패 요인이 네트워크 에러와 같은 임시적인 성질을 가지고 있다면, 예외를 캐치하여 트랜잭션을 재시도하는 편이 바람직하다. 네트워크 에러 외에 예상할 수 있는 실패 요인에는 MVCC write 충돌, primary 레플리카 선정 등이 있다. 몽고디비 클라이언트에는 retryable write를 자동으로 지원하는 연산들이 있다. 이에 대해서는 아래 페이지를 참조한다:

https://www.mongodb.com/docs/manual/core/retryable-writes/

multi-document 트랜잭션의 경우 commit or abort 연산이 실패하는 경우 retryWrite=false 설정이라도 연산을 한 번 재시도한다.

 

쓰기 지연

multi-document 트랜잭션을 사용함으로써 얻어지는 성능상 이점에는 commit 지연 감소가 있다. w:majority write concern을 사용하면 10번의 데이터 변경 연산은 각각 10번의 리플리케이션을 대기해야 한다. 1번 부터 10번 까지의 update가 모두 레플리카들에 전파될 때까지 기다려야 한다는 뜻이다.

반면 10번의 변경이 한 트랜잭션 안에서 작동하면 리플리케이션은 오직 한 번, commit 시점에만 이루어진다. 이는 지연을 상당량 줄일 수 있다.

 

적절한 write concern

몽고디비는 write 연산에 대한 영구성 보장 수준을 조정할 수 있는 write concern이라는 컨셉을 제공한다. write concern은 single/multi-document 트랜잭션 모두에 적용 가능하다.

  • Write Acknowledged: default concern이다. 몽고디비는 모든 write 연산 시도에 대해 각각 성공 여부 확인하며, 클라이언트는 네트워크, 키 중복, 스키마 벨리데이션 및 다른 예외들을 캐치할 수 있다.
  • Journal Acknowledged: 몽고디비는 write 연산의 성공 여부를 primary의 journal 에 flush된 이후에만 확인한다. 이 수준의 설정으로 몽고디비에 문제가 발생한 경우 write 연산이 복구되어 디스크에 저장됨을 보장할 수 있다.
  • Replica Acknowledged: 몽고디비가 write 연산이 적용되는 레플리카 맴버들의 acknowledgment 를 대기하도록 한다. 몇 개의 레플리카 맴버의 ack를 받으면 성공으로 볼 것인지 그 숫자를 설정할 수 있다. 또한 write 연산이 secondaries의 journal에까지 적용됨을 보장한다. 
  • Majority: 몽고디비는 write 연산이 과반수의 레플리카와 electable 맴버에 적용될 때까지 대기한다. 이렇게 함으로써 write 연산은 primary election이 이벤트가 발생해도 롤백되지 않으며, primary를 포함하여 여기에 포함된 모든 레플리카들의 journal에 적용됨을 보장한다.

 

적절한 read concern

몽고비디는 read 연산에 대한 데이터 읽기 보장 수준을 조정할 수 있는 read concern이라는 컨셉을 제공한다. 이는 dbms에서 흔히 등장하는 isolation level이라고 볼 수 있다. write concern과 마찬가지로 single/multi-document 트랜잭션 모두에 적용 가능하다.

  • local: 레플리카셋 즉 리플리케이션을 고려하지 않는 읽기 모드이다. 때문에 이 모드로 읽은 데이터는 롤백될 가능성이 있다. 기본적으로 primary, secondaries에 적용되며, 쿼리 대상 데이터가 여기에만 적용되었다면 데이터를 읽는다. causally consistent session, 트랜잭션 사용 여부에 관계없이 사용할 수 있는 모드이다.
  • available: 레플리카셋 즉 리플리케이션을 고려하지 않는 읽기 모드이다. 때문에 이 모드로 읽은 데이터는 롤백될 가능성이 있다. causally consistent session, 트랜잭션 사용 시 사용할 수 없는 모드이다. 이 모드를 샤딩된 클러스터에 사용할 경우 가장 낮은 읽기 지연을 보인다. 하지만 orphaned document(실패 혹은 불완전한 데이터 처리의 결과로 청크로 남아 있는 데이터)가 함께 읽혀질 사능성이 있다. 이 가능성을 제거하기 위해서는 local 을 사용해야 한다.
  • majority: 과반수의 레플리카 맴버로부터 응답을 받은, 즉 리플리케이션된 데이터를 읽는 모드이다. 이 모드로 읽은 데이터는 안정성이 보장된다. majority-commit 포인트의 in-memory view를 통해 반환될 수 있는 데이터만이 이 모드에서 유효한 데이터로 판단된다. 때문에 다른 모드에 비해 성능상 치러야 할 비용이 존재한다. causally consistent session, 트랜잭션 사용 여부에 관계없이 사용할 수 있는 모드이다. 레플리카셋은 반드시 WiredTiger 스토리지 엔진을 사용해야 한다.
  • linearizable: 읽기 연산이 시작하는 시점 전에 과반수의 레플리카 맴버에게서 write 연산이 성공했다는 응답을 받은 데이터만을 읽는다. 이 모드의 읽기 연산이 write 연산과 동시에 발생할 경우, write 연산이 과반수의 레플리카 맴버에게 전파될 때까지 대기하게 된다. read 연산 이후 과반수의 레플리카 맴버에 장애가 발생하거나 재시작되는 경우, read 연산으로 읽은 데이터의 안정성(durability)은  writeConvernMajorityJournalDefault 설정값에 따라 다르게 판단할 수 있다. 설정값이 true(디폴트)라면 데이터는 안전하다. false라면 몽고디비는 w: "majority" write 연산이 디스크 journal에 적용되기까지 대기하지 않기 때문에, write 연산은 롤백될 가능성이 존재한다. 이 읽기 모드는 primary에만 사용할 수 있으며, causally consistent session, 트랜잭션 사용 시 사용할 수 없다.
  • snapshot: read 연산의 트랜잭션 작동 세션이 causally consistent session이 아닌 경우에, majority write concern에서의 트랜잭션 커밋은 majority-committed 데이터의 스냅샷으로부터 읽는다. read 연산의 트랜잭션 작동 세션이 causally consistent session인 경우에도 마찬가지로 majority write concern에서의 트랜잭션 커밋은 majority-committed 데이터의 스냅샷으로부터 읽는데, 이 데이터는 바로 직전에 시작된 트랜잭션에 causal consistency를 제공한다.

 

Causal Consistency

몽고디비 버전 3.6부터 등장한 개념이다. 연산이 앞선 연산에 논리적인 의존성을 가지는 경우, 이 두 연산은 causal relationship을 가졌다고 표현한다. 예를 들어, 특정 조건을 만족하는 모든 documents를 삭제하는 write 연산이 있고, 이어서 원하는 모든 documents가 잘 삭제되었는지 검증하기 위한 read 연산이 있을 때, 이 두 연산은 causal relationship을 가지며, causal relationship을 함께 가지는 연산들을 causal 연산이라고 한다.

몽고디비는 causally consistent session에서 causal 연산들을 causal relationship의 순서에 맞춰 실행한다. 쉽게 말해 연산이 인과관계에 맞게 실행 순서를 맞춘다는 뜻이다.

몽고디비 3.6은 클라이언트 세션에 causal consistency를 활성화한다. Causal consistency session은 majority read concern, majority write concern 연산들의 순서가 causal relationship을 가졌음을 나타내어 실행 순서가 보장되어야 함을 표시한다. 이 때, 클라이언트 어플리케이션에서는 반드시 한번에 한 쓰레드만이 이 연산들을 시도하도록 보장하여야 한다.

 

Causal Consistency는 필요할 때만 사용한다

Causal consistency는 한 클라이언트 세션의 write-read 연산에서 이 때 어떤 레플리카가 연산을 수행했는지와 상관없이 read 연산은 반드시 write 연산의 결과를 볼 수 있도록 보장한다. 이 보장은 비용을 요구한다. Causal consistency는 단조적 읽기 보장(monotonic read guarantees)이 필요한 곳에만 사용함으로써 causal consistency에 의한 지연을 최소화할 수 있다.

 

레퍼런스

https://www.mongodb.com/blog/post/performance-best-practices-transactions-and-read--write-concerns

https://www.mongodb.com/docs/upcoming/core/transactions/

https://www.mongodb.com/docs/manual/reference/read-concern/

https://www.mongodb.com/docs/manual/core/read-isolation-consistency-recency/?&_ga=2.186870447.620770834.1657759959-1870444183.1654842094#std-label-causal-consistency 

1차 캐시의 특징

  • 1차 캐시의 실체는 JPA의 PersistenceContext 이며, 하이버네이트 구현체로는 Session 이다.
  • 1차 캐시가 작동하기 위해서는 PersistenceContext(Session) 인스턴스가 필요하다. 이 인스턴스는 트랜잭션이 시작될 때 팩토리로부터 생성된다. 즉 1차 캐시가 작동하기 위해서는 트랜잭션이 필요하다.
  • 1차 캐시는 특정 "Session(세션)" 인스턴스에 바인딩된다. 세션은 사실상 하나의 런타임 트랜잭션을 의미하며, 한 트랜잭션에 저장된 캐시는 다른 트랜잭션으로부터 격리된다. 즉 '볼 수 없다.'
  • 캐시된 인스턴스의 범위는 '세션' 까지이다. 인스턴스를 캐싱한 세션이 닫히면, 즉 트랜잭션이 종료되면 그 안에서 저장된 캐시도 모두 소멸된다.
  • 하이버네이트에서 1차 캐시는 기본적으로 활성화(사용) 상태이며, 비활성화 할 수 없다.
  • 한 세션(트랜잭션)에서 한 엔티티를 처음 쿼리할 때, 이 엔티티는 쿼리를 실행하여 DB로부터 읽어들이고 이를 세션에 바인딩된 1차 캐시에 저장한다.
  • 이미 세션의 1차 캐시에 저장된 엔티티를 다시 쿼리하면 이 엔티티는 DB가 아닌 캐시로부터 읽어들인다. 즉 쿼리를 실행하지 않는다.
  • 세션의 1차 캐시에 존재하는 인스턴스는 EntityManager.evict() 메서드를 통해 삭제할 수 있다. 삭제 후 같은 엔티티를 쿼리하면 이는 DB로부터 읽어들인다.
  • 세션의 1차 캐시의 데이터를 모두 삭제하려면 EntityManager.clear() 를 호출한다.
  • 읽기 전용 트랜잭션(@Transactional(readOnly = true)) 에서도 조회된 엔티티는 1차 캐시에 저장된다. 대신, 이 때 저장된 엔티티는 readonly 플래그를 지닌다.

 

엔티티 조회 시 쿼리를 생략하고 1차 캐시를 반환하는 조건 (이 조건은 기본적으로 2차 캐시에도 동일하게 적용)

  • EntityManager.find (by PK)를 통해 조회될 경우에만 1차 캐시를 반환한다. Spring-data-jpa 사용 시 이는 repository.findById 가 된다. 하이버네이트는 1차 캐시에 데이터 저장 시 primary key가 되는 프로퍼티(with @Id)를 키로 사용하기 때문에 다른 컬럼을 사용하여 엔티티 조회 시 1차 캐시에서 이를 찾을 수 없다.
  • 위의 내용에 이어서, 커스텀 쿼리는 1차 캐시를 사용할 수 없다. (ex: Member @Id id: Long, name: String 일 때, memberRepository.findByName 은 1차 캐시에 엔티티가 존재해도 쿼리를 호출한다.) 
  • 위의 내용에 이어서, PK 를 통해 조회하더라도 그것이 커스텀 쿼리라면 1차 캐시를 사용할 수 없다. 예를 들어, findOneById(Long id) 라는 커스텀 쿼리 메서드는 PK 로 조회하지만 1차 캐시가 존재해도 무조건 쿼리를 호출한다.
  • 위의 이유로, JPQL 은 무조건 쿼리를 호출한다. JPQL 은 EntityManager.createQuery 를 통해 쿼리를 직접 생성해 호출하며, EntityManager.find 와는 다르다.
  • 이 조건들은 '1차 캐시를 반환하는' 조건이지, '엔티티를 1차 캐시에 저장하는' 조건이 아니다. 기본적으로 1차 캐시에는 primary key 데이터를 키로 하여 조회된 엔티티를 모두 저장한다. 즉 JPQL로 조회된 엔티티는 1차 캐시에 저장되어 findById 호출 시 1차 캐시로부터 반환된다.

 

캐시의 작동 과정

위 조건을 만족하는 경우, 하이버네이트의 LoadEntityEvent 가 작동한다. 이 인터페이스의 구현체로는 DefaultLoadEventListener가 사용된다. 이 클래스의 인스턴스는 다음 과정으로 엔티티의 로딩을 실행한다.

  1. loadFromSessionCache(): 해당 키를 가진 엔티티가 1차 캐시에 존재하는지 확인한다. 존재한다면 그 엔티티를 반환한다.
  2. loadFromSecondLevelCache(): 1차 캐시에 엔티티가 존재하지 않는다면, 2차 캐시에 같은 방식으로 엔티티 존재 여부를 확인하고, 존재한다면 그 엔티티를 반환한다.
  3. loadFromDataSource(): 2차 캐시에 엔티티가 존재하지 않는다면, 데이터소스(DB)에 SQL을 실행하여 ResultSet을 얻고, 여기서 데이터를 읽어 Object[] 를 생성한다. 여기까지가 엔티티가 '읽혀진' 상태가 된다.
  4. 읽어온 Object[] 를 1차 캐시와 2차 캐시에 저장한다. 그리고 이 Object[ ] 를 엔티티 인스턴스로 변환하고, 이를 1차 캐시에 저장한다. 즉 dirty checking을 위한 데이터는 사실 엔티티로 저장되지 않는다. (이 저장된 데이터가 후에 데이터 변경 여부 확인(dirty checking)에 그대로 사용된다.)

정리하면, 1차 캐시 - 2차 캐시 - DB 순으로 조회하며, DB 조회 시 2차 캐시에는 ResultSet 으로부터 데이터를 추출한 Object[] 를, 1차 캐시에는 Object[], 엔티티를 저장한다. 때문에 cache hit이 발생하여 1차 캐시의 데이터를 반환할 때는 엔티티를 반환하지만, 2차 캐시의 엔티티를 반환할 때는 Object[] 를 반환된다. 그리고 2차 캐시로부터 반환된 Object[] 는 엔티티로 변환되고, 또다시(DB 조회에서와 같이) 1차 캐시에 Object[], 엔티티가 모두 저장된다.

 

레퍼런스:

https://howtodoinjava.com/hibernate/understanding-hibernate-first-level-cache-with-example/

https://stackoverflow.com/questions/64190242/in-spring-data-jpa-a-derived-find-method-doesnt-use-first-level-cache-while-ca

https://vladmihalcea.com/jpa-hibernate-second-level-cache/

'Java' 카테고리의 다른 글

JPA] @OneToOne lazy loading  (0) 2022.08.22
test] @Testcontainers, @Container  (0) 2022.08.03

+ Recent posts