결제대행사 암호화 비활성화
목차
결제대행사 MERCHANT API 쪽 암호화 로직을 임시 비활성화하는 버그 수정을 처리했다. 수정 대상은 내부 클래스 하나였고, 변경 범위 자체는 작았지만 결제 흐름에 직접 닿는 코드라 손이 무거웠음.
암호화를 임시로 꺼야 하는 상황이 왜 생기냐면 - 결제대행사 측에서 API 스펙 업데이트나 키 교체 작업을 진행하는 과도기에 "검증 환경이 준비되기 전까지 plain으로 올려달라"는 안내가 내려오는 경우가 있다. 내부 암호화 로직과 외부 검증 엔드포인트가 버전 미스매치 상태일 때 암호화된 요청이 계속 거절되는 구조라서, 이 시기를 우회하는 임시 처리가 불가피했음. 이런 변경은 "임시"라는 딱지가 붙는 순간 나중에 잊히기 쉬워서, 코드 주석에 복원 시점 조건과 관련 이슈 번호를 같이 달아두는 게 습관이 됐다. 안 그러면 몇 달 뒤에 "이거 왜 꺼져 있지?"가 반드시 나온다.
이번에 수정한 내부 클래스는 결제 페이로드를 대행사 API로 보내기 직전에 암호화하는 역할을 하는 곳이었다. 이 클래스에서 버그가 터지면 암호화 단계 자체가 실패하고, 요청이 막히거나 잘못된 형태로 나가게 됨. 결제 도메인에서 이 위치의 클래스는 민감하다 - 수정 전에 영향 범위를 반드시 그려봐야 하고, 배포 후 같은 요청으로 즉시 재확인하는 게 기본이다.
수정 과정: 재현 → 원인 → 최소 범위 → 배포
재현 단계에서 처음엔 전체 결제 플로우 문제인 줄 알고 넓게 봤음. 그런데 조건을 좁혀보니 암호화 객체 초기화 경로가 상품 타입에 따라 갈리는 케이스에서만 터지는 패턴이었다. 로그에 NPE가 찍혀 있어서 null 체크 방향으로 바로 잡았음.
원인은 조건문 순서 문제였다. 암호화 객체 null 여부를 확인하기 전에 메서드를 먼저 호출하는 구조:
// 수정 전 - encryptor가 null이면 isEnabled() 호출 시점에 NPE
if (encryptor.isEnabled() && encryptor != null) {
payload = encryptor.encrypt(payload);
}
// 수정 후 - null 체크 선행
if (encryptor != null && encryptor.isEnabled()) {
payload = encryptor.encrypt(payload);
}
조건 순서 하나 바꾼 게 전부다. 작아 보이지만 이게 결제 요청 실패로 이어지는 구조였음. 수정 범위는 이 조건문과 암호화 활성 여부 플래그 처리에만 국한했다. 운영 중에는 건드리는 범위가 늘어날수록 예상 못 한 사이드이펙트 가능성이 같이 올라가기 때문에, "최소 범위"를 의식적으로 지키는 게 중요하다. 특히 결제 관련 클래스는 혹시 모르는 부분을 건드렸다가 다른 상품 타입까지 영향이 번지면 롤백 판단을 빠르게 해야 하니까.
운영에서 반복적으로 나오는 버그들을 유형별로 정리해두면 원인 파악 속도가 눈에 띄게 빨라진다. 지금 기준으로 자주 보이는 패턴:
| 패턴 | 증상 |
|---|---|
| null 체크 순서 오류 | NPE (NullPointerException) |
| 경로 예외 처리 누락 | 필터에서 정상 요청 차단 |
| YAML 공백 혼용 | 설정값 파싱 실패 |
| import 누락 | 컴파일 에러 |
새 버그 리포트가 들어오면 이 리스트부터 훑는 게 루틴이 됐음. 이번 케이스는 첫 번째 행이었다. YAML 공백 혼용은 탭과 스페이스를 섞어쓴 설정 파일이 특정 배포 환경에서만 파싱 실패하는 케이스인데, 재현 환경이 제한돼서 원인 찾는 데 시간이 더 걸린다. IDE에서 공백 가시성 옵션 켜두는 게 가장 간단한 예방책임. 컴파일 에러야 빌드 단계에서 잡히니까 낫고, 경로 예외 처리 누락은 정상 요청이 필터에서 차단되는 거라 QA 단계를 잘 통과해도 운영에서 특정 경로에서만 터지는 경우가 있어서 성가시다.
블랙리스트 자동 등록 구조
같은 날 공격성 요청 차단 구조도 정비했다. 수동으로 대응하면 감지 → 확인 → 등록 사이에 시간이 걸리고, 그 간격 동안 동일 IP에서 계속 요청이 들어오는 게 문제였음. DB에 자동 등록하고 다음 요청부터 즉시 차단하는 구조를 만들면 실시간 대응이 가능하다.
자동화할 때 신경 써야 할 포인트가 세 가지였다:
- 오탐 가능성 - 로드 테스트나 배치 작업이 봇으로 오인되지 않도록 화이트리스트 IP 범위와 예외 조건을 함께 관리해야 한다. 자동 등록 트리거 조건을 너무 넓게 잡으면 내부 테스트가 차단돼서 배포 직후에 혼란이 생김.
- 만료 설정 - 영구 차단 대신 TTL을 두어서 오탐 시 자동 복구 가능하게. 반복 감지 시 TTL을 단계적으로 늘리는 방식이 운영하기 편했음. 첫 감지는 6시간, 재감지는 24시간, 세 번째부터는 72시간 식으로.
- 알림 연동 - 자동 차단이 발생하면 슬랙/디스코드 알림으로 모니터링. 차단 건수가 갑자기 치솟으면 오탐일 가능성이 높아서 빠르게 확인할 수 있음.
TTL 설정이 특히 중요하다. 영구 차단으로 계속 쌓기 시작하면 나중에 오탐 복구 요청이 들어왔을 때 어디서부터 어떻게 풀어줄지 관리 비용이 생각보다 올라간다. 처음부터 만료 구조를 잡아두는 게 낫고, 영구 차단이 필요한 케이스는 별도 플래그로 명시적으로 관리하는 게 덜 복잡함.
운영 관점에서 본 Rate Limit
Rate Limit을 설정할 때 단순히 "얼마나 많이 막을 것인가"보다 "정상 사용자를 막지 않으면서 공격을 차단하는가"가 핵심임. 너무 타이트하게 잡으면 정상 사용자가 피해를 보고, 너무 느슨하면 의미가 없다.
Redis 슬라이딩 윈도우 방식은 고정 윈도우 대비 경계 구간 버스팅 공격에 강하다. 고정 윈도우는 윈도우 경계 직전에 집중 요청을 보내면 허용 한도 2배 가까운 요청이 통과되는 허점이 있음. "분당 100건" 고정 윈도우라면 0:59에 100건, 1:00에 100건을 연속으로 보낼 수 있는 구조라서, 공격 입장에서는 경계 타이밍만 잘 잡으면 제한을 사실상 우회하는 셈이다. 슬라이딩 윈도우는 이 경계 허점 자체가 없다.
단, Redis가 단일 장애점이 되지 않도록 연결 풀과 타임아웃 설정을 같이 챙겨야 한다. Redis가 다운됐을 때 Rate Limit을 bypass할지(fail-open) 아니면 전부 차단할지(fail-closed) 정책을 미리 정해둬야 함. 결제 도메인에서는 Redis 장애 시 통과를 선택하는 편임 - Rate Limit 오류로 정상 결제를 막는 손해가 더 크기 때문이다. 반대로 보안 민감도가 높은 인증 엔드포인트라면 fail-closed가 맞을 수 있어서, 서비스 특성에 따라 판단이 달라진다.
이번 작업들은 규모가 크지 않았지만 운영 안정성을 위한 필요한 변경이었음. 이런 작은 수정들이 쌓여서 전체 시스템 신뢰도를 높이는 건데, 그게 눈에 잘 안 보이는 게 아쉬운 점이기도 하다. 암호화 임시 비활성화는 언제 다시 켤지 일정과 조건을 명시적으로 잡아뒀다. "임시"가 영구가 되는 건 항상 일정을 안 잡았을 때다.
댓글 0
첫 댓글 달아줘.