2025. 3. 17. 23:39ㆍLegacy
개발자 G는 몇 개의 JSP와 쿼리 수정작업을 거치며 적응해나갔다. 새로운 회사와 새로운 사람, 새로운 업무는 그런 대로 익어간다. 부족한 게 없는 것 같다. 온보딩도 충분히 받았고, 관심 있는 비즈니스 도메인이기 때문에 업무와 가까워 지는 일이 덕업일치하는 것 같은 느낌도 든다. 하지만 이슈를 처리하고 바람을 쐬러 나갈 때마다, 뭔가 찜찜한 느낌이 G에게서 떠나지 않는다.
왜 이렇게 익숙할까. 왜 새로 해야 하는 일이 아닐까.
또다시 갸우뚱할 시간이 생길 찰나에, 운영팀에서 티켓이 날아왔다. 구장 오픈 시간에 맞춰 구장에 매물로 나와 있는 큐에 대한 홍보 메시지를 보내주는 기능이 있는데, 그 메시지가 간헐적으로 전송되지 않는다는 것이다. 우선 기능을 개발한 이슈를 맡았던 개발자는 이미 퇴사했다. G는 급한 대로 히스토리 파악을 위해 참조자로 달려있는 개발자 A 를 찾아간다.
G : 이슈 하나 링크로 드렸는데요, 이거 히스토리를 알고 계신게 있을까요?
A : 어 그거 좀 되었는데, 그게 전송이 안될 때는 안되고 될때는 1명한테 여러건이 가기도 해요
G : 좀 되었다구요? 그런데 왜 수정이 안되어있어요?
A : 그거 누가 저번에 무슨 처리 해놨다고 들었는데, 그거 찾아보면서 해보시면 될거같아요.
무책임한 부분이라고 G는 생각했다. 본인에게 할당된 이슈가 아님을 양보한다. 서비스가 잘 제공되지 않고 있는 것도 양보할 수 있는 건지는 모르겠지만. 판단을 보류하고 소스를 본다. 소스에는 몇 가지 장애 위험 요소가 있었고, G는 그것을 케이스별로 정리했다.
1. 메시지는 특정 시간에 발송된다. 스케줄러가 문제인가?
로그 파일에도 실행 기록은 1개, 스케줄러는 @Profile 과 @Scheduled 어노테이션으로 관리되고 있어 중복 로직은 없었다.
@Slf4j
@Profile("BATCH_PROFILE")
@RequiredArgsConstructor
@Component
public class MessageSendScheduler {
...
@Scheduled(cron = EVERY_DAY)
public void sendMessage(Obeject params) {
sendMessage(params..);
}
...
}
2. 발송 실패와 중복 발송이 동시에 발생하고 있다. 각각의 원인은 무엇인가?
RestTemplate restTamplate = new RestTemplate();
sendMessage() 메서드는 Spring Framework 의 RestTemplate 을 사용하고 있다. new 키워드로 기본값을 사용하고 있으므로 G는 연결 설정과 재시도 정책의 기본값을 조사한다.
Timeout 은 무한대
Retry 는 미지원
위 RestTemplate 문서에는 Retry 기능에 대한 공식적인 설명이 없고, Retry 기능을 구현하려면 Spring-Retry 프로젝트를 활용하는 것이 Spring 진영의 입장이므로 재시도는 하지 않는다.
3. 스케줄러 서버가 발송 API 서버를 호출하고 있다. 발송 API 문제인가?
기존 비즈니스 로직
...
public void sendMessage() {
...
// 1. 전날 활동회원 전체 조회
List<User> allActiveUsers = userRepository.findAllActiveUsers(yesterday);
// 2. 활동회원을 순회하며 가장 근거리 구장 3개 탐색
for (User user : allActiveUsers) {
List<Club> nearClubs = clubRepository.findNearClubs(user, 3);
// 3. 구장을 순회하며 큐 매물 중 가장 조회수가 많은 3개 선정
Set<Cue> allCues;
for (Club club : nearClubs) {
List<Cue> cues = cueRepository.findTopViewedCues(club, 3);
allCues.addAll(cues);
}
// 4. 유저가 메시지 수신에 동의한 경우 메시지 전송
if (user.isAgreeToReceiveMessage) {
requestToExternalMessagingApi(user, allCues, template);
}
}
}
...
문제는 기존 발송 비즈니스 로직에 있다. isActive 가 True 인 전체 유저를 조회한 뒤에 그 유저를 순회하여 클럽을 찾고, 그 클럽을 순회하며 큐를 찾아 템플릿에 들어갈 데이터를 만든 뒤 메시징 API 에 요청을 보낸다. 복잡도도 높고 실행 시간도 너무 오래 걸린다.
개선한 비즈니스 로직
...
public void sendMessage() {
...
// 1. 전날 활동회원 중 메시지 수신에 동의한 유저만 필터링
List<User> activeAndAgreeUsers = userRepository.findAllActiveUsers(yesterday)
.stream()
.filter(user -> user.isAgreeToReceiveMessage())
.toList();
// 2. 필터링 된 회원을 순회하며 클럽 탐색
List<SendMessageRequest> resquest = new ArrayList<>();
for (User user : activeAndAgreeUsers) {
List<Club> nearClubs = clubRepository.findNearClubs(user, 3);
// 3. 미리 가지고 있던 인기 큐 데이터를 꺼낸다
Map<Club, List<Cue>> hotCues = queue.retrieve(hotQue, user.location());
request.add(SendMessageRequest.from(user, hotCues));
}
// 4. 메시지 벌크 전송
requestToExternalMessagingApi(request, template);
}
...
회원 중 대상이 되는 유저만 미리 쿼리에서 제한해서 불러오는 것, 외부 메시징 서비스 API 를 호출할 때 Bulk 전송을 지원함을 발견하고 적용하였다.
개선 후 1시간 이상 실행 시간이 줄어들었고, 중복 발송되거나 미발송되는 건도 사라졌다. 이것에 대해 더욱 파헤져보고 싶었지만, G가 업무를 해낸 '결과'에 집중한 팀원들은 새로운 이슈를 G에게 맡기기 시작했다. G는 쌓이는 업무량에 이슈를 종결지으면서 코멘트만 달고 넘어갈 수 밖에 없었다.
- RestTemplate 기본값 적용하고 있어 명시적인 Timeout 성능 고려하여 설정하였음
- 비즈니스 로직을 위와 같이 개선함
- Retry 되는 현상은 해결하였으나, 원인 파악은 리서치 필요
- CDN 에서 정책을 가지고 있거나, Load Balancer 설정을 살펴보는 것
이슈의 현상이 해결되면 해결된 이슈라고 볼 수 있나?
원인을 밝히고 예방해야 기록하는 의미가 있지 않을까
'Legacy' 카테고리의 다른 글
5. 이슈는 상시 있는 것 (hard coding) (1) | 2025.04.09 |
---|---|
4. 고치거나 혹은 다시 짜거나 (FCM, Singleton) (0) | 2025.03.23 |
2. 첫 업무 (CASE WHEN END) (0) | 2025.03.10 |
1. 레거시 시스템 회사에 다닌다는 소설 (0) | 2025.03.10 |