일기 slecs

배포 파이프라인 뜯어고치고 앱을 심사 대기열에 올린 날

목차

오늘은 유난히 컨텍스트 전환이 많은 하루였다. 오전에는 CI/CD 워크플로우를 들여다보고 있었고, 오후 중간엔 세션 관련 버그를 잡았고, 어느 순간엔 Flutter 프로젝트의 App Store Connect 심사 노트를 다듬고 있었다. 그날그날 한 가지를 몰아서 파는 날이 있고, 여러 레이어를 왔다 갔다 하는 날이 있는데 오늘은 확실히 후자였다.

커밋이 열여덟 개다. 개수만으로는 뭐가 많은지 모르지만, 파일 목록을 보면 배포 워크플로우, 스프링 설정 클래스, JSP 템플릿, 필터 클래스, 쿼리 매퍼, Flutter l10n arb 파일, 앱 스토어 스크린샷 PNG까지 섞여 있다. 층위가 완전히 다른 것들이 한꺼번에.

사전컴파일을 왜 이제야

JSP 사전컴파일 작업은 오래된 불만에서 나왔다. 배포를 올리고 나면 첫 방문자들이 페이지 로딩이 오래 걸린다는 것. 개발 서버에서 재현해보면 분명히 느리다. 두 번째 접속부터는 괜찮은데, 첫 번째만 유독 느리거나 아예 무한로딩처럼 보이는 것.

JSP의 동작 방식을 생각하면 당연한 일이다. Tomcat은 JSP 파일을 서블릿으로 변환하고 컴파일하는 작업을 최초 요청 시점에 한다. 서버가 방금 올라온 상태에서 누군가 처음 들어오면 WAS가 그 JSP를 컴파일하는 시간을 고스란히 기다려야 한다. 페이지 하나가 여러 JSP를 include 하면 그게 다 쌓인다. 우리 프로젝트는 공통 레이아웃, 메타 태그, QR 미리보기 모달들을 각각 JSP로 분리해 두고 있어서, 첫 요청의 컴파일 비용이 작지 않다.

JspC로 빌드 타임에 미리 컴파일해두면 이 문제가 사라진다. build.gradle에 JspC 태스크를 추가하고, JspPrecompiledConfig.java에서 스프링 컨텍스트에 등록하는 방식. cmmnMeta.jspqr-preview-modal.jsp 같은 공통 JSP들을 우선 대상으로 잡았다. 배포 직후 첫 방문자 경험을 직접적으로 개선하는 것이니, 미뤄왔던 게 조금 아쉽긴 하다.

무중단 롤링의 실체

JspC를 넣다 보니 배포 파이프라인 전반을 손봐야 할 이유가 눈에 밟혔다. 아니, 사실 이미 알고 있었는데 이번 작업이 계기가 된 것에 가깝다. 기존 배포 방식은 WAS를 그냥 내렸다가 코드 올리고 다시 올리는 것이었다. 그 사이에 들어오는 요청은 어떻게 되냐 - 에러다.

트래픽이 많지 않을 새벽에 배포하면 큰 문제가 안 됐는데, 낮에도 부담 없이 배포할 수 있으려면 무중단이 필요했다. WAS가 두 대니까 롤링 방식이 자연스럽다. WAS1을 빼고 교체한 다음 다시 넣고, 그 사이 WAS2가 혼자 받다가, WAS2도 마찬가지로 처리하는 것.

핵심은 LB에서 서버를 빼는 타이밍이다. scripts/클라우드_lb_target.py를 만들어 클라우드 LB의 타깃 풀에서 서버를 제외하거나 다시 추가하는 로직을 담았다. CI/CD 워크플로우에서 이 스크립트를 호출하면서 배포 단계를 조립했다.

드레인이 있다. LB에서 빠진 서버로는 새 연결이 안 오지만, 이미 들어온 요청은 처리가 완료될 때까지 기다려야 한다. 처음에 20초를 잡았다가 10초로 줄였다. 실제 요청 처리 시간을 보고 조정한 것인데, 배포 전체 시간에서 10초는 적지 않다.

WAS1의 LB 복귀를 비동기화한 것도 시간 단축 포인트였다. 클라우드 LB에 서버를 다시 등록하면 상태가 UP으로 바뀌기까지 약 2분이 걸린다. 이걸 순차적으로 기다리면서 WAS2 배포를 시작하면 2분이 그냥 더해진다. WAS1 복귀를 백그라운드로 돌리면, WAS2 배포 안정화 시간이나 detach 가드 대기와 겹쳐서 실질적으로 추가 대기 시간이 거의 없어진다. 워크플로우를 짤 때 이런 중첩을 설계하는 게 재미있으면서도 검증이 까다롭다. 순서가 조금 틀어지면 WAS2가 올라오기도 전에 WAS1이 LB에서 돌아와 홀로 트래픽을 받는 상황이 생길 수 있으니.

복귀 전 자기 검증

롤링 배포를 올리고 나서 바로 다음 커밋이 웜업 게이트였다. 배포 워크플로우를 돌리다 보니, 헬스체크 UP이 됐는데도 첫 요청이 여전히 느릴 수 있겠다는 생각이 들었다. JspC로 사전컴파일을 해도 JVM이 클래스를 실제로 로드하는 시간은 있다. 서버가 살아있다고 해서 바로 실사용 트래픽을 받을 준비가 됐다고 할 수 없는 것.

그래서 LB 복귀 전에 관문을 추가했다. 워크플로우가 직접 대표 페이지 - 홈과 관리자 로그인 - 에 HTTP 요청을 보내서 응답 시간을 잰다. 1.5초 미만으로 2회 연속 확인되면 복귀 허용. 안 되면 잠깐 기다렸다가 다시 시도한다. 1.5초라는 기준은 실사용자가 체감하기에 충분히 빠른 수준으로 잡은 것이다. 그 이하면 페이지가 느리다고 느끼지 않는다는 경험적 판단.

이 게이트가 있으면, JspC 결과물이 제대로 로드됐는지를 실제 HTTP 응답 시간으로 검증하는 셈이다. 형식적인 프로세스 헬스체크보다 훨씬 실질적인 준비 확인이다. 배포 파이프라인이 이 정도 되면 낮 시간에도 부담 없이 배포할 수 있다. 그게 이번 작업 전체의 목표였다.

로그에 묻혀 있던 것들

배포 관련 작업을 하다 보니 운영 로그도 자연스럽게 눈에 들어왔다.

Grafana/Loki에서 같은 초에 찍힌 로그들이 순서가 뒤섞이는 문제가 있었다. logback-spring.xml의 타임스탬프 패턴에 밀리초를 추가하면 되는 것이었다. 고치는 건 1분이지만, 이게 없으면 로그로 요청 흐름을 추적할 때 순서를 신뢰할 수 없다. 특히 에러 디버깅할 때 로그 순서가 틀리면 판단이 틀릴 수 있다. Loki가 수집 시 자체 타임스탬프를 붙이는데 그게 밀리초 이하 단위라서 같은 초 안에서는 원래 순서가 보존이 안 됐던 것.

그리고 운영 로그 노이즈. 무권한 세션 관련 로그가 INFO 레벨로 대량으로 찍히고 있었다. targetSysId 차단 메시지가 유효하지 않은 세션에서 매 요청마다 올라오니까 로그 스트림이 이걸로 도배되는 상황. AppOnlyGate 차단 로그도 비슷했다. 이런 건 DEBUG로 강등하는 게 맞고, AppOnlyGate 쪽엔 10분 dedup을 넣었다. 같은 패턴이 반복될 때 10분에 한 번만 기록하는 것.

근데 노이즈를 줄이면서 트래킹 가용성을 잃으면 안 된다. 그래서 403이 찍힐 때는 계정 정보와 referer를 함께 남기게 했다. 평소엔 조용하다가 뭔가 이상할 때는 충분히 추적할 수 있는 구조. 노이즈 감소와 정보 보존 사이의 균형을 찾는 게 이 류의 작업에서 핵심이다.

세션 폴링이 루프를 만들고 있었다

오늘 중에 제일 오래 들여다본 버그가 이것이었다.

증상은 채팅 PIP 위젯이 세션 없는 사용자 화면에서도 계속 폴링 요청을 보내는 것. 헤더 배지 폴링도 마찬가지였다. 더 들여다보면, 무권한 요청에 서버가 302 리다이렉트를 내리는데 그 목적지가 에러 페이지고, 그 에러 페이지가 풀 HTML로 렌더링되면서 클라이언트가 이걸 응답으로 받아 처리하려다 이상한 루프가 생기는 것이었다. 폴링 코드 입장에서는 요청을 보냈고 뭔가가 돌아왔으니까, 에러인지 모르고 계속 보내는 것.

근본 차단을 두 단계로 했다.

서버 사이드에서는 AJAX 요청이 들어왔을 때 무권한이면 302 대신 403 JSON을 돌려준다. 요청 헤더를 보면 AJAX인지 구분할 수 있다. 이렇게 하면 클라이언트가 HTML 덩어리 대신 구조화된 에러 응답을 받게 된다.

클라이언트 사이드에서는 header.jsp의 헤더 배지 폴링과 chat-widget.js의 채팅 폴링 모두 403 수신 시 폴링 자체를 멈추도록 수정했다. 그 요청만 무시하는 게 아니라 setInterval을 clearInterval 하는 것. 한 번 중단된 다음엔 페이지 리로드나 로그인 없이는 재개되지 않는다.

이 버그는 로컬 실브라우저로 검증까지 했다. 세션 없는 상태에서 위젯이 있는 페이지를 열고, 네트워크 탭에서 폴링 요청이 계속 가는지 확인하는 방식. 코드만 보고 될 것 같다고 올리기에는 찝찝한 종류의 버그라서, 눈으로 확인하고 커밋했다.

에러 페이지에 파트너 포탈 바로가기 버튼을 추가한 것도 이 흐름에서 나왔다. admin-error.jsp를 건드리면서, 관리자 로그인 오류 상황에서 파트너 포탈로 돌아가는 경로가 없다는 게 눈에 들어온 것.

정산 도메인 코드들

오후 들어 업무 도메인 쪽 커밋들이 쌓였다.

예치금 송금 원복은 관리자가 잘못 처리된 송금을 되돌리는 기능이다. 발송예치금에서 차감된 것을 복원하면서, 판매대금이나 쿠폰예치금도 함께 원래대로 돌아와야 한다. 여러 잔액 항목이 한 트랜잭션 안에서 함께 조정되어야 하니까, 원복 타입을 BalanceChangeType.java에 추가하고 파트너 웹 컨트롤러에 엔드포인트를 연결했다. ExcelConfigRegistry.java도 건드린 것을 보면 원복 내역을 엑셀로도 내보낼 수 있게 한 것 같다. API 레퍼런스 문서와 settlement-domain.md 룰 파일도 업데이트했다. 6개월 후에 이 코드를 다시 볼 때 뭘 하려 했던 건지 알 수 있게 하는 게 중요하다.

파트너 목록 상단에 매칭 관리 바로가기 버튼을 추가한 건 간단한 작업이지만, 실제로 쓰는 사람한테는 메뉴 두세 번 클릭을 줄여준다. 자주 오가는 화면은 경로가 짧아야 한다.

송금 이력 원복 버튼 스타일 통일도 있다. btn-xs outline 스타일이 매칭 관리 표의 다른 버튼들과 다르게 생겨서 btn-danger btn-sm으로 맞췄다. 기능은 맞는데 눈에 거슬리는 종류. 이런 걸 방치하면 UI 일관성이 조금씩 무너진다.

발송포탈 회원 자동매핑 스코프 버그는 신경 쓰였다. 매핑 파트너와 그 하위 소속 회원만 허용해야 하는데, 실제로는 범위가 더 넓게 열려 있었다. 쿼리 매퍼에서 조건이 제대로 걸리지 않았던 것. 수정하고 coupon-issue.jsp도 함께 다듬었다. 이건 보안 성격이 있어서 이번에 잡아둔 게 다행이다.

OhmLab - 오늘 심사 대기열에 올라갔다

하루 중에 제일 낯선 순간이 여기였다. 서버 코드 보다가 Flutter 프로젝트로 컨텍스트가 완전히 넘어가는 것.

OhmLab은 전자부품 계산기 앱이다. bendwise 프로젝트를 포크해서 전자공학 툴킷 도메인으로 완전히 재작성한 것. 커밋 순서를 보면 오늘 하루 안에 포크 → 도메인 리라이트(엔진, 화면, 8개 로케일 i18n, 앱 아이콘, 법적 문서 시드) → 심사 요건 수정 → ASC 연동 → 제출까지 전부 찍혀 있다. 물론 실제 작업은 며칠에 걸쳤을 수 있고 오늘 마지막 단계들이 완료된 것일 수도 있다. 어쨌든 오늘 날짜에 launch complete 커밋이 찍혔다.

8개 로케일 i18n은 영어, 독일어, 스페인어, 프랑스어, 이탈리아어까지 arb 파일이 다 있다. 계산기 앱이니까 UI 문자열이 많진 않겠지만, 단위 이름이나 에러 메시지 같은 것들도 다 번역이 필요하다. 로케일별로 미묘하게 다른 표현도 있고.

심사 요건 수정 커밋에 담긴 내용이 흥미롭다. SI 단위 입력에 텍스트 키보드를 쓰게 바꾸는 것, m=milli 파서 처리, EIA-96 표준에서 H=x10 처리, 구매 가용성 재시도 로직, 키워드 90자 이상 확보. 이게 다 애플 리뷰어가 걸 수 있는 포인트들이다. 앱 심사는 단순히 크래시 안 나는 것 이상이라서, 이런 세부 사항을 하나하나 맞춰야 한다. 특히 EIA-96 같은 업계 표준 처리는 리뷰어가 직접 테스트할 수 있는 항목이니까 정확해야 한다. 심사 노트도 별도로 작성해서 기능 설명이 필요한 부분을 리뷰어에게 미리 안내했다.

ASC 연동 커밋에는 스크린샷 파일들이 담겨 있다. iPhone, iPad 각각 영어 기준 여러 장씩. 해상도, 비율, 기기별 요건이 다 다르고, 프레임 처리나 로케일별 변형까지 있으면 이게 보기보다 손이 많이 간다. lib/paywall_shot_harness.dartstore/iap_review_shot.py를 보면 스크린샷 생성을 어느 정도 자동화한 것 같다. 그래도 확인하고 프레임 씌우고 업로드하는 과정이 있다.

마지막 커밋 메시지가 "launch complete - v1.0 + IAP 6.99 both WAITING_FOR_REVIEW". 앱 자체와 인앱구매 6.99달러짜리가 동시에 심사 대기 상태다. 지금은 애플 쪽으로 공이 넘어간 것이다. 통과될지, 거절되면 어떤 이유일지는 모르지만 할 수 있는 준비는 다 했다. 심사 리뷰 수정까지 넣었으니.

나머지, 그리고 자동으로 돌아간 것들

cafe24에서 121 서버로 이전한 내용이 hedvion-CLAUDE.md와 각 프로젝트 CLAUDE.md에 반영됐다. coinbot, kospibot 같은 봇들의 위치 정보와 접속 경로도 업데이트. 이런 문서 작업은 커밋 몇 줄이지만, 인프라 상태를 기록으로 남겨두는 게 나중에 같은 시스템을 건드릴 때 기준점이 된다. 실측 없이 문서만 믿지 말라는 원칙을 갖고 있지만, 그렇다고 문서가 없어도 된다는 얘기는 아니다. 문서가 있어야 무엇을 실측해야 하는지 알 수 있다.

psy 사이트의 테스트 콘텐츠 자동생성은 오늘도 돌아갔다. friend-roommate-type, popup-store-type, spirit-board-midnight 세 개. 봇이 만든 것이고 내가 뭔가 한 게 아니다. 그냥 시스템이 작동하고 있다는 것.


오늘 배포 파이프라인 쪽은 커밋이 네 개 연속으로 이어지면서 점점 정교해졌다. JspC 사전컴파일, 롤링 배포 기본 구조, 웜업 게이트, 타이밍 최적화까지. 각각이 독립적으로 보이지만 전부 같은 문제 - 배포 직후 첫 방문자 경험 - 를 향하고 있었다. 끝에서 보면 제법 믿을 만한 결과물이 됐다고 생각한다. OhmLab은 이제 애플한테 공이 넘어간 상태다.

댓글 0

첫 댓글 달아줘.