spring_2기[본캠프]/과제

[파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 10

minwoo95 2026. 4. 28. 21:31

https://github.com/Hot6-NovelCraft/Hot6-NovelCraft

 

GitHub - Hot6-NovelCraft/Hot6-NovelCraft

Contribute to Hot6-NovelCraft/Hot6-NovelCraft development by creating an account on GitHub.

github.com

개인별 배포용 레퍼지토리 [ AWS ]

https://github.com/MinWoo1995/Hot6-NovelCraft-local

 

GitHub - MinWoo1995/Hot6-NovelCraft-local

Contribute to MinWoo1995/Hot6-NovelCraft-local development by creating an account on GitHub.

github.com

1. 오늘 한 일
오늘은 수익/환전 도메인 전체를 설계부터 구현까지 완료했다. 튜터님이 "어줍잖게 구현하면 포트폴리오에서 발목 잡힌다"고 하신 만큼, 금융 도메인에 맞는 디테일을 최대한 챙기며 작업했다.
수익/환전 도메인 ERD 설계 (BankAccount, AccountVerification, Revenue, Withdrawal 4개 엔티티)
계좌 등록 + 1원 인증 API 구현 (2-step: 인증 요청 / 코드 검증 분리)
수익 현황 조회 API 구현 (Revenue 집계 쿼리 + Redis 캐싱)
환전 신청 API 구현 (Redis 분산락 동시성 제어 + 수수료 계산 + 잔액 트랜잭션)
환전 내역/상세 조회 API 구현 (QueryDSL 동적 쿼리 + 기간/상태 필터 + 페이징)
수익 분석 통계 API 구현 (QueryDSL GROUP BY 월별/주별 집계 + Redis 캐싱)
관리자 환전 승인/거절 서비스 로직 선작업 (TODO: Admin Controller 연결 대기)
API 명세서 작성 (전체 8개 엔드포인트)
총 37개 파일, API 6개(작가측) + 2개(관리자 TODO) 구현 완료.

 

2. 트러블슈팅
2-1. BankVerificationClient 빈 등록 실패
문제 상황
애플리케이션 실행 시 아래 에러가 발생했다.
Parameter 2 of constructor in BankAccountService required a bean of type 'BankVerificationClient' that could not be found.
원인 분석
처음에 외부 은행 API 구현체를 @Profile("dev")와 @Profile("prod")로 분리했는데, spring.profiles.active 설정이 없어서 어떤 프로파일에도 해당하지 않았고 Mock 빈이 등록되지 않은 것이었다.
해결
@Profile 대신 @ConditionalOnProperty로 전환했다. useb.api.enabled: true이면 실제 API 구현체가, 설정이 없거나 false이면 Mock 구현체가 자동으로 등록되도록 변경했다. 이후 금융규제로 인해 실제 은행 API 테스트가 불가능하다는 점을 고려하여, 최종적으로는 UseBVerificationClient를 삭제하고 LocalBankVerificationClient(시뮬레이션)를 @Component로 단순 등록하는 구조로 정리했다. BankVerificationClient 인터페이스는 유지하여 운영 전환 시 실제 구현체로 교체할 수 있는 구조를 열어두었다.

 

2-2. AES 암호화 설정값 미등록으로 빈 생성 실패
문제 상황
@Profile("dev") 문제를 해결한 뒤에도 아래 에러가 발생했다.
Could not resolve placeholder 'encryption.aes.secret-key' in value "${encryption.aes.secret-key}"
원인 분석
계좌번호를 AES 암호화하기 위해 AesEncryptionUtil을 @Component로 등록했는데, application.yml에 encryption.aes.secret-key와 encryption.aes.iv 값을 정의하지 않은 상태였다. Spring이 빈 생성 시 @Value 플레이스홀더를 해석하지 못해 에러가 발생한 것이다.
해결
application.yml에 아래 설정을 추가했다. AES-128은 키와 IV가 정확히 16바이트여야 한다.
yamlencryption:
  aes:
    secret-key: novelcraft1234ab
    iv: novelcraft1234iv

 

2-3. 기존 예외 처리 구조와 불일치
문제 상황
컴파일은 통과했지만, ExchangeException이라는 커스텀 예외 클래스를 별도로 만들어 사용하고 있었는데 기존 프로젝트의 예외 처리 구조와 달랐다.
원인 분석
기존 프로젝트는 ErrorCode 인터페이스 + ServiceErrorException 구조로 GlobalExceptionHandler에서 일괄 처리하는 방식이었다. ExchangeException은 이 핸들러에 잡히지 않아 500 에러로 빠질 수 있었다.
해결
ExchangeException 클래스를 삭제하고, ExchangeExceptionEnum이 기존 ErrorCode 인터페이스를 implements하도록 변경했다. 서비스 레이어에서는 new ServiceErrorException(ExchangeExceptionEnum.XXX) 형태로 던지도록 통일하여 GlobalExceptionHandler에서 정상적으로 처리되도록 했다.

 

3. 의사결정 - 1원 계좌 인증 외부 API 선택
배경
수익 환전 기능에서 계좌 인증이 필수적이었다. 실제 서비스에서는 useB, CODEF 등 외부 은행 API를 통해 1원 입금 → 인증코드 확인 방식으로 계좌를 검증한다. 포트원(PortOne)은 결제와 본인인증에 특화되어 있어 1원 계좌 인증은 지원하지 않았다.
문제
useB, CODEF, 금융결제원 오픈뱅킹 등을 조사한 결과, 모든 서비스가 사업자 등록 또는 기업 계약이 필요했다. CODEF는 샌드박스를 제공하지만 고정 응답값만 반환하는 구조여서 실질적인 테스트 효과는 Mock과 차이가 없었다.
결정
BankVerificationClient 인터페이스로 외부 API 호출을 추상화하고, LocalBankVerificationClient에서 실제 1원 인증 플로우를 자체 시뮬레이션하는 구조로 구현했다. 사전 등록된 가상 계좌 5개(국민/신한/우리/하나/카카오뱅크)를 두고 예금주 확인, 인증코드 생성, 은행 점검시간 체크까지 실제와 동일한 비즈니스 로직이 동작한다. 운영 환경에서는 인터페이스를 구현한 실제 API 연동 구현체로 교체하면 된다.

 

4. 느낀 점
오늘은 수익/환전이라는 금융 도메인을 처음부터 끝까지 설계하고 구현하면서, 일반적인 CRUD와는 차원이 다른 복잡도를 체감했다.
가장 고민이 깊었던 부분은 동시성 제어였다. 같은 작가가 동시에 환전을 신청하면 잔액 초과 환전이 일어날 수 있는데, Redis 분산락으로 하나의 요청만 통과시키는 구조를 잡았다. 단순히 "락을 걸었다"가 아니라 왜 비관적 락 대신 Redis를 선택했는지, finally에서 반드시 락을 해제하는 이유가 뭔지 등을 설명할 수 있어야 포트폴리오에서 의미가 있다는 걸 느꼈다.
외부 API 연동 부분에서는 금융 규제의 벽을 실감했다. 개인 개발자가 실제 은행 API를 테스트할 방법이 사실상 없었다. 처음에는 useB를 연동하려고 했지만, 사업자 등록 없이는 불가능하다는 걸 알게 됐고, 결국 인터페이스 분리 + 시뮬레이션 구현체로 방향을 잡았다. 이 과정에서 "없는 걸 있는 척 하는 것보다, 구조만 깔끔하게 열어두는 게 낫다"는 판단을 했는데, 면접에서도 솔직하게 "금융규제로 실연동은 불가능했고, 대신 비즈니스 로직은 실제와 동일하게 구현했습니다"라고 설명할 수 있을 것 같다.
상태 전이 검증도 인상 깊었다. WithdrawalStatus enum에 canTransitionTo() 메서드를 넣어서 COMPLETED → REJECTED 같은 잘못된 전이를 엔티티 레벨에서 차단하는 구조인데, 이런 방어 로직 하나가 금융 도메인의 신뢰도를 결정한다는 걸 배웠다.
내일은 Postman으로 전체 API 플로우를 테스트하고, 시간이 되면 서비스 레이어 단위 테스트 코드 작성까지 진행할 예정이다.