카프카의 설정은 진짜일까?? - offset(3)

반응형

지금까지 학습한 내용을 간단하게 정리해보면, offset은 broker에 저장된 메시지의 순서를 의미하고, consumer는 이 offset을 기준으로 메시지를 읽고 처리하게 됩니다.

그리고 어디까지 처리했는지를 기록하는 과정을 offset commit이라고 합니다. 이 commit 방식에는 크게 두 가지가 존재합니다. 자동 commit은 일정 주기에 따라 offset을 자동으로 기록해주기 때문에 구현이 간단하다는 장점이 있습니다. 하지만 처리 완료 여부와 관계없이 offset이 기록되기 때문에, 메시지를 처리하는 도중 애플리케이션에 장애가 발생할 경우 문제가 발생할 수 있습니다.

이 경우 Kafka는 해당 메시지를 이미 처리된 것으로 인식하게 되고, consumer가 재시작되더라도 메시지는 다시 전달되지 않습니다. 결과적으로 데이터 유실이 발생할 수 있습니다.
반면 수동 commit은 개발자가 직접 commit 시점을 제어해야 하기 때문에 다소 번거롭지만,
처리 흐름을 보다 명확하게 관리할 수 있습니다. 

자동 commit은 어떻게 하는지 대충 알겠는데 수동 commit은 도대체 어떻게 하는 걸까?

코드를 확인하기 전에 먼저 동작 흐름부터 이해해보겠습니다.
수동 commit은 말 그대로 consumer가 메시지를 처리한 뒤, 개발자가 직접 commit 시점을 결정하는 방식입니다.
consumer는 poll()을 통해 broker로부터 메시지를 가져옵니다. 그리고 가져온 메시지를 바탕으로 비즈니스 로직을 처리한 뒤,
처리가 완료되었다고 판단되는 시점에 commit을 수행합니다. 

여기서 주목해야 할 점은 commit 시점을 개발자가 직접 결정할 수 있다는 것입니다.
즉, 자동 commit처럼 일정 주기에 의해 offset이 기록되는 것이 아니라, 처리가 끝났다고 판단한 시점에 직접 offset을 기록할 수 있습니다. 수동 commit은 말 그대로 consumer가 메시지를 처리한 뒤, 개발자가 직접 commit 시점을 결정하는 방식입니다.

그렇다면, 어떻게 처리가 끝났다고 판단할 수 있을까?

이를 이해하기 위해서는 Acknowledgment 개념을 알아야 합니다.
Acknowledgment는 간단하게 말해, 메시지 처리가 완료되었음을 Kafka에 알리는 신호입니다.

consumer는 메시지를 처리한 뒤 Acknowledgment를 호출하게 되며,
이 시점에 commit이 수행되거나 commit이 가능한 상태가 됩니다.

Acknowledgment는 commit 시점을 어떻게 제어하느냐에 따라 다양한 방식으로 동작하며, 내부적으로는 여러 구현체를 통해 처리됩니다. 대표적으로는 RECORD, BATCH, MANUAL 등의 방식이 있으며, 이러한 방식에 따라 commit 시점이 달라지게 됩니다.

여기서 드는 의문은 어떻게 RECORD인지 BATCH인지 MANUAL인지 알고 다양한 구현체를 리턴해주는 걸까요?
Acknowledgment의 동작 방식은 개발자가 직접 선택하는 것이 아니라, config나 yml에 설정된 AckMode 값을 기준으로 결정됩니다.
 
그렇다면 왜 이렇게 하는걸까요? 이유는 단순합니다. 메시지 처리 방식에 따라 commit 시점이 달라지기 때문입니다.
어떤 경우에는 메시지를 하나씩 처리한 뒤 commit하는 것이 적절할 수 있고,
어떤 경우에는 여러 메시지를 한 번에 처리한 뒤 commit하는 것이 더 효율적일 수 있습니다.
조금더 직관적으로 말씀드리자면, RECORD는 하나씩 안전하게, BATCH는 묶어서 효율적으로 MANUAL은 내가 직접 제어 하는 방법입니다. 그나저나 이상합니다. 분명 이 챕터는 수동 commit에 관한 방식인데 RECORD, BATCH는 자동 commit인 걸까요?
결론부터 말하면 그렇지 않습니다.
RECORD와 BATCH는 Kafka의 자동 commit이 아니라, Spring Kafka의 컨테이너가 메시지 처리 이후 commit을 대신 수행해주는 방식입니다. 

그렇다면, MANAL방식은 어떻게 사용하는 걸까?

사용하는 방식 자체는 크게 어렵지 않습니다.

acknowledgment.acknowledge();
 

를 호출하면, consumer가 처리한 offset이 commit됩니다.
즉, broker 입장에서는 이 메시지는 처리 완료되었다고 판단할 수 있게 됩니다.

그렇다면 여기서 한 가지 의문이 생깁니다.

acknowledgment.acknowledge();
couponIssueRequestHandler.handle(event);
 

처럼 실제 처리 로직인 couponIssueRequestHandler.handle()을 실행하기 전에 commit을 하면 어떻게 될까요?

이 경우 문제가 발생할 수 있습니다.

commit이 먼저 수행되면 Kafka는 해당 메시지를 이미 처리된 것으로 인식합니다.
그런데 그 이후 handle() 실행 중 애플리케이션 장애가 발생한다면, consumer가 재시작되더라도 해당 메시지는 다시 가져오지 않습니다.

결과적으로 처리되지 않은 메시지가 처리된 것으로 기록되어 데이터 유실이 발생할 수 있습니다.

따라서 MANUAL commit에서는 보통 다음 순서가 되어야 합니다.

couponIssueRequestHandler.handle(event);
acknowledgment.acknowledge();

핵심은 단순합니다. commit은 처리 전에 하는 것이 아니라, 처리 완료 이후에 해야 합니다. 다만 commit을 먼저 수행하게 되면,
메시지를 처리하기 전에 이미 offset이 기록되기 때문에 consumer 입장에서의 처리 지연(latency)은 줄어들 수 있습니다.
하지만 이 경우에는 중요한 문제가 발생합니다. 처리 도중 장애가 발생하더라도 해당 메시지는 다시 전달되지 않습니다.

즉, 데이터 유실 가능성을 감수해야 합니다. 따라서 이러한 방식은 정합성보다 처리 속도를 우선하는 경우에만 제한적으로 고려될 수 있습니다.

마무리

벌써 2번째 완료를 미루고 있네요. 원래 이번글에 종료하고 싶었는데 점점길어지네요. 다음에 완료하는 것을 목표로 해보겠습니다.
다음 시간에는 이번에 알아보지 못한 BATCH와 RECORD에 대해 학습해보겠습니다.

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY