Loading the catalog…
Loading the catalog…
이전 글 에서는 Consumer가 필요로 하는 데이터와 그 시점을 기준으로 이벤트에 무엇을 담을지 살펴봤다. 이번에는 메시지를 보낸 뒤, 처리 결과를 사용자에게 어떻게 알려줄지 생각해보려고 한다. 관리자가 대용량 엑셀 파일 생성을 요청하는 기능을 만든다고 해보자. 파일을 만드는 데 수십 초가 걸린다. HTTP 요청을 계속 붙잡아두기 부담스러워 Kafka로 작업을 넘기고, API는 먼저 응답하도록 바꿨다. 응답 시간은 짧아졌다. 그런데 화면에 완료되었습니다 라고 표시해도 될까? Consumer가 아직 메시지를 읽지 않았을 수도 있다. 파일을 만드는 중일 수도 있고, 처리에 실패해 재시도하고 있을 수도 있다. 요청을 접수한 것과 파일을 다 만든 것은 다르다. Kafka가 메시지를 받아들였다는 확인도 Consumer의 업무 완료를 뜻하지 않는다. 사용자가 작업 상태와 결과를 확인할 방법이 필요하다. 1. 토스뱅크: 무엇이 끝나야 성공이라고 할 수 있을까? 토스뱅크의 「은행 최초 코어뱅킹 MSA 전환기 (feat. 지금 이자 받기)」 에는 지금 이자 받기 의 일부 처리를 Kafka로 분리한 사례가 나온다. 기존에는 한 번의 이자 지급을 위해 여러 테이블에 많은 DB 쓰기가 발생했다. 토스뱅크는 이 중 같은 트랜잭션에서 처리하지 않아도 되는 작업을 분리했다. 기준은 고객의 잔액과 통장 데이터에 DB 쓰기 지연이 어떤 영향을 주는지였다. 반드시 함께 처리해야 하는 데이터는 유지하고, 즉시 반영할 필요가 없는 세금 관련 DB 쓰기는 Kafka를 통해 비동기로 처리했다. 이 사례를 사용자 응답의 관점에서 보면, 비동기로 넘길 작업을 고르기 전에 어디까지 처리돼야 완료라고 안내할 수 있는지 부터 정해야 한다. 주문이 생성되면 완료로 안내하는 서비스라면, 안내 메일이 늦어져도 주문은 완료된 상태다. 반면 엑셀 생성 요청을 접수한 시점에는 아직 다운로드할 파일이 없다. 엑셀 기능에서는 우선 생성 요청을 접수했습니다 라고 안내하고, 파일이 실제로 준비됐는지는 이후에 확인할 수 있어야 한다. 2. Microsoft: 비동기 작업은 상태를 조회하게 한다 Microsoft의 「Asynchronous Request-Reply Pattern」 은 오래 걸리는 작업을 HTTP 요청 하나에서 끝까지 기다리지 않는 구조를 설명한다. 클라이언트가 작업을 요청하면 API는 접수 사실과 상태 조회 URL을 반환한다. 백그라운드에서 작업이 진행되는 동안 클라이언트는 별도의 HTTP 요청으로 현재 상태를 확인한다. 이때 최초 응답으로 202 Accepted 를 사용할 수 있다. HTTP 명세 에서 202 는 요청이 처리를 위해 받아들여졌지만, 아직 처리가 완료되지는 않았다는 의미다. 작업 ID로 결과를 조회한다 엑셀 생성 요청을 POST /export-jobs 로 받는다고 해보자. API는 작업 ID와 상태 조회 위치를 반환한다. HTTP/1.1 202 Accepted Location: /export-jobs/job-1001 Retry-After: 5 Content-Type: application/json { "jobId": "job-1001", "status": "PENDING" } Location 은 상태 조회 URL이고, Retry-After: 5 는 다음 조회까지 5초 정도 기다리라는 안내다. 클라이언트는 이 값을 참고해 폴링 간격을 정할 수 있다. 이후 클라이언트가 GET /export-jobs/job-1001 을 호출하면 작업의 현재 상태를 반환한다. 아직 파일을 만드는 중이라면 다음처럼 응답할 수 있다. { "jobId": "job-1001", "status": "PROCESSING" } 파일 생성이 끝나면 다운로드 위치를 제공한다. HTTP/1.1 200 OK Content-Type: application/json { "jobId": "job-1001", "status": "SUCCEEDED", "downloadUrl": "/export-jobs/job-1001/file" } 실패한 작업도 같은 상태 API에서 조회할 수 있다. HTTP/1.1 200 OK Content-Type: application/json { "jobId": "job-1001", "status": "FAILED", "error": { "code": "EXPORT_GENERATION_FAILED", "message": "파일을 생성하지 못했습니다." } } 여기서 200 OK 는 작업 상태를 정상적으로 조회했다 는 의미다. 파일 생성의 성공 여부는 본문의 status 로 구분한다. 작업이 존재하지 않거나 조회 자체에 오류가 발생했다면 그에 맞는 HTTP 오류 상태를 반환한다. Microsoft 문서에는 완료 후 303 See Other 로 결과 위치를 알려주거나, 처리 오류에 맞는 4xx 를 반환하는 방식도 나온다. 이 글에서는 성공과 실패를 같은 형식으로 조회하도록 200 OK 응답의 본문에 작업 상태를 담는다. 클라이언트는 SUCCEEDED 나 FAILED 를 확인하면 폴링을 멈추고, 다운로드 버튼이나 실패 사유를 보여주면 된다. 접수했다고 응답한 작업은 남아 있어야 한다 API가 접수 응답을 보낸 직후 서버가 내려갔다고 해보자. 요청 정보가 메모리에만 있었다면 사용자는 기다리고 있는데 실제로는 아무 일도 일어나지 않을 수 있다. 작업을 DB에 저장하는 것만으로도 해결되지 않는 문제가 있다. Job 저장에는 성공했지만 Kafka에 메시지를 보내기 전에 서버가 내려가면, 작업은 남아 있어도 Consumer가 실행할 계기가 없어진다. 앞서 다룬 Transactional Outbox를 여기에 사용할 수 있다. AWS의 Outbox 설명 처럼 업무 데이터와 전달할 이벤트를 같은 DB 트랜잭션에 저장하는 방식이다. 엑셀 기능에서는 작업을 나타내는 Job 레코드와 Outbox 이벤트를 함께 저장한다. Job과 Outbox의 DB 커밋이 성공하면 API는 202 Accepted 를 반환하고, Relay는 커밋된 이벤트를 읽어 Kafka에 전달한다. 응답 반환과 Relay의 전달은 커밋 이후 각각 진행된다. Job에는 상태뿐 아니라 요청자, 조회 기간, 필터 등 파일 생성에 필요한 조건도 저장한다. 이 예시에서는 API와 Consumer가 같은 Job 저장소를 사용하므로, Consumer는 메시지의 jobId 로 해당 작업의 생성 조건을 읽을 수 있다. 앞선 글의 기준을 여기에 적용하면 ID만 보내도 필요한 정보를 어디서 가져올 수 있는지 가 분명해야 한다. 다른 서비스의 API를 호출해야 한다면 그 API에 대한 의존성이 생긴다. 또한 조회 조건을 저장하는 것만으로 요청 당시의 원본 데이터까지 보존되지는 않는다. 특정 시점의 데이터로 파일을 만들어야 한다면 그 데이터를 조회하거나 보존하는 방법도 정해야 한다. Consumer는 작업을 시작할 때 상태를 PROCESSING 으로 바꾸고, 파일 저장이 끝나 다운로드할 수 있게 되면 파일 위치와 함께 SUCCEEDED 를 기록한다. 상태 조회 API는 Job 저장소에서 현재 상태를 읽는다. 사용자가 결과를 조회할 때마다 Kafka Consumer에 응답을 요청할 필요는 없다. Outbox를 사용해도 메시지가 중복 전달될 수는 있다. 같은 작업 메시지를 다시 받은 Consumer가 어떻게 동작할지는 별도로 정해야 한다. 3. Shopify: Kafka의 데이터를 화면까지 전달한다 상태 조회 API가 있다면 클라이언트는 일정 간격으로 API를 호출해 완료 여부를 확인할 수 있다. 상태가 자주 변하거나 결과를 빠르게 화면에 보여줘야 한다면 서버에서 변경 사실을 전달하는 방법도 있다. Shopify의 「Using Server Sent Events to Simplify Real-time Streaming at Scale」 은 블랙프라이데이·사이버먼데이 기간의 실시간 판매 현황 지도를 다룬다. 기존에는 여러 컴포넌트와 폴링을 거쳐 데이터가 화면에 도달했다. Shopify는 이를 개선하면서 Kafka 토픽을 구독하는 SSE 서버를 두고, 새로운 데이터를 연결된 클라이언트로 전달하도록 구성했다. 이 화면은 서버가 브라우저로 새 데이터를 보내면 됐기 때문에, 단방향 통신인 SSE가 요구에 맞았다. 앞의 엑셀 기능에도 응용할 수 있다. Consumer가 파일을 만들고 Job의 SUCCEEDED 상태를 커밋한 뒤 완료 알림을 SSE로 전달하도록 구현하는 식이다. 브라우저는 알림에 담긴 jobId 로 상태 API를 조회해 다운로드 위치를 확인할 수 있다. 엑셀 파일 하나를 요청하고 결과를 확인하는 기능이라면 우선 폴링으로 시작해도 된다. 페이지를 열어둔 사용자에게 상태 변화를 빠르게 알려줘야 할 때 SSE를 검토할 수 있다. 방식 결과를 확인하는 방법 고려할 점 폴링 클라이언트가 상태 API를 주기적으로 호출 조회 간격, 중단 조건, 요청량 SSE 서버가 연결된 브라우저에 상태 변화를 전달 연결 관리, 재연결 후 누락 보완 웹훅 요청한 시스템의 Callback URL로 결과 전달 인증, 재시도, 중복 수신 결과를 확인하는 주체가 브라우저인지 다른 시스템인지, 얼마나 빨리 알려야 하는지에 따라 전달 방식을 고르면 된다. 알림을 놓쳐도 결과는 확인할 수 있어야 한다 사용자가 브라우저를 닫은 사이에 파일 생성이 끝날 수도 있다. Job 상태는 SUCCEEDED 로 바뀌었지만, SSE 연결이 끊어져 완료 알림은 받지 못한 상황이다. 화면을 다시 열거나 연결이 복구됐을 때 상태 API를 조회하면, 알림을 놓친 사용자도 준비된 파일을 받을 수 있다. Job 상태를 저장한 뒤 알림을 보내기 전에 Consumer가 내려갈 수도 있다. 별도 SSE 서버와 브라우저의 연결은 유지돼도 완료 알림이 전달되지 않는 상황이다. 따라서 화면을 다시 열거나 SSE에 재연결했을 때뿐 아니라, 작업이 끝나지 않은 상태로 일정 시간 알림이 없을 때도 상태 API를 조회하도록 한다. SSE는 상태 변화를 빠르게 알려주고, 상태 API는 저장된 결과를 확인하는 역할 을 맡는다. 4. 처리 중 으로 영원히 남는 작업도 관리해야 한다 메시지가 Kafka에 쌓인 채 처리되지 않거나, Consumer가 파일을 만드는 도중 내려가면 어떻게 될까? 작업 상태를 기록하는 것과 멈춘 작업을 찾아 복구하는 것은 별개의 일이다. 엑셀 작업에는 다음 정도의 상태를 둘 수 있다. 상태 의미 PENDING 요청이 저장되어 실행을 기다림. 이 예시에서는 재시도 대기도 포함 PROCESSING 작업을 시작했으며 아직 완료 결과가 기록되지 않음 SUCCEEDED 결과 파일이 준비되어 조회·다운로드 가능 FAILED 정해진 재시도나 복구 정책에 따라 실패로 확정 PENDING 이 오래 유지된다면 여러 지점을 확인해야 한다. Outbox에서 Kafka로 이벤트가 전달되지 않았을 수도 있고, Consumer가 메시지를 읽지 못하고 있을 수도 있다. 재시도를 기다리는 작업이라면 아직 다음 시도 시간이 오지 않았을 수도 있다. 이 예시에서는 재시도 대기도 PENDING 에 포함하므로, 재시도를 예약하는 로직에서 Job 상태도 함께 갱신해야 한다. PROCESSING 은 작업을 시작했다는 기록이다. Consumer가 지금도 실행 중이라는 뜻은 아니다. 작업 도중 서버가 내려갔다면 저장소에는 계속 PROCESSING 으로 남을 수 있다. requestedAt , startedAt , updatedAt , attempt 를 함께 남겨두면 어디서 얼마나 지연됐고 몇 번째 실행인지 확인하기 쉽다. 처리 시간이 긴 작업이라면 Heartbeat나 작업 임대 시간(lease)으로 실행이 멈췄는지 판단할 수도 있다. 다만 일정 시간이 지났다는 이유만으로 곧바로 실패 처리하고 다시 실행하기는 어렵다. 파일은 이미 저장됐는데 SUCCEEDED 상태를 기록하는 순간에만 장애가 발생했을 수도 있기 때문이다. 따라서 남아 있는 결과 파일을 확인해 상태를 보정하거나, 다시 실행해도 문제가 없도록 중복 처리를 제어한 뒤 재시도해야 한다. DLT에 들어갔다고 작업이 자동으로 실패하는 것도 아니다 여러 번의 재시도에도 처리하지 못한 메시지를 DLT로 옮겼다고 해보자. 메시지는 DLT에 있는데 Job 테이블에는 여전히 PROCESSING 이라고 남아 있을 수 있다. DLT 이동과 Job 상태 변경은 별개의 처리다. 실패를 확정하는 DLT 처리기나 복구 담당 서비스가 jobId 로 작업을 찾아 상태와 실패 사유를 기록해야 한다. Spring Kafka에서는 별도의 DLT 처리 메서드 를 지정하고, 여기에 Job 상태를 변경하는 로직을 구현할 수 있다. 운영자가 원인을 수정한 뒤 재처리하는 정책이라면 FAILED 로 바로 확정하기보다 RECOVERY_PENDING 같은 상태를 추가할 수도 있다. 사용자에게 표시할 상태는 서비스의 복구 정책에 맞게 정해야 한다. 이전 시도의 결과가 현재 상태를 덮어쓰면 안 된다 첫 번째 실행이 멈춘 것으로 판단해 두 번째 실행을 시작했고, 두 번째 실행이 성공했다고 해보자. 그런데 뒤늦게 첫 번째 실행의 실패 결과가 기록되려 한다. 조건 없이 상태를 갱신하면 이미 SUCCEEDED 인 작업이 다시 FAILED 가 될 수 있다. 이를 막으려면 현재 실행 시도가 일치하고, 허용된 상태 전이일 때만 상태를 변경해야 한다. 상태를 먼저 조회한 뒤 나중에 갱신하면 그 사이 다른 실행이 상태를 바꿀 수 있으므로, 조건 확인과 갱신도 원자적으로 처리해야 한다. 예를 들어 jobId , attempt , 현재 상태를 UPDATE 의 조건에 함께 넣을 수 있다. 첫 번째 시도의 결과를 기록하려는데 저장된 attempt 가 이미 두 번째 시도라면 갱신되지 않도록 하는 방식이다. 여기서 attempt 는 애플리케이션이 관리하는 작업 실행 차수다. 새 실행을 배정할 때 원자적으로 증가시키고, 배정된 실행은 그 값을 가지고 결과를 기록한다. 이 조건으로 오래된 실행이 Job 상태를 덮어쓰는 것은 막을 수 있다. 파일 업로드처럼 DB 밖에서 발생하는 작업은 별도로 중복 실행을 고려해야 한다. 5. 같은 요청이 두 번 들어올 수도 있다 사용자가 엑셀 생성 버튼을 눌렀다. 서버에서는 Job과 Outbox 이벤트를 정상적으로 저장했지만, 응답이 전달되기 직전에 네트워크 연결이 끊겼다고 해보자. 사용자는 요청이 실패했다고 생각하고 버튼을 다시 누를 수 있다. 서버가 이를 새로운 요청으로 처리하면 같은 파일을 만드는 Job이 두 개 생긴다. 중복 생성이 문제가 되는 기능이라면 요청에 멱등 키를 사용할 수 있다. Idempotency-Key: export-request-abc123 같은 키로 같은 요청이 다시 들어오면 새 작업을 만들지 않고 기존 jobId 를 반환한다. Microsoft 문서에서도 응답 유실로 POST가 재시도될 때 중복 작업을 막는 방법으로 멱등 키를 소개한다. 이때 클라이언트는 같은 논리적 요청을 재시도할 때 기존 키를 재사용해야 한다. 버튼을 누를 때마다 새 키를 만들면 이 상황의 중복을 막을 수 없다. 사용자가 별도의 파일 생성을 의도적으로 요청할 때는 새 키를 사용한다. 같은 키로 다른 요청이 들어왔을 때의 처리도 정해야 한다. 9월 데이터를 요청할 때 쓴 키로 10월 데이터를 요청했는데 기존 작업을 반환하면 엉뚱한 파일을 안내하게 된다. 이 예시에서는 키와 함께 요청 내용을 보관하고, 같은 키로 다른 생성 조건이 들어오면 오류를 반환하도록 한다. 같은 키의 요청이 동시에 들어올 수도 있다. 요청자별로 키의 유효 범위를 정하고 DB 유일성 제약을 두는 등, 키 확인과 Job 생성을 원자적으로 처리해야 한다. HTTP 요청을 중복 없이 접수했더라도 Kafka 메시지는 다시 전달될 수 있다. 요청 접수의 멱등성과 Consumer 처리의 멱등성은 각각 고려해야 한다. 운영에서는 API 응답 시간만 보면 안 된다 오래 걸리는 작업을 Kafka로 넘기면 API 응답 시간은 짧아질 수 있다. 사용자가 파일을 받는 시간도 짧아졌는지는 따로 확인해야 한다. API가 100ms 안에 응답해도, 실행 대기에 30초, 파일 생성에 40초가 걸린다면 사용자는 약 70초를 기다린다. 폴링 간격에 따라 화면에 완료가 표시되는 시점은 더 늦어질 수도 있다. 이런 비동기 작업은 API 응답 시간 외에도 다음 지표를 함께 봐야 한다. 접수부터 작업 시작까지의 대기 시간 접수부터 결과가 준비될 때까지의 전체 소요 시간 PENDING 이나 PROCESSING 상태에 오래 머무는 작업 수 작업 실패율과 재시도 횟수 Kafka Consumer Lag도 중요한 지표지만, 특정 사용자의 파일이 준비됐는지는 Job 상태와 결과를 통해 확인해야 한다. 사용자가 기다리는 것은 파일이다 엑셀 생성 버튼을 누른 사용자는 Kafka에 메시지가 잘 들어갔는지 궁금해하지 않는다. 지금 기다리면 되는지, 파일이 준비됐는지, 실패했다면 다시 요청해야 하는지 알고 싶다. 접수한 작업을 저장하고, Consumer가 처리 결과를 기록하고, 그 결과를 조회할 수 있게 만드는 이유다. 알림을 놓치거나 화면을 닫았다가 돌아와도 결과를 확인할 수 있어야 한다. API가 응답한 뒤에도 사용자의 기다림은 계속된다. 작업을 비동기로 넘겼다면 그 기다림이 어떻게 끝나는지까지 설계해야 한다. 다음 글에서는 요청은 계속 들어오는데 Consumer가 따라가지 못하는 상황을 살펴보려고 한다. Consumer를 늘리면 메시지 적체도 해결될까? 참고 자료 토스뱅크, 은행 최초 코어뱅킹 MSA 전환기 (feat. 지금 이자 받기) Microsoft, Asynchronous Request-Reply Pattern Shopify, Using Server Sent Events to Simplify Real-time Streaming at Scale IETF, RFC 9110 — HTTP Semantics AWS, Transactional outbox pattern Spring for Apache Kafka, DLT Strategies
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
How Kafka 7 - 응답은 끝났는데, 작업은 아직 끝나지 않았다. 이전 글 에서는 Consumer가 필요로 하는 데이터와 그 시점을 기준으로 이벤트에 무엇을 담을지 살펴봤다. 이번에는 메시지를 보낸 뒤, 처리 결과를 사용자에게 어떻게 알려줄지 생각해보려고 한다. 관리자가 대용량 엑셀 파일 생성을 요청하는 기능을 만든다고 해보자. 파일을 만드는 데 수십 초가 걸린다. HTTP 요청을 계속 붙잡아두기 부담스러워 Kafka로 작업을 넘기고, API는 먼저 응답하도록 바꿨다. 응답 시간은 짧아졌다. 그런데 화면에 완료되었습니다 라고 표시해도 될까? Consumer가 아직 메시지를 읽지 않았을 수도 있다. 파일을 만드는 중일 수도 있고, 처리에 실패해 재시도하고 있을 수도 있다. 요청을 접수한 것과 파일을 다…
Open source