Spring boot 2.1 변화

몇 일전에 Spring boot 2.1이 릴리즈 되었다. 그래서 오늘 이와 관련된 이야기를 해보려고 한다. Spring boot 2.1의 변화. 릴리즈 된 내용들을 살펴보도록 하자. 물론 다 알아보지는 못하고 필자가 아는 내용, 혹은 잘 사용했던 내용 위주로만 설명할 예정이니 더 많은 내용들은 문서를 통해 확인하길 바란다. 또한 꼭 문서를 보는 것은 추천한다.
그럼 한번 살펴보도록 하자.

Deprecations from Spring Boot 2.0

Spring boot 2.0 에서 deprecated 되었던 메서드, 클래스, 생성자, 필드 등이 모두 삭제 되었다. CouchbaseHealthIndicatorProperties, EnvironmentTestUtils, RouterFunctionMetrics 클래스가 삭제 되었고, 여러개의 필드, 메서드들이 삭제 되었다. 더 많은 정보를 원한다면 해당 문서를 찾아보길 바란다.
만약 업그레이드를 한다면 없어진 클래스, 메서드를 사용하지 않길 바란다.

Bean Overriding

Spring boot 2.1 부터는 Bean 오버라이딩 기능이 불가능하다. 이는 실수를 막기위함이라고 하는데.. 예를들어 다음과 같이 코드를 작성했을 경우 에러가 발생한다.



@Configuration
public class SuperConfig {

  @Bean
  Foo foo() {
  return new Foo();
  }
}

@Configuration
public class Config extends SuperConfig {

  @Override
  @Bean
  Foo foo() {
  return new Foo();
  }
}

그럼 다음과 같은 에러가 발생한다.

The bean 'foo', defined in class path resource [SuperConfig.class], could not be registered. A bean with that name has already been defined in class path resource [Config.class] and overriding is disabled.

만약 Bean Overriding 을 허용하고 싶다면 다음과 같이 프로퍼티에 작성하면 된다.

spring.main.allow-bean-definition-overriding=true

Actuator ‘info’ and ‘health’ Endpoint Security

이전 Actuator 를 사용할 경우 spring-security가 클래스패스에 있을 경우 모든 Endpoint가 인증을 시도했다. 하지만 이제부터는 spring-security 가 클래스 패스에 있어도 info, health endpoint는 인증을 시도 하지 않고 사용할 수 있다. 이 것은 디펜더시 이외에 아무 설정 하지 않았을 경우를 의미한다.

Servlet Path

server.servlet.path 프로퍼티가 spring.mvc.servlet.path 프로퍼티로 변경 되었다.

Logging Refinements

이건 뭔소린지 잘 모르겠다. 아래에 logging group하고 관련있는 건지..? 아무튼..

logging.level.web=debug

web 과 관련된 로그를 debug로 남기고 싶다면 위와 같이 작성하면 된다. 그럼 org.springframework.core.codec 패지키, org.springframework.http 패키지, org.springframework.web 패키지 아래의 로그들이 debug으로 출력 된다.

2018-11-04 19:08:37.928 DEBUG 11085 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public java.lang.String ml.wonwoo.springbootnew.SpringBootNewApplication$TestController.hello()
2018-11-04 19:08:37.928 DEBUG 11085 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : GET "/?test=1", parameters={masked}
2018-11-04 19:08:37.929 DEBUG 11085 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public java.lang.String ml.wonwoo.springbootnew.SpringBootNewApplication$TestController.hello()
2018-11-04 19:08:37.930 DEBUG 11085 --- [nio-8080-exec-3] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2018-11-04 19:08:37.930 DEBUG 11085 --- [nio-8080-exec-3] m.m.a.RequestResponseBodyMethodProcessor : Writing ["hello"]

위와 같이 유용한 로그들이 출력된다. Http 메서드, URL, 파라미터 등이 출력 되니 참고하면 된다. 근데 여기서 파라미터들은 마스킹 처리되어 있다. 민감한 정보들이 노출될 수 있어 마스킹 처리는 했지만 해당정보도 보여주고 싶다면 다음과 같이 프로퍼티에 작성하면 된다.

spring.http.log-request-details=true

그럼 이와 같이 파라미터 정보도 출력된다.

2018-11-04 20:47:24.115 DEBUG 11176 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : GET "/?test=1", parameters={test:[1]}

Narayana JTA Support

Narayana JTA 는 공식지원은 중단 되었고 이를 대체할 narayana-spring-boot-starter 가 존재한다. 만약 필요하다면 아래와 같이 업그레이드를 해야 한다.


<dependency>
    <groupId>me.snowdrop</groupId>
    <artifactId>narayana-spring-boot-starter</artifactId>
    <version>2.0.1</version>
</dependency>

HttpPutFormContentFilter

HttpPutFormContentFilter 클래스가 Deprecated 되었고 이를 대체할 FormContentFilter 클래스가 추가 되었다. 또 한 이와 관련된 spring.mvc.formcontent.putfilter.enabled 프로퍼티는 spring.mvc.formcontent.filter.enabled 프로퍼티로 변경 해야 된다.

InfluxDB HttpClient Customization

이전에는 OkHttpClient.Builder 를 빈으로 선언해서 InfluxDB를 사용했다면 이제는 InfluxDbOkHttpClientBuilderProvider 이용해서 사용하는 것은 권장한다. 아마도 다음 버전에는 지원하지 않을 예정으로 보인다. 현재 Spring boot 2.1에서 OkHttpClient.Builder 빈으로 등록해 사용한다면 (InfluxDB를) 경고 로그가 출력 될 것이다.

Spring Version POM Property

spring-boot-dependencies에 정의되어 있는 spring.version 프로퍼티가 변경되었다. 이제는 spring-framework.version을 프로퍼티로 사용해야 한다.

<spring-framework.version>5.0.0.RELEASE</spring-framework.version>

하지만 이것은 권장하지 않는다. Spring boot가 지원해주는 Spring version을 사용하는 것을 더욱 권장하고 있다.

Removal of ‘spring.provides’ Files

spring.provides 파일들이 사라졌다. 이전에는 STS, 다른 IDE들의 의존성들을 파악 할 수 있었으나 starter POM으로 스캐닝이 충분하기 때문에 사라졌다. 맞나?

Thymeleaf Spring Security Extras

thymeleaf-extras-springsecurity4 에서 thymeleaf-extras-springsecurity5 로 업데이트 되었다. 만약 이 모듈을 사용하고 있다면 thymeleaf-extras-springsecurity5로 변경 해야 된다.

Json Simple

json-simple가 이제는 의존성 관리에서 제거 되었다. 또한 json-simple 를 사용하는 JsonParser의 구현체(JsonSimpleJsonParser)도 사라졌다.

Jersey 1

Jersey 1은 이제 더이상 지원하지 않는다 Jersey 2로 업그레이드 해야 한다.

Endpoint ID names

자신만의 Endpoint를 만들 경우 Id를 좀 더 엄격하게 체크한다. 예를들어 id는 영숫자이어야 하며 문자로 시작해야 한다. 만약 숫자로 시작할 경우 에러가 발생한다.
또 한 -, . 특수문자를 사용할 경우 경고 메세지가 출력 된다. 해당 클래스는 EndpointId를 이용하니 참고하면 되겠다.

Third-party Library Upgrades

다들 아시다피시 Spring boot 2.1은 Spring Framework 5.1을 기반으로 하고 있다. 특별한 경우가 아니라면 Spring boot 가 명시해준 Spring Framework를 사용하길 권장한다.
– Tomcat 9
– Undertow 2
– Hibernate 5.3
– JUnit 5.2
– Micrometer 1.1

기타 등이 업그레이드 되었다.

Java 11 Support

Sprig boot 2.1은 java8 도 지원하지만 java10, java11 까지도 지원한다.

DataSize Support

이거슨 옛날 포스팅 참고

Context ApplicationConversionService Support

ApplicationConversionService 에 DataSize 컨버터 도 추가 되었다. 그래서 아래와 같이 사용할 수 있다.

@Value("${my.duration:10s}")
private Duration duration;

이것 역시 위의 포스터 참고
– 수정: 실제 이내용은 아닌데 왜 내가 이런내을 썼나 의문이다.(아마도 바로 위에 DataSize 가 나와서 그런듯 싶다.) 근데 문서도 조금 의아한게 ApplicationConversionService 클래스는 spring boot 2.0 부터 추가 된 클래스이고 DataSizeConverter 가 2.1 부터 추가된 것도 사실이다. 근데 Duration을 언급한게 조금..

Profile Expression

Profile 표현식이 좀 더 향상 되었다. 예를들어 다음과 같은 표현식을 사용할 수 있다.
production &amp; (us-east | eu-central) 프로덕션이 활성화 되어있고 us-east, 또는 eu-central 활성화 되었을 경우 일치하는 표현식이 된다. 해당 Profile을 파싱하는 클래스는 ProfilesParser이다. 참고 하면 되겠다.

Task Execution

이제 Spring boot 는 ThreadPoolTaskExecutor를 자동설정을 제공해준다. @EnableAsync을 사용할 경우 이전에는 SimpleAsyncTaskExecutor 가 빈으로 등록 되었지만 이제는 ThreadPoolTaskExecutor가 기본적으로 빈으로 등록 된다. spring.task.execution 프로퍼티를 이용해서 속성들을 변경 할 수 있다. 또한 TaskExecutorBuilder 클래스로 좀 더 쉽게 설정 할 수 있다.
– 수정: 실제 기본적으로는 SimpleAsyncTaskExecutor 클래스가 빈으로 등록 되지 않는다.

Task Scheduling

위와 동일하게 ThreadPoolTaskScheduler가 자동설정을 제공해준다. 둘다 모두 해당 어노테이션을 선언해야 한다. Task Scheduling 은 @EnableScheduling 어노테이션을 설정 후 spring.task.scheduling 프로퍼티로 속성들을 변경 할 수 있다. 이 역시 TaskSchedulerBuilder를 제공해 준다.

Logging Groups

Logging을 그룹 별로 묶어서 선언할 수 있다. 아까 위에서 봤던 내용이다. 해당 그룹은 사용자들이 직접 만들 수 도 있다.

logging.group.tomcat=org.apache.catalina, org.apache.coyote, org.apache.tomcat
logging.level.tomcat=TRACE

위와 같이 org.apache.catalina, org.apache.coyote, org.apache.tomcat
l 패키지를 tomcat 그룹으로 묶었다. 그 후 그룹을 TRACE 레벨로 로그를 출력하겠다는 의미이다.

Bootstrap mode for JPA setup

Bootstrap mode 가 추가 되었다. 아마도 JPA Repsitories를 초기화 지연을 시킬지 결정하는 설정같다. 하지만 필자는 잘 모르겠다. 이정도까지..
해당 설정은 다음과 같이 하면 된다.

spring.data.jpa.repositories.bootstrap-mode=deferred

deferred 말고도 lazy 도 존재하지만… 별도의 스레드에서 동작한다고 하니 참고하면 되겠다. deferred 와 lazy 차이는 잘 모르겠다.

Kafka Streams Support

Kafka Streams를 지원한다. 해당 자동 클래스는 KafkaStreamsAnnotationDrivenConfiguration 참고하면 되겠다.

Spring Data JDBC Support

Spring Data JDBC를 지원한다. spring-boot-starter-data-jdbc 디펜더시를 받아 사용하면 된다.

JUnit 5

slice test를 Junit5 이용해서 테스트를 진행한다면 @ExtendWith(SpringExtension.class) 어노테이션을 작성하지 않아도 된다. 이미 각 slice test에 해당 어노테이션이 메타 어노테이션으로 작성되어 있기 때문이다.

Actuator Endpoints

actuator endpoint들이 추가 되었다. Caches Endpoint, Spring Integration Graph Endpoint, Health Endpoint 이 추가 및 변경 되었다. 각 엔드포인트들은 해당 문서를 참고 하면 되겠다.

기타 Micrometer, 잡다한 내용들이 변경이 되었으니 이 글은 참고만 하고 해당 공식 문서를 참고하길 바란다.
오늘은 이상으로 Spring boot 2.1 릴리즈 변화에 대해서 알아봤다.

Spring boot NestedCondition

오늘 알아볼 내용은 Spring boot 에서 지원해주는 NestedCondition 에서 대해서 알아보도록 하자. Spring boot 에서는 많은 Condition을 지원해 주고 있지만 그중에서 NestedCondition 에 대해서 알아볼 것이다. 사실 Spring boot **Condition 들의 최상위 인터페이스는 Spring boot 에서 지원해주는 인터페이스가 아닌 Spring 에서 지원해주고 있는 인터페이스이다. 그 중 대표적인 Spring의 condition은 우리가 많이 사용하고 있는 ProfileCondition 이니 참고하면 되겠다.
나머지 Spring boot 의 Condition 들은 여기나 다른 블로그 혹은 문서를 참고하면 되겠다.

NestedCondition는 대표적으로 3의 클래스를 갖고 있는데 그 3개 모두 AbstractNestedCondition 클래스를 구현하고 있다. 가장 기본의 되는 클래스이다. 한개씩 살펴보도록 하자.

AnyNestedCondition

AnyNestedCondition 는 어느 한 조건만 만족하면 된다. 예를들어 어떤 빈들이 존재한다거나, 존재 하지 않다거나 등 여러 조건을 만들수 있는데 그 중 하나의 조건만 만족해도 동작한다. 또한 @ConditionalOnBean이 or 조건 에서 and 조건으로 변경되면서 or 조건을 사용할 경우 이 클래스를 이용하면 된다.
일단 한번 코드를 작성해보자.

public class Bar {
}

public class Foo {
}

public class FooService {
}

이런 샘플 코드가 있다고 가정하자. Foo와 Bar는 조건에 넣을 클래스이고 FooService 경우에는 해당 조건이 만족할 때 등록 되는 빈으로 사용할 예정이다.

@Configuration
@Conditional(AnyCondition.class)
public class AnyConfig {

  @Bean
  FooService fooService() {
    return new FooService();
  }

  static class AnyCondition extends AnyNestedCondition {

    public AnyCondition() {
      super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Foo.class)
    static class FooCondition {

    }

    @ConditionalOnBean(Bar.class)
    static class BarCondition {

    }
  }
}

위와 같이 AnyNestedCondition를 상속받아서 구현?.. 설정하면 된다. ConfigurationPhase 타입의 생성자는 필수이기에 위와 같이 작성해준다. 그리고 나서 해당 조건을 작성하면 된다.

@ConditionalOnBean(Foo.class)
static class FooCondition {

}

이 뜻은 Foo라는 빈이 존재할 경우 해당 설정을 적용하라는 의미이다. 그 아래 Bar 클래스도 동일하다. 위와 같이 작성할 경우엔 FooService 가 빈으로 등록 되지 않는다. 왜냐하면 Foo, Bar 모두 빈으로 등록 되어 있지 않기 때문이다. 만약 FooService를 빈으로 등록 되게 하고 싶다면 아래와 같이 작성해야 한다.

@Component
public class Foo {
}

or 

@Component
public class Bar {
}

Foo, Bar 둘중에 하나만 빈으로 등록 시키면 된다. 어렵지 않다.

NoneNestedConditions

NoneNestedConditions 는 클래스명의 의미와 동일하게 모두 만족하지 않으면 동작한다. 하나라도 만족하면 동작하지 않는다.

@Configuration
@Conditional(NoneCondition.class)
public class NoneConfig {

  @Bean
  FooService fooService() {
    return new FooService();
  }

  static class NoneCondition extends NoneNestedConditions {

    public NoneCondition() {
      super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Foo.class)
    static class FooCondition {

    }
    @ConditionalOnBean(Bar.class)
    static class BarCondition {

    }
  }
}

AnyNestedCondition와 마찬가지로 NoneNestedConditions를 구현하면 된다. 구현은 AnyNestedCondition와 동일하다.

AllNestedConditions

AllNestedConditions은 해당 조건을 모두 만족해야 한다. 이것 역시 하나라도 만족하지 않으면 동작하지 않는다.

@Configuration
@Conditional(AllCondition.class)
public class AllConfig {

  @Bean
  FooService fooService() {
    return new FooService();
  }

  static class AllCondition extends AllNestedConditions {

    public AllCondition() {
      super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Foo.class)
    static class FooCondition {

    }

    @ConditionalOnBean(Bar.class)
    static class BarCondition {

    }
  }
}

AllNestedConditions을 구현하면 된다. 쉽다. 딱히 어려운 부분은 없는 것 같다.

여기에서는 ConditionalOnBean만 사용했는데 꼭 그럴필요는 없다. @ConditionalOnMissingBean, @ConditionalOnClass, @ConditionalOnMissingClass@ConditionalOn** 다 동작할 것이다. (하지만 다 해보진 않았다.)

@ConditionalOnProperty 하나만 예제로 만들어 봤다.

static class AnyCondition extends AnyNestedCondition {

  public AnyCondition() {
    super(ConfigurationPhase.REGISTER_BEAN);
  }

  @ConditionalOnBean(Foo.class)
  static class FooCondition {

  }

  @ConditionalOnProperty(prefix = "foo", name = "name")
  static class BarCondition {

  }
}

이와 같이 작성할 경우 foo.name의 키를 갖고 있는 프로퍼티를 작성해주면 된다.

foo.name=wonwoo

그럼 해당 condition을 만족하여 동작한다.

여기서 ConfigurationPhase 라는 enum이 존재하는데 두가지 타입을 제공해준다. PARSE_CONFIGURATION, REGISTER_BEAN 이다. 아주 자세히는 모르겠지만 해당 문서를 보면 이렇다.

PARSE_CONFIGURATION 경우에는 @Configuration 어노테이션을 파싱할떄 조건을 평가한다. (ConfigurationClassParser)
REGISTER_BEAN 경우에는 빈을 추가 할때 조건을 평가한다. (ConfigurationClassBeanDefinitionReader)

음.. 글쎄다 사실 시점이 문제인 것 같다. 언제 조건을 평가하는지를 설정하는 듯하다. 대부분의 경우에는 빈을 추가할 때 시점으로 사용하면 문제 없을 듯 하다. 언제는 예외는 있기에..

PARSE_CONFIGURATION 사용하는 클래스들을 살펴보면 좀 더 알지 않을까 생각된다.

이상으로 오늘은 Spring boot의 NestedCondition 대해서 알아봤다. 좀 더 많은 내용은 문서를 참고하면 되겠다. 사실 문서에 있는지는 모르겠다. 문서를 보고 한게 아니라..

Spring boot 2.0 의 변화

오늘은 간단하게 Spring boot 2.0 의 변화에 대해서 알아보도록 하자. 물론 다 알아볼건 아니고 필자가 필요로하거나 자주 사용할만 것들, 또는 예전에 알아봤던 내용은 살펴보지 않을 것이니 이런게 있구나 정도만 알고 넘어가고 이후 공식문서등을 참고하면 더 좋을 것 같다.

@ConditionalOnBean

@ConditionalOnBean 어노테이션이 AND 조건으로 변경 되었다. 2.0 이전에는 OR 조건이였지만 지금 현재는 AND 조건으로 모두조건이 만족해야 설정된다.

public class ConditionalBean1 {
}

public class ConditionalBean2 {
}

public class Simple {
}

@ConditionalOnBean({ConditionalBean1.class, ConditionalBean2.class})
@Configuration
public class Config {

  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  @Bean
  Simple simple() {
    logger.info("simple test");
    return new Simple();
  }
}

대략 이런 코드가 있다고 가정하자. 위 코드는 일단 simple test란 로그가 찍히지 않는다. 그 이유는 뭐 둘다 빈으로 등록 되지 않았기 때문이다.

@Configuration
public class ConditionalBean1 {
}

ConditionalBean1 클래스만 빈으로 등록해보자. 이 때 2.0 이전 버전에서는 로그가 출력 되지만 2.0 부터는 로그가 출력 되지 않는다. 만약 로그가 출력 되게 원한다면 아래와 같이 모두 bean으로 등록 시켜야 된다.

@Configuration
public class ConditionalBean1 {
}

@Configuration
public class ConditionalBean2 {
}

-parameters

기본적으로 spring boot 2.0의 spring-boot-starter-parent 에는 -parameters 옵션이 추가 되었다.

@RestController
public class TestController {

  @GetMapping("/")
  public String hello() throws NoSuchMethodException {
    Method name = this.getClass().getMethod("name", String.class);
    return name.getParameters()[0].getName();
  }


  public String name(String id) {
    return "wonwoo";
  }
}

위와 같은 코드를 작성할 경우 (파라미터의 변수명을 가져올 때) 굳이 추가적으로 maven에 작성할 필요가 없다.

mvn install 
java -jar target/blabla.jar

1.5 버전에서는 arg0 로 찍히지만 2.0 에서는 id가 출력 된다.

Spring Data Web

나쁘지 않은 설정이 추가 되었다. Spring Data Web의 기본 페이지 사이즈, 파라미터 명, 첫페이지 인덱스 번호 등을 설정하라면 WebMvcConfigurerAdapter 를 상속받은 후에 PageableHandlerMethodArgumentResolver 클래스를 셋팅해줘야 했다. 하지만 이제는 properties 로 가능해졌다. 나쁘지 않다.

spring.data.web.pageable.default-page-size
spring.data.web.pageable.one-indexed-parameters=
spring.data.web.pageable.page-parameter=
spring.data.web.pageable.size-parameter=
spring.data.web.pageable.max-page-size=
spring.data.web.pageable.prefix=
spring.data.web.pageable.qualifier-delimiter=

관련설정은 SpringDataWebAutoConfiguration 클래스를 참고하면 되겠다.

DurationUnit

바인딩 할 때 유용한 어노테이션이 추가되었다. 유용한지는 나중에 알겠지.. java 1.8에 추가된 Duration@ConfigurationProperties에 작성할 수 있다. 아마도 2.0 이전에는 사용할 수 없었다. 하지만 2.0 부터는 Duration을 사용할 수 있다.

@ConfigurationProperties("foo")
public class FooProperties {

  private Duration period;

  public void setPeriod(Duration period) {
    this.period = period;
  }

  public Duration getPeriod() {
    return period;
  }
}

작성 후에 application.properties에 다음과 같이 작성 가능하다.

foo.period=10s

좀 더 나은 방법으로는 @DurationUnit 어노테이션을 사용해서 기본시간대를 지정할 수 있다.

  //..

  @DurationUnit(ChronoUnit.SECONDS)
  private Duration period;

  //..

이후 application.properties에는 10이라는 숫자만 써도 된다.

foo.period=10

http2 지원

Tomcat, Undertow 및 Jetty에서 http2를 지원한다. 하지만 몇가지 주의사항이 있다. 내 기억이 맞다면말이다. java8에서는 기본적으로 http2를 지원하지 않았다. 그래서 추가적인 모듈을 넣어야 한다고 했던 기억이 나고 java9부터는 기본적으로 지원한다.
또한 https를 사용해야지만 http2를 지원한다. spring boot의 기본설정에는.. 물론 커스텀하게 구현해도 될 것 같긴 한데.. 해보진 않았다.

위 그림을 보면 h2라고 설정 보일 것이다.

Property

env endpoint를 보면 origin 이라는 필드가 추가 되었다. 해당하는 프로퍼티의 파일 명과 라인번호 및 해당 컬럼수를 의미 한다.

{
  "name": "applicationConfig: [classpath:/application.properties]",
  "properties": {
    "management.endpoints.web.exposure.include": {
      "value": "*",
      "origin": "class path resource [application.properties]:1:43"
    },
    "foo.period": {
      "value": "10",
      "origin": "class path resource [application.properties]:3:12"
    }
  }
}

사용할 때가 있긴 한가? 흠흠..

이 외에도 엄청나게 많은 변화가 있지만 다 알아볼 수 는 없어 여기까지만 작성하겠다. 예를들어 Kotlin이 폭넓게 지원이 되고 있으며 Reactive Spring Security, Reactive Spring Data, Actuator의 변화 및 추가, Micrometer, 기타 Data 지원, 애니메이션 배너등 여러가지 지원을 많이 해주고 있다.

더 많은 변화를 알고 싶다면 해당 문서를 참고하면 되겠다.