또 자꾸 헷갈려서 정리

 

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

자꾸 헷갈려서 정리

 

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

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

 

 

 

 

 

 

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

스프링 버전 5.3.18 기준으로 작성됨

 

이 글에서는 용어를 다음과 같이 줄여서 표현한다.

OpenEntityManagerInViewFilter => 필터

WebApplicationContext => wac

TransactionSynchronizationManager => tsm

EntityManagerFactory => emf

EntityManager => em

EntityManagerHolder => emh

AsyncRequestInterceptor => ari

 

Spring-orm 에서 OSIV 처리를 담당하는 주요 클래스는 org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter 이다. 이 클래스는 다음과 같이 작동한다.

 

이 클래스는 OncePerRequestFilter 타입이다. request가 들어오면 doFilterInternal 에서 다음을 수행한다.

 

step 1. 현재 필터 자신의 인스턴스에 emf 인스턴스가 존재하는지(초기화되어 있는지) 확인한다. 없으면 wac 에서 emf를 찾아서 세팅한다.

- 이 동작은 필터가 받는 첫 요청 처리에서 이루어질 것이다.

 

step 2. tsm에 emf를 키로 리소스 존재 여부를 확인해서, 존재할 경우 participate = true 를 실행한다.

- tsm에 이 리소스가 존재한다는 것은 이미 이 요청에 대해 생성된 em이 존재한다는 뜻이다. 때문에 글자 그대로 기존 요청에 '참여' 함을 표시한다.

 

step 3. 이 요청이 async request이면서 AsyncManager가 ari 를 가지고 있다면, ari 에 em 을 바인딩한다. (bindEntityManager) 그리고 step 6 으로 넘어간다.

 

step 4. 이 요청이 async request 가 아니거나, async 라도 AsyncManager가 ari 를 가지고 있지 않다면, 다음 과정을 수행한다. (일반적인 mvc요청은 이 방식으로 이루어진다.)

1. emf 로 em을 생성한다.

2. 생성한 em 을 가지는 emh 를 생성한다.

3. tsm 에 bindResource(emf, emh) 를 호출한다.

- tsm의 bindResource 는 내부 쓰레드로컬에 맵을 세팅하여 그곳에 리소스를 저장해둔다. 때문에 이미 request에 대해 생성된 em이 존재하는 경우, tsm를 통해 이를 확인할 수 있도록 한다. (step 2)

4. emf, emh 를 가지는 ari를 생성하고, AsyncManager에 ari를 등록한다.

- 이렇게 등록된 ari 가 다음번 async 요청에서 바로 ari 에 em을 바인딩할 수 있게 된다. (step 3)

 

step 5. filterChain.doFilter

 

step 6. finally 에서, participate = false 인 경우 tsm에서 em을 제거하고(unbindResource(emf)), async요청이 아닐 경우 EntityManagerFactoryUtils.closeEntityManager 로 em을 완전히 닫는다.

- '참여' 요청이라면 기존 요청이 존재하므로 여기서 em을 unbind 해서는 안 된다. 때문에 participate를 체크한다.

 

1. 디폴트

정적 자원 핸들링에는 디폴트로 ResourcehttpRequestHandler가 사용된다.

디폴트로, 아래 경로의 자원을 '/' URI 패턴으로 제공한다.

classpath:/static

classpath:/public

classpath:/resources

classpath:/META-INF/resources

 

예로, classpath:/static/myface.html 이 있을 때,

http://example.com/myface.html 경로로 접근할 수 있다.

 

 

2. 커스터마이징

주의: 설정 커스터마이징 시 디폴트 설정은 사라진다. 따로 설정하지 않으면 더이상 '/' 패턴으로 정적 자원 접근이 불가능하다.

 

프로퍼티 방식

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=classpath:/mystatic/,file:/shared/mystatic/

 

URI 패턴, 자원 위치 모두 콤마로 구분하여 2개 이상 설정할 수 있다.

 

위 설정은 아래와 같이 매핑된다

http://example.com/mystatic/myface.html => classpath:/mystatic/myface.html (없으면 파일시스템 /shared/mystatic/myface.html)

 

 

위와 같이, 클래스패스가 아닌 일반 파일시스템 접근에는 'classpath' 대신 'file:' 을 사용한다. (윈도우는 'file://')

 

자바 방식

위 설정을 자바 방식으로 하면 아래와 같다.

@Configuration 
public class WebConfig implements WebMvcConfigurer {

	@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { 
    		registry.addResourceHandler("/mystatic/**")
        		.addResourceLocations("classpath:/mystatic/", "file:/shared/mystatic/");
	}    
}

 

WAR 파일의 경우, webapp/mystatic/ 에 자원이 있다면, 아래와 같이 'classpath:'를 빼고 설정한다.

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=/mystatic/

 

 

Java 14

 

JEP 361: Switch Expressions

- Java 12-13 에서 preview feature 였음. Java 14 에서 standard 로 변경

String result = switch (day) {
            case "M", "W", "F" -> "MWF";
            case "T", "TH", "S" -> "TTS";
            default -> {
                if(day.isEmpty())
                    yield "Please insert a valid day.";
                else
                    yield "Looks like a Sunday.";
            }
 
        };

 

JEP 305: instanceof 패턴 매칭

- preview feature

- if (obj instanceof SomeType) 후 if 블럭에서 (SomeType) obj 캐스팅 불필요:

before:

if (obj instanceof SomeType) {
	SomeType someType = (SomeType) obj;
	someType.doSomething();
}

after:

if (obj instanceof SomeType) {
    obj.doSomething();
}

 

JEP 358: NullPointerException 메시지 개선

- 정확히 어디서 null 참조가 발생했는지 알기 쉽게 표시.

- 메서드 체이닝 코드 디버깅에 있어 특히 편해질 것으로 보임.

before:

String name = someInstance.getValue().toString();
 
//Stacktrace
Exception in thread "main" java.lang.NullPointerException
    at NullPointerExample.main(NullPointerExample.java:5)

after:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "SomeValue.toString()" because the return value of "something.getValue()" is null
    at NullPointerExample.main(NullPointerExample.java:4)

 

JEP 359: Records

- preview feature

- 순수 데이터만 담는 클래스.

- 키워드 class 대신 record 를 사용하며, 아래와 같이 선언함:

record Car(String name, String manufacturer) {}

- 컴파일 시 final 클래스가 되며, java.lang.Record 를 상속. (enum 과 유사.)

- 함께 선언한 필드는 모두 final이 되며, 값을 대입하는 public 생성자가 자동으로 생성됨:

public Car(String name, String manufacturer) {
	this.name = name;
	this.manufacturer = manufacturer;
}

- 필드의 getter 메서드도 자동으로 생성됨.

- toString, hashcode, equals 로 자동으로 오버라이딩하며, 필드 값을 기준으로 같은 결과를 반환. 

- 생성자에 사용된 필드 외에 다른 인스턴스 필드를 선언할 수 없음.

- 인스턴스 메서드 선언 가능.

- interface 구현 가능.

- static 필드, 메서드는 선언 가능.

- 생성자 오버로딩 가능.

- 자동 생성된 기본 생성자 변경 가능. 이 때 생성자 파라미터 생략 가능:

record Car(String name, String manufacturer) {
	public Car {	// 파라미터 생략 가능. name, manufacturer 이 숨겨져 있다.
    	// 파라미터 할당문 생략 가능. 컴파일러가 자동 생성.
    }
}

- java.lang.Class 에 isRecord, getRecordComponents 가 추가됨. isRecord는 이 클래스가 record 타입인지 확인하고, getRecordComponents는 record 필드를 가져옴.

 

JEP 368: Text Blocks

- preview feature (second preview)

- Java 13 에서 등장한 Text Blocks 에 새로운 이스케이프 2 개 추가

- \ (백슬래시) : 자동 줄바꿈을 방지함:

String inline = """
		1\
                2\
                3\
                """;
System.out.println(inline); // "123"

- \s: 공백을 유지함. Text Blocks 에서는 문자열이 자동으로 stripped되기 때문에, 기본적으로 앞뒤 공백이 제거됨. 공백을 유지하고 싶은 경우 사용.

 

JEP 343: Packing Tool (jpackage)

- incubator

- 자바 어플리케이션 패키징을 손쉽게 하기 위해 사용.

- 플랫폼에 맞는 패키징을 돕는다. (Linux: deb, rpm / macOS: pkg, dmg / Windows: msi, exe)

 

JEP 345: NUMA-Aware Memory Allocation for G1

- Non-Uniform Memory Access (NUMA) 는 멀티프로세싱 시스템에서의 멀티프로세서 클러스터링 아키텍처로, 각 프로세스가 독립적인 로컬 메모리를 보유하도록 하여 성능 향상을 도모한다. 프로세서와 그 프로세서의 메모리(로컬 메모리) 로 하나의 NUMA node를 구성하며, 프로세서가 자신의 node의 메모리에 빠르게 접근할 수 있다. 외부 node로의 접근은 상대적으로 느리며, 그 속도는 node의 거리가 멀수록 더 느리다.

- Java 14 에서는 G1이 NUMA를 인식하여 새로 메모리를 할당할 때 현재 쓰레드가 위치한 같은 NUMA node에 가용 공간이 있는지를 먼저 찾고, 없으면 가장 인접한 node를 검색하는 방식으로 NUMA 아키텍처의 성능 이점을 활용하도록 한다.

- 다음 파라미터로 활성화함:

+XX:+UseNUMA

- 위 파라미터를 지정하면 JVM이 초기화될 때 전체 NUMA nodes에 regions를 분포하도록 한다.

 

JEP 349: JFR Event Streaming

- JFR 이벤트를 수신할 수 있는 API 를 제공.

- in-process, out-of-process 어플리케이션 모두에 사용 가능.

- jdk.jfr.consumer 패키지 사용.

- CPU 사용률과 모니터 락 경합 이벤트를 10ms 주기로 수신하는 예시 코드:

try (var rs = new RecordingStream()) {
  rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
  rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
  rs.onEvent("jdk.CPULoad", event -> {
    System.out.println(event.getFloat("machineTotal"));
  });
  rs.onEvent("jdk.JavaMonitorEnter", event -> {
    System.out.println(event.getClass("monitorClass"));
  });
  rs.start();
}

 

JEP 352: Non-Volatile Mapped Byte Buffers

- java.nio.MappedByteBuffer 가 non-volatile memory (NVM) 의 파일을 로드 처리할 수 있도록 함.

 

JEP 364: ZGC on macOS

- experimental

- macOS에서 ZGC 지원.

- macOS에서 메모리 멀티 매핑 지원.

- 인접하지 않은 메모리 reservations 지원.

 

JEP 365: ZGC on Windows

- experimental

- Windows에서 ZGC 지원.

- Windows 10 지원.

- WIndows Server version < 1803 미지원.

 

JEP 370: Foreign-Memory Access API

- incubator

- 새로운 native memory 접근 API 추가.

- 자바 어플리케이션의 힙 메모리 사용은 메모리 효율성, (상대적으로)낮은 성능, GC 등의 이슈가 있음. 

- 이에 때에 따라서 외부 메모리(native memory)에 접근할 필요가 있는데, 기존 접근 방법은 ByteBuffer API, Unsafe API 두 가지가 있음.

- ByteBuffer API는 native memory 할당을 가능하게 하지만, 가용 메모리가 최대 2 기가라는 점과, 메모리 해제를 GC가 책임져야 한다는 한계가 있음. 때문에 잘못된 방법으로 API를 사용하면 메모리 릭이 발생할 수 있고 OOM으로 연결됨.

- Unsafe API는 메모리 관리에 굉장히 효율적일 수 있지만, 잘못 사용하면 JVM 크래시를 일으킬 수 있고, 표준 API가 아니라는 단점이 있음.

- Java 14 는 native memory 접근을 위한 세 가지 추상화를 제공: MemorySegment, MemoryAddress, MemoryLayout.

- MemorySegment: 힙 또는 native memory에 연속된 메모리 공간을 할당함. 다음과 같이 사용할 수 있음:

MemorySegment memorySegment = MemorySegment.allocateNative(200);  // 200 바이트의 native memory 할당.
MemorySegment memorySegment = MemorySegment.ofArray(new long[100]);  // 자바 힙 메모리 segment.
MemorySegment memorySegment = MemorySegment.ofByteBuffer(ByteBuffer.allocateDirect(200));  // buffer memory segment.

mapped memory file에도 사용할 수 있음. (mapped memory segment):

MemorySegment memorySegment = 
	MemorySegment.mapFromPath(Path.of("/tmp/memory.txt"), 200, FileChannel.MapMode.READ_WRITE);

 

memory segment는 현재 쓰레드에 묶임. 다른 쓰레드에 있는 memory segment에 접근하려면 acquire 메서드를 통해 접근 권한을 획득해야 함.

memory segment는 JVM 안정성을 위해 그 접근에 있어 두 가지 바운더리가 있음:

Spatial boundary: 공간적 경계. 접근 가능한 범위가 정해져 있어, 이 범위 밖의 메모리에 접근 시도하면 exception 발생.

Temporal boundary: 시간적 경계. 생성 및 사용되고, 더 이상 사용되지 않으면 closed된다. closed된 영역에 접근 시도하면 exception 발생.

 

- MemoryAddress: memory segment의 memory offset. 메모리의 데이터를 검색하거나 할 때 사용됨:

MemoryAddress address = MemorySegment.allocateNative(100).baseAddress();

- MemoryLayout: memory segment의 내용을 제공. 특히 메모리를 점유하는 요소의 사이즈를 제공하여 메모리가 요소에 어떤식으로 분산할지 정의할 수 있음.

int numberOfPoints = 10;
MemoryLayout pointLayout = MemoryLayout.ofStruct(
  MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN).withName("x"),
  MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN).withName("y")
);
SequenceLayout pointsLayout = 
  MemoryLayout.ofSequence(numberOfPoints, pointLayout);

- MemoryHandles: MemorySegment로 할당한 메모리를 사용함:

long value = 10;
MemoryAddress memoryAddress = MemorySegment.allocateNative(8).baseAddress();
VarHandle varHandle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());
varHandle.set(memoryAddress, value);
 
assertThat(varHandle.get(memoryAddress), is(value));

- 이 외에 다양한 MemoryLayouts가 존재함: ValueLayout, SequenceLayout, GroupLayout.

 

 

출처: 

https://www.journaldev.com/37273/java-14-features

https://dzone.com/articles/a-first-look-at-records-in-java-14

https://openjdk.java.net/jeps/349

https://www.baeldung.com/java-foreign-memory-access

'Java > Core' 카테고리의 다른 글

Java 13 간략히 정리  (0) 2020.07.16
Java 12 간략히 정리  (0) 2020.07.15
Java 11 간략히 정리  (0) 2020.07.15
Java 10 간략히 정리  (0) 2020.07.15
IO, NIO, NIO2  (0) 2020.02.26

Java 13

 

JEP 355: Text Blocks

- preview feature

- String str = """ 문자열 """;

- 멀티라인 문자열을 더 보기 좋고 편하게 작성할 수 있음.

String textBlock = """
		This
		is
		a
		new
		feature,
		Text
		Block!""";
System.out.println(textBlock);
String inline = "\nThis\nis\anew\nfeature,\nText\nBlock!";
System.out.println(textBlock.equals(inline));

output:

This
is
a
new
feature,
Text
Block!
true

줄바꿈 문자가 자동으로 포함된다. 자바 코드로 json, html 텍스트를 작성할 때 아주 편리할 듯.

 

텍스트 블록용 String 메서드 추가

- formatted(Object ...args): String.format() 와 비슷한 기능. 텍스트블록에 사용됨.

- stripIndent(): 문자열 안의 모든 라인에 strip를 적용하여 앞 뒤 공백을 제거함.

- translateEscapes(): 연속된 이스케이프 문자를 변환 - ""\\t" => "\t"

 

JEP 354: Switch Expressions Enhancements

- preview feature

- yield 키워드:

switch(mode) {
	case "a", "b":
		yield 1;
	case "c", "d", "e":
		yield 2;
	default:
		yield -1;
};

- yield 는 값을 반환하고 switch를 빠져나감 (break)

- arrrow 도 여전히 사용 가능.

 

JEP 353: Socket API 재구현

- 신규 클래스 sun.nio.ch.NioSocketImpl 추가되어 java.net.PlainSocketImpl 을 대체함.

- NioSocketImpl 는 내부에서 synchronized 대신 java.util.concurrent 패키지의 locks를 사용함.

- 아래 아규먼트로 Socket API 사용 가능:

-Djdk.net.usePlainSocketImpl

 

JEP 350: Dynamic CDS Archive

- JVM 옵션을 통해 CDS를 더 편하게 사용 가능:

$ java -XX:ArchiveClassesAtExit=my_app_cds.jsa -cp my_app.jar
$ java -XX:SharedArchiveFile=my_app_cds.jsa -cp my_app.jar

 

JEP 351: ZGC:Uncommit Unused Memory

- ZGC가 사용하지 않는 힙 메모리를 OS에 반환하도록 함.

 

static java.nio.file.FileSystems.newFileSystem 메서드 추가

- newFileSystem(Path, ClassLoader)

- newFileSystem(Path, Map<String, ?>)

- newFileSystem(Path, Map<String, ?>, ClassLoader)

 

DOM and SAX Factories with Namespace Support

- newDefaultNSInstance()

- newNSInstance()

- newNSInstance(String, ClassLoader)

 

 

출처: 

https://www.journaldev.com/33204/java-13-features

'Java > Core' 카테고리의 다른 글

Java 14 간략히 정리  (0) 2020.07.16
Java 12 간략히 정리  (0) 2020.07.15
Java 11 간략히 정리  (0) 2020.07.15
Java 10 간략히 정리  (0) 2020.07.15
IO, NIO, NIO2  (0) 2020.02.26

Java 12

 

언어 상 변화

Switch Expressions (JEP 325)

- preview feature

before:

int numLetters;
switch (day) {
	case MONDAY:
	case FRIDAY:
	case SUNDAY:
		numLetters = 6;
		break;
	case TUESDAY:
		numLetters = 7;
		break;
	case THURSDAY:
	case SATURDAY:
		numLetters = 8;
		break;
	case WEDNESDAY:
		numLetters = 9;
		break;
	default:
		throw new IllegalStateException("Huh? " + day);
}

after:

int numLetters = switch (day) {
	case MONDAY, FRIDAY, SUNDAY -> 6;
	case TUESDAY -> 7;
	case THURSDAY, SATURDAY -> 8;
	case WEDNESDAY -> 9;
	default -> throw new IllegalStateException("Huh? " + day);
};

 

라이브러리 변화

Teeing Collectors

- Collectors.teeing(Collector, Collector, BiFunction)

- 1, 2 번 아규먼트 컬렉터의 연산 결과를 BiFunction 아규먼트로 넘겨 BiFunction 이 최종 컬렉팅을 수행한다:

double mean = Stream.of(1, 2, 3, 4, 5)
                .collect(Collectors.teeing(
                        summingDouble(i -> i),
                        counting(),
                        (sum, n) -> sum / n));

System.out.println(mean); // 3

 

InputStream.skipNBytes(n)

- 정확히 n 만큼의 바이트를 스킵함.

- n <= 0 이면 스킵하지 않음.

 

java.lang.constant 패키지 추가

 

java.lang.Character

- 체스 심볼 추가

- 마야 숫자 표현 추가

- 소그디니아어 추가

 

java.lang.Class 메서드 추가

- arrayType(): 해당 클래스의 array type 클래스를 반환:

jshell> "foo".getClass().arrayType()
$15 ==> class [Ljava.lang.String;

- componentType(): 기존 getComponentType()과 동일?

- descriptorString(): java.lang.Class에 TypeDescriptor 인스페이스 구현이 추가되어 이 메서드를 오버라이딩함. 현재 getName()과 동일한 결과를 반환함.

 

java.lang.String 메서드 추가

- indent(n): 들여쓰기 수행. 아래 과정으로 처리된다:

Step 1. String.lines() 를 통해 라인을 쪼갠다.

Step 2. n > 0 이면 라인마다 n 만큼의 공백을 추가한다. n < 0 이면 n 라인마다 만큼의 공백을 제거한다. 공백이 없으면 제거하지 않는다. n == 0 이면 그냥 둔다.

Step 3. 라인을 모두 합친다. (줄바꿈 문자열은 유지한다.)

 

- transform(Function<String>): 문자열에 Function을 실행한 결과를 반환한다:

hello".transform(input -> input + " world!"); // "hello world!"

 

java.net.SecureCacheResponse, java.net.ssl.HttpsConnection 메서드 추가

- Optional<SSLSession> getSSLSession(): 현재 커넥션의 SSL 세션을 반환.

 

long java.nio.files.Files.mismatch(Path, Path)

- 두 파일 내용을 바이트로 비교하여 처음 차이가 발견된 바이트의 위치를 반환한다. 차이가 발견되지 않으면 -1L을 반환한다.

 

java.text.CompactNumberFormat

- NumberFormat의 서브클래스로, 1,000,000 => 1M 으로 변환하는 등의 포맷팅을 수행한다.

 

java.util.concurrent.CompletionStage 메서드 추가

- ComplitionStage<T> exceptionallyAsync(Function<Throwable, ? extends T>): CompletableFuture.supplyAsync() 에서 발생한 에러를 비동기로 핸들링한다.

- ComplitionStage<T> exceptionallyAsync(Function<Throwable, ? extends T> Executor executor): 기능은 위와 같음. 비동기 에러 핸들링을 수행할 Executor를 지정한다.

- ComplitionStage<T> exceptionallyCompose(Function<Throwable, ? extends CompletionStage<T>>): CompletableFuture.supplyAsync() 에서 에러가 발생하면 Function 을 수행한다. 이 Function 에서 반환한 CompletableStage 는 다음 수행 Future 가 된다. 

- ComplitionStage<T> exceptionallyComposeAsync(Function<Throwable, ? extends CompletionStage<T>>): 기능은 위와 같음. 비동기로 수행한다.

- ComplitionStage<T> exceptionallyComposeAsync(Function<Throwable, ? extends CompletionStage<T>>, Executor): 기능은 위와 같음. 이 로직을 수행할 Executor를 지정한다.

 

javax.crypto.Cipher.toString()

- 암호화 모드, 알고리즘 등에 대한 정보를 제공하도록 변경됨.

 

javax.naming.ldap.spi 패키지 클래스 두 개 추가

- LdspDnsProvider: LDAP DNS 룩업을 위한 서비스 프로바이더.

- LdapDnsProviderResults: LDAP DNS 룩업 결과를 캡슐화.

 

File[] javax.swing.filechooser.FileSystemView.getChooserShortcutPanelFiles()

- 스윙 파일 선택기 창의 숏컷 패널의 파일(디렉토리) 정보를 java.io.File 배열로 반환

 

JVM 변화

JEP 189: Shenandoah

- OpenJDK 에 포함됨

 

JEP 344: Abortable Mixed Collections for G1

- G1 GC는 힙 공간을 2048개의 regions로 나누고, 각 영역은 youg과 old 로 논리적으로 구분된다. G1 GC는 가비지 컬렉션을 수행하기 위해 수행 대상 regions를 선택하는데 (collection set), young과 old가 섞이게 되는 mixed collections에서 old regions가 너무 많이 잡히게 되면 pause time이 길어져 목표치를 초과하게 될 수 있다. 이를 방지하기 위해서는 G1 GC가 collection set을 만드는 과정에서 잘못된 수의 regions을 반복적으로 select하는 때를 감지하여 실제 가비지 컬렉션 과정 중간에 작동을 취소할 수 있게끔 해야 한다 (aborable).

 

위 목표를 실현하기 위해 Java 12 의 G1 GC는 잘못된 수의 regions가 반복적으로 select됨을 감지하면 더욱 점진적인 mixed collections 과정을 취한다: collection set을 mandatory part, optional part 둘로 나눈다. mandatory part는 young regions와 같이 G1 GC가 점진적 처리를 할 수 없는 영역으로 구성하지만, 효율성을 위해 old regions도 포함할 수 있다.

그리고 나머지는 old regions로만 구성된 optional part가 된다. 이 과정의 결과로 collection set은, 예를 들어 80%의 mandatory part, 20%의 optional part와 같이 나뉘게 된다.

 

우선 mandatory part의 가비지 컬렉션을 마치고 optional part에 대해서는 훨씬 더 세분화된 방법을 취한다. pause time 여유가 남아 있다면 그 시간에 비례하여 old regions를 처리하는데, 최대 한 번에 한 old region씩 처리한다. 처리 도중 남은 시간이 없다면 처리 중인 region까지만 작업을 마치고 다음 가비지 컬렉션을 중단한다(abort).

 

mandatory part을 다시 구성할 때 까지, 위 과정이 반복되면서 optional part는 점점 작아지게 된다. 그러다 다시 collection set을 나누게 되면 mandatory part와 optional part를 새로 만든다.

 

JEP 346: Promptly Return Unused Committed Memory from G1

- 기존 G1 GC에는 시간에 따른 JVM 메모리 릴리즈가 없었다. 기존 G1 GC가 OS에 메모리를 반환하는 때는 full GC or concurrent GC cycle 이다. 그런데 G1 GC는 full GC를 최대한 피하도록 설계되었고, concurrent GC cycle은 힙 메모리 사용량과 할당 작업에서만 작동하기 때문에, 결과적으로 기존 G1 GC는 OS에 메모리를 잘 반환하지 않는다. 그리고 이는 사용 자원에 따라 요금을 부과하는 클라우드 환경에서 불필요한 비용을 초래한다. 어플리케이션이 놀고 있는 때에도 메모리를 잡고 있으니, 사용하지 않는 물건에 대해 요금을 계속 지불하게 되는 것이다.

 

이 문제를 완화하기 위해 Java 12 의 G1 GC는 적절한 시간 안에서 어플리케이션이 사용되지 않는다고 판단되면 사용하지 않는 메모리 일부를 OS에 반환하여 JVM 힙 크기를 조정하도록 한다.

 

JDK 새로운 기능

JEP 230: 마이크로벤치마킹 슈트

- 성능 측정 도구인 Java Microbenchmarking Harness (JMH) 가 OpenJDK에 포함되었다. 

 

JEP 341: Default CDS 아카이브

- 상용 Oracle JDK 의 기능이었던 CDS가 OpenJDK에 포함되었다. CDS를 사용하기 위해 lib/server 디렉토리에 classes.jsa 파일이 생성된다. 

 

 

출처:

https://dzone.com/articles/39-new-features-and-apis-in-jdk-12

https://www.baeldung.com/java12-string-api

https://openjdk.java.net/jeps/344

'Java > Core' 카테고리의 다른 글

Java 14 간략히 정리  (0) 2020.07.16
Java 13 간략히 정리  (0) 2020.07.16
Java 11 간략히 정리  (0) 2020.07.15
Java 10 간략히 정리  (0) 2020.07.15
IO, NIO, NIO2  (0) 2020.02.26

+ Recent posts