Navigation

ARTIST PROP

아티스트가 이미지와 파일, 약력 같은 포트폴리오 자산을 관리하고 공유하는 플랫폼입니다. 파일과 인증, 구독 결제 API를 중심으로 구현했습니다. 사용자 서비스와 운영 화면이 같은 정책을 사용하도록 파일 사용량과 결제 결과, 예외 처리를 API에서 연결했습니다.

핵심 기여

과제본인 기여코드에서 확인한 내용
파일 자산과 사용 정책등록과 삭제, 소유자 검사 호출과 사용량 반영메타데이터와 S3 객체 처리를 유즈케이스에서 연결
외부 결제 결과와 구독 상태구독 신청과 결제 이력 API 구현결제 결과 기록 후 성공했을 때 구독 갱신
인증과 사용자 화면의 계약이메일 인증과 비밀번호 재설정 API 정리인증 방식을 바꿀 때 관련 허용 경로 반영

개요

항목내용
기간2024.07 ~ 2024.10
역할회원, 약력, 파일, 인증, 구독과 결제 관련 API 구현, 공통 프론트엔드 구조의 방향 지원
서비스아티스트 포트폴리오 자산 관리, 공유, 구독 결제와 운영 관리
기술Java 17, Spring Boot, Spring Security, JPA, Querydsl, AWS S3, AWS SES, PortOne, NaverPay, KCP
역할 경계프론트엔드 화면 전체의 직접 구현과 공통 구조 검토를 구분했습니다

맡은 범위

파일의 등록과 삭제, 다중 업로드와 사용량 반영, 이메일 인증과 비밀번호 재설정, 구독 신청과 결제 이력 응답을 구현했습니다. S3와 SES, 외부 결제와 본인인증 연동에 필요한 API와 예외를 정리했습니다.

문제와 해결

파일의 화면 정보와 저장소 객체를 같은 정책으로 다루기

문제. 사용자 화면의 파일 목록은 메타데이터이고 실제 내용은 S3에 있습니다. 등록과 삭제에서 한쪽만 처리하면 사용량과 목록, 원격 객체가 다른 상태를 가리킬 수 있습니다.

판단. 유즈케이스에서 구독 상태, 파일 정보와 원격 객체 처리, 사용량 갱신의 순서를 연결했습니다. 삭제에는 기존 파일 도메인의 소유자 검사를 호출하도록 연결했습니다.

flowchart LR
    Request["등록 요청"] --> Policy["구독 상태 확인"]
    Policy --> Metadata["파일 메타데이터 저장"]
    Metadata --> Object["S3 경로 복사와 정보 갱신"]
    Object --> Usage["사용량 재계산"]

구현. 파일을 등록한 뒤 식별자가 포함된 저장 경로로 객체를 복사하고 주소를 갱신했습니다. 삭제 때는 요청자의 소유 여부를 확인한 다음 원격 객체 삭제와 사용량 재계산을 연결했습니다.

결과. 파일 화면이 사용하는 정보와 원격 저장소의 처리를 하나의 유즈케이스에서 읽을 수 있게 했습니다. 외부 객체 저장소까지 DB와 원자적으로 롤백되는 구조로 설명하지는 않았습니다.

한글 파일명의 표기 차이를 등록 경계에서 처리하기

문제. 같은 이름처럼 보이는 한글 파일명도 Unicode 정규화 형식이 다를 수 있습니다. 원본 문자열을 그대로 보관하면 파일 정보의 표기가 달라지는 문제가 있었습니다.

판단. 파일 생성 경계에서 파일명을 NFC로 정규화했습니다. 이름의 표현을 화면마다 보정하기보다 등록된 정보의 기준을 맞췄습니다.

Normalizer.normalize(fileName, Normalizer.Form.NFC)

구현. 파일명 길이를 확인하고 FileInfo를 생성할 때 정규화한 이름을 사용했습니다.

결과. 파일 등록 데이터에 적용할 이름의 기준을 한 곳에 두었습니다. 이번 수정은 등록 경계의 파일명 정규화에 해당합니다.

결제 시도와 구독 반영을 구분하기

문제. 결제 API를 호출했다는 사실과 구독이 유효해졌다는 사실은 다릅니다. 외부 결제의 실패 결과도 이력에는 남아야 하고 구독 상태는 결과에 따라 달라져야 했습니다.

판단. 결제 결과를 이력에 저장하는 단계와 회원의 구독을 갱신하는 단계를 구분했습니다.

flowchart LR
    Apply["구독 신청"] --> Payment["외부 결제 요청"]
    Payment --> Record["결제 결과 이력 저장"]
    Record --> Result{"성공 여부"}
    Result -->|성공| Subscribe["회원 구독 갱신"]
    Result -->|실패| Fail["구독 갱신 없이 반환"]
구현 코드: 결제 결과와 구독 갱신
paymentPersistenceAdapter.save(payment);

if (paymentResult.success()) {
    member.applySubscription(
        subscriptionPlan, billingCycle, paymentMethod, paymentBillingKey
    );
    memberPersistenceAdapter.save(member);
    return member.getSubscription();
}
return null;

결제 요청과 결과 객체 생성 부분은 생략했습니다. 실패 반환값은 당시 코드의 계약을 그대로 남겼습니다.

구현. 빌링키와 구독 플랜으로 결제를 요청하고 결과와 가격, 결제 식별자를 이력에 기록했습니다. 관리자 결제 내역의 가격과 응답, 조회 조건도 보강했습니다.

결과. 결제 결과와 사용자 구독 상태를 구분한 API 흐름을 만들었습니다. 외부 결제 엔진의 구현이나 모든 중복 결제와 환불을 보장하는 성과로 쓰지 않았습니다.

결과와 확인 범위

프론트엔드가 사용하는 파일 정책과 결제 상태를 API 계약까지 따라가며 구현한 경험입니다. 구현한 유즈케이스와 본인 변경 이력을 근거로 정리했고 매출, 운영 오류 감소율과 서비스 전체의 구현 소유권은 추가하지 않았습니다.