
서비스 소개
소식봇(SOSIK_BOT)은 가상 화폐 거래소에서 진행되는 다양한 이벤트 소식을 빠르게 전달하고, 해당 이벤트의 참여 보상 금액을 예측해주는 Telegram 기반 자동 알림 서비스입니다.
주요 기능
- 가상 화페 거래소의 새로운 이벤트 알림
- 당일 참여 가능한 이벤트 리마인드 알림
- 이벤트 참여 시 예상 리워드 금액 알림
- 월간 이벤트 참여 결과 레포트 제공
소식하는 소식지
소소한 코인 이벤트 알람
t.me
이 글은 가상화폐 거래소의 이벤트 소식을 전달하는 소식봇(SOSIK_bot)의 리팩토링 과정을 기록한 시리즈 중 네 번째 게시글입니다. 이번 글에서는 예외 처리 체계 개선과 실시간 대응 시스템 도입을 어떻게 했는지, 그 과정에서 어떤 판단과 변화가 있었는지를 설명합니다.
글의 가독성을 위해 리팩토링 전의 코드는 ver1, 리팩토링 후의 새로운 코드는 ver2로 서술합니다.
Ⅰ 기존 구조의 예외 처리 문제
일반적으로 프로그램에서 예외 상황이 발생하면, 프로그램이 중단되지 않고 계속 실행되도록 하기 위해 try-catch를 이용해 예외를 처리한다. 이는 예측하지 못한 문제로부터 시스템을 보호하는 전형적인 방식이다.
소식봇 ver1에서도 try-catch를 이용해 예외 처리를 했는데, 특히 크롤링 로직에서는 전혀 예상하지 못한 예외가 발생할 수 있어 전체 로직을 try-catch로 감싸는 방식이 사용됐다. 하지만 이 방식은 에러가 발생한 위치나 원인을 정확히 파악하기 어렵게 만들었다. 크롤링 로직 전체를 try-catch로 감싸면, 예외는 잡지만 어떤 코드에서 문제가 발생했는지를 알기 어렵기 때문이다.
1. 불명확한 예외 처리
ver1에서는 크롤링 메서드 전체를 try-catch로 감싸는 방식으로 예외를 처리했다. 크롤링 도중 예외가 발생하면 catch에서 예외를 잡아 로그 파일에 기록했는데, 문제는 발생한 원인을 구체적으로 기록하지 않았다는 점이다.
public class 크롤링서비스 {
public List<List<Airdrop>> 크롤링메서드() {
try {
...
크롤링 로직들
...
} catch {
logger.error(e.getMessage(), e);
}
}
}
이러한 방식은 문제가 생겼을 때 로그 파일을 열어 내용을 직접 일일이 확인해야 했고, 에러 메시지가 단순해 처음 보는 에러일 경우 원인을 파악하는 데 시간이 걸렸다.
2. 난잡하게 기록된 로그
ver1에서는 예외가 발생하면 별도의 에러 메시지 처리 없이 로그 전체가 그대로 출력되었기 때문에, 로그 파일을 읽기가 어려웠다. 다음은 실제 로그 파일 일부이다.

이처럼 에러의 본질과 무관한 스택 트레이스(Stack Trace)가 장황하게 기록되면서, 어떤 상황에서 어떤 예외가 발생했는지 한눈에 파악하기 힘들었다.
3. 예외 상황 실시간 대처 불가
소식봇은 가상화폐 거래소 공지사항을 일정 주기로 크롤링하는 구조다. 새로 생기는 이벤트는 예고 없이 등장하는데, 크롤링에 실패해도 이를 즉시 알 수 있는 구조가 아니었다. 만약 텔레그램 알림이 오지 않았다면 왜 알림이 실패했는지 확인하는 데 시간이 걸릴 수밖에 없었다.
특히, 메세지가 오지 않을 때 새로운 이벤트가 없어 크롤링된 내용이 없는건지, 시스템 예외 때문인지를 판단하기 어렵기 때문에 에러 상황을 즉시 인지하고 대응하는 것이 사실상 불가능했다. 이런 이유때문에 지난 6개월간 소식봇을 운영하면서 에러 상황 대처가 빠르고 깔끔하지 못했고, 이번 리팩토링 과정에서는 예외 상황을 실시간으로 확인할 수 있는 기능을 추가하는 방향으로 목표를 잡았다.
Ⅱ 새롭게 적용한 구조
1. 커스텀 예외 클래스를 통한 명확한 예외 처리
소식봇 ver2에서는 예외 상황을 보다 명확하게 관리하기 위해 커스텀 예외 클래스를 도입했다. BotException은 enum 형태로 정의되어 있으며, 에러의 유형(type), 에러 내용(content), 구체적인 이유(opinion)를 포함한다. 각 비즈니스 로직에서는 예외 발생 가능성이 있는 지점에서 사전에 정의한 예외를 던지는 방식으로 예외를 처리한다.
public enum BotException {
// MESSAGE
SEND_MESSAGE_FAIL("MESSAGE ERROR", "SEND MESSAGE FAIL", "메세지 전송 실패"),
...
// AIRDROP CRAWLING
NOT_FOUND_DATE("CRAWLING ERROR", "NOT FOUND DATE", "공지사항에서 날짜 추출 실패"),
NOT_FOUND_TITLE("CRAWLING ERROR", "NOT FOUND TITLE", "공지사항에서 제목 추출 실패"),
...
// IMAGE DOWNLOAD
IMAGE_DOWNLOAD_FAIL("IMAGE DOWNLOAD ERROR", "IMAGE DOWNLOAD FAIL", "이미지 다운로드 실패"),
IMAGE_CONNECTION_FAIL("IMAGE DOWNLOAD ERROR", "IMAGE CONNECTION FAIL", "이미지 다운로드 중 HTTP 연결 실패"),
...
;
private final String type;
private final String content;
private final String opinion;
BotException(String type, String content, String opinion) {
this.type = type;
this.content = content;
this.opinion = opinion;
}
}
/// [Util] Extract Title From Notice
private String extractTitleFromNotice(WebElement element) {
// 이벤트 제목 추출 로직
throw new BusinessException(BotException.NOT_FOUND_TITLE);
}
/// [Util] Download a Base64-encoded image string (data:image/~) to a file.
private static boolean downloadBase64Image(String base64Data, File outputFile) throws Exception {
String[] parts = base64Data.split(",");
if (parts.length != 2 || !parts[0].startsWith("data:image")) {
throw new BusinessException(BotException.IMAGE_FORMAT_NOT_SUPPORTED);
}
...
try {
imageBytes = Base64.getDecoder().decode(parts[1]);
} catch (IllegalArgumentException e) {
throw new BusinessException(BotException.IMAGE_DOWNLOAD_FAIL);
}
...
}
리팩토링 효과
- 에러 메시지 일관성 확보
- 로직별 예외 상황을 명확히 구분
- 전달할 메시지를 미리 정의하여 처리 속도 및 품질 향상
2. 로그 기록 양식 표준화
ver1에서는 예외 발생 시 Exception 전체를 그대로 로그로 기록했기 때문에 가독성이 떨어지고, 에러의 성격을 파악하기 어려웠다. ver2에서는 예외를 KnownException과 UnknownException으로 구분하여 처리하고, 정해진 양식에 따라 로그에 기록되도록 개선하였다.
- KnownException : 예상 가능한 커스텀 예외 (BusinessException)
- UnknownException : 시스템 예외 또는 예상하지 못한 예외
# KnownException
2025-05-28 14:12:04.854 [ERROR] - [API ERROR] JSON PARSING FAIL
2025-05-28 16:23:01.630 [ERROR] - [MESSAGE ERROR] SEND MESSAGE FAIL
# UnknownException
2025-05-28 15:28:37.090 [ERROR] - Text '2025-05-29' could not be parsed at index 4 java.time.format.DateTimeFormatter parseResolved0 2052
2025-05-28 15:28:37.090 [ERROR] - Cannot invoke "java.util.List.isEmpty()" because "airdropList" is null com.nemojin.sosikbot.bot.scheduled.GopaxScheduler
detectNewAirdropEventAndSendMessage 31
리팩토링 효과
- 로그 파일만 보고 예외의 위치, 원인, 해결 방향을 빠르게 파악 가능
- 에러 발생 패턴 분석 및 디버깅 효율 대폭 향상
3. 예외 상황 실시간 전송 기능
추가로, 예외가 발생했을 때 로그 파일에만 기록하는 것이 아니라 Telegram 개인 메시지로 예외 상황을 실시간 전송하는 기능을 구현하였다. 이를 위해 MessageDispatcher 클래스에 sendKnownException()과 sendUnknownException() 메서드를 정의하였다.
public class MessageDispatcher {
/// Send Unknown Exception Message To Admin
public void sendUnknownException(Exception ex) {
logger.error(ex.getMessage());
String msgText = createUnknownMessage(ex);
SendMessage message = new SendMessage();
message.setChatId(rootId);
message.setText(msgText);
try {
messageSender.execute(message);
} catch (TelegramApiException e) {
throw new BusinessException(BotException.SEND_MESSAGE_FAIL);
}
}
/// Send known Exception Message To Admin
public void sendKnownException(BusinessException bex) {
logger.error(bex.getExceptionLog());
String msgText = createKnownMessage(bex);
SendMessage message = new SendMessage();
message.setChatId(rootId);
message.setText(msgText);
try {
messageSender.execute(message);
} catch (TelegramApiException e) {
throw new BusinessException(BotException.SEND_MESSAGE_FAIL);
}
}
...
}
리팩토링 효과
- 예외 발생 시 관리자에게 즉시 알림 전송 가능
- 시스템의 장애 인지 및 대응 시간 단축
- 서비스 안정성 향상
이 구조를 통해 기존의 불명확하고 단편적인 예외 처리 방식을 보완하고, 예외 처리의 일관성, 실시간 대응력, 유지보수성 모두를 향상시킬 수 있었다.
Ⅲ 마무리하며
소식봇 ver1을 개발할 당시에는 효과적인 예외 처리에 대한 이해가 부족했다. 이후 작년 말쯤부터 Spring의 REST API 환경에서 @RestControllerAdvice를 통해 전역 예외 처리가 가능하다는 점을 알게 되었고, 그 이후로는 새로운 프로젝트마다 이를 적극 활용해왔다.
이러한 경험이 있었기에, 이번 소식봇 리팩토링을 계획할 때도 예외 처리 강화를 핵심 목표 중 하나로 삼았다. 다만 소식봇은 REST API가 아닌, 주기적으로 데이터를 크롤링하고 외부 API와 통신하는 비동기 시스템이기 때문에, @RestControllerAdvice 같은 전통적인 방식이 적용되지 않았다. 대신 이에 맞는 구조를 새롭게 고민하고, 알림 기능까지 포함된 자체 예외 처리 체계를 구축하게 되었다.
이번 리팩토링을 통해 소식봇 운영 중 발생할 수 있는 다양한 예외 상황에 더 빠르게 대응할 수 있게 되었고, 서비스의 안정성과 운영 효율이 높아지길 기대해본다.
'Project > 소식봇' 카테고리의 다른 글
| [소식봇 리팩토링 #5] 확장성을 위한 구조 개선과 추상화 도입 (8) | 2025.06.13 |
|---|---|
| [소식봇 리팩토링 #3] 재사용 가능한 유틸 클래스 추출 과정 (2) | 2025.06.10 |
| [소식봇 리팩토링 #2] 메세지 처리 책임의 분리와 구조 리팩토링 (1) | 2025.06.07 |
| [소식봇 리팩토링 #1] 기존 구조의 한계와 리팩토링 방향성 (5) | 2025.06.05 |
| [소식봇] 가상화폐 이벤트 알림 봇 개발 후기 (2) | 2025.06.04 |