포스트

[업무일지] 레거시 배치를 항목 단위로 끄면서 놓친 부수효과

레거시 배치의 상태체크를 항목 단위로 새 배치 서버에 옮기고 있습니다. 검사 한 줄을 주석 처리했더니 그 반환값을 쓰던 아래 if 블록이 같이 죽었고, 그 안에 새 서버가 안 하는 조치가 하나 있었습니다. 다음 날 되돌렸습니다.

레거시 API 서버(Node)에 야간 배치가 같이 붙어 있습니다. 그걸 새로 세운 Java 배치 서버로 옮기는 중입니다. Java 21, Spring Boot 3.x, Quartz JDBC JobStore 를 쓰는 환경입니다.

옮기는 대상 중에 상태체크 배치가 있습니다. 매일 새벽 쇼핑몰에서 셀러 정보를 크롤링해서 계좌 주인이 바뀌었거나 사업자가 폐업했거나 부도가 났으면 지급을 막는 배치인데, 막는 방식은 블랙리스트 테이블에 사유와 함께 한 줄 넣는 겁니다. 검사 항목은 여덟 개입니다.

검사 항목이 한 함수에 순서대로 늘어서 있었다

레거시 쪽 코드는 이렇게 생겼습니다. 문제되는 부분만 남겨서 최소 형태로 다시 썼습니다.

1
2
3
4
5
6
7
8
9
10
11
// 레거시 배치 (Node.js 16, node-schedule)
async function checkData(crawlData, target) {
    await checkBlBusinessNumber(crawlData, target);
    const isChangedOwner = await checkBlBankOwner(crawlData, target);
    await checkBlBankAccount(crawlData, target);
    await checkBlHoldPayment(crawlData, target);

    if (isChangedOwner) {
        await User.unlinkAuthKey(target.user_id);
    }
}

이관 단위로 딱 좋아 보였습니다. 항목이 함수 하나씩 분리돼 있으니까 새 서버에 같은 검사를 붙이고 레거시에서는 그 한 줄만 주석 처리하면 되겠거든요. 여덟 개를 한 번에 다 옮길 수는 없어서 한 달쯤 두 서버를 같이 돌릴 계획이었고, 그러면 항목별로 붙였다 껐다 하면서 진행 상황을 눈으로 볼 수 있으니 안전하다고 봤습니다.

한 줄을 주석 처리했더니 아래 if 문이 같이 죽었다

예금주 변경 검사를 주석 처리했습니다. 그러니 그 반환값을 담던 변수가 없는 변수가 되고, 아래 if 블록도 같이 주석 처리해야 했습니다. 그 안에 있던 게 문제였습니다.

대표자가 바뀌면 기존 대표자의 외부 인증키 연결을 끊는 처리였습니다. 대표자가 바뀌었다는 건 그 계좌로 인증을 걸어둔 사람이 더 이상 그 사업자의 대표가 아니라는 뜻이니, 연결을 그대로 두면 안 됩니다. 새 서버에는 그게 없었습니다. 저는 검사만 옮겼거든요.

옮긴 것은 판정이고, 판정에 딸린 조치 하나가 빠진 겁니다.

다음 날 되돌렸다

원복은 커밋 하나였습니다. 그 하루 동안 대표자가 바뀐 셀러는 블랙리스트에는 올라갔는데 인증키 연결은 그대로 남아 있었습니다. 지급을 막는 판정 자체는 새 서버가 이어받아서 하고 있었으니, 빠진 건 판정이 아니라 판정 뒤에 따라붙는 조치 하나였습니다. 그래서 하루로 끝났습니다.

여기서 제가 뭘 잘못 봤는지가 이 글의 알맹이입니다. 저는 이관 단위를 함수 호출 한 줄로 봤습니다. 실제 단위는 그 검사가 만드는 상태 변화 전체였습니다.

검사 하나가 실제로 만드는 걸 나열해보면 이렇습니다.

  • 블랙리스트 테이블에 사유 row 등록
  • 변경 이력 테이블에 스냅샷 적재
  • 담당자 슬랙 알림
  • 외부 인증키 연결 해제

앞의 셋은 검사 함수 안에 있어서 같이 옮겨졌습니다. 넷째만 빠졌습니다. 빠진 이유는 그게 함수 밖에 있었기 때문입니다. 실행 순서상으로는 검사 바로 다음인데 코드 위치가 다른 블록이라, 한 줄씩 주석 처리하다 보니 눈에 안 들어왔습니다.

붙이고 확인하고 끄는 순서로 바꿨다

남은 항목은 순서를 뒤집어서 옮겼습니다. 먼저 새 서버에 검사와 부수효과를 다 붙입니다. 그다음 양쪽을 며칠 같이 돌립니다. 그러고 레거시를 끕니다.

간격은 항목마다 달랐습니다. 휴/폐업 체크는 새 서버에 붙인 날 레거시를 껐습니다. 원칙대로면 며칠 뒀어야 하는데, 판정 조건이 레거시와 같고 딸린 조치도 슬랙 알림 하나뿐이라 볼 게 없다고 봤거든요. 부도 체크는 붙인 다음 날 껐고, 계좌상태 체크는 여드레 뒤에 껐습니다. 조치가 많이 딸린 항목일수록 오래 뒀습니다. “같이 돌려서 확인”이라고 해도 실제로 한 건 다음 날 아침에 양쪽 로그와 블랙리스트 등록 건수를 눈으로 맞춰본 것뿐이었는데, 검사 항목이 여덟 개고 하루에 도는 대상이 수천 건이라 눈으로 보는 것도 등록 건수 합계와 사유별 분포까지가 한계였습니다.

두 서버가 같은 블랙리스트를 만지는 기간

이관 중에는 두 서버가 같은 블랙리스트 테이블을 씁니다. 여기서 하나 더 걸렸습니다.

새 서버 코드에 상태체크를 시작하기 전에 조회실패 블랙리스트를 지운다는 로직이 있었습니다. 조회실패는 크롤링 자체가 안 됐다는 뜻이고, 다시 성공하면 지워주는 게 맞습니다. 그런데 조회실패 블랙리스트를 등록하는 쪽이 아직 레거시였고, 새 서버가 그걸 지우면 레거시가 방금 내린 판정을 새 서버가 조용히 무효화하는 셈이 됩니다.

그래서 삭제만 꺼두고 재활성화 조건을 주석으로 남겼습니다.

1
2
3
4
5
// Java 21, Spring Boot 3.x
// 상태체크 전 크롤링 실패 블랙리스트 삭제
// 재활성화 조건: 레거시 상태체크 배치를 모두 옮긴 뒤
// blacklistRepository.deleteByUserIdAndChannelIdAndBlType(
//         userId, channelId, BlackListType.SEARCH_FAIL.getCode());

주석 처리한 코드를 남기는 건 원래 안 좋아합니다. 여기서는 지우는 것보다 낫다고 봤습니다. 이 코드가 왜 지금 안 도는지가 조건과 함께 남아야 하거든요. 지워버리면 이관이 끝난 뒤에 “조회실패 블랙리스트는 이제 누가 지우나”를 처음부터 다시 발견하게 되고, 그때는 이 코드가 있었다는 사실 자체가 아무 데도 안 남아 있습니다.

토글로 만드는 방법도 있었습니다. 안 했습니다. 이관이 끝나면 토글도 지워야 하고, 켜고 끌 일이 한 번뿐인 토글은 그 자체로 부채가 되고요.

맺음말

이관하면서 제가 실제로 고친 건 코드가 아니라 순서입니다. 새 서버에 붙이고, 며칠 돌려보고, 그다음에 레거시를 끕니다. 반대로 하면 그 사이가 공백이 됩니다.

포기한 게 둘 있습니다. 양쪽 결과를 자동으로 비교하는 장치는 안 만들었습니다. 새벽 배치 결과를 다음 날 아침에 눈으로 봤습니다. 항목이 여덟 개고 이관 기간이 한 달 정도라 그 정도면 된다고 봤지만, 항목이 더 많거나 기간이 더 길거나 한쪽만 판정을 바꾸는 케이스가 드물게 섞여 있었으면 눈으로 맞춰보는 방법은 확실히 안 통했을 겁니다. 그리고 이관 완료 여부를 코드가 아니라 주석에 적어뒀습니다.

남은 한계도 그겁니다. 조회실패 블랙리스트 삭제는 아직 꺼져 있습니다. 주석은 컴파일러가 안 봐줍니다. 이관이 다 끝났는지 누가 확인하지 않으면 그대로 남을 거고요. 날짜가 지나면 스스로 없어지는 조치로 만들 수도 있었을 텐데, 이관 완료 시점을 못 정해서 날짜를 쓸 수가 없었습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.