오류 처리 - AWS Flow Framework 자바용

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

오류 처리

Java의 try/catch/finally 구성체는 오류 처리를 간소화하며 유비쿼터스 방식으로 사용됩니다. 이를 통해 사용자는 오류 처리기를 코드 블록에 연결할 수 있습니다. 내부적으로 이 작업은 호출 스택에서 오류 처리기에 관한 추가 메타데이터를 스터핑하는 방식으로 수행됩니다. 예외가 발생하면 실행 시간에서는 호출 스택에서 연결된 오류 처리기를 검색하여 호출하고, 적절한 오류 처리기를 찾을 수 없으면 예외를 호출 체인 상위로 전파합니다.

이 작업은 동기식 코드에서는 잘 수행되지만, 비동기 및 분산 프로그램에서 오류를 처리하면 추가적인 문제가 발생합니다. 비동기식 호출에서는 값을 즉시 반환하므로 비동기식 호출이 실행될 때 호출자는 호출 스택에 있지 않습니다. 이는 비동기식 코드의 미처리 예외는 호출자가 통상적인 방식으로는 처리할 수 없음을 뜻합니다. 일반적으로 비동기식 코드에서 시작되는 예외는 오류 상태를 비동기식 메서드로 전달되는 콜백으로 전달하여 처리합니다. 또는 Future<?>가 사용되고 있다면 사용자가 이 코드에 액세스하려 할 때 오류를 보고합니다. 이것은 바람직한 상태는 아닌데, 그 이유는 예외를 수신하는 코드(Future<?>를 사용하는 콜백 또는 코드)에는 원본 호출의 컨텍스트가 없고 예외를 적당히 처리하지 못할 수 있기 때문입니다. 뿐만 아니라 분산 비동기 시스템에서는 동시에 실행되는 구성 요소에서 두 개 이상의 오류가 동시에 발생할 수 있습니다. 이러한 오류의 유형과 심각도는 여러 가지일 수 있으므로 그에 맞게 처리해야 합니다.

비동기식 호출 후에 리소스를 정리하는 것 역시 까다롭습니다. 동기식 코드와 달리 사용자는 리소스를 정리하기 위해 호출 코드에서 try/catch/finally를 사용할 수는 없는데, 그 이유는 try 블록에서 시작된 작업이 finally 블록이 실행될 때에도 여전히 지속되고 있을 수 있기 때문입니다.

프레임워크에서는 분산 비동기식 코드에서 오류를 처리하는 방식을 Java의 try/catch/finally와 유사하게 만드는, 그리고 그와 거의 같은 수준으로 간소화하는 메커니즘을 제공합니다.

ImageProcessingActivitiesClient activitiesClient = new ImageProcessingActivitiesClientImpl(); public void createThumbnail(final String webPageUrl) { new TryCatchFinally() { @Override protected void doTry() throws Throwable { List<String> images = getImageUrls(webPageUrl); for (String image: images) { Promise<String> localImage = activitiesClient.downloadImage(image); Promise<String> thumbnailFile = activitiesClient.createThumbnail(localImage); activitiesClient.uploadImage(thumbnailFile); } } @Override protected void doCatch(Throwable e) throws Throwable { // Handle exception and rethrow failures LoggingActivitiesClient logClient = new LoggingActivitiesClientImpl(); logClient.reportError(e); throw new RuntimeException("Failed to process images", e); } @Override protected void doFinally() throws Throwable { activitiesClient.cleanUp(); } }; }

TryCatchFinally 클래스와 그 변형인 TryFinallyTryCatch는 Java의 try/catch/finally와 유사한 방식으로 작동합니다. 사용자를 이를 사용해 오류 처리기를 비동기식 및 원격 작업으로 실행될 수 있는 워크플로 코드 블록에 연결할 수 있습니다. doTry() 메서드는 논리적으로 try 블록과 같습니다. 프레임워크에서는 코드를 doTry()에서 자동으로 실행합니다. Promise 객체 목록은 TryCatchFinally의 생성자로 전달될 수 있습니다. doTry 메서드는 생성자로 전달된 모든 Promise 객체가 준비 상태가 될 때까지 실행됩니다. doTry() 내에서 비동기식으로 호출된 코드에서 예외를 제기하면 doTry()의 모든 대기 중 작업이 취소되고 예외 처리를 위해 doCatch()가 호출됩니다. 예를 들어 위 목록에서 downloadImage에 예외가 발생하면 createThumbnailuploadImage는 취소됩니다. 끝으로 모든 비동기식 작업이 완료되면(완료, 실패 또는 취소) doFinally()가 호출됩니다. 이 메서드는 리소스 정리에 사용할 수 있습니다. 또한 사용자는 필요에 맞게 이 클래스를 중첩할 수 있습니다.

doCatch()에서 예외가 보고되면 프레임워크에서는 비동기 및 원격 호출을 포함하는 완결된 논리 호출 스택을 제공합니다. 이 스택은 디버깅할 때 유용하며, 특히 사용자에게 다른 비동기식 메서드를 호출하는 비동기식 메서드가 있는 경우에 유용합니다. 예를 들어 downloadImage에서 발생한 예외에서는 다음과 같은 예외를 생성합니다.

RuntimeException: error downloading image at downloadImage(Main.java:35) at ---continuation---.(repeated:1) at errorHandlingAsync$1.doTry(Main.java:24) at ---continuation---.(repeated:1) …

TryCatchFinally 의미론

AWS Flow Framework for Java 프로그램 실행은 동시 실행 브랜치의 트리로 시각화할 수 있습니다. 비동기식 메서드, 활동 및 TryCatchFinally 자체를 호출하여 이 실행 트리에 새 브랜치를 생성할 수 있습니다. 예를 들어 이미지 처리 워크플로는 다음 그림의 트리와 같은 모양일 수 있습니다.

비동기식 실행 트리

실행의 브랜치 하나에서 오류가 발생하면 해당 브랜치가 해제되는데, 이는 예외로 인해 Java 프로그램에서 호출 스택이 해제되는 것과 마찬가지입니다. 해제는 오류가 처리되거나 트리의 루트에 도달할 때까지(이때 워크플로 실행이 종료됨) 실행 브랜치 위쪽으로 계속해서 이동합니다.

프레임워크에서는 발생하는 오류를 보고함과 동시에 작업을 예외로 처리합니다. 또한 TryCatchFinally에 정의된 예외 처리기(doCatch() 메서드)를 상응하는 doTry()에서 코드가 생성한 모든 작업에 연결합니다. 시간 초과나 처리되지 않은 예외로 인해 작업이 실패하면 적절한 예외가 발생하고 해당 doCatch()가 간접적으로 호출되어 이를 처리합니다. 이 작업을 완료하기 위해 프레임워크에서는 Amazon SWF와 협력하여 원격 오류를 전파하고 이 오류를 호출자의 컨텍스트에서 예외로 다시 생성합니다.

취소

동기식 코드에서 예외가 발생하면 컨트롤에서는 try 블록에 남아 있는 모든 코드를 무시하고 직접 catch 블록으로 건너뜁니다. 예:

try { a(); b(); c(); } catch (Exception e) { e.printStackTrace(); }

이 코드에서 b()에 예외가 발생하면 c()는 호출되지 않습니다. 이를 워크플로에 비유하면 다음과 같습니다.

new TryCatch() { @Override protected void doTry() throws Throwable { activityA(); activityB(); activityC(); } @Override protected void doCatch(Throwable e) throws Throwable { e.printStackTrace(); } };

이 경우 activityA, activityBactivityC를 호출하면 모두 성공적으로 값을 반환하고, 그 결과 비동기식으로 실행될 세 가지 작업이 생성됩니다. 나중에 activityB의 작업에 오류가 발생한다고 가정하면 Amazon SWF에서는 이 오류를 내역에 기록합니다. 이를 처리하기 위해 프레임워크에서는 먼저 동일한 doTry()의 범위 내에서 시작된 다른 모든 작업(이 경우에는 activityAactivityC)을 취소하려고 할 것입니다. 그러한 작업이 모두 완료되면(취소, 실패 또는 성공적으로 완료) 적절한 doCatch() 메서드가 호출되어 오류를 처리합니다.

c()가 실행되지 않은 동기식 예시와 달리 activityC가 호출되었고 작업이 실행 예약되었으므로 프레임워크에서는 이를 취소하려고 하겠지만, 취소된다는 보장은 없습니다. 취소를 보장할 수 없는 이유는 활동이 이미 완료되었을 수 있거나 취소 요청을 무시할 수 있거나 오류로 인해 실패할 수 있기 때문입니다. 하지만 프레임워크에서는 상응하는 doTry()에서 시작된 모든 작업이 완료된 후에만 doCatch()가 호출되도록 보장합니다. 또한 doTry()doCatch()에서 시작된 모든 작업이 완료된 후에만 doFinally()가 호출되도록 보장합니다. 예를 들어 위 예시의 활동이 서로에게 의존한다면, 즉 activityB에서 activityA를 의존하고 activityC에서 activityB를 의존한다면 다음과 같이 activityCactivityB가 완료될 때까지 Amazon SWF에 예약되어 있지 않기 때문에 즉시 취소됩니다.

new TryCatch() { @Override protected void doTry() throws Throwable { Promise<Void> a = activityA(); Promise<Void> b = activityB(a); activityC(b); } @Override protected void doCatch(Throwable e) throws Throwable { e.printStackTrace(); } };

활동 하트비트

AWS Flow Framework for Java의 협력적 취소 메커니즘을 통해 진행 중인 활동 작업이 정상적으로 취소될 수 있습니다. 취소가 트리거되면 차단되거나 작업자에게 할당되기를 기다리던 작업은 자동으로 취소됩니다. 그러나 작업이 이미 작업자에게 할당된 경우 프레임워크에서는 활동에게 취소를 요청합니다. 활동 구현에서는 그러한 취소 요청을 명시적으로 처리해야 합니다. 이렇게 하려면 활동의 하트비트를 보고하면 됩니다.

하트비트 보고를 통해 활동 구현에서는 진행 중인 활동 작업의 진행 상황을 보고할 수 있습니다. 이는 모니터링에 유용하며 이를 통해 활동에서는 취소 요청을 확인할 수 있습니다. 취소 요청이 이루어지면 recordActivityHeartbeat 메서드에서는 CancellationException 예외가 발생합니다. 활동 구현에서는 이 예외를 포착하고 취소 요청에 따라 작업을 수행하거나 예외를 삼켜 요청을 무시할 수 있습니다. 취소 요청을 준수하기 위해서는 활동에서 원하는 대로 정리 작업을 수행한 후(해당되는 경우) CancellationException 예외를 다시 제기합니다. 이 예외가 활동 구현에서 발생되었다면 프레임워크에서는 이 활동 작업이 취소됨 상태로 완료되었다고 기록합니다.

다음 예시에서는 이미지를 다운로드하고 처리하는 활동을 보여줍니다. 이 활동은 각 이미지를 처리한 후 하트비트하고 취소가 요청되면 정리한 후 예외를 다시 제기하여 취소를 인정합니다.

@Override public void processImages(List<String> urls) { int imageCounter = 0; for (String url: urls) { imageCounter++; Image image = download(url); process(image); try { ActivityExecutionContext context = contextProvider.getActivityExecutionContext(); context.recordActivityHeartbeat(Integer.toString(imageCounter)); } catch(CancellationException ex) { cleanDownloadFolder(); throw ex; } } }

보고 활동 하트비트는 필요하지 않지만, 해당 활동이 장시간 실행되거나 오류 조건 하에서는 취소되길 원하는 고비용 작업을 수행할 수 있는 경우에는 사용하는 것이 좋습니다. 사용자는 활동 구현에서 heartbeatActivityTask를 주기적으로 호출해야 합니다.

활동에서 제한 시간을 초과하면 ActivityTaskTimedOutException 예외가 발생하고 예외 객체의 getDetails에서는 상응하는 활동 작업을 위해 heartbeatActivityTask에 대한 마지막 성공적 호출로 전달된 데이터를 반환합니다. 워크플로 구현에서는 이 정보를 사용하여 제한 시간을 초과하기 전까지 활동 작업이 얼마나 진행되었는지 확인할 수 있습니다.

참고

Amazon SWF에서는 하트비트 요청을 제한할 수 있으므로 너무 자주 하트비트하는 것은 좋지 않습니다. Amazon SWF에서 설정한 제한에 대해서는 Amazon Simple Workflow Service 개발자 안내서를 참조하십시오.

명시적으로 작업 취소

오류 조건 외에도 사용자가 명시적으로 작업을 취소할 수 있는 경우가 있습니다. 예를 들어 신용 카드 결제를 처리하는 활동은 사용자가 주문을 취소한 경우 취소되어야 합니다. 프레임워크를 통해 사용자는 TryCatchFinally 범위에서 생성된 작업을 명시적으로 취소할 수 있습니다. 다음 예시에서는 결제가 처리되고 있는 도중에 신호가 수신되면 결제 작업이 취소됩니다.

public class OrderProcessorImpl implements OrderProcessor { private PaymentProcessorClientFactory factory = new PaymentProcessorClientFactoryImpl(); boolean processingPayment = false; private TryCatchFinally paymentTask = null; @Override public void processOrder(int orderId, final float amount) { paymentTask = new TryCatchFinally() { @Override protected void doTry() throws Throwable { processingPayment = true; PaymentProcessorClient paymentClient = factory.getClient(); paymentClient.processPayment(amount); } @Override protected void doCatch(Throwable e) throws Throwable { if (e instanceof CancellationException) { paymentClient.log("Payment canceled."); } else { throw e; } } @Override protected void doFinally() throws Throwable { processingPayment = false; } }; } @Override public void cancelPayment() { if (processingPayment) { paymentTask.cancel(null); } } }

취소된 작업에 대한 알림 수신

작업이 취소됨 상태로 완료되면 프레임워크에서는 CancellationException 예외를 제기하여 워크플로 로직에 이를 알립니다. 활동이 취소됨 상태로 완료되면 내역에 레코드가 생성되고 프레임워크에서는 CancellationException으로 적절한 doCatch()를 호출합니다. 앞의 예시와 같이 결제 처리 작업이 취소되면 워크플로에서는 CancellationException을 수신합니다.

미처리 CancellationException은 기타 예외와 마찬가지로 상위 실행 브랜치로 전파됩니다. 그러나 doCatch() 메서드에서는 해당 범위에 다른 예외가 없는 경우에만 CancellationException을 수신하고, 다른 예외는 취소보다 높은 우선 순위를 부여받습니다.

중첩 TryCatchFinally

사용자는 필요에 따라 TryCatchFinally를 중첩할 수 있습니다. 각 TryCatchFinally에서는 실행 트리에 새 브랜치를 생성하므로 사용자는 중첩된 범위를 생성할 수 있습니다. 상위 범위에 예외가 발생하면 그 안에 있는 중첩 TryCatchFinally에서 시작된 모든 작업에 대해 취소 시도가 이루어집니다. 그러나 중첩 TryCatchFinally에서 발생한 예외는 상위로 자동 전파되지는 않습니다. 예외를 중첩 TryCatchFinally에서 이를 포함하는 TryCatchFinally로 전파하고 싶다면 이 예외를 doCatch()에서 다시 제기해야 합니다. 바꿔 말하면 Java의 try/catch와 마찬가지로 미처리 예외만 표시됩니다. 취소 메서드를 호출하여 중첩 TryCatchFinally를 취소하면 중첩 TryCatchFinally가 취소되지만 이를 포함하는 TryCatchFinally는 자동으로 취소되지 않습니다.

중첩 TryCatchFinally
new TryCatch() { @Override protected void doTry() throws Throwable { activityA(); new TryCatch() { @Override protected void doTry() throws Throwable { activityB(); } @Override protected void doCatch(Throwable e) throws Throwable { reportError(e); } }; activityC(); } @Override protected void doCatch(Throwable e) throws Throwable { reportError(e); } };