Home 메시지 유실은 언제 발생할 수 있을까?
Post
Cancel

메시지 유실은 언제 발생할 수 있을까?

1. 글을 작성하게 된 계기


메시지 유실(Message Loss) 이 발생할 수 있는 구간에 대해 학습하는 과정에서 글을 작성하게 되었습니다.

AWS CloudWatch, SNS, SQS를 예로 설명하기 때문에 AWS 기본 인프라를 이해하고 있으면 좋습니다.





2. 메시지 유실과 구간


메시지 유실(Message Loss)은 생산자(Producer)가 발행한 메시지가 컨슈머(Consumer)에 도착해서 처리되지 못하는 것을 말합니다. 이는 네트워크, 인프라 장애 또는 서버 다운 등으로 발생할 수 있습니다.

Messages can be lost due to network failures, system crashes, or other unexpected events.





메시지 유실은 메시지 발행 전 , 메시지 발행 후 - 메시지 큐 전달 전, 메시지 큐 - 메시지 수신 전, 메시지 수신 후 크게 네 구간에 걸쳐 발생할 수 있습니다. 이는 애플리케이션 두 구간, 네트워크 두 구간에서 발생할 수 있는데, 이에 대해 살펴보겠습니다.

image

메시지 유실이 발생할 수 있는 구간을 더 세분화할 수 있지만, 설명의 편의를 위해 크게 네 구간으로 나누었습니다.







2-1. 메시지 발행 전

먼저 메시지 발행 전 구간입니다. 사용자 요청이 애플리케이션에 진입한 후, 메시지를 발행하기 전, 비즈니스 로직을 실행하는 구간입니다. 이는 예외 발생, 서버 다운애플리케이션 레벨 에서 메시지 유실이 발생할 수 있는 구간입니다.

image





예를 들어, 알림을 전송할 때, 알림 저장 로직은 실행됐지만 예외 발생, 서버 다운 등으로 알림 전송 로직이 실행되지 않는 경우입니다. 이는 애플리케이션 레벨 이기 때문에 코드로 제어 할 수 있어 비교적 해결책이 명확합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class AlarmService {
    private final AlarmRepository alarmRepository;
    private final AwsSnsClient awsSnsClient;

    public AlarmService(
        final AlarmRepository alarmRepository,
        final AwsSnsClient awsSnsClient
    ) {
        this.alarmRepository = alarmRepository;
        this.awsSnsClient = awsSnsClient;
    }

    public void sendAlarm(
        final LoginUser loginUser,
        final Alarm alarm
    ) {
        // 1. 알림 저장
        String alarmId = alarmRepository.save(alarm);
        
        // 2. 알림 전송 실패(예외 발생, 서버 다운)  
        awsSnsClient.sendAlarm(loginUser.getUserId(), alarmId);
    }
}







단순 예외가 발생한 경우, 강한 검증 이나 테스트 코드 작성 을 통해 문제를 해결할 수 있습니다. 물론 데드 레터(Dead Letter) 를 통해 유실된 메시지는 재발행 해줘야 하고요. 메시지를 재발행할 때, 일정 주기로 데드 레터를 풀링(Polling) 하며, 일정 재시도 횟수를 초과하면 개발자가 개입 할 수 있도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class AlarmService {
    private final AlarmRepository alarmRepository;
    private final AwsSnsClient awsSnsClient;

    ......

    @Scheduled(cron = "*/5 * * * * *")
    public void pollDeadLetters() {

        try {
            // Polling
        } catch (Exception ex) {
            // 실패 시 로직
        }

    }
}







서버 다운의 경우, Graceful Shutdown 설정을 통해 애플리케이션이 받은 요청까지는 안전하게 처리할 수 있습니다. 물론 이 또한 완전 하지는 않은데요, 이 경우 로그 를 통해 유실된 메시지를 복구할 수 있습니다.

This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted.







2-2. 메시지 발행 후 - 메시지 큐 전달 전

두 번째는 메시지 발행 후부터 메시지 큐 전달 전 구간입니다. 이는 네트워크 또는 인프라 장애 로 인해 메시지 유실이 발생할 수 있습니다. 애플리케이션을 벗어난 후 예외가 발생했기 때문에, 메시지 발행 후에는 코드 레벨에서 이를 제어할 방법이 없습니다.

image







이를 해결하기 위해서는 사용자 요청이 애플리케이션에 진입한 후, 혹은 메시지가 발행되기 전, 로그 를 남겨둬야 합니다. 이를 통해 유실된 메시지를 재발행하는 것이죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class AlarmService {
    private final AlarmRepository alarmRepository;
    private final AwsSnsClient awsSnsClient;

    ......

    @Scheduled
    public void sendAlarm(
        final LoginUser loginUser,
        final Alarm alarm
    ) {
        String alarmId = alarmRepository.save(alarm);

        try {
            // 1. 메시지 발행 전 로그
            log.info("loginUser:{}, alarm:{}");
            
            // 2. 메시지 발생
            awsSnsClient.sendAlarm(loginUser.getUserId(), alarmId);
        } catch (Exception ex) {
            awsSnsClient.pushToDeadLetter(loginUser.getUserId(), alarmId);
        }
    }
}

예제 코드는 서비스 레이어에 로그를 남겼지만, AOP 를 활용하면 훨씬 더 깔끔하게 이를 처리할 수 있습니다.







만약 이 구간에서 메시지 유실이 발생하면, 로그를 조회해 객체로 만든 후, 이를 재발행 해줄 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class AlarmService {
    private final AlarmRepository alarmRepository;
    private final AwsSnsClient awsSnsClient;

    ......

    @Scheduled
    public void publishDeadLetter(
        final String requestId
    ) {
        try {
            final RequestLog requestLog = awsSnsClient.findByRequestLogId(requestId);
            final int retryCount = requestLog.getRetryCount();
            
            // 일정 재시도 횟수를 초과하면 개발자가 수동으로 개입할 수 있는 데드레터를 만든 후 처리.
            if (retryCount > 3) {
                awsSnsClient.pushToManualDeadLetter(new RequestLog(requestLog.getId(), retryCount + 1));
                return;
            }
            awsSnsClient.publishEvent(requestLog);
        } catch (Exception ex) {
            awsSnsClient.pushToDeadLetter(requestId);
        }
    }
}







2-3. 메시지 큐 - 메시지 수신 전

세 번째로 메시지 큐를 나온 후, 메시지 수신 전입니다. 이 구간도 네트워크 또는 인프라 장애로 메시지가 유실될 수 있기 때문에 로그를 통해 메시지를 복구해야 합니다.

image







여기서는 어느 구현체 를 사용하는지에 따라 다른데요, 카프카(Kafka)와 같이 메시지 유실을 보완하기 위한 자체 재발행 기능이 있는 구현체를 사용하면 문제 해결이 간단해집니다. AWS의 SNS 또는 SQS도 마찬가지고요. 반면 RabbitMQ와 같은 구현체를: 사용하면 로그를 통해 유실된 메시지를 복구하고, 이를 재발행 해줘야 합니다.

서비스 성격과 규모에 따라 인프라에 의존하는게 힘든 경우도 존재합니다. 만약 하둡과 같은 자체 로그 보관 시스템이 있다면 이 문제를 훨씬 간단하게 해결할 수 있습니다.







2-4. 메시지 수신 후

마지막으로 메시지 수신 후입니다. 애플리케이션에서 메시지를 수신했지만, 예외 또는 서버 다운으로 로직이 처리되지 않아 메시지가 유실되는 경우 입니다. 이 또한 2-1과 동일한 방법으로 데드레터와 메시지 재발행을 통해 문제를 해결할 수 있습니다.

image







이를 위해 메시지를 수신한 후, 반드시 로그를 남기도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class AlarmService {
    private final AlarmRepository alarmRepository;
    private final AwsSnsClient awsSnsClient;

    ......

    @SqsListener
    public void listenAlarmMessage(final AlarmMessage message) {
        try {
            log.info("message:{}", message);
            
            ......
            
        } catch (Exception ex) {
            ......
        }
    }
}







3. 모든 메시지 유실을 막을 수 있을까?


아무리 철저하게 대비한다 해도 모든 메시지 유실을 막을 수는 없습니다. 클라이언트에서 서버로 요청하는 과정에서 네트워크 장애가 발생할 수도 있으니까요.

image







만약 중간에 로드밸런서나 게이트웨이, 리버스 프록시를 도입했다면, 신경 써야 할 구간이 하나 더 생깁니다. 이 경우 Zipkin 이나 Pinpoint, Istio 와 같은 분산추적(Distributed Tracing) 툴을 별도로 도입하는 것을 고려해 봅니다.

image







결국 철저하게 대비를 해야 하지만, 어느 정도의 메시지 유실 가능성은 염두에 두고, 운영 으로 이를 해결할 방안도 함께 고려해야 합니다. 모든 것들을 시스템으로 제어할 순 없으니까요. 마지막으로 관심이 있다면 아래와 같은 추가 아티클도 함께 읽어볼 것을 권장드립니다.







4. 정리


메시지 유실이 발생할 수 있는 구간에 대해 살펴보았습니다. 메시지 큐는 도입하기는 정말 쉬운데요, 이를 도입했을 때 생기는 그 복잡도가 너무나도 큰 것 같습니다. 잘 대비하더라도 메시지 유실을 완벽히 막을 수는 없는데요, 어느 구간에서 메시지 유실이 발생할 수 있는지 정확히 알고 가능한 한 철저하게 이를 대비하도록 합니다.


This post is licensed under CC BY 4.0 by the author.