Claude가 대부분의 코드를 작성하며 6개월 동안 국가 SSO 플랫폼을 혼자 개발한 경험
요약
17년 차 아키텍트가 Claude를 활용해 6개월 만에 국가 SSO 플랫폼을 단독 개발한 사례를 공유합니다. 헥사고날 아키텍처와 CQRS 패턴을 적용하여 프레임워크 의존성을 최소화한 설계와 AI를 통한 압도적인 생산성을 입증했습니다.
핵심 포인트
- Claude를 활용해 엔지니어 3~4명의 3개월 분량을 혼자 수행
- 헥사고날 아키텍처와 CQRS를 적용한 견고한 시스템 설계
- 도메인 계층에서 프레임워크 의존성을 제거하여 테스트 용이성 확보
- 모의 어댑터를 활용해 외부 시스템 의존성 없이 E2E 테스트 완료
17년 차 솔루션 아키텍트(Solution architect)입니다. 대략 2025년 12월부터 2026년 7월까지, 저는 한 걸프(Gulf) 국가의 항만 커뮤니티 시스템(국가 항만 및 물류 부문을 전면에 내세운 플랫폼)의 ID 및 싱글 사인온(SSO, Single-Sign-On) 레이어를 담당하는 유일한 실무 개발자였습니다. 이 시스템은 현재 운영 환경(Production)에서 가동 중입니다.
'유일한(sole)'이라는 표현을 정확히 하고 싶은데, 이는 중요한 문제이기 때문입니다. 다른 사람들도 CI, 자동화 테스트, 몇 가지 수정 사항, 다운스트림 통합 작업 등 두 리포지토리(repos)에 커밋을 남기긴 했습니다. 하지만 제가 백엔드 커밋의 약 80%, 프론트엔드의 약 70%를 작성했습니다. 핵심 시스템에 대해 실무적인 기능 개발(feature development)을 수행한 사람은 저뿐이었습니다. 이것이 정직한 버전입니다.
git log --numstat을 통해 확인한 작업량:
- 602개의 커밋, 1,423개 파일에 걸쳐 약 184,000라인 변경
- 백엔드: 383개의 Java 파일 / 약 21k LOC(Lines of Code), 19개의 컨트롤러(controllers), 83개의 REST 엔드포인트(endpoints)
- 프론트엔드: 152개의 TS/HTML 파일 / 약 15.3k LOC, 62개의 Angular 컴포넌트(components)
- 6개의 외부 통합(external integrations), 각각 실제 어댑터와 모의(mock) 어댑터 포함
- 4개의 환경(environments), 2회의 독립적인 침투 테스트(penetration tests) 수행 및 조치 완료
이 모든 논쟁이 생기기 전인 4월에 경영진에게 직접 작성하여 보고했던 추정치는 다음과 같습니다: AI 도구 없이 엔지니어 3~4명이 3개월간 작업해야 하는 분량입니다.
아키텍처 (Architecture)
헥사고날(Hexagonal, 포트 및 어댑터) + CQRS 방식입니다. 도메인(domain) 패키지에는 Spring이나 JPA 임포트(import)가 전혀 없습니다. 이는 검색(greppable)이 가능하며, 아무것도 반환하지 않습니다. 이것이 헥사고날이 단순한 유행어(buzzword)인지 아니면 실제 설계 결정인지를 판가름하는 진짜 테스트이며, 헥사고날을 표방하는 대부분의 코드베이스는 이 검증에서 실패합니다.
domain/ 순수 도메인 — 모델(models), 값 객체(value objects), 이벤트(events), 포트(ports). 프레임워크 임포트 없음.
application/ 유스케이스(Use cases) — 커맨드(Commands, 쓰기) 및 쿼리(Queries, 읽기), DTO, 어셈블러(assemblers).
dataprovider/ 어댑터(Adapters) — JPA 엔티티(entities), Spring Data 리포지토리(repos), SOAP 클라이언트(clients).
...
모든 기능은 쌍을 이루는 XxxCommand/XxxCommandImpl(트랜잭션 쓰기) 및 XxxQuery/XxxQueryImpl(읽기 전용)로 구성됩니다. 모든 외부 의존성은 포트(port) 뒤에 위치하며, Spring 프로필(profile)에 의해 실제 어댑터와 모의(mock) 어댑터가 선택됩니다. 덕분에 QA 및 UAT(사용자 수용 테스트) 과정에서 정부 시스템의 가동 여부에 의존하지 않고 전체 엔드 투 엔드(end-to-end) 흐름을 실행할 수 있었습니다. 실제로 정부 시스템은 가동되지 않는 경우가 빈번했습니다.
훔쳐 가볼 만한 JPA 디테일: find-then-update 저장 패턴을 사용한 @Version 낙관적 잠금(optimistic locking) 방식입니다. 특히 Spring Data가 null 버전은 새로운 엔티티로 간주하여 발생하는 중복 키 오류를 피하기 위함이죠. 구현하기는 쉽지만, 디버깅하기는 고통스럽습니다.
사용자 생명 주기(User lifecycle)는 상태 패턴(State pattern)을 사용하여 실제 상태 기계(PENDING_VERIFICATION → ACTIVE → LOCKED/INACTIVE)로 관리됩니다. 따라서 유효하지 않은 전환은 UI에 흩어져 있는 if 문으로 보호되는 것이 아니라 도메인 계층에서 실패합니다.
읽어볼 만한 네 가지 문제점
1. Java의 X.509 파서가 정부 인증서를 거부함
JDK의 엄격한 파서가 전면적으로 거부하는, 형식은 잘못되었지만 실제 환경에서 매우 흔하게 발생하는 인코딩 오류였습니다. 그래서 BouncyCastle의 X.509 팩토리를 사용하여 수동 메타데이터 파서(manual metadata parser)를 작성했습니다.
그러자 해당 부처가 운영 환경에서 서명 인증서를 교체했는데, 그래도 시스템은 다운되었습니다. 그래서 저는 실패 시 새로 고침(on-failure refresh) 기능이 있는 방식으로 인증서를 핫 리로드 가능하게 만들었습니다. 그리고 20일 후에 또다시 교체가 이루어졌는데, 그때는 사람이 호출할 필요 없이 시스템 스스로 복구되었습니다.
2. 모두가 네트워크 탓으로 돌린 간헐적인 500 에러
인도에 있는 테스터와 오만(Oman)에 서버가 있기 때문에
10분간의 인보이스(invoice) 만료 시간, 약 2분간의 프론트엔드 폴링(polling) 윈도우, 그리고 60초 동안 실행되는 백엔드 조정(reconciliation) 작업. 3-D Secure 확인이 느려지면 결제가 성공하더라도 프론트엔드의 대기 시간을 초과할 수 있었습니다.
해결책은 브라우저가 여전히 열려 있는지 여부와 관계없이 안전망 역할을 하는 두 개의 백엔드 스케줄러(scheduler)를 도입하는 것이었습니다. 여기에 두 개의 독립적인 최종화 경로(finalization paths)를 추가했습니다. 하나는 은행 자체의 서버 간 콜백(server-to-server callback, 사용자가 탭을 닫아도 작동하는 권위 있는 방식)이고, 다른 하나는 SPA의 폴링(polling, 폴백(fallback) 방식)입니다. 이 두 경로는 모두 단일 멱등성 가드(idempotency guard)를 통해 전달되므로, 어떤 경로가 승리하든 하위 시스템의 동기화(sync)는 정확히 한 번만 실행됩니다.
관련된, 그리고 더 어처구니없는 문제도 있었습니다. 브라우저가 은행의 결제 엔드포인트(endpoint)로 직접 GET 요청을 보내면 POST 바디(body)가 조용히 누락되었습니다. 해결책은 백엔드에서 렌더링된 자동 제출(auto-submit) 폼을 사용하여 브라우저가 해당 엔드포인트로 아예 GET 요청을 보내지 않도록 하는 것이었습니다.
4. Chrome은 XHR에 대해서는 TLS 재협상(renegotiation)을 차단하지만, 내비게이션에 대해서는 차단하지 않는다
SAML 리다이렉트(redirect)는 완벽하게 작동하는 반면, 모든 API 호출은 net::ERR_FAILED와 함께 중단되었습니다.
이러한 비대칭성이 비용을 발생시켰습니다. 인증 리다이렉트는 성공하지만 이후의 모든 API 호출이 실패하는 시스템은 백엔드 인증 버그처럼 보이기 때문입니다. 실제로는 인프라 설정 문제였습니다.
아무도 경고해주지 않는 재작업(rework)
두 리포지토리(repo) 전체에 걸쳐 16번의 별도 롤백 및 재구축(revert-and-rebuild) 사이클이 발생했습니다. 특히 기억에 남는 두 가지는 다음과 같습니다.
- 하위 토큰 교환(token-exchange) 핸드셰이크(handshake)가 단 하루 만에 네 가지 다른 프로토콜 — 2단계, 서비스-JWT 베어러(service-JWT bearer), app_secret 베어러(app_secret bearer), 인증 헤더 없음 — 을 거쳤으며, 이후 며칠 동안 두 번 더 롤백된 끝에 단일 단계 교환 방식으로 정착되었습니다.
- 비즈니스 라인 정규화 규칙: 단 하루 동안 네 개의 PR(Pull Request)을 통해 네 가지 전략이 구축되었으나, 상대측에서 자신들의 API가 저장된 값 그대로를 원한다는 것을 확인하자마자 당일 모두 롤백되었습니다. 이후
main브랜치에서 완전히 제거되었다가 2주 뒤 세 번째 방식으로 재구축되었습니다.
이 중 어느 것도 AI의 잘못도, 제 잘못도 아니었습니다. 이는 요구 사항이 아직 확정되지 않은 팀들과 통합 작업을 진행할 때 발생하는 현상입니다.
Claude가 실제로 도움이 된 부분과 도움이 되지 않은 부분
그것(Claude)이 대부분의 코드를 작성했습니다. 단순한 스캐폴딩 (Scaffolding) 수준이 아니었습니다. 어댑터 쌍 (Adapter pairs), 수십 개의 기능에 걸친 CQRS (Command Query Responsibility Segregation) 커맨드 및 쿼리 구현, JPA 매핑 컨벤션 (Mapping conventions), Angular 컴포넌트, 그리고 통합 클라이언트 (Integration clients)의 상당 부분이 포함되었습니다. 이러한 압축 (Compression) 덕분에 혼자서 3~4인분의 업무 범위를 소화할 수 있었습니다.
그것은 위에서 언급한 네 가지 문제를 전혀 찾아내지 못했습니다. 모든 문제는 무엇을 의심해야 할지 아는 것에서부터 시작되었습니다. 인증서 오류는 일반적인 SAML 오류로 나타났습니다. 500 에러는 회의실에 있던 모든 사람이 확신을 가지고 오진했습니다. 레이스 컨디션 (Race condition)은 테스트 중에 전혀 나타나지 않았습니다. TLS 버그는 인증 문제로 나타났습니다.
정확히 식별하지 못한 문제에 대해서는 프롬프트 (Prompt)만으로 해결책을 찾아낼 수 없습니다. 당신은 이미 해석한 증상을 설명할 뿐이며, 그 해석이야말로 본연의 업무입니다.
또한 아무도 언급하지 않는 측면도 있습니다. 저는 5개월 동안 모델 사용량 제한 (Usage limits)에 반복적으로 걸렸습니다. 속도 제한 (Rate limits)은 단순한 각주 수준이 아니라 실제 일정상의 제약 사항이었습니다.
제가 다르게 했을 부분
백엔드의 테스트 커버리지 (Test coverage)는 명령 커버리지 (Instruction coverage) 기준으로 약 23%입니다. 이는 낮은 수치이며, 이를 미화하고 싶지는 않습니다. 만약 두 번째 엔지니어가 있었다면 훨씬 더 일찍 자동화된 커버리지에 투자했을 것이고, 구현 단계에서 네 차례의 실시간 재작업을 통해 불일치를 발견하는 대신 구현 전에 다운스트림 통합 프로토콜 (Downstream integration protocols)을 검토하도록 했을 것입니다.
어떤 내용이든 더 자세히 알고 싶으시다면 기꺼이 답변해 드리겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기