티스토리 뷰
안녕하세요. Shopping Service Backend팀 안재열입니다.
저희 팀에서는 여러 팀에서 생성하고 관리한 상품과 관련된 데이터를 적절하게 가공하여 상품 목록 정보를 제공하는 API를 개발하는 업무를 담당하고 있습니다. 이 과정에서 다양한 팀에서 제공하는 다수의 API를 사용하여 한데 묶어주는 메시업(Mashup) 작업활동을 수행하게 됩니다. 그런데 다양한 곳의 API를 엮다보면 예기치 못한 문제가 발생하기도 합니다.
이번 글에서는 API 메시업 활동 중에 만나게 되는 문제점과 해결 방안에 관해서 살펴보겠습니다.
API 메시업(API Mashup)
API는 "Application Programming Interface"의 약자로, 다양한 소프트웨어나 애플리케이션들이 서로 상호작용하게 해주는 도구라고 생각하면 됩니다. API 메시업(Mashup)은 두 개 이상의 기존의 API를 결합하여 새로운 서비스를 만드는 것을 의미합니다. 이는 다양한 서비스의 데이터와 기능을 재구성하고 결합하여, 사용자가 원하는 형태의 새로운 서비스나 애플리케이션을 생성할 수 있게 합니다.
예를 들어, A 회사에서 지도 서비스를 운영하고 있고, 이를 API를 통해 제공하고 있다고 가정해 보겠습니다. 이런 경우 다른 회사의 웹사이트 개발자는 A회사의 지도 API를 사용하여 자신의 웹 페이지에 지도를 표시하는데 사용합니다. 여기까지는 일반적인 API의 사용법입니다. API 메시업은 여기서 더 나아가 API에 본인의 비즈니스 혹은 기술을 더하여 새로운 가치를 추가로 창출하고자 합니다. 만약 한 회사가 고객에게 배송 정보를 보여주는 서비스를 만들고 싶다면, A 회사의 지도 API와 자신들의 배송 API를 메시업하여 새로운 서비스를 만들 수 있습니다. 이렇게 하면 고객은 배송이 어디에 있는지를 실시간으로 지도에서 확인할 수 있게 됩니다.
하지만 API 메시업 과정에서는 주의 할 사항이 있습니다. 보안 문제, 데이터 무결성, 그리고 서로 다른 API들 사이의 호환성 문제 등에 주의해야 합니다. 저희 팀에서도 지마켓의 여러 백엔드 팀에서 제공하는 API를 이용해서 지마켓과 옥션에서 노출할 데이터들을 메시업 하는 일을 합니다. 이 과정에서 API 메시업의 문제들을 마주하게 됩니다. 저희 팀에서는 이런 문제들이 지마켓과 옥션의 고객의 구매경험에 부정적인 영향을 끼칠 수 있기에 최대한 회피하고 해결하고자 노력합니다. 따라서 이를 해결하기 위한 방법으로 Fault Tolerance 시스템을 고려하며 개발을 하고 있습니다.
Fault Tolerance
Tolérance(톨레랑스)는 사전적 의미로는 ‘용인’, ‘관용’, ‘이해’, ’허용’ 을 의미합니다. 다만 그 쓰임새는 조금 더 관념적입니다. 좁은 뜻으로는 남의 잘못이나 허물을 너그러이 용서하는 것을 뜻하고 넓게는 자신과 다른 특성을 가진 사람의 인격권과 자유를 인정하는 것이라는 뜻으로 사용됩니다. 요약하면 차이를 인정하고 통합을 지향하는 열린(Open)사고나 시스템을 말할 때 사용하는 것으로 이해하면 됩니다.
API 메시업을 하다보면 간혹 호출하는 API의 응답이 늦거나 실패하는 경우, 요청 및 응답의 인터페이스 방식이 변경되는 경우 등 호환성 및 가용성 문제가 있을 수 있습니다. 바로 이 시점에 Tolerance가 필요합니다.
API 메시업 과정에서의 Tolerance는 특정 API의 호환성이나 가용성에 제약이 발생할 수 있는 것을 인정하고 허용하는 것을 말합니다. 만약, API를 사용할 수 없거나, 호환성이 깨진 경우라도 서비스는 계속적으로 중단 없이 운영이 될 수 있도록 대응을 미리 해 두는 것이죠. 이런 관용적인 시스템을 분산시스템에서는 Fault Tolerance를 지원하는 혹은 갖춘 시스템이라고 합니다. Fault Tolerance System(장애허용시스템, 결함감내시스템)이라고 축약해서 부르기도 합니다.
Fault Tolerance 시스템은 하나 이상의 구성 요소가 실패하더라도 서비스 중단 없이 계속 동작할 수 있습니다. 이를 통해 사용자에게 지속적이고 끊김 없는 서비스를 제공하고자 노력합니다. 시스템 관점에서는 아래와 같은 구성을 고민하여 Fault Tolerance 시스템 구성을 고민해 볼 수 있습니다.
- Redundancy: 여러 복제본을 만들어 하나가 고장나도 다른 복제본이 작동하도록 하는 것입니다. 이는 하드웨어 (예: RAID 디스크 배열), 소프트웨어 (예: 클러스터링), 또는 데이터 (예: 데이터 미러링)에 적용될 수 있습니다.
- Failover(장애 전환): 프라이머리 시스템에 문제가 발생하면, 장애 전환 과정을 통해 세컨더리 시스템으로 작업을 자동으로 전환하는 것입니다.
- Error Checking and Correction (에러 체크 및 수정): 시스템이 데이터 전송 오류를 자동으로 감지하고 수정할 수 있도록 하는 것입니다.
- Load Balancing(로드 밸런싱): 시스템에 부하가 고르게 분산되도록 하여 특정 구성 요소에 과부하가 걸리는 것을 방지하는 것입니다.
시스템의 구성 뿐만 아니라, 애플리케이션 개발 단계(프로그래밍)에서도 아래와 같은 기능들을 구현하는 것을 전략적으로 고려할 수 있습니다.
- Circuit Breaker: 서킷 브레이커 패턴을 구현하여, 원격 시스템에 대한 호출이 실패하거나 응답 시간이 길어질 경우 서비스를 중단시키고 빠르게 복구할 수 있도록 돕습니다.
- Rate Limiter: 일정 시간 동안 특정 서비스에 대한 요청 수를 제한합니다. 이를 통해 과도한 트래픽으로 인한 서비스의 과부하를 방지할 수 있습니다.
- Bulkhead: 시스템의 한 부분이 실패하더라도 전체 시스템이 중단되지 않도록 격리를 제공합니다.
- Retry: 실패한 작업을 재시도하는 기능을 제공합니다. 여러 가지 재시도 전략을 설정할 수 있습니다.
- Time Limiter: 임계 시간을 초과하는 작업을 취소합니다.
- Result Caching: 데이터의 결과값을 캐싱을 통해 사전에 획득 후 활용합니다.
Fault Tolerance를 지원하는 라이브러리는 대표적으로 Resilience4j와 Hystrix 가 있습니다.
// 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<String> 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<String> 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));
CompletableFuture<String> future = Decorators.ofSupplier(supplier)
.withThreadPoolBulkhead(threadPoolBulkhead)
.withTimeLimiter(timeLimiter, scheduler)
.withCircuitBreaker(circuitBreaker)
.withFallback(asList(TimeoutException.class, CallNotPermittedException.class, BulkheadFullException.class),
throwable -> "Hello from Recovery")
.get().toCompletableFuture();
Resilience4J는 Hystrix 라이브러리와 비교하여 더 경량화되어 있고, 더 많은 커스터마이징 옵션을 제공합니다. Hystrix는 Netflix에서 개발하였지만, 현재는 Maintenance 단계로 더 이상 신규 기능이 업데이트 되지 않습니다.
외부 API 호출을 위한 Fault Tolerance 구현
OpenFeign은 HTTP client 라이브러리입니다. OpenFeign을 사용하면 HTTP client 서비스를 인터페이스로 정의할 수 있습니다. 개발자가 네트워킹 세부 사항에 신경 쓰지 않고도 서비스 간의 통신을 단순화시키는 데 큰 도움이 됩니다.
https://spring.io/projects/spring-cloud-openfeign
@SpringBootApplication
@EnableFeignClients
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
@FeignClient("name")
static interface NameService {
@RequestMapping("/")
public String getName();
}
}
OpenFeign는 Resilience4J와 함께 Spring Framework에서 사용되어 Fault Tolerance 시스템 구축에 활용됩니다. Spring Framework에서는 Spring Cloud 프로젝트에 포함되어 Resilience4J는 Spring Cloud Circuit breaker로, OpenFeign는 Spring Cloud OpenFeign으로 구성되어 운영되고 있습니다.
https://spring.io/projects/spring-cloud-circuitbreaker
https://spring.io/projects/spring-cloud-openfeign
// For Maven
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
// For Gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
이번 글에서는 Fault Tolerance에 초점을 맞추기 때문에 서킷 브레이커에 대해서 깊게 다루지 않습니다. 서킷 브레이커에 대한 자세한 내용은 아래 마틴 파울러의 블로그 글을 참조해주세요.
서킷 브레이커(Circuit breaker)는 이름에서 알 수 있듯이 전기 회로에서 오류나 과부하를 방지하기 위한 전기 설비입니다. 소프트웨어에서도 분산시스템의 Fault Tolerance 문제를 다루는 설계로서 사용됩니다.
- 닫힌 상태(Closed): 요청이 정상적으로 처리되어 정상 응답(Normal Response)을 반환합니다. 만약 이 상태에서 요청이 실패하게 된다면, 서킷 브레이커는 열린 상태로 전환됩니다.
- 열린 상태(Open): 모든 요청이 즉시 실패로 처리되며 대체 응답(Fallback Response)을 반환합니다. 일정 시간이 지나면 서킷 브레이커는 반열린 상태로 전환됩니다.
- 반열린 상태(Half-Open): 이 상태에서는 일부 요청만 허용됩니다. 이 요청이 성공하면, 서킷 브레이커는 다시 닫힌 상태로 전환되며, 실패하면 열린 상태로 유지됩니다.
우선 Fault Tolerance를 구현하는 전략 중에서 가장 이해하기 쉬운 Time Limiter를 기반으로 가볍게 살펴보겠습니다.
Time Limiter는 임계 시간이 초과한 작업을 취소와 이에 대응하는 전략을 말합니다.
resilience4j를 이용하여 OpenFeign으로 호출하는 특정 API의 임계 시간을 설정할 수 있습니다.
아래의 설정은 search-item-name.xyz/task 라는 외부 서비스 API 호출에 대한 임계치 설정(3초)예시 입니다.
// application.properties
resilience4j.timelimiter.instances.TaskClientgetTask.timeoutDuration: 3s
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "TaskClient", url = "search-item-name.xyz", fallback = TaskClientFallback.class)
public interface TaskClient {
@RequestMapping(method = RequestMethod.GET, value = "/task")
String getTask();
}
만약 API 요청 후 설정한 시간이 지난 경우, OpenFeign은 Fallback으로 설정한 객체의 메서드를 호출합니다. 아래 코드의 경우에는 Fallback response 라는 문자열을 반환하게 됩니다. 물론, 실제 구현에서는 Fallback 로직을 더 복잡하게 구현 할 수 있습니다.
package com.example.openfeginexamples.client;
import org.springframework.stereotype.Component;
@Component
public class TaskClientFallback implements TaskClient {
@Override
public String getTask() {
return "Fallback response";
}
}
더 자세한 사항은 아래 버전별 Spring-cloud-openfeign 공식 문서를 보시면 도움이 됩니다.
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker
https://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.0.0.RELEASE/single/spring-cloud-openfeign.html#spring-cloud-feign-hystrix-fallback
저희 팀에서는 서비스 가용성 향상을 위한 여러 전략을 적용하고 있습니다. 이에는 Time Limiter, Circuit Breaker, Result Caching 등의 전략이 포함되며, 이런 전략들은 OpenFeign, resilience4j, hystrix와 같은 도구들을 통해 구현되고 있습니다. 이런 방법들을 통해 API 호출이 실패하더라도 즉각적으로 대응할 수 있도록 준비하고 있습니다. 이를 통해 API 메시업 과정에서 만날 수 있는 잠재적인 실패 상황을 대비하며, 지속적으로 서비스 가용성을 유지하고 고객에게 안정적인 경험을 제공하는데 목표를 두고 있습니다.
API 호출 이후의 Fault Tolerance
변화하는 값의 문제
여러 API를 이용하여 제공받은 결과 데이터를 조합하는 과정에서 발생 할 수 있는 문제도 있습니다. 응답받은 결과 데이터의 일부 필드가 사전에 합의한 '인터페이스 표준과 다를 경우'가 가장 대표적입니다. Java에서는 DTO(Data Transfer Object) 등의 형태로 데이터 타입이 미리 지정되어 있어, 표준 자체가 변경되면 개발자가 쉽게 감지할 수 있습니다. 규격이 변경되면 API를 통해서 제공받은 데이터를 객체로 직렬화하는 과정에서 오류가 발생하기 때문입니다.
하지만 이러한 타입 관련 이슈는 앞서 살펴본 OpenFeign 등의 API Client에서 직렬화와 관련된 설정 및 기존 DTO의 수정을 통해 비교적 빠르게 해결할 수 있습니다. DTO 필드 수정의 경우, 컴파일러의 컴파일 오류와 IDE의 경고 등을 통해 부수적 효과(side-effect)를 직관적으로 파악할 수 있어 부담이 적습니다.
또한, 우리가 빈번하게 접하게 되는 문제 중 하나는 '값'의 변화입니다. 아래의 API를 통해 받아온 응답 데이터를 들어보면, JSON 결과에서 'brand' 항목이 없는 경우를 생각해볼 수 있습니다. 이 경우, 값은 null이나 ""로 표현될 수 있습니다.
심지어 상황에 따라서는 'null'이나 'Null' 같은 문자열, 혹은 'empty', 'default' 같은 사용자 정의 문자열로 나타나기도 합니다. 이런 사용자 정의 문자열은 주로 다른 개발팀과의 협력을 통해 미리 정의되며, 정의된 내용은 API 문서를 통해 확인할 수 있습니다. 따라서 이런 상황에 자주 직면하는 일은 드물지만, API를 제공하는 개발팀의 실수나 버그로 인해 예상치 못한 값이 반환되는 경우를 완전히 배제할 수는 없습니다. 뿐만 아니라, 'brand' 이외에도 'categoryCode'와 같은 미리 합의된 코드나 'discountPercentage'와 같은 수치 범위 등, 값에 따라 발생할 수 있는 예외 상황은 다양하게 존재합니다. 이런 값의 변화는 값을 직접 다루는 연산 작업과정에서 예외(Exception)상황을 야기하고 이를 동작중(Runtime)에 에러를 유발합니다. 따라서 Fault Tolerance를 지원하는 시스템에서는 사전에 예외처리를 고려한 코드작업을 준비해두어야 합니다.
{
"id": 9,
"title": "Infinix INBOOK",
"description": "Infinix Inbook X1 Ci3 10th 8GB...",
"price": 1099,
"discountPercentage": 11.83, // 만약 100.0이 넘는다면?
"rating": 4.54,
"stock": 96,
"brand": "Infinix", // 만약, brand의 값이 변경된다면?
"categoryCode": "laptops", // 만약, Code의 정의가 변경된다면?
"thumbnail": "https://i.dummyjson.com/data/products/9/thumbnail.jpg",
"images": [
"https://i.dummyjson.com/data/products/9/1.jpg",
"https://i.dummyjson.com/data/products/9/2.png",
"https://i.dummyjson.com/data/products/9/thumbnail.jpg"
]
}
값의 변경에 따른 예외 처리 방법
값의 변경에 따른 예외(Exception)처리를 하는 방법은 다양합니다.
자바에서 값의 변경에 따른 예외 처리를 하려면 몇 가지 전략을 사용할 수 있습니다.
1) Try-Catch 블록: 가장 기본적인 예외 처리 방법은 try-catch 블록을 사용하는 것입니다. 이것은 예외가 발생할 수 있는 코드를 try 블록 안에 넣고, 만약 예외가 발생하면 그것을 catch 블록에서 처리하는 방식입니다. 이 방식을 사용하면 예외가 발생해도 프로그램의 다른 부분이 계속 실행될 수 있습니다.
try {
// 예외가 발생할 수 있는 코드
} catch (Exception e) {
// 예외 처리 코드
}
2) throws 키워드: 함수나 메서드에서 예외를 처리하려면, 해당 함수나 메서드의 시그니처에 'throws' 키워드를 사용하면 됩니다. 이렇게 하면 해당 함수나 메서드를 호출하는 쪽에서 예외를 처리해야 합니다.
public void method() throws Exception {
// 예외가 발생할 수 있는 코드
}
3) 사용자 정의 예외: 자바에서는 사용자 정의 예외를 생성하여 특정 상황에서 발생할 수 있는 예외를 더 세밀하게 처리할 수 있습니다. 이를 위해서는 Exception 또는 RuntimeException 클래스를 상속받아 새로운 예외 클래스를 만들면 됩니다.
public class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
위의 방법 외에도 자바 8 이후에는 Optional 클래스를 사용하여 값이 null일 경우를 처리하는 등의 방법도 있습니다. Optional 클래스는 값의 존재 여부를 명시적으로 표현할 수 있어 NullPointerException을 방지하는 데 도움이 됩니다.
Optional<String> optional = getOptionalValue();
if (optional.isPresent()) {
String value = optional.get();
// value를 사용한 코드
} else {
// 값이 없을 때의 처리 코드
}
이러한 방법들을 적절하게 혼합하여 사용하면 값의 변경에 따른 예외 처리를 효과적으로 할 수 있습니다. 사용하는 방법은 상황에 따라 다르며, 특히 예외 처리 전략은 전체 애플리케이션의 안정성과 가독성에 중요한 영향을 미칩니다.
저희 팀에서는 기본적인 예외 처리 방법들을 사용하면서도, 코드의 가독성과 유지보수를 용이하게 하기 위한 다양한 방법을 추가적으로 활용하고 있습니다. 이 중에서도 자주 사용되며, 팀원들에게 많은 도움이 되는 방법으로는 @RestControllerAdvice와 Vavr 라이브러리가 있습니다.
@RestContollerAdvice를 이용한 일괄처리
@RestControllerAdvice는 Spring Framework에서 제공되며, 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있게 해줍니다. 이를 통해 중복 코드를 줄이고, 전체 애플리케이션에 걸쳐 일관된 예외 처리 방식을 유지할 수 있습니다
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html
예를 들어, CustomerNotFoundException이라는 예외가 있다고 가정해봅시다.
public class CustomerNotFoundException extends RuntimeException {
....
}
이 예외를 핸들링하는 @RestControllerAdvice 클래스는 다음과 같습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomerNotFoundException.class)
@ResponseStatus(HttpStatus.OK)
public TaskResponse handleCustomerNotFoundException(CustomerNotFoundException e) {
return new TaskResponse("1", "fallback-id", "fallback-name");
}
}
// Response
@AllArgsConstructor
@Data
public class TaskResponse {
private String code;
private String id;
private String title;
}
위의 예제에서는, @ExceptionHandler 어노테이션을 사용하여 CustomerNotFoundException을 처리하도록 지정했습니다. 이 예외가 발생하면 handleCustomerNotFoundException 메소드가 호출되어, 이 메소드는 TaskResponse 객체를 반환합니다.
@ResponseStatus 어노테이션은 이 예외가 발생했을 때 반환할 HTTP 상태 코드를 지정합니다. HttpStatus.NOT_FOUND 등 400번대의 에러를 표기하여 에러를 명시적으로 나타낼 수 있지만, 위의 예제 코드처럼 응답 데이터를 모의 데이터(Mocking Data)로 생성하여 대응하는 방법도 가능합니다.
이제 위의 예제 코드에 따르면, CustomerNotFoundException이 발생하면 컨트롤러와 서비스에 대한 공통적인 fallback 처리가 모의 데이터를 반환하는 구조로 이루어집니다.
@RestController
@RequiredArgsConstructor
public class GreetingController {
private final TaskService taskService;
@GetMapping("/task")
public TaskResponse task(){
return taskService.getTask();
}
}
@Service
public class TaskService {
public TaskResponse getTask() throws CustomerNotFoundException {
throw new CustomerNotFoundException();
}
}
Vavr Try를 이용한 부분처리
각 메서드 수준에서의 예외에 대한 처리가 필요한 경우도 있습니다. 일반적으로는 Try-Catch 문법을 이용하여 처리 할 수 있습니다. Try-Catch는 연속적인 데이터를 처리할 때 가독성이 간혹 떨어지는 경우가 있습니다. 저희팀에서는 이러한 문제를 Vavr 라이브러리의 Try를 통해서 조금 더 보기 좋게 처리하고 있습니다.
Vavr은 함수형 프로그래밍 패러다임을 지원하는 자바 라이브러리로 Resilience4j에서 사용하던 라이브러리입니다. 이를 사용하면 'Try'와 'Either'와 같은 클래스를 제공하여 예외 처리를 보다 우아하게 할 수 있습니다. 예를 들어, 'Try'는 예외 발생 가능성이 있는 코드 블록을 감싸고, 결과를 성공 또는 실패로 나타내는 객체를 반환합니다.
https://www.vavr.io
만약 우리가 generateThatMightThrowException 메서드를 사용하려 하는데, 이 메서드와 대안(fallback)으로 사용할 fallbackThatMightThrowException 메서드 모두 예외를 발생시키는 상황에 직면했다고 가정해 봅시다. 이럴 경우, Try-Catch 블록을 활용한다면 아래와 같습니다.
public String execute(String arg){
try {
return generateThatMightThrowException(arg);
} catch (SecurityException e){
try {
return fallbackThatMightThrowException(arg);
} catch (Exception e2) {
return "###";
}
} catch (Exception e){
return "";
}
}
위 코드를 살펴보면 복구하는 내용이 catch 블록 안에 숨겨져 있어서 보기가 어렵습니다. 이 경우 Vavr 라이브러리의 Try를 통해 보기 좋게 가독성을 개선할 수 있습니다. 아래 코드의 경우 예외를 잡아서(Catch) 처리하는 내용보다는 시도(Try) 후에 실패 시 복원(Recover)한다는 이름의 메서드를 직접 사용함으로써 가독성을 개선하고 있습니다. 더불어 예외의 각 상황은 패턴매칭을 이용하여 더욱 다양한 조건으로 이용할 수 있습니다.
public String executeInVavr(String arg){
return Try.of(() -> generateThatMightThrowException(arg))
.recover((e) -> Match(e).of(
Case($(instanceOf(SecurityException.class)), //<-- 패턴 매칭
Try.of(() -> fallbackThatMightThrowException(arg)).recover((e2) -> "###").get()),
Case($(), "")
)).get();
}
람다식을 통한 Arrow 표기를 사용하는 경우 vavr을 사용하면 더욱 일관된 흐름으로 표기할 수 있습니다. 아래의 코드의 경우 Array 방식의 흐름으로 코드를 읽다가 try, catch 형태의 Brace 표기 방식으로 구분하며 읽어야 합니다.
public List<String> execute(String ... args){
return Arrays.stream(args).map((arg) -> {
try {
return generateThatMightThrowException(arg);
} catch (SecurityException e){
try {
return fallbackThatMightThrowException(arg);
} catch (Exception e2) {
return "###";
}
} catch (Exception e){
return "";
}
}).collect(Collectors.toList());
}
그러나 Vavr을 사용한 경우에는 Arrow 표기법 그대로 흘러가듯 맥락이 이어지며 읽을 수 있게 됩니다.
public List<String> executeInVavr(String ... args){
return Arrays.stream(args).map((arg) ->
Try.of(() -> generateThatMightThrowException(arg))
.recover((e) -> Match(e).of(
Case($(instanceOf(SecurityException.class)),
Try.of(() -> fallbackThatMightThrowException(arg)).recover((e2) -> "###").get()),
Case($(), "")
)).get()
).collect(Collectors.toList());
}
Vavr 참고
vavr는 resilience4j 에서 사용되던 라이브러리입니다. 그러나 resilience4j가 다양한 프로젝트에서 널리 사용되면서, 사용자들로부터 더 가벼운 라이브러리로의 개선을 요청받게 되었습니다. 이러한 사용자의 요구에 따라, resilience4j는 2.0.0 버전 이후로 Vavr를 사용하지 않는 새로운 구조로 변경되었습니다. resilience4j 프로젝트의 주요 개발자 Robert Winkler는 Vavr의 사용을 선호한다고 밝히며 짤막하게 변경 이유를 남겼습니다.
Reference
https://en.wikipedia.org/wiki/Fault_tolerance
https://homepage.divms.uiowa.edu/~ghosh/16618.html
https://en.wikipedia.org/wiki/Mashup_(web_application_hybrid)
'Backend' 카테고리의 다른 글
오픈마켓에서 여행 플랫폼으로 살아남기 (feat. msa) (0) | 2023.10.11 |
---|---|
G마켓 쿠폰적용가 도입기 - 캐시를 중심으로 (0) | 2023.08.02 |
일관적인 에러응답을 달라! (1) | 2023.06.21 |
코루틴(Coroutine)에 대하여 (1) | 2023.06.14 |
Apache Commons IO 취약점 파헤치기 (0) | 2023.06.07 |