English | 한국어
A backend portfolio project that reproduces contact-eligibility decisions and controls access to personal data at the customer touchpoints of an insurance General Agency (GA) and its call center.
The detailed design covers 58 tables and 28 target APIs. This repository implements the first delivery slice — customer, consent, contact restriction, and audit — as a working Java application. Rather than plain CRUD, it focuses on the controls that must not fail in an insurance sales system: tenant isolation, sensitive-data encryption, idempotency, the Transactional Outbox pattern, and fail-closed policy.
| Item | Detail |
|---|---|
| Runtime | Java 21, Spring Boot 4.1.0, Maven Wrapper |
| Persistence | MySQL 8.4, Spring JDBC, Flyway |
| Security | OAuth2 Resource Server, JWT permission/tenant claims, RBAC + tenant guard |
| Privacy | AES-256-GCM, HMAC-SHA256 equality search, masked by default |
| Reliability | Idempotency-Key, Outbox, audit hash chain, fail closed |
| Implemented | 6 domains, 9 API operations, 58-table baseline |
| Verification | 8 tests + MySQL 8.4.10 HTTP end-to-end flow |
Areas where local consistency between domains matters live inside a modular monolith. Recording verification, STT, search, external integration, and data destruction are designed to scale out as Outbox-driven asynchronous workers.
com.company.insurance.sales
├── customer customer creation, search, masking
├── privacy current consent state and immutable events
├── contactpolicy contact restrictions and eligibility policy
├── audit audit queries
└── shared
├── security JWT permissions, tenant claim checks, request context
├── persistence encryption, HMAC search, tenant reference
├── integration idempotency handling and transactional outbox
└── web RFC 9457 errors and correlation ID
The full logical model is available in the design artifacts below.
- Customer / consent / contact-control ERD
- Sales / consultation / recording ERD
- Contract / compliance / lifecycle ERD
- Security / audit / integration ERD
- Every
/v1/**request requires anX-Tenant-Idheader. - The header is not a value the caller freely chooses — it is checked against the
tenantsclaim in the JWT. - Repository SQL always includes
tenant_idin its predicates, and only ULIDs are exposed externally. - Guessing another tenant's ULID or tampering with the header is blocked, and this is covered by HTTP integration tests.
- Customer names and phone numbers are encrypted with AES-256-GCM.
- Tenant, resource, and field name are combined into the AAD to prevent ciphertext substitution attacks.
- Search uses secret-keyed HMAC-SHA256 tokens instead of a plaintext index.
- API responses return masked values only, such as
홍*동and+821****5678. - Without production keys the application fails to start; no plaintext fallback is permitted.
Contact eligibility is composed in the following order.
ABSOLUTE BLOCK
→ EXTERNAL DNC
→ COMPANY OPT-OUT
→ CONSENT / LEGAL BASIS
→ CONTACT TIME
→ FREQUENCY
→ AGENT ELIGIBILITY
→ CAMPAIGN POLICY
Each decision is stored as a snapshot containing the policy version, the highest-priority reason, the evaluation timestamp, and the evidence ID. When the external Do-Not-Call system cannot be reached, the production default is UNAVAILABLE and marketing contact is resolved to DENY.
- POSTs at risk of duplication store an
Idempotency-Keyplus a request hash and replay the identical response. - Reusing the same key with a different body returns
409 IDEMPOTENCY_CONFLICT. - Consent, opt-out, and contact decisions write business data and the Outbox event in a single transaction.
- Sensitive reads and writes are recorded as audit events chaining the correlation ID and the previous event hash.
- Registering an opt-out immediately removes scheduled follow-up tasks and campaign targets.
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| POST | /v1/customers |
customer.write |
Create an encrypted customer |
| GET | /v1/customers |
customer.read |
HMAC-based customer search |
| GET | /v1/customers/{customerId} |
customer.read |
Masked customer detail |
| POST | /v1/customers/{customerId}/consents |
consent.write |
Register consent and its immutable event |
| GET | /v1/customers/{customerId}/consents |
consent.read |
Read a customer's consent history |
| POST | /v1/customers/{customerId}/contact-restrictions |
contact.write |
Register opt-out / contact restriction |
| GET | /v1/customers/{customerId}/contact-restrictions |
contact.read |
Read contact restrictions |
| POST | /v1/contact-eligibility/evaluations |
contact.evaluate |
Evaluate contact eligibility just before dialing |
| GET | /v1/audit-events |
audit.read |
Cursor-based audit search |
The full target OpenAPI 3.1 contract defines 28 operations spanning campaigns, CTI/recording, consultation, contracts, complaints, and data destruction. Later APIs are deliberately left out of the table above to keep the implemented scope distinct from the target contract.
- Java 21
- Docker Desktop with Compose v2
./mvnw verifyThe default test run applies the full 58-table Flyway baseline to H2 in MySQL compatibility mode.
./scripts/test-mysql.shThe script waits for the MySQL container to become healthy, recreates an isolated insurance_ga_it database on every run, and then verifies over real HTTP the flow of customer creation → consent → contact allowed → opt-out → contact denied, along with idempotent replay and cross-tenant blocking. The insurance_ga local development database is left untouched. The container stays up after the test and can be stopped with:
docker compose downdocker compose up -d --wait mysql
SPRING_PROFILES_ACTIVE=local ./mvnw spring-boot:runThe local profile idempotently seeds development reference data for tenant, organization, agent, and consent.
curl -i http://localhost:8080/v1/customers \
-H 'Content-Type: application/json' \
-H 'X-Tenant-Id: 01J00000000000000000000000' \
-H 'Idempotency-Key: customer-demo-0001' \
-d '{
"partyType": "PERSON",
"displayName": "홍길동",
"mobileNumber": "010-1234-5678"
}'Header-based development authentication is enabled only in the local profile. If X-Actor-Id, X-Permissions, and X-Allowed-Tenants are omitted, the local agent and the default permissions for the implemented scope are used.
Local verification results as of 2026-07-19.
| Check | Result |
|---|---|
./mvnw verify |
8 tests, 0 failures, 0 errors |
| MySQL image | MySQL Community Server 8.4.10 |
| Flyway | V1__baseline.sql, success |
| Schema | 58 design tables confirmed created |
| End-to-end | customer → consent → ALLOW → opt-out → DENY passed |
| Evidence | 5 outbox records and 5 audit events confirmed |
GitHub Actions also runs the H2 verification and the MySQL 8.4 service-container verification as separate jobs.
Use .env.example as a reference, and never commit production secrets to the repository.
| Variable | Purpose |
|---|---|
DB_URL, DB_USERNAME, DB_PASSWORD |
Production database connection details |
APP_DATA_KEY_BASE64 |
Base64 value of the AES-256 data key |
APP_HMAC_KEY_BASE64 |
Base64 value of the search HMAC key |
OAUTH2_JWK_SET_URI |
Production IdP JWKS URL |
EXTERNAL_DNC_MODE |
ALLOW, DENY, UNAVAILABLE |
The local profile is not used in production. The JWT permissions array is interpreted as API permissions, and the tenants array as the accessible tenants.
- Detailed design document (DOCX)
- MySQL/MariaDB DDL
- OpenAPI 3.1
- Executable Flyway migration
- Notes on the source artifacts
- Internal primary keys are
BIGINTwhile external identifiers are ULIDs, trading off index efficiency against unguessability. - Complex queries use Spring JDBC for explicit SQL control, and API DTOs are kept separate from database models.
- The external DNC integration is currently a mode-based adapter and fails closed on marketing requests until the production integration lands.
- Recording originals are designed to be stored in S3-compatible object storage rather than as database BLOBs — design only at this stage.
- Next up: CTI inbox, recording integrity verification, consultation, insurer contract synchronization, legal hold, and the destruction worker.
This repository is a portfolio demonstrating architecture and backend implementation skills. It is not a product ready for real insurance sales operations or legal advice. Retention periods, consent wording, Do-Not-Call integration, outsourcing relationships, and access rights must all be reviewed against the laws in force at the time of adoption and by the company's compliance and privacy functions.
Code is available under the MIT License.
English | 한국어
보험 GA·콜센터의 고객 접점에서 연락 가능성 판단을 재현하고 개인정보 접근을 통제하는 백엔드 포트폴리오 프로젝트입니다.
58개 테이블과 28개 목표 API로 구성된 상세설계 중 첫 번째 구현 단계인 고객·동의·접촉 제한·감사 영역을 실제 동작하는 Java 애플리케이션으로 구현했습니다. 단순 CRUD보다 테넌트 격리, 민감정보 암호화, 멱등성, Transactional Outbox, Fail Closed 정책처럼 보험 영업 시스템에서 실패하면 안 되는 통제에 집중했습니다.
| 항목 | 내용 |
|---|---|
| Runtime | Java 21, Spring Boot 4.1.0, Maven Wrapper |
| Persistence | MySQL 8.4, Spring JDBC, Flyway |
| Security | OAuth2 Resource Server, JWT permission/tenant claims, RBAC + tenant guard |
| Privacy | AES-256-GCM, HMAC-SHA256 동등검색, 기본 마스킹 |
| Reliability | Idempotency-Key, Outbox, 감사 해시 체인, Fail Closed |
| Implemented | 6개 도메인, 9개 API operation, 58개 테이블 기준선 |
| Verification | 8 tests + MySQL 8.4.10 HTTP end-to-end flow |
도메인 간 로컬 정합성이 중요한 영역은 모듈형 모놀리스에 두고, 녹취 검증·STT·검색·외부 연계·파기는 Outbox 기반 비동기 워커로 확장하는 구조입니다.
com.company.insurance.sales
├── customer 고객 생성·검색·마스킹
├── privacy 동의 현재 상태와 불변 이벤트
├── contactpolicy 접촉 제한과 연락 가능성 정책
├── audit 감사 조회
└── shared
├── security JWT 권한, tenant claim 검증, request context
├── persistence 암호화, HMAC 검색, tenant reference
├── integration 멱등 처리와 transactional outbox
└── web RFC 9457 오류와 correlation ID
전체 논리 모델은 다음 설계 자료에서 확인할 수 있습니다.
- 모든
/v1/**요청에X-Tenant-Id를 요구합니다. - 헤더 값은 사용자가 자유롭게 선택하는 값이 아니라 JWT의
tenantsclaim과 대조합니다. - Repository SQL은 항상
tenant_id를 조건으로 포함하고, 외부에는 ULID만 노출합니다. - 타 테넌트 ULID를 추측하거나 헤더를 변조하는 흐름을 HTTP 통합 테스트에서 차단합니다.
- 고객명과 연락처 원문은 AES-256-GCM으로 암호화합니다.
- 테넌트·리소스·필드명을 AAD로 결합해 암호문 교체 공격을 방지합니다.
- 검색은 평문 인덱스 대신 비밀키 기반 HMAC-SHA256 토큰을 사용합니다.
- API 응답에는
홍*동,+821****5678처럼 마스킹 값만 반환합니다. - 운영 키가 없으면 애플리케이션 시작을 실패시키며 평문 fallback을 허용하지 않습니다.
연락 가능성 평가는 다음 순서로 합성됩니다.
ABSOLUTE BLOCK
→ EXTERNAL DNC
→ COMPANY OPT-OUT
→ CONSENT / LEGAL BASIS
→ CONTACT TIME
→ FREQUENCY
→ AGENT ELIGIBILITY
→ CAMPAIGN POLICY
판단 결과에는 정책 버전, 최우선 사유, 평가시각, 증적 ID를 스냅샷으로 저장합니다. 외부 두낫콜 시스템을 확인할 수 없는 경우 운영 기본값은 UNAVAILABLE이며 마케팅 연락을 DENY 처리합니다.
- 중복 위험 POST는
Idempotency-Key와 요청 해시를 저장하고 동일 응답을 재현합니다. - 같은 키를 다른 본문에 사용하면
409 IDEMPOTENCY_CONFLICT를 반환합니다. - 동의·수신거부·연락 판단은 업무 데이터와 Outbox 이벤트를 한 트랜잭션에 기록합니다.
- 중요 조회·변경은 correlation ID와 이전 이벤트 해시를 연결한 감사 이벤트로 남깁니다.
- 수신거부 등록 시 예약 후속업무와 캠페인 대상을 즉시 제외합니다.
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| POST | /v1/customers |
customer.write |
암호화 고객 생성 |
| GET | /v1/customers |
customer.read |
HMAC 기반 고객 검색 |
| GET | /v1/customers/{customerId} |
customer.read |
마스킹 고객 상세 |
| POST | /v1/customers/{customerId}/consents |
consent.write |
동의와 불변 이벤트 등록 |
| GET | /v1/customers/{customerId}/consents |
consent.read |
고객 동의 이력 조회 |
| POST | /v1/customers/{customerId}/contact-restrictions |
contact.write |
수신거부·접촉 제한 등록 |
| GET | /v1/customers/{customerId}/contact-restrictions |
contact.read |
접촉 제한 조회 |
| POST | /v1/contact-eligibility/evaluations |
contact.evaluate |
발신 직전 연락 가능성 평가 |
| GET | /v1/audit-events |
audit.read |
커서 기반 감사 검색 |
전체 목표 OpenAPI 3.1 계약은 캠페인·CTI/녹취·상담·계약·민원·파기까지 28개 operation을 정의합니다. 현재 구현 범위와 목표 계약을 구분하기 위해 후속 API는 위 표에 포함하지 않았습니다.
- Java 21
- Docker Desktop with Compose v2
./mvnw verify기본 테스트는 H2의 MySQL 호환 모드에 전체 58개 테이블 Flyway 기준선을 적용합니다.
./scripts/test-mysql.sh스크립트는 MySQL 컨테이너를 healthy 상태까지 기다리고 격리된 insurance_ga_it 데이터베이스를 매번 새로 만든 후, 고객 생성 → 동의 → 연락 허용 → 수신거부 → 연락 차단 흐름과 멱등 재현·타 테넌트 차단을 실제 HTTP로 검증합니다. insurance_ga 로컬 개발 DB는 변경하지 않습니다. 테스트 후 컨테이너는 유지되며 다음 명령으로 중지할 수 있습니다.
docker compose downdocker compose up -d --wait mysql
SPRING_PROFILES_ACTIVE=local ./mvnw spring-boot:run로컬 프로필은 개발용 테넌트·조직·상담사·동의 기준정보를 멱등하게 준비합니다.
curl -i http://localhost:8080/v1/customers \
-H 'Content-Type: application/json' \
-H 'X-Tenant-Id: 01J00000000000000000000000' \
-H 'Idempotency-Key: customer-demo-0001' \
-d '{
"partyType": "PERSON",
"displayName": "홍길동",
"mobileNumber": "010-1234-5678"
}'로컬 프로필에서만 헤더 기반 개발 인증이 활성화됩니다. X-Actor-Id, X-Permissions, X-Allowed-Tenants를 생략하면 로컬 상담사와 구현 범위의 기본 권한을 사용합니다.
2026-07-19 기준 로컬 검증 결과입니다.
| Check | Result |
|---|---|
./mvnw verify |
8 tests, 0 failures, 0 errors |
| MySQL image | MySQL Community Server 8.4.10 |
| Flyway | V1__baseline.sql, success |
| Schema | 설계 테이블 58개 생성 확인 |
| End-to-end | customer → consent → ALLOW → opt-out → DENY 통과 |
| Evidence | Outbox 5건, audit event 5건 생성 확인 |
GitHub Actions도 H2 검증과 MySQL 8.4 서비스 컨테이너 검증을 별도 job으로 실행합니다.
.env.example을 참고하되 운영 비밀값을 저장소에 커밋하지 않습니다.
| Variable | Purpose |
|---|---|
DB_URL, DB_USERNAME, DB_PASSWORD |
운영 DB 접속 정보 |
APP_DATA_KEY_BASE64 |
AES-256 데이터 키의 Base64 값 |
APP_HMAC_KEY_BASE64 |
검색 HMAC 키의 Base64 값 |
OAUTH2_JWK_SET_URI |
운영 IdP JWKS URL |
EXTERNAL_DNC_MODE |
ALLOW, DENY, UNAVAILABLE |
운영 환경에서는 local 프로필을 사용하지 않습니다. JWT의 permissions 배열은 API 권한으로, tenants 배열은 접근 가능한 테넌트로 해석합니다.
- 내부 PK는
BIGINT, 외부 식별자는 ULID를 사용해 인덱스 효율과 추측 방지를 절충했습니다. - 복잡 조회는 SQL 제어가 명확한 Spring JDBC를 사용하고 API DTO와 DB 모델을 분리했습니다.
- 현재 외부 DNC는 모드 기반 adapter이며 운영 연계 전까지 마케팅 요청을 Fail Closed 처리합니다.
- 녹취 원본은 DB BLOB이 아니라 S3 호환 오브젝트 스토리지에 저장하도록 설계만 포함합니다.
- 다음 단계는 CTI Inbox, 녹취 무결성 검증, 상담, 보험사 계약 동기화, Legal Hold와 파기 워커입니다.
이 저장소는 아키텍처·백엔드 구현 역량을 보여주기 위한 포트폴리오이며 실제 보험 영업 또는 법률 자문에 바로 사용할 수 있는 제품이 아닙니다. 보존기간, 동의 문안, 두낫콜 연계, 위탁관계, 접근 권한은 적용 시점의 법령과 회사 준법·개인정보보호 검토를 거쳐야 합니다.
Code is available under the MIT License.
