
함수형 프로그래밍을 위한 서킷 브레이커, 레이트 리미터, 재시도, 벌크헤드, 타임아웃 및 캐시 데코레이터를 제공하는 Java용 내결함성 라이브러리입니다.
= 함수형 프로그래밍을 위해 설계된 내결함성 라이브러리 :author: Robert Winkler and Bohdan Storozhuk :icons: :toc: macro :numbered: 1 ifdef::env-github[] :tip-caption: 💡 :note-caption: ℹ️ :important-caption: ❗ :caution-caption: 🔥 :warning-caption: ⚠️ endif::[]
image:https://github.com/resilience4j/resilience4j/actions/workflows/gradle-build.yml/badge.svg["Build Status"] image:https://img.shields.io/nexus/r/io.github.resilience4j/resilience4j-circuitbreaker?server=https%3A%2F%2Foss.sonatype.org["Release"] image:https://img.shields.io/nexus/s/io.github.resilience4j/resilience4j-circuitbreaker?server=https%3A%2F%2Foss.sonatype.org["Snapshot"] image:http://img.shields.io/badge/license-ASF2-blue.svg["Apache License 2", link="http://www.apache.org/licenses/LICENSE-2.0.txt"]
image:https://sonarcloud.io/api/project_badges/measure?project=resilience4j_resilience4j&metric=coverage["Coverage", link="https://sonarcloud.io/dashboard?id=resilience4j_resilience4j"] image:https://sonarcloud.io/api/project_badges/measure?project=resilience4j_resilience4j&metric=sqale_rating["Maintainability", link="https://sonarcloud.io/dashboard?id=resilience4j_resilience4j"] image:https://sonarcloud.io/api/project_badges/measure?project=resilience4j_resilience4j&metric=reliability_rating["Reliability", link="https://sonarcloud.io/dashboard?id=resilience4j_resilience4j"] image:https://sonarcloud.io/api/project_badges/measure?project=resilience4j_resilience4j&metric=security_rating["Security", link="https://sonarcloud.io/dashboard?id=resilience4j_resilience4j"] image:https://sonarcloud.io/api/project_badges/measure?project=resilience4j_resilience4j&metric=vulnerabilities["Vulnerabilities", link="https://sonarcloud.io/dashboard?id=resilience4j_resilience4j"] image:https://sonarcloud.io/api/project_badges/measure?project=resilience4j_resilience4j&metric=bugs["Bugs", link="https://sonarcloud.io/dashboard?id=resilience4j_resilience4j"]
toc::[]
== 소개
Resilience4j는 함수형 프로그래밍을 위해 설계된 경량 내결함성 라이브러리입니다. Resilience4j는 고차 함수(데코레이터)를 제공하여 모든 함수형 인터페이스, 람다 표현식 또는 메서드 참조를 회로 차단기(Circuit Breaker), 속도 제한기(Rate Limiter), 재시도(Retry) 또는 벌크헤드(Bulkhead)로 강화할 수 있게 해줍니다. 어떤 함수형 인터페이스, 람다 표현식 또는 메서드 참조에도 데코레이터를 여러 개 쌓을 수 있습니다. 장점은 필요한 데코레이터만 선택할 수 있고 그 외에는 아무것도 필요하지 않다는 것입니다.
Resilience4j 3는 Java 21이 필요합니다.
// Create a CircuitBreaker with default configuration CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendService");
// Create a Retry with default configuration // 3 retry attempts and a fixed time interval between retries of 500ms Retry retry = Retry.ofDefaults("backendService");
// Create a Bulkhead with default configuration Bulkhead bulkhead = Bulkhead.ofDefaults("backendService");
Supplier supplier = () -> backendService .doSomething(param1, param2);
// Decorate your call to backendService.doSomething() // with a Bulkhead, CircuitBreaker and Retry // **note: you will need the resilience4j-all dependency for this Supplier decoratedSupplier = Decorators.ofSupplier(supplier) .withCircuitBreaker(circuitBreaker) .withBulkhead(bulkhead) .withRetry(retry) .decorate();
// Execute the decorated supplier and recover from any exception String result = Try.ofSupplier(decoratedSupplier) .recover(throwable -> "Hello from Recovery").get();
// When you don't want to decorate your lambda expression, // but just execute it and protect the call by a CircuitBreaker. String result = circuitBreaker .executeSupplier(backendService::doSomething);
// You can also run the supplier asynchronously in a ThreadPoolBulkhead ThreadPoolBulkhead threadPoolBulkhead = ThreadPoolBulkhead .ofDefaults("backendService");
// The Scheduler is needed to schedule a timeout on a non-blocking CompletableFuture ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3); TimeLimiter timeLimiter = TimeLimiter.of(Duration.ofSeconds(1));
NOTE: Resilience4j를 사용하면 모든 것을 한 번에 적용할 필요가 없습니다. https://mvnrepository.com/artifact/io.github.resilience4j[*필요한 것만 선택*]할 수 있습니다.
== 문서
설정 및 사용 방법은 *https://resilience4j.readme.io/docs[사용자 가이드]*에 설명되어 있습니다.
https://github.com/resilience4j-docs-ja/resilience4j-docs-ja[有志による日本語訳(非公式) Japanese translation by volunteers(Unofficial)]
https://github.com/lmhmhl/Resilience4j-Guides-Chinese[这是Resilience4j的非官方中文文档 Chinese translation by volunteers(Unofficial)]
== 개요
Resilience4j는 여러 핵심 모듈을 제공합니다:
메트릭, Feign, Kotlin, Spring, Ratpack, Vertx, RxJava2 등을 위한 추가 모듈도 있습니다.
NOTE: 전체 모듈 목록은 *https://resilience4j.readme.io/docs#section-modularization[사용자 가이드]*에서 확인하세요.
TIP: 핵심 모듈 패키지 또는 +Decorators+ 빌더는 *https://mvnrepository.com/artifact/io.github.resilience4j/resilience4j-all[resilience4j-all]*을 참조하세요.
== 모범 사례
=== 인스턴스 관리: 인스턴스를 공유해야 하는 경우와 공유하지 말아야 하는 경우
새로 배우는 사용자가 이해해야 할 가장 중요한 개념 중 하나는 언제 별도의 인스턴스를 만들고 언제 서로 다른 원격 서비스 또는 백엔드 간에 인스턴스를 공유할지입니다.
==== 고유 인스턴스가 중요한 이유
백엔드 서비스마다 별도의 인스턴스를 만드는 것은 다음 이유로 중요합니다:
==== 인스턴스 인식 패턴과 인스턴스 비인식 패턴
복원력 패턴마다 인스턴스 분리에 대한 요구 사항이 다릅니다:
===== 인스턴스 인식 패턴 (반드시 공유 금지)
이러한 패턴은 특정 서비스에 고유한 상태를 유지하므로 반드시 별도의 인스턴스가 있어야 합니다:
// CORRECT: Separate CircuitBreaker for each service CircuitBreaker paymentServiceCB = CircuitBreaker.ofDefaults("paymentService"); CircuitBreaker inventoryServiceCB = CircuitBreaker.ofDefaults("inventoryService"); CircuitBreaker notificationServiceCB = CircuitBreaker.ofDefaults("notificationService");
// CORRECT: Separate Bulkhead for each service Bulkhead paymentBulkhead = Bulkhead.ofDefaults("paymentService"); Bulkhead inventoryBulkhead = Bulkhead.ofDefaults("inventoryService");
===== 인스턴스 비인식 패턴 (공유할 수는 있지만 권장하지 않음)
이러한 패턴은 각 실행마다 새로운 컨텍스트를 생성하며 서비스별 상태를 유지하지 않습니다:
// TECHNICALLY OK: Retry doesn't maintain state between calls Retry sharedRetry = Retry.ofDefaults("shared");
// BETTER: Unique instances provide better metrics and monitoring Retry paymentRetry = Retry.ofDefaults("paymentService"); Retry inventoryRetry = Retry.ofDefaults("inventoryService");
==== 권장 방법: 항상 고유 인스턴스 사용
공유할 수 있는 패턴의 경우에도 고유 인스턴스를 만드는 것이 권장됩니다. 그 이유는 다음과 같습니다:
===== 전체 예제: 여러 서비스
// You have 3 backend services to protect public class ServiceOrchestrator {
// Payment Service protection
private final CircuitBreaker paymentCB = CircuitBreaker.ofDefaults("paymentService");
private final Retry paymentRetry = Retry.ofDefaults("paymentService");
private final Bulkhead paymentBulkhead = Bulkhead.ofDefaults("paymentService");
// Inventory Service protection
private final CircuitBreaker inventoryCB = CircuitBreaker.ofDefaults("inventoryService");
private final Retry inventoryRetry = Retry.ofDefaults("inventoryService");
private final Bulkhead inventoryBulkhead = Bulkhead.ofDefaults("inventoryService");
// Notification Service protection
private final CircuitBreaker notificationCB = CircuitBreaker.ofDefaults("notificationService");
private final Retry notificationRetry = Retry.ofDefaults("notificationService");
public Order processOrder(OrderRequest request) {
// Each service call is protected by its own set of resilience instances
// Call payment service
Supplier<PaymentResult> paymentCall = () -> paymentService.charge(request);
PaymentResult payment = Decorators.ofSupplier(paymentCall)
.withCircuitBreaker(paymentCB)
.withRetry(paymentRetry)
.withBulkhead(paymentBulkhead)
.decorate()
.get();
// Call inventory service
Supplier<InventoryResult> inventoryCall = () -> inventoryService.reserve(request);
InventoryResult inventory = Decorators.ofSupplier(inventoryCall)
.withCircuitBreaker(inventoryCB)
.withRetry(inventoryRetry)
.withBulkhead(inventoryBulkhead)
.decorate()
.get();
// Call notification service (demonstrates circuit isolation: the notification circuit breaker remains independent of the payment circuit breaker, even if payment opens its circuit in other calls)
Runnable notificationCall = () -> notificationService.send(request);
Decorators.ofRunnable(notificationCall)
.withCircuitBreaker(notificationCB)
.withRetry(notificationRetry)
.decorate()
.run();
return new Order(payment, inventory);
}
NOTE: 레지스트리는 자동 구성되고 구성 속성에 따라 인스턴스가 요청 시 생성되는 Spring Boot 애플리케이션에서 특히 유용합니다.
==== 요약
[cols="<.<*", options="header"] |=== |패턴 |공유 가능? |공유해야 함? |이유?
|CircuitBreaker |아니요 |아니요 |상태는 서비스별로 고유합니다. 공유하면 한 서비스의 실패가 다른 모든 서비스에 영향을 미칩니다.
|Bulkhead |아니요 |아니요 |적절한 리소스 관리를 위해 동시 호출 한도는 서비스별로 격리되어야 합니다.
|RateLimiter |아니요 |아니요 |속도 제한은 일반적으로 서비스마다 다르며, 공유하면 목적이 무의미해집니다.
|Retry |예 |아니요 |기술적으로 공유해도 안전하지만, 고유 인스턴스가 더 나은 메트릭과 관찰성을 제공합니다.
|TimeLimiter |예 |아니요 |기술적으로 공유해도 안전하지만, 고유 인스턴스가 더 나은 메트릭과 관찰성을 제공합니다.
|Cache |상황에 따라 다름 |상황에 따라 다름 |캐시된 데이터가 사용 사례 간에 진정으로 동일한 경우에만 공유하세요.
|===
== 복원력 패턴
[cols="<.<*", options="header"] |=== |이름 |작동 방식 |설명 |링크
|Retry |실패한 실행을 반복합니다. |많은 오류는 일시적이며 짧은 지연 후 자체적으로 해결될 수 있습니다. |<<circuitbreaker-retry-fallback,개요>>, https://resilience4j.readme.io/docs/retry[문서], https://resilience4j.readme.io/docs/getting-started-3#annotations[Spring]
|Circuit Breaker |잠재적 실패를 일시적으로 차단합니다. |시스템이 심각하게 문제를 겪고 있을 때는 클라이언트가 기다리게 하는 것보다 빨리 실패하는 것이 낫습니다. |<<circuitbreaker-retry-fallback,개요>>, https://resilience4j.readme.io/docs/circuitbreaker[문서], https://resilience4j.readme.io/docs/feign[Feign], https://resilience4j.readme.io/docs/getting-started-3#annotations[Spring]
|Rate Limiter |기간당 실행 횟수를 제한합니다. |들어오는 요청의 속도를 제한합니다. |<<ratelimiter,개요>>, https://resilience4j.readme.io/docs/ratelimiter[문서], https://resilience4j.readme.io/docs/feign[Feign], https://resilience4j.readme.io/docs/getting-started-3#annotations[Spring]
|Time Limiter |실행 시간을 제한합니다. |일정 대기 시간이 지나면 성공적인 결과가 나올 가능성이 낮습니다. |https://resilience4j.readme.io/docs/timeout[문서], https://resilience4j.readme.io/docs/getting-started-3#annotations[Spring]
|Bulkhead |동시 실행을 제한합니다. |리소스가 풀로 격리되어 하나가 실패해도 나머지는 계속 작동합니다. |<<bulkhead,개요>>, https://resilience4j.readme.io/docs/bulkhead[문서], https://resilience4j.readme.io/docs/getting-started-3#annotations[Spring]
|Cache |성공적인 결과를 기억합니다. |일부 요청은 유사할 수 있습니다. |https://resilience4j.readme.io/docs/cache[문서]
|Fallback |실패 시 대체 결과를 제공합니다. |실패는 여전히 발생합니다. 그런 상황에서 무엇을 할지 계획하세요. |<<circuitbreaker-retry-fallback,Try::recover>>, https://resilience4j.readme.io/docs/getting-started-3#section-annotations[Spring], https://resilience4j.readme.io/docs/feign[Feign]
|===
위 표는 https://github.com/App-vNext/Polly#resilience-policies[Polly: 복원력 정책]을 기반으로 합니다.
NOTE: 복원력 패턴에 대한 자세한 내용은 link:#Talks[Talks] 섹션을 확인하세요. 구성 요소에 대한 자세한 내용은 *https://resilience4j.readme.io/docs/getting-started-2[사용자 가이드]*에서 확인하세요.
== Spring Boot
Spring Boot 3에서의 설정 및 사용 방법은 https://github.com/resilience4j/resilience4j-spring-boot3-demo[여기]에서 확인할 수 있습니다.
가상 스레드 지원 (Java 21 Project Loom) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Starting with Resilience4j 3 you can switch the internal schedulers to Java virtual threads.
동일한 설정은 JVM 인수로도 활성화할 수 있습니다:``` -Dresilience4j.thread.type=virtual
속성(또는 시스템 속성)을 생략하면 라이브러리는 일반 *플랫폼* 스레드로 폴백(fallback)됩니다.
== 사용 예제
[[circuitbreaker-retry-fallback]]
=== CircuitBreaker, Retry 및 Fallback
다음 예제는 CircuitBreaker로 람다 표현식(Supplier)을 데코레이션하고, 예외가 발생했을 때 호출을 최대 3번 재시도하는 방법을 보여줍니다. 재시도 사이의 대기 간격을 구성할 수 있으며, 사용자 정의 백오프 알고리즘도 구성할 수 있습니다.
이 예제는 Vavr의 Try 모나드를 사용하여 예외를 복구하고, 모든 재시도가 실패한 경우 폴백으로 다른 람다 표현식을 호출합니다.
[source,java]
----
// Simulates a Backend Service
public interface BackendService {
String doSomething();
}
// Create a CircuitBreaker (use default configuration)
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
// Create a Retry with at most 3 retries and a fixed time interval between retries of 500ms
Retry retry = Retry.ofDefaults("backendName");
// Decorate your call to BackendService.doSomething() with a CircuitBreaker
Supplier<String> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, backendService::doSomething);
// Decorate your call with automatic retry
decoratedSupplier = Retry
.decorateSupplier(retry, decoratedSupplier);
// Use of Vavr's Try to
// execute the decorated supplier and recover from any exception
String result = Try.ofSupplier(decoratedSupplier)
.recover(throwable -> "Hello from Recovery").get();
// When you don't want to decorate your lambda expression,
// but just execute it and protect the call by a CircuitBreaker.
String result = circuitBreaker.executeSupplier(backendService::doSomething);
----
==== CircuitBreaker 및 RxJava2
다음 예제는 사용자 정의 RxJava 연산자를 사용하여 Observable을 데코레이션하는 방법을 보여줍니다.
[source,java]
----
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
Observable.fromCallable(backendService::doSomething)
.compose(CircuitBreakerOperator.of(circuitBreaker))
----
NOTE: Resilience4j는 `+RateLimiter+`, `+Bulkhead+`, `+TimeLimiter+` 및 `+Retry+`를 위한 RxJava 연산자도 제공합니다. 자세한 내용은 *https://resilience4j.readme.io/docs/getting-started-2[사용자 가이드]*를 참조하세요.
==== CircuitBreaker 및 Spring Reactor
다음 예제는 사용자 정의 Reactor 연산자를 사용하여 Mono를 데코레이션하는 방법을 보여줍니다.
[source,java]
----
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
Mono.fromCallable(backendService::doSomething)
.transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
----
NOTE: Resilience4j는 `+RateLimiter+`, `+Bulkhead+`, `+TimeLimiter+` 및 `+Retry+`를 위한 Reactor 연산자도 제공합니다. 자세한 내용은 *https://resilience4j.readme.io/docs/getting-started-1[사용자 가이드]*를 참조하세요.
[[ratelimiter]]
=== RateLimiter
다음 예제는 특정 메서드의 호출 속도를 초당 1회를 넘지 않도록 제한하는 방법을 보여줍니다.
[source,java]
----
// Create a custom RateLimiter configuration
RateLimiterConfig config = RateLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(100))
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(1)
.build();
// Create a RateLimiter
RateLimiter rateLimiter = RateLimiter.of("backendName", config);
// Decorate your call to BackendService.doSomething()
Supplier<String> restrictedSupplier = RateLimiter
.decorateSupplier(rateLimiter, backendService::doSomething);
// First call is successful
Try<String> firstTry = Try.ofSupplier(restrictedSupplier);
assertThat(firstTry.isSuccess()).isTrue();
// Second call fails, because the call was not permitted
Try<String> secondTry = Try.of(restrictedSupplier);
assertThat(secondTry.isFailure()).isTrue();
assertThat(secondTry.getCause()).isInstanceOf(RequestNotPermitted.class);
----
[[bulkhead]]
=== Bulkhead
두 가지 격리 전략 및 Bulkhead 구현이 있습니다.
==== SemaphoreBulkhead
다음 예제는 Bulkhead로 람다 표현식을 데코레이션하는 방법을 보여줍니다. Bulkhead는 병렬 실행 수를 제한하는 데 사용할 수 있습니다. 이 벌크헤드 추상화는 다양한 스레딩 및 I/O 모델에서 잘 작동해야 합니다. 세마포어 기반이며, Hystrix와 달리 "shadow" 스레드 풀 옵션을 제공하지 않습니다.
[source,java]
----
// Create a custom Bulkhead configuration
BulkheadConfig config = BulkheadConfig.custom()
.maxConcurrentCalls(150)
.maxWaitDuration(100)
.build();
Bulkhead bulkhead = Bulkhead.of("backendName", config);
Supplier<String> supplier = Bulkhead
.decorateSupplier(bulkhead, backendService::doSomething);
----
[[threadpoolbulkhead]]
==== ThreadPoolBulkhead
다음 예제는 제한된 큐와 고정 스레드 풀을 사용하는 ThreadPoolBulkhead로 람다 표현식을 사용하는 방법을 보여줍니다.
[source,java]
----
// Create a custom ThreadPoolBulkhead configuration
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
.maxThreadPoolSize(10)
.coreThreadPoolSize(2)
.queueCapacity(20)
.build();
ThreadPoolBulkhead bulkhead = ThreadPoolBulkhead.of("backendName", config);
// Decorate or execute immediately a lambda expression with a ThreadPoolBulkhead.
Supplier<CompletionStage<String>> supplier = ThreadPoolBulkhead
.decorateSupplier(bulkhead, backendService::doSomething);
CompletionStage<String> execution = bulkhead
.executeSupplier(backendService::doSomething);
----
[[events]]
== 발생된 이벤트 소비
`+CircuitBreaker+`, `+RateLimiter+`, `+Cache+`, `+Bulkhead+`, `+TimeLimiter+` 및 `+Retry+` 구성 요소는 이벤트 스트림을 발생시킵니다. 이는 로깅, 검증 및 기타 목적으로 소비할 수 있습니다.
=== 예제
`+CircuitBreakerEvent+`는 상태 전환, 서킷 브레이커 리셋, 성공적인 호출, 기록된 오류 또는 무시된 오류일 수 있습니다. 모든 이벤트에는 이벤트 생성 시간 및 호출 처리 시간과 같은 추가 정보가 포함됩니다. 이벤트를 소비하려면 이벤트 소비자를 등록해야 합니다.
[source,java]
----
circuitBreaker.getEventPublisher()
.onSuccess(event -> logger.info(...))
.onError(event -> logger.info(...))
.onIgnoredError(event -> logger.info(...))
.onReset(event -> logger.info(...))
.onStateTransition(event -> logger.info(...));
// Or if you want to register a consumer listening to all events, you can do:
circuitBreaker.getEventPublisher()
.onEvent(event -> logger.info(...));
----
RxJava 또는 Spring Reactor 어댑터를 사용하여 `+EventPublisher+`를 리액티브 스트림으로 변환할 수 있습니다. 리액티브 스트림의 장점은 RxJava의 `+observeOn+` 연산자를 사용하여 CircuitBreaker가 옵저버/소비자에게 알림을 보낼 때 사용할 다른 Scheduler를 지정할 수 있다는 점입니다.
[source,java]
----
RxJava2Adapter.toFlowable(circuitBreaker.getEventPublisher())
.filter(event -> event.getEventType() == Type.ERROR)
.cast(CircuitBreakerOnErrorEvent.class)
.subscribe(event -> logger.info(...))
----
NOTE: 다른 구성 요소의 이벤트도 소비할 수 있습니다. 자세한 내용은 *https://resilience4j.readme.io/[사용자 가이드]*를 참조하세요.
== 발표
[cols="4*"]
|===
|0:34
|https://www.youtube.com/watch?v=kR2sm1zelI4[서킷 브레이커의 대결: Resilience4J vs Istio]
|Nicolas Frankel
|GOTO Berlin
|0:33
|https://www.youtube.com/watch?v=AwcjOhD91Q0[서킷 브레이커의 대결: Istio vs. Hystrix/Resilience4J]
|Nicolas Frankel
|JFuture
|0:42
|https://www.youtube.com/watch?v=KosSsZEqS-k&t=157[Hystrix 이후 세상의 복원 패턴]
|Tomasz Skowroński
|Cloud Native Warsaw
|0:52
|https://www.youtube.com/watch?v=NHVxrLb3jFI[Spring Boot 및 Resilience4j를 사용하여 견고하고 복원력 있는 앱 구축]
|David Caron
|SpringOne
|0:22
|https://www.youtube.com/watch?v=gvDvOWtPLVY&t=140[Hystrix는 죽었습니다. 이제 무엇을?]
|Tomasz Skowroński
|DevoxxPL
|===
== Resilience4j를 사용하는 기업
* *Deutsche Telekom* (하루 4억 건 이상의 요청을 처리하는 애플리케이션)
* *AOL* (낮은 지연 시간 요구 사항이 있는 애플리케이션)
* *Netpulse* (40개 이상의 통합을 가진 시스템)
* *wescale.de* (B2B 통합 플랫폼)
* *Topia* (마이크로서비스 아키텍처로 구축된 HR 애플리케이션)
* *Auto Trader Group plc* (영국 최대 디지털 자동차 마켓플레이스)
* *PlayStation Network* (플랫폼 백엔드)
* *TUI InfoTec GmbH* (숙박 예약 워크플로 스트림 내의 백엔드 애플리케이션)
== 라이선스
Copyright 2020 Robert Winkler, Bohdan Storozhuk, Mahmoud Romeh, Dan Maas 및 기타
Apache License, Version 2.0 (이하 "라이선스")에 따라 사용이 허가됩니다.
이 라이선스를 준수하지 않는 한 이 파일을 사용할 수 없습니다.
라이선스 사본은 다음에서 얻을 수 있습니다.
http://www.apache.org/licenses/LICENSE-2.0
해당 법률에서 요구하거나 서면으로 합의하지 않는 한, 이 라이선스에 따라 배포되는 소프트웨어는 "있는 그대로" 기준(AS IS BASIS)으로 배포되며,
명시적이든 묵시적이든 어떠한 종류의 보증이나 조건 없이 제공됩니다.
라이선스에 따른 특정 권한과 제한 사항을 확인하려면 라이선스 전문을 참조하십시오.