일기 slecs

보안 패치 18건이 흘러가는 동안 다섯 프로젝트가 끓었다

목차

오전에 커밋 목록을 처음 열었을 때 기분이 묘했다. 보안 감사 결과 첫 묶음이 12건이었다. 하나씩 읽으면서 "이게 지금까지 열려있었다고?" 싶은 것들이 섞여 있었다. 관리자 인가 우회, 엑셀 다운로드 스코프 검증 누락, 파트너 데이터 과노출. 특히 엑셀 쪽은 ap/excel, ap/member, ap/partner, ap/pay, ap/settlement까지 컨트롤러 여섯 패키지에 분산돼 있었다. 인가 체크가 없거나 약하게 걸려있는 엔드포인트들을 한 커밋에 묶어서 방어했다.

오후에 두 번째 배치가 왔다. 6건 추가. 이번엔 종류가 달랐다. CSRF UA 우회, 파트너 스코프 IDOR, rate limiter fail-open. rate limiter 쪽이 제일 황당했다. 레이트 리미터가 예외를 만나면 조용히 통과시키는 구조였던 거다. 막으라고 만든 장치가 오류 상황에서는 그냥 열리는 설계. 언제부터 그랬는지는 파악하지 못했고, 오늘은 일단 닫는 것에 집중했다.

이 두 배치 모두 codex가 코드를 훑어 항목을 먼저 뽑아냈고, 나는 그 결과를 받아서 실제 코드로 방어하는 역할이었다. 커밋 제목에 "codex 후속"이라는 표현이 붙은 게 그 흔적이다. 자동화 감사가 선행되고 사람이 뒤따라가며 막는 흐름인데, 이 리듬이 자리잡으니까 18건이라는 숫자도 하루 안에 소화할 수 있었다. 혼자 처음부터 다 찾았으면 이틀은 걸렸을 것이다.

돈이 걸린 트랜잭션은 다르게 긴장한다

보안 이슈 중에 F1 예치금이체 원자성·데드락 보강이 있었다. 인가 버그와는 결이 다르다. 잘못 건드리면 이체 후 잔액이 안 맞거나, 교착 상태로 트랜잭션이 아예 멈출 수 있다. 잠금 획득 순서가 코드 경로마다 달랐다. A가 X를 잠그고 Y를 기다리는데, B는 Y를 잠그고 X를 기다리는 패턴이었다. 고전적인 데드락이다. 잠금 순서를 정렬해서 항상 같은 방향으로 획득하게 바꾸고, 트랜잭션 경계를 명시적으로 다시 묶었다.

이 작업은 건드리기 전에 플로우를 머릿속에 한 번 완전히 그려보는 시간이 따로 필요하다. 급하게 손대면 더 헷갈려진다. 같은 커밋에 JSP 워밍업 시크릿 게이트도 들어갔다. 워밍업 엔드포인트가 인증 없이 외부에서 호출 가능하면 문제가 생길 수 있어서, 시크릿 검증 단계를 앞에 추가했다. 새 기능 추가할 때 이런 게이트를 빠뜨리는 경우가 생각보다 많다.

JSP 워밍업 본체는 그보다 앞서 오전 첫 커밋이었다. JspWarmupRunner를 만들어서 배포 직후 주요 공개 페이지를 미리 컴파일해두는 방식이다. 배포 후 첫 방문자가 들어올 때 JSP 컴파일 지연으로 5~10초가 걸리던 문제가 있었다. 사용자 입장에서는 그냥 느린 사이트로 인식하고 나가버리는 게 전부다. 이유가 배포 타이밍이었다는 걸 알 리 없으니까. application-prod.yml에 설정을 빼서 운영 환경에서 끄고 킬 수 있게 했다.

파트너 쪽은 오늘 세 방향으로 움직였다

파트너 조회 API를 오늘 새로 냈다. 포탈용 API 문서 페이지도 같이 붙었다. WebMvcConfig.java에 손이 갔다는 건 MVC 설정 레벨에서 경로나 인터셉터가 추가됐다는 뜻이고, 별도 interceptor 패키지가 생겼다. 외부 파트너가 인증 후 자기 데이터를 조회하는 엔드포인트다.

그런데 응답에 개인정보가 날것으로 나가고 있었다. 이름, 회원번호, 바코드. API를 만들면서 응답 마스킹을 동시에 처리하지 않으면 이렇게 된다. 마스킹 처리를 응답 레이어에 추가했다. 내부 서비스 레이어에 넣으면 내부 로직도 마스킹이 묻어나고, 컨트롤러 근처에 넣으면 외부 응답에만 적용된다. 오늘은 외부 API 전용 처리였으니 컨트롤러 계층에서 가려냈다.

담당 대리점 스코프 UI는 처음 설계에서 파트너 포탈 전체 사용자에게 보이게 됐던 것 같다. 실제로는 영업대행사 소속 매니저에게만 노출해야 하는 기능이었다. members.jsp랑 member-detail.jsp에서 세션 속성 기반으로 조건 분기를 추가했다. 이 조건을 잘못 짜면 보이면 안 되는 사람한테 보인다. 검토를 두 번 했다.

스코프 변경과 함께 admin-patterns.md도 갱신했다. PARTNER 세션 개념, 포탈 이전 맥락, 스코프 커버 범위. 코드 변경과 문서 갱신을 같은 날 묶어두는 습관이 나중에 "왜 이렇게 돼 있지?" 질문을 줄여준다.

앱 게이트 - 하드코드에서 관리자 설정으로

앱 전용 게이트를 필터 하드코드에서 관리자 설정으로 빼는 작업도 오늘 마무리됐다. 앱 전용 도메인을 필터에서 직접 비교하는 구조였는데, 조건이 바뀔 때마다 배포가 필요했다. 당연히 비효율적이다.

사이트 관리 컨트롤러에서 DB 값을 읽고 필터가 그 값을 참조하게 바꿨다. sqlmap 쿼리 매퍼가 생겼고, form.jsp에 관리자 UI가 추가됐다. 세 레이어를 한 번에 손댄 구조 변경이라 커밋이 연속으로 세 개다. 먼저 게이트 필터 자체를 추가하고, 다음 커밋에서 차단 시 리다이렉트 로직을 붙이고, 마지막에 하드코드를 관리자 설정으로 전환하는 순서였다. 리다이렉트는 차단 시 www. 도메인으로 보내는 것이다. 사용자 입장에서 앱 전용 페이지가 막혔을 때 빈 화면이 아니라 웹 사이트로 안내받는 게 낫다.

소비정산 lot별 전환 - 정산 이의제기의 밑천

소비정산 수수료 기록 방식을 lot별로 바꿨다. 기존에는 수수료가 한 덩어리로 집계됐는데, 이제는 원본 충전건(charge_sn)에 직결된다. 혼합 차감, 즉 여러 충전분에서 조금씩 빠지는 케이스에서도 어느 충전건에서 얼마가 나갔는지 추적이 가능해진다.

이게 왜 필요하냐면 정산 이의가 들어왔을 때 근거를 보여줄 수 있어야 하기 때문이다. "수수료가 이만큼입니다"만 있고 어느 충전분에서 어떻게 계산됐는지 출처가 없으면 설명이 안 된다. 충전건과 수수료 기록 사이에 연결이 생기는 스키마 레벨 변경이라 기존 데이터 정합성을 신경 써야 했다.

컴플라이언스 리포트 데이터 오류 3건도 잡았다. 누락이랑 과대표시. 쿼리 두 건은 집계 로직 문제였고 JSP 표시 로직 쪽도 하나 수정했다. 리포트 숫자가 틀리는 건 실무자 신뢰 문제로 직결되기 때문에 빠르게 처리했다. 쿼리가 맞아도 JSP에서 잘못 표시하는 경우가 있어서 detail.jsp까지 같이 확인했다.

즐거운 대량환불 체크리스트를 §10으로 문서화했다. 운영 중 대량환불 기능을 활성화할 때 빠뜨리면 안 되는 항목들이다. 이런 체크리스트가 생길 때는 항상 이유가 있다. 아슬아슬하게 넘겼거나 한 번 틀렸거나.

app-review-watch - 오늘 하루에 RSS부터 ASC API까지

오늘 완전히 새로 태어난 봇이 하나 있다. app-review-watch. 앱스토어 신규 리뷰를 디스코드로 알리는 봇이다.

시작은 RSS였다. 앱스토어 14개국 RSS 피드를 폴링해서 새 리뷰가 올라오면 디스코드 카드로 알리는 것. artistId를 입력하면 그 개발자의 앱을 자동으로 발견하게도 만들었다. watch.py의 초기 버전이 이 상태였다. 여기까지는 하루 작업이라고 하기에 충분한 양인데, 그게 끝이 아니었다.

알림만 오면 아쉽다. 봤을 때 바로 답변을 달 수 있어야 진짜 쓸모가 있다. 그래서 ASC API 연동을 추가했다. App Store Connect API를 asc.py로 감싸고, reply.py가 개발자 답변 게시 루프를 맡았다. 디스코드 카드 UI도 달라졌다. 버튼이 생겼고, 버튼 클릭 시 모달이 뜨고, 모달에서 내용을 입력하면 ASC API로 게시된다. interactions.py가 그 엔드포인트 역할이다. Flask 쪽이 하나 더 생겼다는 뜻이다.

삽질은 답변 교체 로직에서 났다. 기존에 달려있는 답변이 있는 리뷰에 새 답변을 쓰면, 내부적으로 기존 것을 삭제하고 새로 게시하는 흐름이었다. 삭제는 됐는데 재게시가 실패하면 기존 답변이 그냥 사라진다. 날아가는 거다. 운영에서 이게 터지면 꽤 당황스럽다. 재시도 5회를 넣고, 실패 시에도 원복할 수 있게 처리했다.

모달에 기존 답변 프리필도 붙었다. 기존 내용을 보면서 수정하는 게 자연스러운 흐름이니까. 카드 footer에 "답장하면 교체됨" 안내 문구도 추가했다. 없으면 "기존 답변 어디 갔어요?" 질문이 꼭 온다. 문구 하나가 혼란을 없앤다.

이 봇이 오늘 하루 동안 RSS 폴링에서 ASC API 연동, 디스코드 버튼 모달 UI, 답변 교체 실패 버그 픽스, 프리필, 안내 문구까지 갔다. 꽤 긴 거리다. CLAUDE.md에 봇 등재도 했다.

Pilmori - 필터 23종, 8개국어, 그리고 심사 제출

iOS 앱 Pilmori에서 오늘 세 가지 일이 있었다. 필터 추가, i18n, 심사 제출.

올드폰 필터 6종이 Y2KFilterChain.swift에 들어갔다. 3G, VGA, FLIP, XENON, OLED, MMS. 전부 Pro로 분류해서 페이월 뒤에 놓았다. 22종이 됐고, 여기서 WEBCAM 필터 하나가 더 붙어 23종. WEBCAM은 2000년대 초 웹캠 셀카 특유의 뽀샤시하고 초점이 좀 흐른 룩을 구현한 것이다. 필터를 추가할 때마다 PaywallView.swift에서 Pro 개수 텍스트를 수동으로 고쳐야 하는 게 번거로웠는데, 오늘 동적화했다. 이제 필터 배열 길이에서 읽어온다.

8개국어 i18n이 제일 손이 많이 갔다. ko, en, ja, es, pt-BR, id, hi, ar. Xcode의 String Catalog 방식을 쓰는데, Localizable.xcstrings와 InfoPlist.xcstrings 둘 다 건드렸다. 카메라 퍼미션 문자열, 워터마크 텍스트, 페이월 UI 문자열들이 전부 들어갔다. 아랍어(ar)는 RTL이라 레이아웃 방향 처리를 따로 확인해야 하는 경우가 있다. SwiftUI에서 대부분은 자동 처리되지만 커스텀 컴포넌트가 있으면 직접 확인이 필요하다. 오늘 작업 후 i18n 키 누락이 없는지 내일 다시 확인해야 한다. 한 번에 8개 언어를 넣으면 빠진 키가 꼭 나온다.

이걸 build 7, 버전 1.4로 묶어서 심사 제출했다. project.yml에 버전 번호가 들어갔고, CLAUDE.md에 제출 완료 기록을 박아뒀다.

Casaproof - 오늘 새로 심사에 들어간 앱

홈 인벤토리와 보험 증빙 PDF를 만들어주는 앱 Casaproof 1.0이 오늘 iOS 심사에 들어갔다. ASC ID 6792828978. 포트폴리오 페이지에 카드도 추가됐다. index.html에 링크, thumbs/에 썸네일.

출시 과정에서 함정 두 가지를 문서화했다. iCloud 컨테이너 설정과 프로비저닝 프로파일은 선행 작업이 필요한데, 이걸 나중에 하면 빌드 단계에서 막힌다. reviewSubmission에서 copyright 필드 409 충돌은 이미 등록된 값이 있어서 중복으로 시도할 때 나는 오류다. docs/mobile-app-launch.md에 이런 함정들을 계속 쌓아가고 있다. 다음 앱 낼 때 보면 시간을 아낀다.

bloomly 스샷 삽질 로그

bloomly는 Flutter 앱인데, 오늘 스크린샷 관련 일이 여러 건 겹쳤다.

2.3.10 반려가 왔다. 안드로이드 상태바가 찍힌 스크린샷이 iOS 앱 스토어 제출에서 사용됐던 게 문제였다. 규정 5.1.5로 분류됐는데, 오심이라고 판단해서 Resolution Center에 회신 대응을 기록했다. 동시에 스크립트를 만들었다. iosify_regen.py가 안드 스샷을 iOS 상태바 형태로 재합성하고, asc_fix_ss.py가 ASC에 교체 업로드한다. ar, en, es, hi, id 로케일에서 장당 2~3장씩이니 총 10장 이상이 재합성됐다.

1번 장 로케일 재사용 발각도 있었다. en 리스팅에 한글 UI 스샷이 올라가 있었다. 2번 장부터는 로케일별로 처리했는데 1번 장만 공용으로 쓰고 있었던 거다. iosify_regen.py를 확장해서 1번 장도 로케일별로 생성했다. 커밋 제목에 "발각"이라는 표현이 쓰인 걸 보면 그때 좀 당황했겠지 싶다.

심사 중에 스샷을 삭제하면 심사가 자동 취소된다는 함정도 문서화했다. 이 함정은 모르면 한 번 당한다. 심사 대기 큐에서 날아가면 다시 제출하고 기다려야 한다.

진상 리뷰 오보가 12건이었다. 리뷰 내용을 검증 없이 보고했다가 사실과 다른 케이스가 12건 나왔다는 것이다. 건당 독립 반박 검증 후 확정만 보고하는 프로세스를 지침화했다. 앱 리뷰 중에는 구버전 얘기거나, 재현이 안 되거나, 다른 앱을 착각한 것들이 섞인다. 무조건 받아들이면 엉뚱한 방향으로 대응하게 된다.

조용한 배경에서 돌아가던 것들

psy는 심리테스트 콘텐츠 사이트인데 매일 자동으로 테스트가 생성된다. 오늘은 friend-lie-reaction, korean-holiday-type, midnight-gumiho 셋이었다. JSON 콘텐츠 파일과 webp 커버 이미지가 자동으로 생겼다. 파이프라인이 돌아가는 것이라 손댈 게 없다.

AdMob 스토어 연결 완료 기록이 여러 레포 CLAUDE.md에 오늘 날짜로 찍혔다. 연결 작업이 끝날 때마다 각 앱 문서에 상태를 기록해두는 루틴이다. 여러 앱에 동시에 처리한 것 같다.


오늘 커밋 수를 세면 30개가 넘는다. 보안 배치가 두 번, 파트너 API 신설, 앱 게이트 재설계, 예치금 트랜잭션 보강, 소비정산 구조 변경, 리포트 버그 3건, 봇 하나가 오늘 처음부터 끝까지 만들어졌고, iOS 앱 두 개가 심사에 들어갔고, Flutter 앱은 스샷 반려 대응으로 합성 스크립트 두 개가 생겼다.

그 어떤 항목 하나만 있어도 꽤 묵직한 날인데, 전부 같은 날에 쏟아진 것이다.

내일은 Pilmori i18n 키 누락 확인이랑 bloomly 재제출 상태부터 봐야 한다. 보안 감사 3차 배치가 올 가능성도 있다.

댓글 0

첫 댓글 달아줘.