Onion Store와 내가 맡은 범위
- 프로젝트 명: onion store
- 인원: 5명
- 프로젝트 기간: 10일
- 배경: 한 명의 판매자가 직접 상품을 판매하는 단일 판매자 쇼핑몰
- 주요 기능: 고객의 장바구니·주문·결제, 관리자의 상품·주문 관리, 채팅(cs)
- 담당 : 결제, 환불
요약
처음에는 결제 기능을 구현하고, 결제창을 닫은 사용자의 미완료 주문을 정리하는 스케줄러를 붙이면 된다고 생각했다. 하지만 결제창 이탈을 처리하려는 작은 요구사항은 결제 상태 확인, 웹훅, 금액 불일치 취소, 부분 환불, 외부 결제 취소 결과를 다시 확인하는 방법에 대한 고민까지 이어졌다.
이번 글에는 짧은 기간의 팀 프로젝트에서 정책이 정해지지 않은 기능을 구현으로 먼저 해결하려 했을 때, 설계와 PR 단위가 왜 함께 어려워졌는지 돌아본다.
스케줄러 하나면 될 줄 알았다
결제창을 열면 서버에는 주문과 결제 대기 데이터가 남는다. 사용자가 결제를 끝내지 않고 창을 닫았다면, 오래 남은 주문을 취소하고 점유한 재고를 복구할 필요가 있다고 생각했다. 그래서 일정 시간이 지난 결제를 찾아 처리하는 스케줄러를 고려했다.
처음에는 만료 시간이 지나면 주문과 결제 상태를 변경하고 재고를 복구하면 된다고 생각했다. 이때 서버에 남은 주문과 결제 대기 데이터를 삭제할지, 상태만 바꾸고 보관할지도 함께 고민했다.
그런데 시간이 지났다는 이유만으로 정말 이 주문을 취소해도 될까?
- 사용자가 결제창을 닫은 경우
- 실제 결제는 성공했지만 우리 서버가 완료 요청의 응답을 받지 못한 경우
- 결제대행사 처리 또는 네트워크 문제로 서버에 결과가 늦게 도착한 경우
이 상태들을 모두 같은 방식으로 취소하거나 삭제하면, 이미 결제가 완료된 주문의 상태를 잘못 처리하거나 원인을 확인할 데이터를 잃을 수 있다. 결제창을 닫았는지가 아니라 실제 결제가 어떤 상태인지 확인해야 했고, 상태가 종료된 뒤 어떤 데이터를 얼마나 남길지도 정해야 했다.
웹훅을 받으면 본문만 보고 상태를 바꾸지 않고, PortOne에 결제 정보를 다시 조회해 실제 결제 상태와 금액을 확인하도록 했다.
결제 검증에서 환불 도메인까지 범위가 넓어졌다
결제 검증 과정에서 승인 금액이 주문 금액과 다를 수 있다는 경우를 발견했다. 이런 경우에는 주문을 완료 처리하면 안 되고, 이미 승인된 결제를 취소하는 흐름이 필요했다. 나는 이 금액 불일치 취소 로직을 먼저 작성했다.
기존에 있던 금액 불일치로 인한 시스템 취소 로직과 고객이 요청하는 환불을 같은 흐름으로 만들고 싶었다.
여기서 전액 환불뿐 아니라 부분 환불, 고객 요청과 관리자 승인, 환불 수량 검증, 결제 상태 변경, 재고 복구를 함께 고려하게 됐다.
환불 정책도 단순하지 않았다. 진행 중인 환불은 결제당 하나만 허용하고, 관리자가 승인 후에 남은 수량 범위에서 다음 부분 환불을 허용하는 식으로 정리했다. 동시에 여러 건을 처리하지 않고, 완료된 환불을 누적해 관리하는 방식이었다.
여기에 외부 취소 요청의 결과가 바로 확정되지 않는 경우까지 붙었다. 요청 시간 초과나 응답 유실이 발생하면 취소가 실패한 것인지, 결제사에서는 이미 처리됐지만 우리 서버만 결과를 받지 못한 것인지 알 수 없다. 취소 식별자 저장, 같은 요청의 중복 실행 방지, 웹훅 도착 순서, 요청 상태를 다시 확인하는 스케줄러까지 구상하게 됐다.
코드를 보고 있으면 또 다른 예외 상황이 보였고, 이를 처리하려 하면 상태나 정책이 하나 더 필요했다. 당시에는 어디까지 구현하고 멈춰야 할지가 가장 어려웠다.
회의를 미루기로 한 결과
새로운 상태와 테이블, 스케줄러 정책이 필요하다고 느낀 시점은 프로젝트 4일 차, 팀의 중간 점검일이었다. 다른 팀원은 이미 각자 맡은 기능을 구현하고 있었고, 이때 다시 회의를 잡는 것이 현재 작업 흐름을 끊을 수 있다고 보았다. 그래서 팀은 추가 회의를 열기보다 각자 맡은 범위를 우선 진행하기로 합의했다.
결국 정책 결정은 뒤로 밀렸고, 구현을 진행하면서도 다음 질문들을 그때그때 다시 결정해야 했다.
- 대기 결제는 언제, 어떤 근거로 종료할 것인가
- 종료된 대기 주문·결제 데이터는 삭제할 것인가, 상태 이력으로 남길 것인가
- 결제 조회가 실패했을 때 바로 취소할 것인가, 재조회할 것인가
- 스케줄러는 상태 변경과 재고 복구 중 어디까지 책임질 것인가
- 금액 불일치 취소와 고객 환불은 같은 도메인으로 관리할 것인가
- 부분 환불의 횟수, 수량, 승인 조건은 무엇인가
구현하면서 계속 걸렸던 건 코드 자체보다 "이게 맞는 정책인가?"라는 질문이었다. 코드를 작성한 뒤에도 이 상태 전이가 맞는지, 실패하면 어디까지 되돌려야 하는지를 다시 결정해야 했다. 구현과 요구사항 검토를 동시에 하는 느낌이 들었다.
이 비용은 내 작업에만 머물지 않았다. 결제와 환불은 주문 상태와 재고 처리에도 영향을 주기 때문에, 이 흐름의 정책과 완료 시점이 흔들리면 다른 팀원의 작업 계획과 통합 일정도 함께 불확실해질 수 있었다. 데드라인이 가까워질수록 내 기능을 끝내야 한다는 압박과, 내 작업의 지연이 팀 전체 계획에 영향을 줄 수 있다는 부담을 함께 느꼈다.
팀 컨벤션이 환불 도메인에서 더 크게 느껴진 이유
우리 팀은 파사드 계층을 두고, 다른 도메인의 조회나 상태 변경도 해당 도메인의 Service를 통해 DTO로 주고받기로 합의했다. Repository를 다른 도메인에서 직접 호출하지 않도록 경계를 지키고, 구현 변경이 밖으로 퍼지는 것을 줄이기 위한 선택이었다.
이 규칙자체도 일반적인 기능의 방향을 잡는 데는 문제가 없었다.
하지만 환불은 주문, 결제, 재고, 환불처럼 여러 도메인의 상태를 조율해야 했다.
파사드가 흐름을 조합하고 각 서비스가 조회와 검증, 상태 변경을 맡는 구조를 지키려 할수록 서비스 간 호출과 DTO 변환이 늘어났다.
하나의 서비스에 조회, 계산, 검증, 상태 변경이 모이면서 메서드가 지나치게 커지는 것처럼 느껴졌다. 환불 책임을 세 개의 서비스로 나누기도 했지만, 계산·조회·상태 변경을 더 분리해야 하는지 고민했고 CQRS 같은 구조까지 검토하게 됐다.
지금 다시 보면 여기서 CQRS까지 생각한 건 조금 멀리 갔던 것 같다. 서비스가 커지는 게 불편해서 구조부터 바꾸려고 했지만, 당시에는 환불 정책을 먼저 정리했어야 했다.
구현보다 PR 단위가 더 어려웠다
팀 컨벤션에는 PR을 기능 단위로 만들고, 변경이 크면 분리하며, 두 명의 리뷰 승인을 받도록 정해 두었다. 코드 리뷰에서는 요구사항 충족 여부뿐 아니라 다른 도메인과의 의존성, 예외 상황, 불필요한 복잡성도 확인하기로 했다.
문제는 이 변경을 리뷰어가 이해할 수 있는 PR 단위로 나누는 일이었다. 환불 상태, 부분 환불 허용 기준, 외부 취소의 결과 불확실성, 스케줄러의 책임까지 함께 검토해야 하는 PR이 될 가능성이 컸다. 변경을 작게 나누면 각 PR이 최종 정책을 충분히 설명하지 못하거나, 중간 상태의 코드만 남겨 리뷰 기준이 모호해질 수 있었다.
클린 코드 관점에서 책임을 나누기 위한 리팩터링도 진행하고 있었다. 하지만 기능 정책 변경과 리팩터링이 한 번의 변경 묶음에 섞이면서, 팀원이 무엇을 기준으로 리뷰해야 하는지 더 불분명해졌다. 나는 이 작업을 팀이 이해하고 검토할 수 있는 독립적인 PR들로 나눌 자신이 없었다.
우선 정책을 합의하고 PR의 경계를 정했어야 했다. 결제 웹훅 동기화, 금액 불일치 취소, 고객 환불 요청과 승인, 외부 취소 결과 동기화, 미확정 상태 재조회 스케줄러는 서로 다른 PR단위로 나눌 수 있었을 것이다. 각 단계에서 이번 PR이 보장하는 상태와 다음 단계로 미루는 실패 처리를 적어 두었다면, 리뷰어도 코드와 정책을 함께 따라가기 쉬웠을 것이다.
AI로 구현할 수 있어도 멈춘 이유
이 흐름을 계속 확장하는 구현 자체는 AI의 도움으로 가능했을 것이다. 외부 취소 요청, 멱등 키, 웹훅 처리, 재조회 스케줄러, 서비스 분리도 각각의 코드 예시는 받을 수 있다.
기능이 동작한다고 해서 그 코드를 팀에 넣어도 되는 걸까?
환불 상태 전이와 외부 결제 처리, 팀 컨벤션에 맞춘 책임 분리까지 결합되자 각 선택의 이유를 내가 팀원에게 설명하고 리뷰에서 방어할 수 있을지 확신이 없었다. AI가 제안한 코드를 연결해 기능을 완성할 수는 있어도, 왜 그 상태가 필요한지와 왜 이 계층이 그 책임을 가져야 하는지를 설명하지 못하면 팀 코드가 될 수 없다고 생각했다.
그래서 부분 환불의 기본 흐름까지만 구현하고, 외부 취소의 비동기 확정과 재처리까지는 더 확장하지 않았다. 당시에는 설명할 수 없는 설계를 도입하기보다 팀이 리뷰할 수 있는 범위에서 멈추는 편이 낫다고 판단했다.
외부 API는 응답만 보면 끝나는 게 아니었다
결제와 환불을 다루며 외부 API 연동은 요청을 보내고 응답을 받는 일로 끝나지 않는다는 것을 구체적으로 알게 됐다. 응답이 늦거나 사라질 수 있고, 결제사에서는 처리됐지만 우리 서버는 결과를 알지 못할 수 있다.
웹훅처럼 별도의 경로로 결과가 도착할 수도 있다. 그래서 상태를 확정하는 근거, 재시도할 수 있는 요청인지, 결과가 불확실할 때 어떻게 남겨 둘지를 함께 고민하게 됐다.
프론트 화면이나 Swagger로 요청을 보내는 방식은 정상 흐름을 확인하는 데 유용했지만, 응답 유실, 웹훅과 Confirm 요청의 도착 순서, 같은 요청의 재시도처럼 외부 요인이 얽힌 상황을 충분히 확인하기에는 한계가 있었다.
이 경험을 통해 결제 상태 전이와 외부 API 응답별 처리를 코드로 검증할 수 있는 테스트 코드의 필요성을 더 크게 느꼈다.
정상 흐름 밖에서 어떤 상태가 생길 수 있고, 그 상태를 누가 어떤 근거로 확정해야 하는지 고민해 본 경험이 남았다.
구현을 시작하기 전에 맞췄어야 했던 것들
이번 경험 뒤에는 새로운 상태, 테이블, 백그라운드 작업이 필요해지는 시점을 단순한 구현 이슈로 보지 않게 됐다. 그 시점은 정책 결정을 위해 팀과 다시 맞춰야 한다는 신호에 가깝다.
팀마다 컨벤션과 작업 방식은 다르겠지만, 새 요구사항이 기존 범위를 크게 넓힐 때는 아래 세 가지를 먼저 짧게 정리할 필요가 있다고 생각한다.
- 기능이 정상적으로 끝났다고 볼 기준과 상태를 확정하는 근거
- 지금 처리할 실패 흐름과 후속 작업으로 남길 복구 흐름
- 팀원이 한 번의 PR에서 이해하고 리뷰할 수 있는 범위, 그리고 마감일까지 가능한 작업량
이번에는 이 질문들을 구현하면서 하나씩 발견했다. 회의를 길게 하는 것이 목적은 아니다. 결정이 필요한 질문을 먼저 드러내고 팀의 방식에 맞춰 합의하면, 구현 중에 계속 정책을 다시 결정하는 일을 줄일 수 있다.
마감일도 이 기능을 지금 구현할지, 핵심 흐름까지만 다룰지, 후속 작업으로 나눌지를 판단하는 기준이 된다.
처음에는 스케줄러 하나를 추가하려고 시작한 일이었다. 끝날 무렵에는 결제와 환불 기능을 어디까지 만들지보다그 범위를 언제 팀과 다시 맞춰야 하는지가 더 중요하다고 느꼈다.
https://github.com/Makgoli-SuyuK/onion-store
GitHub - Makgoli-SuyuK/onion-store: 결제와 판매자와 고객이 채팅을 할 수 있는 프로그램
결제와 판매자와 고객이 채팅을 할 수 있는 프로그램. Contribute to Makgoli-SuyuK/onion-store development by creating an account on GitHub.
github.com