WordPress Api

elasticsearch local 로 띄어보기

Posted by wonwoo on 2018-09-26

오늘 작성할 내용은 elasticsearch 소스를 직접 받아서  local(IDEA) 로 띄어보는 것을 해보도록 하자. 딱히 어렵거나 복잡하진 않지만 몇가지 할 작업이 있기에 작성해보도록 하자. 

기준은 mac과 intellij, elasticsearch 버전은 6.4  버전 gradle 4.9 기준으로 설명한다.

git clone https://github.com/elastic/elasticsearch.git

특정 작업할 공간에서 위와 같이 elasticsearch 를 받아오자. 물론 IDEA에서 직접 받아와도 된다. 

elasticsearch 를 받아왔다면 다음과 같이 커멘드창에 작성해야 한다.

cd elasticsearch
git checkout 6.4
gradle idea

저렇게 작성했는데 필자는 다음과 같은 에러가 발생하였다.

> Failed to apply plugin [id 'elasticsearch.build']
   > the environment variable JAVA_HOME must be set to a JDK installation directory for Java 1.10 but is [/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home] corresponding to [1.8]

정확한 이유는 모르겠으나 기존에 설치되어 있던 java10으로 변경하였다. java10이 설치 되어 있어 그런지.. 원래 6.4버전은 java8 부터 가능하다고 문서에 나와있는데. java8보다 높은 버전이 있어서 그런지 몰라도 java10으로 바꾸니 잘된다.

gradle idea 가 끝나면 elasticsearch 를 IDEA로 띄어보자. 그 후에 gradle를 리플레시 해보면 소스들이 모듈별로 잘 나눠져 있을 것이다. 

이정도만 해도 일단 반은 끝났다. 솔직히 여기까지 오는데만 해도 시간이 꽤 걸렸다. elasticsearch 자체만 빌드하는 것만으로도 오래걸린다. 소스가 워낙 많아서..

실제 서버를 돌리는 모듈은 server 라는 모듈에 있다.

캡쳐가 왜 이렇게 크지.. 

아무튼 server라는 모듈에 src/main/java/org/elasticsearch/bootstrap/Elasticsearch 보면 main 함수가 존재한다. 그걸 이용해 서버를 돌리면 된다. 한번 Run을 해보자. 아마도 안될 것이다. 이렇게 쉽게 된다면 굳이 이 글을 쓸필요가 없었을 것이다. 

일단 돌려보면 아래와 같은 메세지가 나온다.

ERROR: the system property [es.path.conf] must be set

es.path.conf 라는 프로퍼티가 필요 하다는 것이다. 일단 설정 파일이 필요한데 그 파일은 distribution/src/config 폴더 밑에 elasticsearch.yml, jvm.options, log4j2.properties 복사하여 설정할 경로에 복사하면 된다. 물론 이것을 그대로 `es.path.conf` 넣어도 상관은 없다. 하지만 필자는 따로 폴더를 만들어서 거기에 복사해 넣었다. elasticsearch.yml 보면 ${path.data} 와 같은 문자들이 있는데 지금은 필요 없다. 다 삭제해도 된다.

그 후에 다음과 같이 intellij 에 vm options으로 해당 프로퍼티에 경로를 작성해준다. 

다시 한번 Run 버튼을 눌러보자. 이번은 다른 에러가 발생한다.

Exception in thread "main" java.lang.IllegalStateException: path.home is not configured
	at org.elasticsearch.env.Environment.<init>(Environment.java:103)
	at org.elasticsearch.env.Environment.<init>(Environment.java:94)
	at org.elasticsearch.node.InternalSettingsPreparer.prepareEnvironment(InternalSettingsPreparer.java:86)
	at org.elasticsearch.cli.EnvironmentAwareCommand.createEnv(EnvironmentAwareCommand.java:95)
	at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86)
	at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:124)
	at org.elasticsearch.cli.Command.main(Command.java:90)
	at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:93)
	at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:86)

이번엔 path.home이 없다는 에러이다. 할일이 많다. path.home 도 역시 원하는 폴더를 만들어 작성하면 된다.

필자는 다음과 같이 작성하였다.

이제 다시 한번 돌려보자.  다시 돌려보면 일단 많은 에러가 발생한다. 한개씩 해결해보자.

org.elasticsearch.bootstrap.StartupException: java.lang.IllegalStateException: modules directory [/Users/wonwoo/elastic/modules] not found

첫 번째로 위와 같은 에러가 발생한다. 해당 설정 폴더에 modules  이라는 폴더를 찾을 수 없다는 것이다. 이 에러는 git으로 다운 받은 모듈들을 복사해서 넣어도 되고 아니면 실제 elastic  사이트가서 elasticsearch 를 다운받은 후에 모듈폴더만 가져와도 된다. 필자의 경우에는  elasticsearch 사이트가서 다운받은 후 해당 폴더에 넣어 두었다.  참고로 다운받는 것을 추천한다. git으로 clone한 모듈은 intellij 관련한 파일들도 있어 잘 파싱이 안되는 것 같았다. 

위와 같이 /Users/wonwoo/elastic/modules 폴더안에 elasticsearch 모듈들을 넣었다.  위와 같이 폴더를 넣고 다시 돌려보자. 

이제는 다음과 같은 에러가 발생한다.

main ERROR Could not register mbeans java.security.AccessControlException: access denied ("javax.management.MBeanTrustPermission" "register")

이 것은 정확한 원인은 잘 모르겠지만 해결 방법은 존재한다. 아마도 권한이 없어서 그런거 같은데.. 저것은 그냥 -Dlog4j2.disable.jmx=true log4j2의 jmx를 disable로 하는 것으로 끝냈다. 하지만 정확한 원인은 필자도 잘.. 

다시 한번 돌려보자. 그럼 아래와 같은 에러가 또 발생한다. 이놈의 에러는 언제쯤..

java.lang.NoClassDefFoundError: org/elasticsearch/plugins/ExtendedPluginsClassLoader

ExtendedPluginsClassLoader  클래스가 정의가 안되어 있다고 에러가 발생한다. 이것은 intellij 와 gradle 과의 관련이 있는 듯 하다. intellij에서 Edit Configurations 에 들어가 Include dependencies with "Provided" scope 를 다음과 같이 체크해 준다.

그 후에 다시 Run을 해보자. 

아마도 마지막 에러일 듯 싶다.

org.elasticsearch.bootstrap.StartupException: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "createClassLoader")

이 것 역시 권한과 관련이 있어보인다.  이것의 해결방법은 ${JAVA_HOME}/conf/security/java.policy 파일을 열어서 다음과 같이 추가해주면 된다.

permission java.lang.RuntimePermission "createClassLoader";

이제 마지막으로 서버를 돌려보자. 그럼 아무 에러없이 정상적으로 서버가 올라가는 것을 볼 수 있다. 

정상적으로 서버가 동작하는지 확인하기 위해 다음과 같이 커멘드창에 입력하거나 브라우저를 띄워서 확인해보자.

$ http :9200
curl http://localhost:9200

{
    "cluster_name": "elasticsearch",
    "cluster_uuid": "ierbMoD4SDmXbY9xbsrQng",
    "name": "fcsWvlW",
    "tagline": "You Know, for Search",
    "version": {
        "build_date": "Unknown",
        "build_flavor": "unknown",
        "build_hash": "Unknown",
        "build_snapshot": true,
        "build_type": "unknown",
        "lucene_version": "7.4.0",
        "minimum_index_compatibility_version": "5.0.0",
        "minimum_wire_compatibility_version": "5.6.0",
        "number": "6.4.1"
    }
}

그럼 위와 같이 정상적으로 response가 나온것을 확인할 수 있다. 

오늘은 이렇게 elasticsearch 소스를 받아서 로컬로 띄어보는 작업을 해봤다. 별거 아닐줄 알았는데 해보니까 시간이 꽤 걸렸다. 혹시나 elasticsearch에 기여할 것이 있다면 위와 같이 해당 소스를 받아서 테스트를 해보도록 하자. 

그럼 오늘은 이만!

Spring 의 새로운 클래스

Posted by wonwoo on 2018-09-06

오늘 이야기 할 내용은 Spring과 Spring boot의 새로운 클래스에 대해서 알아보려고 한다.  제목은 Spring 이라고 했지만 Spring boot 도 포함되어 있다. 대략 4가지 정도 클래스를 알아볼 예정인데 각 설명마다 프로젝트와 버전을 명시하겠다. 

보다 많은 클래스가 추가 되었지만 다 알아 볼 수도 없고 또 한 클래스들이 디펜더시도 있어 유틸성의 클래스들만 알아보도록 하자.

DataSize

DataSize 클래스는 Spring 5.1에 포함될 예정이다. 아직 릴리즈는 되지 않았지만 util 클래스라 크게 바뀌지 않을 듯으로 보인다. 한번 알아보도록 하자.

DataSize dataSize = DataSize.of(10, DataUnit.TERABYTES);
System.out.println(dataSize);
System.out.println(dataSize.toGigaBytes());

아주 간단하다. 특정한 DataSize를 작성하면 그에 맞게 원하는 데이터 형식으로 바꿔준다.  위의 코드는 10테라 바이트를  기가 바이트로 바꾸어 리턴해주는 코드이다. 실제로 내부적으로는  long 형태의 byte 로 저장하고 있다. 지금 현제 지원해주는 단위는 BYTES, KILOBYTES, MEGABYTES, GIGABYTES, TERABYTES 를 지원해주 고 있다. 뭐 아직까지는 테라바이트 이상으로는 필요 없을 듯으로 판단하여 그런듯 하다. 

위의 예제는 of 메서드를 이용해서 생성했지만 굳이 그럴 필요 없이 ofXXX 메서드를 이용해서 사용하면 더욱 편리하다.

DataSize bytes = DataSize.ofBytes(100);
DataSize kiloBytes = DataSize.ofKiloBytes(100);
DataSize megaBytes = DataSize.ofMegaBytes(100);
DataSize gigaBytes = DataSize.ofGigaBytes(100);
DataSize teraBytes = DataSize.ofTeraBytes(100);

좀 더 간편하게 사용할 수 있을 듯하다. 이것은 Spring에서 지원해주는 클래스지만 Spring boot 2.1에서도 직접적으로 사용할 수 있다.

@ConfigurationProperties("foo")
public class DataSizeProperties {

    private DataSize tempSize;

    public DataSize getTempSize() {
        return tempSize;
    }

    public void setTempSize(DataSize tempSize) {
        this.tempSize = tempSize;
    }
}

우리가 자주 사용하는 @ConfigurationProperties 어노테이션에서도  DataSize 클래스를 사용해서 매핑할 수 있다. 

사용법은 아래와 같이 간단하다.

foo.temp-size=10
foo.temp-size=10MB
foo.temp-size=10GB
...

기본적으로 아무 단위가 없다면 Byte로 설정된다. 만약 원하는 단위가 있다면 위 처럼 해당 단위를 작성해주면 된다. 하지만 여기서 주의할 점은 모두 대문자를 이용해야 한다는 것이다.  왜소문자는 파싱을 안되게 했을까? 흠흠 Enum 타입에 있는 문자로 결정하는 것 같은데 자세히는 살펴보지 않았다. 아무튼 그렇다. 

만약 해당 타입을 기본적인 단위로 지정해주고 싶다면 다음과 같이작성해도 무방하다.

@ConfigurationProperties("foo")
public class DataSizeProperties {

    @DataSizeUnit(DataUnit.GIGABYTES)
    private DataSize tempSize;

    public DataSize getTempSize() {
        return tempSize;
    }

    public void setTempSize(DataSize tempSize) {
        this.tempSize = tempSize;
    }
}

위와 같이 @DataSizeUnit 어노테이션을 이용해서 해당 단위를 기본적으로 설정 할 수 있다. 그러면 프로퍼티에 굳이 해당 단위를 명시해주지 않아도 된다. 만약 명시해준다면  기본타입은 무시가 되고 작성한 타입으로 동작된다.

AnnotatedClassFinder

이 클래스는 Spring boot 2.1 에 새롭게 나타난 클래스이다. 그렇다고 해서 완전하게 새로운 클래스는 아니다.(?) 기존의 있던 SpringBootConfigurationFinder 클래스를 살짝 변경한? 클래스이다. 기존에는 @SpringBootConfiguration 어노테이션만을 파싱하기 위한 클래스라면 AnnotatedClassFinder  클래스 경우에는 원하는 어노테이션을 파싱할 수 있다. 

SpringBootConfigurationFinder() {
    this.scanner = new ClassPathScanningCandidateComponentProvider(false);
    this.scanner.addIncludeFilter(
            new AnnotationTypeFilter(SpringBootConfiguration.class));
    this.scanner.setResourcePattern("*.class");
}

위의 코드는 기존의 클래스이며 아래의 코드는 새롭게 탄생한 코드이다.

public AnnotatedClassFinder(Class<? extends Annotation> annotationType) {
    Assert.notNull(annotationType, "AnnotationType must not be null");
    this.annotationType = annotationType;
    this.scanner = new ClassPathScanningCandidateComponentProvider(false);
    this.scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
    this.scanner.setResourcePattern("*.class");
}

차이라곤 뭐 생성자에 어노테이션을 받는 부분이 추가 된 것 뿐이다.  

사용법은 해당 클래스를 보면 쉽게 알 것 같다.  그래서 생략 하겠다..

PropertyMapper

PropertyMapper 클래스는 Spring boot 2.0 에서 추가된 클래스이다.
이 클래스의 용도는 프로퍼티들을 매핑 시켜주는? 그런 역할을 하는 클래스이다. 예를들어 Spring boot 의 @ConfigurationProperties 를 사용해서 프로퍼티들을 매핑 시켰다면 그 이후에 그 프로퍼티들을 실제 사용하는 프로퍼티에 매핑을 시켜주는 그런? 클래스이다. 흠 말로는 어려우니 코드를 살펴보도록 하자.

class FooProperties {
    private int timeout;
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    public int getTimeout() {
        return timeout;
    }
}

class FooTemplate {
    private int timeout;
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    public void bar() {
        //...
        //this.timeout
    }
}

예를 들어 위와 같은 클래스가 있다고 가정하자. FooProperties 클래스는 클라이언트로부터 프로퍼티를 받는 클래스이고 FooTemplate 은 실제 그 프로퍼티로 어떤 작업등을 하는 클래스라고 가정해보자. 그럼 다음과 같이 사용할 수 있다.

PropertyMapper map = PropertyMapper.get();
FooProperties properties = new FooProperties();
properties.setTimeout(10);
FooTemplate fooTemplate = new FooTemplate();
map.from(properties::getTimeout).to(fooTemplate::setTimeout);

위와 같이 FooProperties 클래스로 부터 받은 프로퍼티를 FooTemplate 의 속성으로 넣는 작업이다. 실제로 FooTemplate 의 timeout 속성에는 10이라는 값이 들어가 있다. 

또한 좀 더 나은 방식도 제공해준다. 예를들어 source 의 프로퍼티가 null일 경우에 굳이 target 프로퍼티에게 null을 넣을 필요가 없다면 다음과 같이 작성하면 된다.

map.from(properties::getTimeout).whenNonNull().to(fooTemplate::setTimeout);

그러면 fooTemplate 클래스에 timeout은 영향 받지 않는다. 또한 간단하게 타입도 변경할 수 있다. 예를들어  FooTemplate의 timeout의 속성이 String이라면 다음과 같이 타입도 변경할 수 있다.

map.from(properties::getTimeout).as(String::valueOf).to(fooTemplate::setTimeout);

필자가 말한 위의 메서드뿐만아니라 많은 메서드가 존재하니 필요하다면 한번씩 살펴보는 것도 나쁘지 않다.

TestPropertyValues

이 클래스는 Spring boot 2.0에 새로생긴 클래스 이다. 하지만 우리가 딱히 직접적으로 사용할 클래스는 아니다.  기존의 존재했던 클래스 (EnvironmentTestUtils) 클래스가 Deprecated 되고 해당 클래스가 생겼다.

예전에 Test 할 때 주로 EnvironmentTestUtils.addEnvironment 메서드를 사용했지만 이제는 TestPropertyValues를 사용하면 되는데 해당 클래스를 직접적으로 사용할 일은 없다. 뭐 있을 수는 있겠지만 딱히 커스텀하게 만들지 않는 이상은..

왜냐하면 기존의 Config를 테스트 하던 클래스가 Spring boot 2.0 부터 새롭게 바뀌면서 ApplicationContextRunner 를 이용하면 되기 때문이다. 

예전에 RC 버전일때 포스팅한 내용은 여기있다. 궁금하다면 참고하면 되겠다.

여기에 존재하는 withPropertyValues 가 내부적으로 바로 TestPropertyValues 이다. 하지만 우리는 그냥 String 타입으로 넘겨서 알아채지 못했을 수도 있다.

private final TestPropertyValues environmentProperties;

//...

public SELF withPropertyValues(String... pairs) {
    return newInstance(this.contextFactory, this.initializers,
            this.environmentProperties.and(pairs), this.systemProperties,
            this.classLoader, this.parent, this.configurations);
}

딱히 사용할일은 없지만 그래도 이런게 있다고는 알아봤다. 오늘 내용은 Spring 의 새로운 클래스들을 몇개 알아봤다. 물론 다 알아 보고 싶지만 그러지 못한다. 깊은 내용의 클래스들도 있고 뭐가 뭔지 모르는 클래스도 있고 아직 확인 못한 클래스도 있으니.. 오늘은 여기까지만 알아보도록 하자. 추후에 더 알아 볼 수 있으면 알아보도록 하자. 

유틸성 클래스들이라 해당 클래스 직접사용해봐도 쉽게 이해할 수 있을 것으로 판단 된다. 

그럼 오늘은 이만!

Spring boot NestedCondition

Posted by wonwoo on 2018-07-28

오늘 알아볼 내용은 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 대해서 알아봤다. 좀 더 많은 내용은 문서를 참고하면 되겠다. 사실 문서에 있는지는 모르겠다. 문서를 보고 한게 아니라..