결제, 앱, 로밍, 프라이싱, 멤버십, 프로모션, DX, 개인 서비스의 주요 사례

프로젝트 포트폴리오

경력기술서와 이력서에 정리한 프로젝트 가운데, 멀티 벤더 결제, 앱/WebView 브릿지, 로밍 안정화, 프라이싱 플랫폼, 멤버십 이관, 포인트 지갑 설계, DX 자동화, 그리고 Commit Map처럼 개인 문제를 제품으로 풀어본 사례를 골라 정리했습니다.

Projects

11

Primary Stack

Kotlin Spring Boot MySQL

Focus

아키텍처, 상태 전이, 운영 복원력

01

Kotlin/Spring Boot 중심의 백엔드 개발 경험

02

OAuth2, PG, 게이트웨이, 외부 인증서 서비스가 포함된 MSA 연동 경험

03

Flutter/WebView 기반 하이브리드 앱과 앱 검증 도구 개발 경험

04

공공 연계 로밍 데이터의 이벤트 처리, 재처리, 월간 재동기화 기반 운영 안정화 경험

05

Pub/Sub DLQ, Athena 배치, 월간 파티션, 서킷브레이커 기반 운영 안정화 경험

06

run-gemini-cli, Docusaurus, Firebase App Distribution, capture/replay 익스텐션, Jenkins/TestFlight 기반 DX 개선 경험

Project

01

LG유플러스 볼트업 / 2024.10 - 현재

멀티 벤더 결제 시스템 및 미수 복구 안정화

서비스 초기 설계부터 구현까지 전담

결제 서비스 초기 설계부터 구현까지 전담하며, 단일 결제 구조를 멀티 벤더사를 수용하는 모듈 구조로 확장하고, DLQ 기반 미수 이벤트 처리와 자동 복구 체계를 구성했습니다. 이후 DLQ retry stuck 방지, PG not-found 후속 재결제, 취소 알림톡 분기까지 운영 안정화 영역을 보강했습니다.

Kotlin Spring Boot Spring Batch GCP Pub/Sub Cloud SQL

설계 배경

단일 결제 중심 구조에서 복수 벤더를 같은 방식으로 수용하고, 이후 새로운 벤더가 늘어나더라도 같은 확장 지점으로 붙일 수 있는 구조가 필요했습니다.

강조 포인트

PG 확장과 미수 이벤트 재처리 기준을 함께 설명할 수 있는 프로젝트입니다.

핵심 구현

  • 추상 클래스 기반 벤더 전략 패턴으로 PG사 통합 아키텍처를 구성했습니다.
  • GCP Pub/Sub 기반 DLQ 패턴을 구현하고 실패 이벤트를 별도 큐로 격리해 미수 처리 대상이 추적 가능하도록 구성했습니다.
  • 재시도, DLQ, NACK 기반 자동 복구 경로를 구현해 미수 이벤트가 결제 보완 처리 흐름으로 이어지도록 구성했습니다.
  • FAILOVER 상태 전이와 retry timestamp 기록을 보강해 dead-letter 재시도가 stuck 되지 않도록 만들고, PG not-found 응답에서도 미수 재결제 플로우가 끊기지 않도록 수정했습니다.
  • 전액 취소, 부분 취소, 로밍 결제 취소 알림톡 context를 분리하고 금액 포맷팅을 정리해 사용자 커뮤니케이션 정확도를 높였습니다.

엔지니어링 관점

  • 결제 요청, 포인트 hold/confirm, 성공·실패 업데이트를 PaymentProcessor 중심으로 묶어 상태 전이를 한 곳에서 추적할 수 있게 설계했습니다.
  • 같은 사용자의 중복 결제 시도가 포인트 hold를 동시에 건드리지 않도록, 유저 단위 락 안에서 hold와 payment READY 생성을 함께 처리했습니다.
  • PG 네트워크 불확실성은 repair, 컨슈머 재시도, failover 배치로 층을 나눠 즉시 복구와 운영 개입 경계를 분리했습니다.
  • 재시도 자체도 운영 관찰 대상이라고 보고, 실패 이벤트가 어디에서 멈췄는지 latestRetriedAt과 상태 전이로 남겨 후속 보정 판단이 가능하게 했습니다.

Mermaid로 보는 핵심 구조

Mermaid View

멀티 벤더 결제 오케스트레이션: 락, hold, 상태 전이

PaymentProcessor 기준으로, 요청 데이터와 락 키, 포인트 hold, payment READY, `PG Vendor` 내부 승인 단계, 성공/실패 상태 전이가 한 장 안에서 자연스럽게 이어지도록 정리했습니다.

flowchart TD
  Req["pay / rePay<br/>order=ORD-240915-001<br/>user=421 method=17 point=2000"] --> Lock["distributed lock<br/>payment-user-process:421"]
  Lock --> Hold["PointUpdater.hold()<br/>wallet -> HOLD 2000P"]
  Hold --> Ready["createWithReady()<br/>payment READY"]
  Ready --> Sub["resolve subscription<br/>methodId=17 or primary"]
  Sub --> Vendor["PG Vendor Router<br/>supports: KakaoPay / TossPayments / KakaoT<br/>selected: KakaoT"]
  subgraph PGV["PG Vendor Layer"]
    direction TD
    Vendor --> Keys["read vendor keys<br/>pgPayKey + token"]
    Keys --> Api["vendor client.pay(...)"]
    Api --> Tx["save pgTransactionId<br/>paymentId / tid / paymentKey"]
  end
  Tx --> Result{"approval result"}
  Result -->|success| Done["updateSuccess<br/>payment PAID<br/>point HOLD->CONFIRM"]
  Result -->|fail| Fail["updateFailed<br/>releaseHold(order)"]
  Fail --> Recovery["repair / retry / failover"]
  classDef vendor fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  class Vendor,Keys,Api,Tx vendor;
  style PGV fill:#eef7fb,stroke:#0f4c81,stroke-width:2px,color:#0f172a;

Mermaid View

멀티 Vendor 시스템: 필수 계약과 선택 확장

`VendorType` 확장 지점 위에 `VendorChecker.select()`를 두고, 모든 벤더에 필요한 계약과 특정 벤더에만 필요한 기능을 분리했습니다. `@RequiredVendor`와 `VendorRequirementsValidator`가 앱 시작 시 필수 구현 누락을 막고, `VendorPaymentOnceProcessor(KakaoPay)`나 repair 서비스는 필요한 벤더에만 붙도록 구성했습니다.

flowchart TD
  Vendors["VendorType<br/>KakaoPay / TossPayments / KakaoT"] --> Select["VendorChecker.select(vendorType)"]
  Select --> Required["Required on all vendors<br/>VendorPaymentProcessor<br/>VendorMethodProcessor"]
  Select --> Partial["Required on some vendors<br/>VendorPaymentOnceProcessor<br/>(KakaoPay only)"]
  Select --> Optional["Optional extensions<br/>RepairService / vendor hooks"]
  Required --> Validate["@RequiredVendor<br/>+ VendorRequirementsValidator"]
  Partial --> Validate
  Validate --> Boot{"startup validation"}
  Boot -->|missing| Error["application start fail"]
  Boot -->|ok| Route["route to concrete impl"]
  classDef core fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef optional fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  classDef error fill:#fff1f2,stroke:#be123c,stroke-width:2px,color:#0f172a;
  class Vendors,Select,Required,Partial,Validate,Route core;
  class Optional optional;
  class Error error;

운영 결과

  • 복수 PG를 단일 인터페이스로 통합하고, 새로운 벤더가 추가돼도 같은 구조로 확장 가능한 결제 아키텍처를 만들었습니다.
  • DLQ 기반 미수 이벤트 처리와 자동 복구 경로를 운영에 적용했습니다.
  • 외부 PG 응답 예외와 취소 알림 분기를 보강해 미수 복구 흐름과 사용자 안내 메시지의 신뢰도를 높였습니다.
Project

02

LG유플러스 볼트업 / 2025.07 - 현재

카카오T 계정 링크 및 결제수단 등록

회원 식별 통합, AuthMethod 연결, 카드 등록 상태와 t_partner_user_token 기준 설계

VoltUp 회원가입 이후 카카오T 외부 계정을 암호화된 CI 기준으로 연결하고, FEAPP 한 스텝 API에서 결제수단 등록 세션 생성부터 billing의 READY 상태 전환, ACTIVE 상태 전환까지 이어지는 흐름을 설계했습니다. 이후 `t_partner_user_token`을 외부 결제수단과 내부 사용자 컨텍스트를 잇는 기준 키로 정리해 검색, 해지 검증, 앱 콜백 activate 흐름을 안정화했습니다.

이력서 연결 지점

유저 사이드 백엔드

이력서의 유저 사이드 백엔드 항목은 카카오T 연동을 중심으로 차량/PnC(Plug & Charge), 인증서 안정화 등 사용자 경험 핵심 흐름을 함께 다룬 이 영역으로 연결됩니다.

Kotlin Spring Boot OAuth2 Flyway T Partner API

참고 화면

Preview

로그인 수단 연결: 동일 회원 아래 카카오T 계정 연결

동일 사용자 기준으로 여러 로그인 수단을 묶고, 카카오T 계정 연결 이후 결제수단 등록 흐름으로 이어지도록 설계한 화면입니다.

VoltUp 로그인 수단 연결 화면

로그인 수단 연결: 동일 회원 아래 카카오T 계정 연결

동일 사용자 기준으로 여러 로그인 수단을 묶고, 카카오T 계정 연결 이후 결제수단 등록 흐름으로 이어지도록 설계한 화면입니다.

결제수단 등록: 카카오T / 카카오페이 / 일반 카드 분기

추가하기 한 번으로 카카오T, 카카오페이, 일반 카드 등록 경로를 한 바텀시트에서 노출해 사용자 선택 흐름을 단순화한 화면입니다.

VoltUp 결제수단 등록 바텀시트 화면

결제수단 등록: 카카오T / 카카오페이 / 일반 카드 분기

추가하기 한 번으로 카카오T, 카카오페이, 일반 카드 등록 경로를 한 바텀시트에서 노출해 사용자 선택 흐름을 단순화한 화면입니다.

설계 배경

기존 VoltUp 회원과 KakaoT 유저를 중복 계정 없이 합쳐야 했고, 그 위에서 카드 등록 세션과 최종 결제수단 상태가 같은 사용자 컨텍스트를 공유해야 했습니다.

강조 포인트

회원 식별 통합과 카드 등록 상태 전이를 함께 설명하기 좋은 프로젝트입니다.

핵심 구현

  • KakaoT OAuth 결과의 `externalId`, `ci`를 기준으로 기존 VoltUp 회원을 찾고 `AuthMethod(KAKAO_T)`를 추가하는 연결 흐름을 설계했습니다.
  • FEAPP에서 현재 사용자의 암호화된 CI를 기준으로 카카오T 결제수단 연동 세션을 생성하는 한 스텝 API 흐름을 정리했습니다.
  • billing에서는 `pg_payloads`에 `session_key`를 저장하고, confirm 시 `pgPayKey`와 `t_partner_user_token`을 확정하는 상태 전이를 구현했습니다.
  • `t_partner_user_token` 검색 필터와 복합 인덱스를 추가하고, 카카오T 해지 시 현재 사용자의 T_PAYMENTS인지 검증하는 경로를 보강했습니다.
  • 앱 콜백 전용 activate API를 분리하고 DTO alias, `@JsonProperty`, 검색 로그를 보강해 외부 스키마 차이와 운영 추적성을 흡수했습니다.

엔지니어링 관점

  • auth는 외부 계정 정보 수집, user-service는 동일인 판단과 AuthMethod 소유권, billing은 결제수단 상태를 맡도록 경계를 분리했습니다.
  • 회원 연결이 끝나기 전에는 카드 등록을 진행하지 않고, 연결 완료 후에만 session 생성과 activate를 진행해 사용자 식별 불일치를 막았습니다.
  • `READY -> ACTIVE` 전이에서 필요한 `session_key`, `partner_user_token`, `pgPayKey`를 같은 subscription 행에 모아 이후 승인/취소 호출도 재사용 가능하게 했습니다.
  • `t_partner_user_token`을 단순 응답 필드가 아니라 사용자-외부 결제수단 정합성을 확인하는 운영 키로 보고, 조회와 해지 검증이 같은 기준을 공유하도록 정리했습니다.

Mermaid로 보는 핵심 구조

Mermaid View

VoltUp 회원과 카카오T 계정 연결 후 카드 등록

회원 식별, 암호화된 CI 기준 계정 연결, link session 생성, READY 상태 전환과 ACTIVE 상태 전환 흐름을 한 번에 따라갈 수 있게 정리했습니다.

flowchart TD
  User["VoltUp member<br/>encrypted CI"] --> OAuth["KakaoT OAuth<br/>external account + encrypted CI"]
  OAuth --> Link["user-service<br/>AuthMethod(KAKAO_T) link"]
  Link --> Session["FEAPP link session<br/>ACCOUNT + PAYMENT"]
  Session --> Ready["billing init<br/>READY 상태 전환"]
  Ready --> Active["confirm success<br/>ACTIVE 상태 전환"]

운영 결과

  • 기존 VoltUp 회원과 KakaoT 계정을 동일인 기준으로 연결한 뒤 카드 등록을 이어가는 사용자 흐름을 정리했습니다.
  • 결제수단 등록 완료 후 subscription이 ACTIVE 상태를 유지하며 승인, 취소, 조회가 같은 식별 컨텍스트를 재사용하도록 만들었습니다.
  • 앱 콜백, 웹 로그인, 해지 검증이 섞이는 상황에서도 현재 사용자와 결제수단의 매칭 기준이 흔들리지 않도록 만들었습니다.
Project

03

LG유플러스 볼트업 / 2025.07 - 현재

차량 등록 / 차량 고유 키 자동 매핑 / PnC(Plug & Charge) 인증

차량 정보 검증, 양방향 자동 매핑, PnC(Plug & Charge) 인증 흐름 설계

차량번호 기반 차량 정보와 바로충전용 차량 엔티티가 서로 다른 시점에 들어오는 구조에서, 매핑되지 않은 쌍이 정확히 1개일 때만 자동 연결하고 이후 PnC(Plug & Charge) 인증이 같은 차량 컨텍스트를 참조하도록 구성했습니다.

Kotlin Spring Boot JPA Redis

참고 화면

Preview

차량 관리: 등록 차량과 바로충전 진입 화면

차량 등록 이후 바로충전(PnC) 기능으로 이어지는 사용자 화면 예시로, 차량 컨텍스트와 충전 인증 흐름이 서비스 안에서 어떻게 만나는지 보여줍니다.

VoltUp 차량 등록 및 바로충전 화면

차량 관리: 등록 차량과 바로충전 진입 화면

차량 등록 이후 바로충전(PnC) 기능으로 이어지는 사용자 화면 예시로, 차량 컨텍스트와 충전 인증 흐름이 서비스 안에서 어떻게 만나는지 보여줍니다.

설계 배경

차량 정보(`plateNumber`)와 바로충전 식별자(`evccId`)는 서로 다른 시점에 등록되기 때문에, 잘못된 자동 연결을 막으면서도 사용자가 매번 수동 선택하지 않도록 매핑 기준이 필요했습니다.

강조 포인트

차량번호와 차량 고유 키가 언제 자동 연결되고 언제 수동 선택으로 넘겨야 하는지 설명하기 좋은 프로젝트입니다.

핵심 구현

  • 차량 정보 등록과 PnC(Plug & Charge) 등록 양쪽에서 모두 “매핑 안 된 대상이 정확히 1개인지”를 검사하는 양방향 자동 매핑 규칙을 적용했습니다.
  • 차량 정보는 `plateNumber`, PnC(Plug & Charge) 차량은 `evccId`를 중심으로 따로 저장하고, 연결 시점에만 `userVehicleInfo.userVehicleId`를 채우는 방식으로 상태를 분리했습니다.
  • 이후 `authorizeByEvccId` 경로가 매핑된 차량 정보를 참조하도록 만들어, 충전기 인증 시에도 차량번호와 사용자 컨텍스트가 일관되게 이어지게 했습니다.

엔지니어링 관점

  • 자동 매핑은 편의 기능이지만 잘못 연결되면 위험하므로, “정확히 1개일 때만 연결”이라는 보수적 규칙으로 설계했습니다.
  • 차량 정보와 PnC(Plug & Charge) 엔티티를 동일 테이블에 억지로 합치지 않고 분리 저장한 뒤 링크로 결합해 등록 시점 차이를 자연스럽게 흡수했습니다.
  • 매핑 이후에는 `evccId -> userId/plateNumber` 조회가 가능해져 실제 충전 인증 경로가 데이터 모델 위에서 바로 설명되도록 만들었습니다.

Mermaid로 보는 핵심 구조

Mermaid View

차량 정보와 차량 고유 키를 안전하게 자동 매핑하는 흐름

차량 정보와 차량 고유 키 등록 예시 값을 노드에 직접 넣고, 자동 링크 조건과 최종 인증 결과까지 한 흐름 안에서 읽히게 했습니다.

flowchart TD
  Vehicle["vehicleInfo 501<br/>plate=12가3456"] --> Match{"unlinked pair<br/>count == 1 ?"}
  Pnc["userVehicle 88<br/>evccId=EVCC-A1B2"] --> Match
  Match -->|yes| Link["linkPncVehicle(501,88)"]
  Match -->|no| Manual["manual select"]
  Link --> Auth["authorizeByEvccId<br/>user=421 plate=12가3456"]
  Auth --> Charge["start charge"]

운영 결과

  • 차량 정보 등록과 바로충전 등록 어느 쪽을 먼저 하더라도 조건이 맞으면 자동 매핑되도록 정리했습니다.
  • PnC(Plug & Charge) 인증 시 `evccId`로 사용자를 식별하고 연결된 차량번호를 함께 참조하는 흐름을 운영 기준으로 만들었습니다.
Project

04

LG유플러스 볼트업 / 2025.07 - 현재

통합 프로모션(쿠폰/포인트) 플랫폼 구축 및 고도화

coupon-service 정책 확장, 결제수단 제한, 포인트 지갑 구조 설계

VoltUp `coupon-service`에서는 쿠폰팩의 등록 기간과 사용 기간을 기준으로 코드 발급, 코드 등록, 코드 없이 유저 직접 할당, 만료 알림 배치를 처리했고, 제휴 쿠폰별 허용 결제수단 정책까지 발급/조회/사용/Admin 생성 흐름에 반영했습니다. 별도로 포인트는 accrual 단위 만료를 다루기 위해 지갑 구조와 차감 순서를 설계했습니다.

Kotlin Spring Boot Spring Batch MySQL JPA QueryDSL JDBC Distributed Lock

설계 배경

넥센, 도요타, 블루멤버스 등 제휴사별 요구사항을 수용하면서도, 쿠폰은 코드형 발급과 무코드 직접 할당을 같이 지원해야 했고, 일부 쿠폰팩은 카카오T/일반 카드/카카오페이처럼 허용 결제수단이 달라야 했습니다. 포인트는 적립 건마다 다른 만료일을 가진 구조를 안정적으로 처리해야 했습니다.

강조 포인트

쿠폰 서비스의 코드 발급/무코드 할당/만료 알림과 포인트 지갑 차감 구조를 함께 설명하기 좋은 프로젝트입니다.

핵심 구현

  • 쿠폰은 `couponPack`에 `registerStartAt/registerEndAt`, `usableStartAt/usableEndAt`를 두고, 등록 가능 기간과 사용 가능 기간을 분리해 관리했습니다.
  • 코드형 쿠폰은 `batchIssue`에서 Snowflake 기반 ID를 SHA-256 후 base36 10자리 코드로 변환해 bulk 저장하고, 충돌 시 개별 저장 fallback으로 마무리했습니다.
  • 코드 없이 유저에게 바로 주는 경우에는 `mapping(code=null)` 또는 `batchMapping(userIds)`로 쿠폰 행을 직접 만들고, 코드 등록은 `coupon-mapping:{code}` 락 안에서 사용자와 연결했습니다.
  • `allowedPaymentVendors`를 쿠폰팩 정책으로 추가하고, 빈 값은 전체 허용으로 해석해 기존 쿠폰과의 호환성을 유지하면서 발급/조회/사용 단계에 같은 제한을 적용했습니다.
  • Admin 쿠폰팩 생성 폼에는 허용 결제수단 멀티셀렉과 Encoded ID 노출을 추가해 운영자가 정책을 생성 시점부터 확인할 수 있게 했습니다.
  • 포인트는 `addBulk`에서 `expiredAt`이 있으면 새 `PointWallet`을 만들고, 없으면 같은 `type + chargeType` 지갑에 합산해 적립 단위와 만료 단위를 함께 관리했습니다.
  • 포인트 사용 시에는 활성 지갑을 페이지 조회한 뒤 `FREE -> expiredAt 빠른 순`으로 정렬해 여러 wallet을 순차 hold하고, 실패 시에는 hold에 참여한 지갑만 정확히 복원했습니다.
  • 쿠폰 만료는 `couponExpiryReminderJob`에서 `usableEndAt` 기준 윈도우를 읽어 `coupon.event`의 `EXPIRED` 이벤트를 발행하도록 했고, 포인트는 당월 만료분을 다음 달 히스토리 배치에 반영했습니다.

엔지니어링 관점

  • 쿠폰은 `couponPack`이 기간과 할인 정책을 소유하고 개별 `coupon`이 유저 매핑과 사용 상태를 가지도록 나눠, 코드형/무코드형 발급을 같은 모델 안에서 처리했습니다.
  • 코드 등록은 `coupon-mapping:{code}` 락, 사용 처리는 `coupon-process:{userId}` 락, DB는 `unique(code)`와 `unique(userId,couponPackId)`로 보강해 중복 등록과 중복 발급을 동시에 막았습니다.
  • 만료는 별도 상태를 추가하기보다 `usableEndAt` 기준으로 만료 대상 쿠폰팩을 읽고, 미사용 쿠폰 사용자에게 `EXPIRED` 이벤트를 발행하는 배치 경로로 분리했습니다.
  • 결제수단 제한은 화면 조건으로만 두지 않고 쿠폰팩 도메인 정책으로 끌어올려, Admin에서 만든 정책이 사용자 발급/조회/사용 단계까지 같은 의미로 흐르도록 했습니다.
  • 포인트를 단일 잔액이 아닌 `PointWallets` 엔티티로 분리하고, `expiredAt`이 있는 적립은 새 wallet, 없는 적립은 동일 `type + chargeType` 지갑에 합산해 만료 규칙이 데이터 구조에 직접 드러나게 했습니다.
  • 사용 시에는 활성 wallet을 읽어 `FREE -> expiredAt asc` 순으로 hold를 배분하고, `releaseHold(order)`는 실제 hold된 `pointWalletId`만 다시 찾아 복원하게 해서 순차 차감과 복구 기준을 일치시켰습니다.

Mermaid로 보는 핵심 구조

Mermaid View

coupon-service: 코드 발급, 무코드 할당, 만료 알림

쿠폰팩 기준으로 코드 발급, 유저 코드 등록, 코드 없이 직접 할당, `usableEndAt` 기반 만료 알림 배치까지 실제 `coupon-service` 흐름을 한 장으로 정리했습니다.

flowchart TD
  Pack["couponPack 71<br/>register 10/01~10/31<br/>usable 10/01~11/30"] --> Code["batchIssue(size=1000)<br/>Snowflake -> SHA-256/base36<br/>code=37PRPT85WA"]
  Pack --> Direct["mapping(code=null) / batchMapping<br/>user=421 or [421,422]"]
  Pack --> VendorPolicy["allowedPaymentVendors<br/>KAKAO_T / CARD / KAKAO_PAY"]
  Code --> Claim["mapping(user=421, code=37PRPT85WA)<br/>lock coupon-mapping:37PRPT85WA"]
  Claim --> Guard["DB unique guard<br/>code / (userId,couponPackId)"]
  Direct --> Guard
  Guard --> Ready["coupon row<br/>userId=421 status=READY"]
  Ready --> Process["process(price=32000, user=421)<br/>lock coupon-process:421<br/>READY -> PROCESSING"]
  VendorPolicy --> Process
  Process --> Finish["complete -> COMPLETE<br/>rollback -> READY"]
  Pack --> Expire["couponExpiryReminderJob<br/>usableEndAt D+3 window"]
  Expire --> Scan["getAllByCouponPackId<br/>completeAt is null"]
  Scan --> Event["publish coupon.event<br/>eventType=EXPIRED"]

Mermaid View

포인트 지갑: 제휴별 적립과 만료일 순차 차감

쿠폰 흐름과 분리해서, 여러 제휴 포인트가 wallet으로 쪼개지고 활성 지갑 조회 -> 정렬 -> hold -> confirm/release로 어떻게 관리되는지 예시 데이터와 함께 보여줍니다.

flowchart TD
  Grant["addBulk / partner accrual<br/>BASE 1200P exp 10-18<br/>TOYOTA 3000P exp 10-20<br/>BLUEMEMBERS 800P exp 10-22<br/>NEXEN 5000P exp 10-31<br/>EVENT 700P exp 11-15<br/>BASE 900P exp null"] --> Rule["wallet rule<br/>expiredAt 있으면 new wallet<br/>null이면 same type+chargeType merge"]
  Rule --> Wallets["wallet #11 BASE/FREE 1200 exp 10-18<br/>wallet #12 TOYOTA/FREE 3000 exp 10-20<br/>wallet #13 BLUEMEMBERS/FREE 800 exp 10-22<br/>wallet #14 NEXEN/CHARGE 5000 exp 10-31<br/>wallet #15 EVENT/FREE 700 exp 11-15<br/>wallet #16 BASE/FREE 900 exp null"]
  Wallets --> Active["active wallet scan<br/>expiredAt > now only<br/>createdAt asc page 조회"]
  Active --> Order["deduction order<br/>FREE first<br/>then expiredAt asc"]
  Order --> Hold["hold 4500P<br/>#11 -1200<br/>#12 -3000<br/>#13 -300"]
  Hold --> Usage["point_usage rows<br/>order=ORD-240915-001<br/>walletId=11,12,13<br/>status=HOLD"]
  Usage --> Result{"payment result"}
  Result -->|success| Confirm["confirm<br/>HOLD -> CONFIRM<br/>wallet amount final"]
  Result -->|fail| Release["releaseHold(order)<br/>wallet 11 +1200<br/>wallet 12 +3000<br/>wallet 13 +300<br/>usage -> FAILED"]
  Wallets --> Expire["expiry handling<br/>expired wallet = active 제외<br/>expiringSoon 별도 조회"]
  classDef wallet fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef state fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Wallets,Active,Order,Hold,Usage wallet;
  class Confirm,Release,Expire state;

운영 결과

  • 코드형 쿠폰 발급, 코드 등록, 무코드 직접 할당, 만료 알림 배치를 `coupon-service` 안에서 같은 모델로 운영할 수 있게 정리했습니다.
  • 제휴 프로모션의 결제수단 제한 요구를 쿠폰팩 정책으로 흡수해 할인 정책과 실제 결제/정산 조건이 어긋날 가능성을 줄였습니다.
  • 포인트 지갑, 만료 순서 차감, 다음 달 히스토리 배치 반영도 함께 운영해 쿠폰과 포인트를 하나의 프로모션 백엔드에서 설명할 수 있게 했습니다.
Project

05

LG유플러스 볼트업 / 2024.12 - 현재

볼트업 하이브리드 앱: WebView 브릿지와 네이티브 기능

Flutter 하이브리드 앱 런칭, JSBridge, QR/권한/푸시/강제 업데이트 흐름 설계

VoltUp 2.0 런칭을 위해 Flutter 기반 Android/iOS 하이브리드 앱을 빠르게 구축하고, WebView 화면이 네이티브 기능을 안정적으로 호출할 수 있도록 JSBridge와 앱 핵심 흐름을 설계했습니다. 이후 QR 스캔, 카메라 권한, FCM, 강제 업데이트, Crashlytics 기반 안정화까지 운영 중인 앱의 품질 개선을 이어갔습니다.

Flutter Dart Kotlin Swift WebView JSBridge ML Kit FCM Crashlytics

설계 배경

짧은 일정 안에 Android/iOS 앱을 런칭해야 했고, 서비스 화면은 WebView로 빠르게 확장하면서도 QR 스캔, 카메라 권한, 새창 처리, 푸시, 강제 업데이트처럼 앱만이 처리할 수 있는 기능은 네이티브 계층에서 안정적으로 제공해야 했습니다.

강조 포인트

사용자 앱을 빠르게 런칭한 경험과 WebView-네이티브 브릿지, QR/권한/푸시/업데이트 같은 앱 고유 기능 설계를 함께 설명하기 좋은 프로젝트입니다.

핵심 구현

  • Flutter 기반 하이브리드 앱 구조를 잡고 2개월 내 Android/iOS 런칭을 목표로 WebView 중심 화면과 네이티브 기능 호출 경계를 설계했습니다.
  • JSBridge를 통해 프론트엔드가 새창, 외부 URL, QR 스캔, 카메라 권한, 앱 메시지, 강제 업데이트 같은 네이티브 기능을 호출하는 규약을 구현했습니다.
  • QR 인식 경험을 제어하기 위해 ML Kit 기반 커스텀 QR 스캐너 페이지와 반응형 스캔 UI를 구현하고 기존 스캐너 의존성을 줄였습니다.
  • Crashlytics 기반으로 Dart/native 오류 수집 경로를 연결하고, null-safe 처리, 카메라 lifecycle 예외, FCM token upload throttle을 보강했습니다.

엔지니어링 관점

  • 하이브리드 앱에서 WebView는 빠른 화면 확장성을 맡고, 네이티브 계층은 OS 권한과 하드웨어 기능을 맡도록 경계를 분리했습니다. JSBridge는 이 둘 사이의 제품 계약으로 보고, 프론트엔드가 호출할 수 있는 기능을 명시적인 메시지 흐름으로 정리했습니다.
  • QR 스캔과 카메라 권한은 충전 시작의 핵심 진입점이라 단순 패키지 적용보다 기기별 레이아웃, lifecycle, 권한 상태를 앱 UX 안에서 제어할 수 있게 만드는 데 초점을 뒀습니다.
  • 운영 중 앱 안정성은 Crashlytics 신호를 기준으로 개선했습니다. NPE 후보, camera pause 중 예외, FCM token upload 중복/회복력 같은 작은 크래시 원인을 묶어 사용자 진입 흐름의 안정성을 높였습니다.

Mermaid로 보는 핵심 구조

Mermaid View

WebView 화면과 네이티브 기능을 잇는 앱 브릿지

WebView 화면에서 JSBridge를 통해 새창, QR 스캔, 카메라 권한, FCM, 강제 업데이트 같은 네이티브 기능으로 이어지고, Crashlytics 신호로 안정화하는 흐름을 정리했습니다.

flowchart TD
  Web["VoltUp WebView 화면"] --> Bridge["JSBridge 계약<br/>frontend -> native"]
  Bridge --> Window["새창 / 외부 URL 처리"]
  Bridge --> QR["QR 스캔<br/>ML Kit custom scanner"]
  Bridge --> Camera["카메라 권한 / lifecycle"]
  Bridge --> Push["FCM push token"]
  Bridge --> Version["강제 업데이트 / version branch"]
  QR --> Native["Android / iOS native layer"]
  Camera --> Native
  Push --> Native
  Version --> Native
  Native --> Observe["Crashlytics<br/>Dart + native error tracking"]
  Observe --> Fix["NPE / camera / token throttle 안정화"]
  classDef web fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef native fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef ops fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Web,Bridge web;
  class Window,QR,Camera,Push,Version,Native native;
  class Observe,Fix ops;

운영 결과

  • 서비스 2.0 앱을 Android/iOS 양쪽에 빠르게 런칭하고, WebView 화면에서 네이티브 기능을 호출하는 공통 규약을 운영 기준으로 만들었습니다.
  • QR 스캔, 카메라 권한, 푸시, 강제 업데이트처럼 앱이 담당해야 하는 핵심 기능을 네이티브 계층에서 안정적으로 처리하도록 정리했습니다.
  • Crashlytics 기반으로 실제 운영 크래시를 추적하고 수정해 앱 핵심 진입점의 안정성을 지속적으로 개선했습니다.
Project

06

LG유플러스 볼트업 / 2026.05 - 현재

볼트업 앱 검증/운영 보정 익스텐션

앱 연결 없는 기능 검증, API capture/replay, Admin 미지원 운영 보정

앱 개발 중 매번 `voltup-app`을 연결해야 검증할 수 있던 새창, QR 스캔, 카메라 권한, 강제 업데이트 버전 분기 등을 브라우저 익스텐션에서 재현해 검증 시간을 줄였습니다. 이후 같은 capture/replay 구조를 충전존 생성 오류 대응처럼 Admin 화면에서 직접 지원하지 않는 단일 API 보정 작업까지 확장했습니다.

TypeScript Chrome Extension Vitest GitHub Actions API Replay WebView Debugging

설계 배경

앱 기능 검증은 준비 비용이 컸습니다. 간단한 API 흐름이나 WebView-앱 브릿지 동작을 확인하려 해도 앱을 연결해야 했고, 운영에서는 충전존 생성 오류처럼 Admin 화면에 기능이 없지만 단일 API로는 보정 가능한 상황이 반복될 수 있었습니다.

강조 포인트

앱 기능 자체가 아니라 앱 개발과 운영 대응을 빠르게 만드는 도구성 프로젝트입니다. 병목을 발견하고 작은 내부 도구로 구체화하는 일하는 방식을 보여주기에 좋습니다.

핵심 구현

  • Chrome Extension에서 앱이 제공하는 새창, QR 스캔, 카메라 권한, 강제 업데이트 버전 조건을 조정/재현할 수 있는 검증 흐름을 만들었습니다.
  • API 요청을 캡처하고 row 기반 입력으로 replay할 수 있는 구조를 만들어, 앱 연결 없이도 반복 QA와 API 흐름 확인을 빠르게 수행할 수 있게 했습니다.
  • Admin 화면에서 직접 지원하지 않는 단일 API 보정 작업을 위해 variable template, row parser, executor를 구성하고 Bulk Replay로 실행할 수 있게 했습니다.
  • 호스트별 popup 모드를 분리하고, 실행 전 confirm, 401/403 조기 중단, skip 일괄 통보, Vitest 기반 parser/executor 테스트와 CI를 구성했습니다.

엔지니어링 관점

  • 이 도구는 처음부터 운영 자동화만을 목표로 한 것이 아니라, 앱 연결이 필요한 개발 검증 병목을 먼저 줄이는 데서 출발했습니다. 이후 같은 capture/replay 구조가 운영 보정에도 유효하다는 점을 확인하고 범위를 넓혔습니다.
  • 충전존 생성 오류 대응 때 JS `fetch` 스크립트를 직접 세팅해 처리했던 경험을, 매번 새로 짜는 임시 스크립트가 아니라 팀이 다시 쓸 수 있는 row 기반 실행 도구로 바꿨습니다.
  • app/admin 호스트가 섞여 있는 환경에서는 잘못된 화면에 잘못된 조작을 노출하지 않도록 호스트별 UI를 분리하고, 권한 만료나 접근 오류는 대량 replay 전에 멈추도록 설계했습니다.

Mermaid로 보는 핵심 구조

Mermaid View

앱 검증 병목에서 운영 보정 replay까지

앱 연결 없이 앱 의존 흐름을 재현하고, 캡처한 API 요청을 row 기반 replay로 바꿔 개발 QA와 Admin 미지원 운영 보정을 같은 도구 구조로 다루는 흐름입니다.

flowchart TD
  Pain["앱 연결 검증 병목<br/>새창 / QR / 카메라 / 버전"] --> Extension["Chrome Extension<br/>app-like controls"]
  Extension --> Sim["브라우저에서 앱 의존 흐름 재현"]
  Extension --> Capture["API request capture"]
  Capture --> Template["row parser<br/>variable template"]
  Template --> Replay["Bulk Replay executor"]
  Replay --> Guard["confirm / 401·403 early stop<br/>skip 일괄 통보"]
  Guard --> QA["개발 QA 반복 시간 단축"]
  Replay --> Ops["Admin 미지원 단일 API<br/>운영 보정"]
  Ops --> Share["일회성 JS fetch -> 팀 도구"]
  classDef pain fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef tool fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef result fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Pain pain;
  class Extension,Sim,Capture,Template,Replay,Guard tool;
  class QA,Ops,Share result;

운영 결과

  • 앱 없이도 앱 의존 흐름을 브라우저에서 빠르게 확인할 수 있어 개발 검증의 대기 시간과 반복 조작을 줄였습니다.
  • Admin 미지원 단일 API 보정 작업을 일회성 스크립트가 아니라 반복 가능한 내부 도구 절차로 다룰 수 있게 했습니다.
  • 병목이 보이면 작은 도구로 만들어 공유하는 작업 방식을 실제 앱 개발/운영 맥락에서 보여주는 사례가 됐습니다.
Project

07

LG유플러스 볼트업 / 2026.02 - 현재

로밍 서비스 안정성: 공공 연계 상태 재동기화와 재처리

환경부 로밍 카드 상태 재설계, 공공 API 재처리, 월간 전체 재동기화

기후에너지환경부 공공 로밍 연계에서 회원카드 상태가 외부 시스템과 장기적으로 어긋나지 않도록 카드 상태 갱신 기준을 결제 응답에서 빌링 미수 이벤트 중심으로 재설계했습니다. 공공 API 오류 재처리와 월 1회 전체 재동기화 스케줄러를 더해 이벤트 누락이나 일시 장애 이후에도 기준 데이터를 회복할 수 있게 했습니다.

Kotlin Spring Boot Spring Batch GCP Pub/Sub Scheduler

설계 배경

공공 로밍 연계 데이터는 외부 시스템의 상태와 계속 맞아야 하지만, 온라인 이벤트만으로는 누락이나 일시 장애 뒤의 불일치를 자연스럽게 회복하기 어려웠습니다. 회원카드처럼 기준 데이터에 가까운 항목과 충전기 상태처럼 일부 누락을 감내할 수 있는 항목도 같은 우선순위로 처리하면 재처리 비용이 커질 수 있었습니다.

강조 포인트

외부 공공 시스템과 내부 상태를 장기적으로 맞추기 위해 온라인 이벤트, 재처리, 전체 재동기화를 함께 설계한 운영 안정화 프로젝트입니다.

핵심 구현

  • 카드 상태 업데이트 기준을 결제 응답 중심에서 빌링 미수 이벤트 중심으로 바꾸고, 미수 발생 건에 대해서만 선별적으로 상태를 갱신하도록 정리했습니다.
  • 로밍 카드 상태 처리 경로를 단순화하고 빌링 조회를 통합해 변환/조회 오버헤드를 줄였습니다.
  • 공공 API 오류 재처리는 회원카드처럼 기준 데이터 성격이 강한 항목을 우선 처리하고, 충전기 상태처럼 누락 허용 가능한 항목은 후순위로 분리했습니다.
  • 환경부 회원카드 월 1회 전체 재동기화 스케줄러와 task seed를 추가해 온라인 이벤트가 놓친 차이를 주기적으로 복구할 수 있게 했습니다.

엔지니어링 관점

  • 상태 갱신 기준을 결제 응답에 묶어두면 정상 결제 흐름까지 로밍 상태 변경의 원인이 될 수 있어, 실제 보정이 필요한 미수 이벤트로 기준을 좁혔습니다.
  • 재처리는 “무조건 다시 시도”가 아니라 데이터 중요도에 따라 우선순위를 나누는 운영 설계로 봤습니다. 회원카드는 기준 데이터라 먼저 회복하고, 충전기 상태는 후순위로 두어 비용을 조절했습니다.
  • 월간 전체 재동기화는 온라인 이벤트 처리의 보완재로 두었습니다. 이벤트 누락을 완전히 없애려 하기보다, 누락이 생겨도 장기 drift가 누적되지 않는 회복 경로를 만든 것입니다.

Mermaid로 보는 핵심 구조

Mermaid View

공공 로밍 데이터의 이벤트 처리, 재처리, 월간 재동기화

빌링 미수 이벤트 기준 상태 갱신, 중요도별 공공 API 재처리, 월간 전체 재동기화를 함께 두어 외부 시스템과의 장기 drift를 줄이는 구조입니다.

flowchart TD
  Source["환경부 로밍 API<br/>회원카드 / 충전기 상태"] --> Online["온라인 이벤트 처리<br/>card state update"]
  Online --> Arrears["billing 미수 이벤트 기준<br/>필요 건만 상태 갱신"]
  Source --> Retry["공공 API 오류 재처리<br/>중요도별 우선순위"]
  Retry --> Member["회원카드 우선 재처리<br/>기준 데이터 회복"]
  Retry --> Charger["충전기 상태 후순위<br/>누락 허용 항목 분리"]
  Source --> Monthly["월 1회 전체 재동기화<br/>dynamic scheduler + seed"]
  Monthly --> Baseline["외부 시스템과 장기 drift 방지"]
  Arrears --> Stable["운영 데이터 정확성"]
  Member --> Stable
  Baseline --> Stable
  classDef external fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef process fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef result fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Source external;
  class Online,Arrears,Retry,Member,Charger,Monthly process;
  class Baseline,Stable result;

운영 결과

  • 카드 상태 업데이트 기준을 재설계해 불필요한 상태 변경 가능성을 줄이고 데이터 정확성을 높였습니다.
  • 공공 API 오류 이후에도 중요 데이터가 우선 복구되는 재처리 경로를 운영 기준으로 만들었습니다.
  • 월간 전체 재동기화로 이벤트 누락이나 일시 장애 이후에도 회원카드 기준 데이터가 외부 시스템과 다시 맞춰지는 안전망을 확보했습니다.
Project

08

LG유플러스 볼트업 / 2024.10 - 현재

개발 생산성 자동화 및 DevOps 개선

AI 리뷰, Vault-로컬 동기화, 내부 API 표준, 모바일 CI/CD 보안 개선

볼트업 조직에서 반복적으로 발생하던 PR 리뷰, 로컬 환경 셋업, 내부 API 연동, 서비스/앱 배포 작업을 공통 workflow로 묶었습니다. 특히 로컬 환경값은 공개 저장소에 둘 수도 없고 개별 전달도 번거로워서, Vault 값을 `application-local.yaml`로 바로 동기화하는 Gradle 로직을 만들었습니다. 이후 Admin internal API 호출 규약과 Workload Identity 기반 모바일 배포까지 표준화했습니다.

Vault CLI Gradle Kotlin DSL GitHub Actions Jenkins ArgoCD Workload Identity Firebase CLI Gemini API GitHub Copilot Claude Code gcloud CLI

설계 배경

MSA가 늘수록 코드 리뷰 기준, 마이크로서비스별 작업 컨벤션, 반복 작업 방식, 로컬 환경값 전달, 내부 API 호출 방식, 배포 절차가 사람마다 달라지기 쉬웠습니다. 모바일 배포는 Service Account JSON 키 보관과 외부 CDN rate-limit, skip 조건 오발동 같은 운영 리스크도 함께 줄여야 했습니다.

강조 포인트

보안 때문에 공개할 수 없는 로컬 환경값 문제를 Vault-local sync 구조로 풀고, 내부 API 호출/모바일 배포 인증처럼 운영 중 반복적으로 흔들리는 경계를 표준화한 DX/DevOps 프로젝트입니다.

핵심 구현

  • `voltup-workflow`에 `/gemini-review` 댓글 트리거형 GitHub Actions 워크플로우를 만들고, Organization Secret의 `GEMINI_API_KEY`와 저장소별 `project-context`, `review-template`, `docs`를 읽어 재사용 가능한 1차 코드 리뷰 체계를 구성했습니다.
  • MSA 저장소에는 `.agent/workflows`, `.github/skills`, `.github/prompts`, `copilot-instructions.md`를 넣어 여러 생성형 LLM에서 활용할 수 있도록 각 마이크로서비스의 작업 컨벤션, 공통 작업 형상, API 우선 개발 흐름, 보안 규칙을 재사용 가능한 스킬 체계로 정리했습니다.
  • 루트 `build.gradle.kts`에는 base yaml의 placeholder를 Vault에서 치환해 `application-local.yaml`을 생성하는 로직을 넣고, 프로젝트 경로와 `secret/SHARED/voltup/dev`를 순차 조회하도록 만들었습니다. Vault CLI 로그인 확인, 비대화형 환경 대응, `gcloud` 계정 기반 `IAM_DB_USER_NAME` 치환까지 포함해 새 키가 추가돼도 개발자별 local 환경이 자동으로 같은 기준을 유지하도록 했습니다.
  • `feapp-domain-service`에는 인증서와 `conf.json`을 Base64 인코딩해 Vault에 반영하는 `updateCodefCertificatesToVault` 태스크를 만들어, 민감 파일을 저장소나 메신저로 공유하지 않고도 필요한 개발자가 스스로 갱신할 수 있게 했습니다.
  • Admin 내부 연동 API가 늘어나는 상황에서 `admin-internal-*` 클라이언트 패턴과 `X-Internal-Caller` 헤더 규약을 문서화하고 적용해 호출 주체와 신뢰 경계를 일관되게 관리했습니다.
  • 배포는 `devops-cicd` Jenkins shared library 위에서 서비스별 `Jenkinsfile`이 job name으로 API/BATCH/CONSUMER/APP target을 분기하고, Docker build/push 후 ArgoCD 배포로 이어지도록 통일했습니다. Android 앱은 cache, track 선택, 알림까지 같은 패턴으로 자동화했습니다.
  • Android 배포를 fastlane/Service Account JSON 키 중심에서 Workload Identity 기반 Gradle/Play Store REST API와 `firebase-tools` 흐름으로 전환하고, Jenkins 로깅·Slack 알림·토큰 노출 방지를 보강했습니다.
  • iOS 배포 workflow의 skip 조건이 커밋 본문에 의해 오발동하지 않도록 수정하고, CocoaPods CDN raw.githubusercontent.com 429 회피를 위해 netrc 인증을 추가했습니다.

엔지니어링 관점

  • AI 도입을 “모델 하나 붙이기”가 아니라 재사용 가능한 workflow와 repo-local context를 설계하는 문제로 보고, 프로젝트별 문맥을 자동 리뷰 품질에 직접 연결했습니다.
  • 로컬 환경 셋업은 “누가 비밀값을 전달하느냐”보다 “Vault와 local 환경을 직접 동기화해 인증된 개발자가 같은 기준의 설정을 자동으로 받게 하자”는 방향으로 풀었습니다. 공개 저장이나 수동 배포 대신 Vault CLI 인증을 전제로 yaml 생성과 인증서 갱신을 자동화해, 키가 늘어나도 개발자 간 동기화가 흐트러지지 않도록 만들었습니다.
  • 배포 파이프라인은 서비스별로 완전히 다르게 두지 않고 job name 기반 target 분기와 shared library 위로 수렴시켜, 운영 절차를 공통화하면서도 앱/백엔드 차이는 target 수준에서만 드러나게 했습니다.
  • 모바일 배포 인증은 오래 보관되는 JSON 키를 없애는 방향으로 보고 Workload Identity로 바꿨고, 배포 실패 원인은 Slack 메시지와 로그에서 바로 추적할 수 있게 했습니다.
  • 반복 작업이 팀 운영 리스크가 된다고 느낀 지점에서는 문서만 남기지 않고 직접 Gradle 태스크, workflow, Jenkins 파이프라인으로 만들어 팀이 바로 쓸 수 있게 바꿨습니다.

Mermaid로 보는 핵심 구조

Mermaid View

AI 리뷰, Vault-로컬 동기화, 배포 표준화

반복 작업을 발견한 뒤 AI 리뷰 workflow, Vault-local 동기화 기반 yaml 생성, Jenkins/ArgoCD 배포 표준화로 나눈 구조를 한 장에 정리했습니다.

flowchart TD
  Pain["반복 작업<br/>PR 리뷰 / 로컬 ENV / 배포 / 내부 API"] --> Review["voltup-workflow<br/>/gemini-review<br/>org reusable workflow"]
  Review --> Context["project-context + prompts + skills<br/>repo별 규칙 주입"]
  Pain --> Local["Gradle generateYamlAction<br/>application-local.yaml"]
  Local --> Vault["Vault CLI login<br/>project path + SHARED path<br/>secret commit 없음"]
  Local --> IAM["gcloud account -><br/>IAM_DB_USER_NAME"]
  Pain --> Internal["admin-internal-* client<br/>X-Internal-Caller"]
  Pain --> Deploy["Jenkins shared library<br/>job name -> target 분기"]
  Deploy --> Build["docker/app build<br/>cache / track / notifications"]
  Deploy --> Android["Android Workload Identity<br/>Play REST API + firebase-tools"]
  Deploy --> IOS["iOS workflow hardening<br/>skip 조건 / CocoaPods CDN"]
  Build --> Argo["deployArgoCD"]
  Android --> Argo
  IOS --> Argo
  classDef ai fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef sec fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  classDef ops fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  class Review,Context ai;
  class Local,Vault,IAM,Internal sec;
  class Deploy,Build,Android,IOS,Argo ops;

운영 결과

  • 조직 공통 AI 리뷰 워크플로우와 마이크로서비스별 컨벤션·공통 작업 형상 스킬 체계를 만들어, 신규 저장소나 신규 작업도 같은 기준으로 빠르게 온보딩할 수 있게 했습니다.
  • 민감한 환경값을 저장소에 두지 않으면서도 Vault와 local 환경을 바로 동기화해, 키가 추가될 때도 개발자 간 설정 sync가 자동으로 유지되도록 만들었습니다.
  • Jenkins shared library와 ArgoCD 중심 배포 패턴으로 서비스/앱 배포 절차를 단순화하고 수작업 분기를 줄였습니다.
  • Admin internal API와 모바일 배포 인증 방식을 표준화해 운영자 도구 확장과 앱 릴리즈에서 반복되는 보안/운영 리스크를 줄였습니다.
Project

09

카카오스타일 / 2023.12 - 2024.09

프라이싱 플랫폼: 상품 관리 시스템과 프로모션 서비스

PIM과 프로모션을 분리해 고객 노출 최적가 흐름 설계

상품 관리 시스템(PIM)은 외·내부 상품 매칭, 다이나믹 프라이싱, 쇼핑 카탈로그 Engine Page를 담당하고 프로모션 서비스는 멤버십과 파이널 프라이싱을 담당하도록 경계를 나눠, 프로모션의 최종 혜택가와 외부 상품 값을 함께 비교해 고객에게 노출할 합리적 최적가를 계산하도록 정리한 프로젝트입니다.

이력서 연결 지점

내/외부 동일 상품 매칭

이미지 유사도, same-shop exact match, winner score를 기반으로 비교 가능한 상품군을 안정적으로 만드는 상품 매칭 영역입니다.

이력서 연결 지점

파이널 프라이싱 (각 서비스별 가격 계산 로직 통합 API)

멤버십·쿠폰·프로모션·배송비를 포함한 혜택가를 하나의 파이널 프라이싱 API로 표준화한 영역입니다.

이력서 연결 지점

쇼핑 카탈로그 Engine Page 및 최저가 갱신 (네이버쇼핑 / 유튜브쇼핑)

변경 상품만 추려 Engine Page, 쇼핑 피드 CSV, 동기화 데이터셋을 빠르게 생성하는 쇼핑 연동 영역입니다.

Kotlin Spring Boot DGS Framework(GraphQL) AWS Athena

설계 배경

외부 상품 가격, 내부 최적화 점수, 멤버십·쿠폰 혜택처럼 가격 결정 요소가 여러 서비스에 흩어져 있던 상태에서, 운영 정책은 자주 바뀌고 고객에게는 일관된 합리적 최적가를 보여줘야 했기 때문에 PIM과 프로모션의 책임을 나누면서도 한 흐름으로 연결할 구조가 필요했습니다.

강조 포인트

PIM이 외부 상품 값과 프로모션의 파이널 프라이싱 값을 함께 받아 고객에게 보여줄 합리적 최적가를 노출하도록 만든 서비스 경계를 설명하기 좋은 프로젝트입니다.

핵심 구현

  • 상품 관리 시스템의 상품 매칭은 version cache 기준으로 `productId -> matchingId`를 조회하고, 같은 shop의 exact match와 winner score를 묶어 프라이싱 기준 상품군을 정리했습니다.
  • 가격 최적화는 Athena 적용 대상을 읽어 내부/외부 상품을 다시 구성하고, `SUPERIOR / EQUAL = 100`, `UNKNOWN = 50` 규칙으로 price score를 upsert하는 배치 흐름을 운영했습니다.
  • 쇼핑 카탈로그 영역은 상품 업데이트·가격 업데이트 이벤트를 받아 변경 상품만 추려 Engine Page, 쇼핑 피드 CSV, 동기화 데이터셋을 생성하는 공통 경로로 운영했습니다.
  • 프로모션 서비스에서는 멤버십 혜택 조건과 `product / item / order final price` API를 나누고, shipping fee는 `MappedBatchLoader`로 묶어 최종 혜택가를 조합했습니다.

엔지니어링 관점

  • 상품 관리 시스템은 외·내부 상품 매칭, 다이나믹 프라이싱, 쇼핑 카탈로그를 운영하고 프로모션 서비스는 멤버십과 파이널 프라이싱을 담당하도록 경계를 나눠, PIM이 프로모션의 최종 혜택가와 외부 상품 값을 함께 비교해 고객 노출 최적가를 결정하도록 했습니다.
  • 상품 매칭은 version cache와 same-shop exact match 기준을 사용해 비교 가능한 상품군을 먼저 안정화했고, winner score를 함께 노출해 운영 판단 근거도 남겼습니다.
  • 전체 상품을 매번 다시 읽어 변경값을 보내던 흐름 대신, 상품 업데이트와 가격 업데이트 이벤트를 받아 변경 상품만 추려 Engine Page와 네이버 쇼핑이 읽는 CSV·동기화용 데이터셋을 만드는 공통 경로로 바꿔 CPS 2시간 갱신 기준을 맞추고, 같은 구조를 구글 Engine Page(유튜브 쇼핑)에도 빠르게 확장할 수 있게 했습니다.
  • 파이널 프라이싱은 `product / item / order` 경계를 분리하고, 멤버십·쿠폰·프로모션·배송비를 한 응답 안에서 조합하면서도 shipping 조회 비용은 DataLoader로 제어했습니다.

Mermaid로 보는 핵심 구조

Mermaid View

상품 관리 시스템에서 프로모션 서비스까지 이어지는 프라이싱 흐름

PIM은 외·내부 상품 매칭, 다이나믹 프라이싱, 쇼핑 카탈로그를 담당하고 프로모션 서비스는 멤버십과 파이널 프라이싱을 담당한 뒤, PIM이 프로모션의 최종 혜택가와 외부 상품 값을 함께 비교해 고객 노출 최적가를 만드는 구조를 한 장으로 정리했습니다.

flowchart TD
  Req["request<br/>product=421 user=3001 site=KR"] --> Match
  Req --> Member
  subgraph PIMSYS["상품 관리 시스템 (PIM)"]
    direction TD
    Match["외·내부 상품 매칭<br/>matchingId / same-shop / winner score"]
    Optimize["다이나믹 프라이싱<br/>price score / compare set"]
    Catalog["쇼핑 카탈로그 Engine Page<br/>Naver / YouTube feed sync"]
    External["외부 상품 값<br/>lowest price / sync dataset"]
    Match --> Optimize --> Catalog --> External
  end
  subgraph PROMO["프로모션 서비스 (Promotion)"]
    direction TD
    Member["Membership<br/>grade / eligibility"]
    Final["Final Pricing API<br/>coupon / promotion / shipping"]
    Member --> Final
  end
  External --> Expose
  Final --> Expose
  Expose["PIM 고객 노출 가격<br/>promotion final + external price"] --> Resp["response<br/>합리적 최적가 노출"]
  classDef pim fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef promo fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef expose fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Match,Optimize,Catalog,External pim;
  class Member,Final promo;
  class Expose,Resp expose;

운영 결과

  • 상품 관리 시스템이 프로모션의 파이널 프라이싱 값과 외부 상품 값을 함께 받아 고객 노출 최적가를 계산하도록 구조를 정리했습니다.
  • 전체 상품 갱신에 기대면 약 6시간이 걸리던 구조에서, 변경 상품만 이벤트 기반으로 반영하는 경로를 추가해 네이버 쇼핑용 CSV와 동기화 데이터셋을 1시간 이내에 생성할 수 있게 했습니다.
  • 네이버 쇼핑 기준으로 만든 Engine Page·최저가 갱신 구조를 공통화해 구글 Engine Page(유튜브 쇼핑)도 빠르게 반영할 수 있는 확장 기반을 마련했습니다.
  • 상품, 아이템, 주문 단위의 파이널 프라이싱 응답을 표준화해 멤버십·쿠폰·프로모션 혜택가를 여러 지면과 운영 배치에서 같은 계약으로 재사용할 수 있게 했습니다.
  • 상품 관리 시스템 쪽 정책이 바뀌어도 그쪽 입력과 운영 로직만 조정하고, 프로모션 서비스의 사용자 응답 계약은 안정적으로 유지할 수 있게 했습니다.
Project

10

카카오스타일 / 2023.04 - 2023.06

멤버십/마일리지 서비스 이관 및 고도화

레거시 API 응답 동등성 검증 기반 Spring Boot 무중단 이관

리텐션 강화를 위해 멤버십 등급 체계를 재설계하고, 레거시 멤버십 서비스(cormo.js 기반)를 Spring Boot로 1:1 DB 마이그레이션 및 무중단 이관했습니다. 특히 기존 멤버십 API에서 실제 request·response 셋을 수집해 테스트케이스를 만들고, 이를 Spring 로직에 직접 재주입해 응답 차이를 비교한 뒤 게이트웨이를 점진 전환하는 방식으로 오픈했으며, 월간 등급 산정도 Athena partition source 기반으로 다시 정리했습니다.

Kotlin Spring Boot Spring Batch DGS Framework(GraphQL) JPA MySQL Kafka AWS Athena

참고 화면

Preview

지그재그 멤버십: 등급 혜택 노출 화면

확장된 멤버십 등급 체계와 등급별 혜택이 실제 사용자 화면에서 어떻게 노출되는지 보여주는 예시입니다.

지그재그 멤버십 혜택 화면

지그재그 멤버십: 등급 혜택 노출 화면

확장된 멤버십 등급 체계와 등급별 혜택이 실제 사용자 화면에서 어떻게 노출되는지 보여주는 예시입니다.

설계 배경

기존 cormo.js 기반 서비스를 1:1 DB 마이그레이션으로 옮기면서도 실제 사용자 응답이 달라지지 않게 유지해야 했고, 월간 등급 산정 배치가 사용자 수와 월 수가 늘어날수록 더 넓은 범위를 재조회하는 구조가 되지 않도록 막아야 했습니다.

강조 포인트

실제 레거시 API 응답 셋을 수집해 Spring 구현과 동등성 비교를 거친 뒤 점진 오픈한 무중단 이관과, Athena 기반 멤버십 배치 최적화를 함께 설명하기 좋은 프로젝트입니다.

핵심 구현

  • 기존 멤버십 API에서 실제 request·response 셋을 수집하고, query·body·경계 케이스까지 테스트케이스로 정리한 뒤 Spring Boot 구현에 같은 입력을 직접 넣어 응답 차이를 비교했습니다.
  • 멤버십 등급 산정 기간을 3개월에서 6개월로 확대했습니다.
  • 응답 동등성 검증을 통과한 뒤 게이트웨이를 점진적으로 전환해, 사용자 응답을 깨지 않고 Spring Boot 서비스로 무중단 오픈했습니다.
  • Athena `vip_confirmed_paid_partitioned`를 `stamp_date` 기준으로 조회하고, `queryExecutionId + nextToken`으로 페이지 처리한 뒤 `UserConfirmedPaid(cashAmountConfirmed, cashAmountPredicted)`로 변환해 월간 배치 입력으로 사용했습니다.
  • `cashAmountConfirmed`를 최근 6개월 누적 확정금액으로 사용해 등급을 계산하고, 결과는 `saveMembershipBatchInsert` / `updateMembershipBatchUpdate`로 batch upsert했으며, 조회는 `getConfirmedAmountUpdateDateMonthYmSet(3/2)`와 `membership_logs`의 `date_applied_ym` RANGE PARTITION으로 최근 월 범위만 다루도록 정리했습니다.

엔지니어링 관점

  • 무중단 이관은 기능 추가보다 응답 동등성 확보를 먼저 두고, 실제 레거시 API request·response 셋을 테스트 자산으로 바꿔 Spring 구현에 반복 재주입하는 방식으로 검증했습니다.
  • 월간 배치는 Athena partitioned source에서 필요한 `stamp_date`만 읽고, `queryExecutionId + nextToken`으로 잘게 나눠 가져오도록 구성해 대량 대상도 한 번에 메모리로 끌어오지 않게 했습니다.
  • `UserConfirmedPaid`에 최근 6개월 확정금액과 이번 달 포함 누적값을 분리해 담고, 이를 `confirmedAmount`, `confirmedAmountNow`, `confirmed5MonthAmount`로 저장해 등급 산정과 이후 조회 기준이 데이터 모델에 직접 남도록 했습니다.
  • 배치 쓰기는 JDBC batch insert/update로 묶고, 조회 쪽은 `getConfirmedAmountUpdateDateMonthYmSet`으로 최근 월 집합만 보게 하며 `membership_logs`는 `date_applied_ym` RANGE PARTITION으로 관리해 데이터가 쌓여도 필요한 월만 다루게 했습니다.

Mermaid로 보는 핵심 구조

Mermaid View

기존 API 응답 동등성 검증 기반 무중단 이관

기존 멤버십 API에서 request·response 셋을 수집해 테스트케이스로 만들고, Spring Boot 구현에 같은 입력을 재주입해 응답 차이를 비교한 뒤 게이트웨이를 점진 전환하는 무중단 이관 흐름을 보여줍니다.

flowchart TD
  Legacy["기존 멤버십 API<br/>request / response set 수집"] --> Cases["테스트 케이스화<br/>query / body / edge case"]
  Cases --> Replay["Spring Boot 로직에<br/>동일 입력 재주입"]
  Replay --> Compare{"legacy 응답과 동일?"}
  Compare -->|yes| Ready["배포 후보 확정"]
  Compare -->|no| Fix["로직 / serializer diff 수정"]
  Fix --> Replay
  Ready --> Switch["게이트웨이 점진 전환"]
  Switch --> Open["무중단 오픈"]

Mermaid View

월간 누적합과 월간 파티셔닝 기반 멤버십 배치

Athena의 `vip_confirmed_paid_partitioned`를 `stamp_date` 기준으로 읽고, `UserConfirmedPaid`를 거쳐 멤버십을 batch upsert한 뒤, 최근 월 집합 조회와 `membership_logs` 파티셔닝으로 범위를 제한하는 흐름을 보여줍니다.

flowchart TD
  Source["Athena source<br/>vip_confirmed_paid_partitioned<br/>stamp_date=2023-10-08"] --> Reader["reader paging<br/>queryExecutionId + nextToken"]
  Reader --> Paid["UserConfirmedPaid<br/>user=421 confirmed=330000<br/>predicted=350000"]
  Paid --> Calc["level calc<br/>최근 6개월 누적합 기준"]
  Calc --> Upsert["membership upsert<br/>dateAppliedYm=202310"]
  Upsert --> Current["memberships<br/>batch insert / update"]
  Upsert --> Archive["membership_logs<br/>RANGE(date_applied_ym)"]
  Current --> Query["recent Ym lookup<br/>202310, 202309, 202308"]
  Archive --> Query

운영 결과

  • 월간 누적합과 월간 파티셔닝 구조로 DB 부하를 임계치 70%에서 30% 이내로 낮췄습니다.
  • 기존 API request·response 셋 기반 테스트케이스와 Spring 응답 비교를 통과한 뒤 게이트웨이를 점진 전환해 무중단 배포를 성공시켰습니다.
  • Athena partitioned source, page reader, JDBC batch upsert, 최근 월 집합 조회를 조합해 대량 고객 데이터가 누적돼도 월별 등급 산정 성능을 안정적으로 유지했습니다.
Project

11

개인 프로젝트 / 운영 중

Commit Map

자연어 여행 루트를 구조화된 지도 콘텐츠로 바꾸는 작성 플로우 설계

개인 여행 계획을 지인과 공유하려고 만든 지도 기반 여행 계획 서비스입니다. 여행지와 이동 루트를 자연어로 적으면 AI 워크플로우가 일정 초안을 만들고, 이후 Markdown으로 직접 세부 동선을 고도화할 수 있게 구성했습니다.

Astro React Leaflet TypeScript Markdown GitHub Pages Antigravity

참고 화면

Preview

메인 화면: 세계 지도와 여행 계획 카드

국가 필터, 세계 지도, 여행 계획 카드가 한 화면에서 이어지는 구성을 참고 이미지로 정리했습니다.

Commit Map 메인 화면 참고 이미지

메인 화면: 세계 지도와 여행 계획 카드

서비스 보기

국가 필터, 세계 지도, 여행 계획 카드가 한 화면에서 이어지는 구성을 참고 이미지로 정리했습니다.

설계 배경

여행 계획은 메신저 대화, 지도 링크, 메모가 흩어지기 쉬워 공유와 수정이 번거롭고, 처음부터 장소 좌표와 일정 구조를 모두 수작업으로 넣는 것도 비용이 컸습니다.

강조 포인트

취미 프로젝트이지만, 자연어 입력 → 구조화된 초안 → 사람이 고도화하는 워크플로우를 실제 서비스 형태로 풀어본 사례입니다.

핵심 구현

  • Astro + React + Leaflet으로 여행 카드, 상세 지도, 타임라인을 결합한 정적 웹 서비스를 만들고, 장소 타입·순서·방문일 기준으로 동선을 시각화했습니다.
  • 여행 포스트를 Markdown frontmatter와 location 스키마로 관리해, 일정·좌표·노트·링크를 구조화된 데이터로 다루고 추후 수동 수정이 쉬운 형태로 유지했습니다.
  • 프로젝트에 포함한 AI 워크플로우로 여행지와 이동 루트를 자연어로 입력하면 초안 포스트, 장소 후보, 기본 일정 구성을 빠르게 만들고, 이후 제가 직접 세부 계획과 콘텐츠를 고도화하는 플로우로 설계했습니다.
  • GitHub Pages 기반 정적 배포로 운영해 여행 계획 링크를 바로 공유할 수 있게 하고, 특정 여행 포스트를 URL 단위로 바로 전달할 수 있게 구성했습니다.

엔지니어링 관점

  • AI는 완성본을 대신 쓰게 하기보다, 처음 루트를 잡아주는 초안 생성기로 두고 최종 계획의 진실은 Markdown 데이터에 남기도록 설계했습니다.
  • 여행 콘텐츠는 글만 있는 블로그보다 지도, 타임라인, 장소 데이터가 함께 보여야 공유 가치가 높다고 보고, 시각화와 데이터 구조를 한 흐름으로 묶었습니다.
  • 개인 프로젝트여도 운영 부담이 커지지 않도록 정적 배포와 콘텐츠 파일 기반 운영으로 유지비를 낮추고, 필요한 부분만 점진적으로 확장할 수 있게 했습니다.

운영 결과

  • 여행 계획을 메신저 조각 대신 링크 하나로 공유할 수 있는 개인 서비스로 운영하고 있습니다.
  • 여행지와 이동 루트만 적어도 AI 워크플로우가 초안 계획을 빠르게 만들어주고, 이후 직접 세부 동선을 고도화할 수 있는 작성 흐름을 만들었습니다.
  • 지도, 타임라인, 장소 메타데이터를 같은 콘텐츠 모델로 묶어, 여행 기록과 향후 여행 계획을 같은 방식으로 운영할 수 있게 했습니다.