일기 slecs

공용 서브넷의 벽과 애플의 벽 사이에서

목차

아침에 제일 먼저 열어본 게 앱스토어 메일이었다. 예상은 했지만 막상 보면 기분이 좋진 않다. 2.1 거절. ATT.

"앱이 추적하지 않는다고 했는데 바이너리에 추적 관련 문자열이 남아있습니다" - 대충 그런 맥락이다. App Tracking Transparency 위반인데, 이 앱은 진짜로 추적을 안 한다. 광고 SDK도 없고, IDFA도 안 쓴다. 그런데 이전 버전에서 ATT 퍼미션 문자열을 Info.plist에 넣어뒀던 게 화근이었다. 기능을 실제로 쓰지 않아도 NSUserTrackingUsageDescription 키가 파일에 살아있으면, 애플 입장에선 추적 의도가 있는 앱으로 잡는다.

de, en, es, fr, it - 6개 언어 파일 전부를 열어서 해당 키를 제거했다. 그러면서 App Store Connect 쪽 App Privacy 설정을 "데이터 수집 없음"으로 바꾸고 Resolution Center에 답변도 달았다. 빌드 번호 1.1.0(8)로 올려서 재제출.

근데 여기서 한 번 막혔다. ATT 문자열을 다 지우고 빌드를 올렸는데 Submit 버튼이 비활성화 상태였다. 한참 왜 안 되는지 봤더니, ASC 쪽 App Privacy 탭의 설정값이 이전 상태 그대로였다. 바이너리랑 메타데이터 설정이 불일치하면 재제출 자체가 잠기는 구조다. 이걸 몰랐다. 탭 들어가서 "데이터를 수집하지 않음"으로 바꾸고 나서야 버튼이 풀렸다. 이 흐름을 docs에 기록해뒀다. 나중에 또 잊어먹을 게 뻔하니까.


ORDER BY 하나가 없으면 민원 배분 순서가 복불복

iOS 마무리하고 나서 본 프로젝트로 넘어갔다. 민원 홀드 쪽 쿼리가 오전부터 마음에 걸렸다.

먼저 잡은 건 2건 이상 민원 홀드가 동시에 처리될 때 배분 순서 문제다. ORDER BY가 없으면 어느 민원이 먼저 처리될지 보장이 없다. 먼저 등록된 민원부터 처리하는 게 상식인데, complaint_sn 기준 오름차순이 빠져있었다. fix 커밋 하나로 끝났지만, 이런 게 실제 운영에서 불필요한 분쟁 소지가 된다.

그다음이 홀드 이력 표시 방식 개선이다. BALANCE_IN - 판매대금 유입 - 이 연속으로 여러 건 들어오는 경우, 기존은 분 단위로 줄줄이 펼쳐서 보여줬다. 담당자가 "이 기간 동안 총 얼마가 유입됐나"를 파악하려면 직접 더해야 하는 상황. 화면도 지저분하고.

연속 구간을 묶어서 한 줄로 합산하는 방향으로 바꿨다. 쿼리에서 구간 병합 로직을 짰고, JSP 두 곳(detail 페이지, partner portal의 complaint 페이지)을 같이 손봤다. 윈도우 함수로 연속 구간을 탐지하는 방식인데, 생각보다 깔끔하게 나왔다. 연속 구간 합산 결과를 보여주고 분 단위 상세는 접어두는 형태다.


레플리카 폴백 구조, 그리고 Redis로 토글 이사

민원 쪽을 마무리하고 나서 오늘 가장 큰 코드 작업으로 들어갔다. 읽기/쓰기 DB 라우팅 인프라.

서버가 늘어나면서 레플리카를 어떻게 활용할지에 대한 얘기가 계속 있었다. analytics, dashboard, excel, partner, pay - 다섯 개 도메인 컨트롤러에 읽기 전용 쿼리는 레플리카로, 쓰기 쿼리는 마스터로 보내는 구조를 잡았다. 레플리카가 죽거나 지연이 심하면 마스터로 폴백한다. deploy-prod-server3 워크플로도 이 시점에 같이 추가했다. 서버3이 들어오면서 배포 파이프라인에서 빠져있던 부분이었다.

그런데 처음 설계에서 라우팅 토글 저장소를 사이트 설정 테이블에 뒀다. 짜고 나서 보니까 의미 불일치가 명확했다. DB 문제가 생겼을 때 DB를 조회해서 DB 라우팅을 조정해야 하는 구조가 되는 거다. 글로벌 스위치인데 DB 테이블에 있으면 안 맞다.

ReplicaRouteToggle을 Redis 전역 키로 옮겼다. Redis는 이미 세션이랑 캐시로 쓰고 있어서 인프라 추가도 없다. 리팩터 자체는 크지 않았지만, 이 판단은 일찍 할수록 좋다. 운영 중에 고치면 더 번거로웠을 거다.

점검모드 필터는 거기서 이어서 붙였다. Java 필터 클래스 하나, maintenance.html 하나, application-prod.yml에 프로퍼티 추가. NAT 전환 작업 당일에 서비스를 잠깐 내려야 할 수 있어서 미리 만든 것. Redis 키 기반으로 온오프 하고, 허용 IP 목록은 통과시키는 구조다. 내부 직원이나 제휴사 IP는 점검 중에도 접근이 필요하니까.


공용 서브넷이 막고, 그다음엔 8줄로 뚫었다

오후 들어 NAT 판정 작업에 본격적으로 시간을 썼다. 오늘 가장 머리를 많이 쓴 부분이다.

계획은 인터넷 게이트웨이를 완전히 들어내고 NAT Gateway로 전환하는 것이다. 아웃바운드는 NAT GW를 타고, 인바운드는 로드밸런서가 받는 구조. 이론상 명확한데, 실제 클라우드 API가 이론대로 안 움직일 때가 있다. 오늘 직접 검증했다.

첫 번째로 확인한 건 치명적인 제약이었다. 공용 서브넷의 기본 라우트, 즉 0.0.0.0/0 → IGW로 박혀있는 그것을 API로 교체하려고 했더니 오류가 났다. "이미 존재하는 라우트" 류의 에러다. 공용 서브넷의 기본 라우트는 특수 취급이라 단순 업데이트나 교체가 안 된다. 이 실측 결과를 바탕으로 첫 번째 판정 보고서를 썼다. "전면 NAT 불가 확정."

근데 거기서 멈추기가 찜찜했다. 결론 자체가 너무 아쉽기도 했고, 정말 우회로가 없나 싶어서 더 팠다.

0.0.0.0/0 하나로 못 바꾸면, 더 구체적인 CIDR 여러 개로 쪼개면 어떨까. 구체적인 라우트가 기본 라우트보다 우선순위가 높으니까, 기본 라우트를 건드리지 않고도 사실상 전체 트래픽을 NAT로 보낼 수 있다. IP 공간 전체를 커버하는 8개 라우트를 넣으면 된다 - 1.0.0.0/8 ~ 128.0.0.0/1 범위. 이걸 전부 NAT GW로 향하게 하면 실질적으로 전면 NAT다.

실제로 테스트해서 동작을 확인했다. 그래서 두 번째 판정 보고서가 나왔다. "8줄 분할 커버로 가능 확정." 같은 날 같은 파일명으로 판정이 뒤집어졌다. 첫 번째 보고서 쓸 때의 기분이랑 두 번째 쓸 때의 기분이 많이 달랐다.


스크립트, 워크플로, 런북 - 내일을 위한 세팅

판정이 확정되고 나서 클라우드_nat_routes.py를 짰다. 8개 라우트를 한 번에 투입하고, 롤백할 때도 한 번에 원복하는 스크립트다. applyrollback 두 명령이다. 내일 작업 당일에 실수 없이 빠르게 실행하려고 만든 것이고, 롤백 경로를 같이 만든 건 당연한 수순이다.

CI/CD 워크플로 수정도 같이 했다. 전면 NAT가 켜지면 서버들의 아웃바운드 IP가 NAT GW 고정 IP로 바뀐다. 지금 셀프호스팅 러너가 서버에 직접 붙는 구조인데, NAT 뒤에서는 그 경로가 달라진다. 점프 서버를 경유해야 한다.

deploy-prod.yml, deploy-prod-all.yml, deploy-prod-server2.yml, deploy-prod-server3.yml 네 워크플로 전부에 via_jump 스위치를 추가했다. 평소엔 false, NAT 전환 후엔 true로 바꾸면 점프 서버 경유 배포로 전환된다. jump_runner_setup.sh는 점프 서버에 CI/CD 러너를 설치하는 스크립트다. 내일 순서는 NAT 켜기 전에 점프 서버 러너를 먼저 올려두고, NAT 투입 후 via_jump: true로 스위치 올리는 것이다.

오늘 마지막은 제휴사 허용 IP 런북이랑 증설 통합계획서였다. NAT 전환 후 아웃바운드 IP가 고정 IP로 바뀌면, 지금 우리 서버 IP를 허용해둔 제휴사들한테 새 IP 등록 요청을 넣어야 한다. 어떤 제휴사가 있고, 연락 채널이 뭔지, IP를 어떻게 전달하는지 런북으로 정리했다. 내일 NAT 켜고 나서 바로 써야 하는 문서다.

증설 통합계획서는 내일 하루 안에 해야 하는 것들을 순서대로 묶은 것이다. NAT 투입, 제휴사 IP 알림, 점프 러너 확인, 배포 워크플로 스위치, 서버3 실전 배포 테스트. 이 순서를 공유 문서로 정리해서 직원들이랑 맞췄다.


psy 자동 테스트는 오늘도 조용히 돌았다. change-acceptance-type, destiny-butterfly, midnight-abandoned-park 세 개, 커버 이미지까지 생성됐다. 따로 건드린 게 없으니 자동화가 잘 돌고 있다는 것이다.


오늘 하루를 압축하면, 서로 성격이 다른 압박 세 개가 동시에 있었다. 애플 심사는 재제출 마감 같은 느낌이라 아침에 먼저 치워야 했다. 민원이랑 DB 라우팅은 운영 이슈라서 미루기가 어려웠다. NAT 판정과 준비 작업은 내일 실행이 있어서 오늘 반드시 완료해야 했다.

세 줄기를 하루에 다 마무리했다는 점은 뿌듯하다. 그 중에 가장 잘 됐다고 생각하는 건 NAT 판정이 "불가"에서 "가능"으로 뒤집어진 것. 처음 보고서 쓰고 나서 진짜로 "이거 못 하겠다" 싶었는데, 조금 더 파보니까 우회로가 있었다. 실측으로 확인하고 나서야 확신이 생겼고, 확신이 생기고 나서야 스크립트를 만들 수 있었다. 실측하지 않고 첫 판정에서 멈췄으면 오늘 결론이 많이 달랐다.

찜찜한 건 남는다. 내일 실제로 켜보기 전까지는 스크립트가 완전히 맞다는 보장이 없다. 롤백 경로가 있으니까 최악의 경우엔 원복하면 된다고 생각하지만, 제휴사 연동이 끊기는 시간이 길어지면 바로 문제가 된다. 내일 이 시간에는 NAT가 켜져있거나, 아니면 롤백된 상태이거나 둘 중 하나다.

댓글 0

첫 댓글 달아줘.