네트워크가 끊겨도 판매 원장과 outbox를 로컬 SQLite에 함께 기록하고, 연결 복구 후 SQL Server로 중복 없이 동기화하는 Windows POS 포트폴리오입니다. CJ올리브영 Windows Application Engineer (POS 시스템) 공고에 맞춰 C#, .NET Framework 4.8 WPF, Windows Raw Input, REST, TCP 소켓, SQLite와 MSSQL 경계를 한 개의 실행 가능한 수직 슬라이스로 구성했습니다.
이 저장소는 개인 검증 프로젝트입니다. 실제 올리브영 POS, 카드사 승인망, 벤더 장비 SDK를 연동하거나 운영한 결과가 아닙니다.
이 프로젝트는 네트워크가 끊겨도 판매를 멈추지 않는 Windows POS를 검증합니다.
| 질문 | 이 저장소의 답 |
|---|---|
| 무엇을 해결했나 | 중앙 서버가 끊겨도 Sales + SaleItems + Outbox를 SQLite 한 transaction으로 확정 |
| 어떻게 중복을 막나 | at-least-once 전송과 MSSQL MessageId + payload hash 멱등 consumer 조합 |
| 무엇으로 증명했나 | 실제 SQLite/LocalDB 통합 테스트 76개, 응답 유실·동시 중복·payload 충돌 fixture |
| 채용 담당자가 먼저 볼 것 | Showcase 화면 · 2페이지 기술서 · 상세 Markdown 문서는 로컬 전용 |
핵심 구현보다 중요한 것은 무엇을 보장하고 무엇을 보장하지 않는지를 분리한 점입니다. 이 프로젝트가 보장하는 것은 중복 매출 등록 방지 설계이며, 실제 카드 승인 중복 방지나 운영 경험을 주장하지 않습니다.
2026-07-17, Windows 11과 SQL Server LocalDB 환경에서 다음을 확인했습니다.
Release전체 빌드: 경고 0개, 오류 0개- 자동 테스트: 76개 통과, 실패·건너뜀 0개
- NuGet 알려진 취약 package: 0개
- 실제 SQLite 통합 테스트: 판매·판매항목·outbox 원자 커밋과 rollback, lease 만료 복구
- 실제 SQL Server LocalDB 통합 테스트: 순차/16개 동시 중복, payload 충돌, 정산 프로시저
- SQLite → HTTP → LocalDB E2E 테스트: 오프라인 판매 5건 복구, 서버 커밋 후 응답 유실 복구
- 부하 도구 smoke: LocalDB API 20건 모두
Applied; SQLite 100,000행/20,000회 warm-cache 조회 1회 성공 - 정식 합성 sync run: 5,000건, 50개 batch, 동시성 4, 3,414 sales/s, batch p50 88.706ms / p95 310.092ms / p99 310.150ms, 모두
Applied - WPF 운영 흐름: 상품 검색·카테고리·키보드 단축키, 당일 매출 KPI, 거래 내역 조회·선택 영수증 재출력
검증한 범위와 아직 검증하지 않은 범위는 로컬 전용 docs/implementation-status.md에 분리해 두었습니다.
- Windows 11(현재 검증 환경)
- .NET SDK 10.0.301 또는 같은 10.0 feature band의 최신 patch
- .NET Framework 4.8 targeting pack과 WPF 빌드 도구
- SQL Server LocalDB의
(localdb)\MSSQLLocalDB인스턴스 - demo helper script를 사용할 경우
sqllocaldb와sqlcmd
Docker는 사용하지 않습니다. 서버를 처음 실행하면 OfflinePOS LocalDB 데이터베이스와 테이블·인덱스·프로시저를 내장 SQL 스크립트로 생성합니다.
저장소 루트의 PowerShell에서 빌드와 테스트를 실행합니다.
sqllocaldb start MSSQLLocalDB
dotnet restore OfflinePOS.sln
dotnet build OfflinePOS.sln -c Release --no-restore
dotnet test OfflinePOS.sln -c Release --no-build세 process를 helper로 시작할 수도 있습니다. WPF 창은 직접 닫고, 종료 script는 server와 terminal helper process를 정리합니다. 로컬 실행 정책이 script를 막으면 아래처럼 process 범위에서만 bypass합니다.
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-demo.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\stop-demo.ps1이후 PowerShell 창 세 개를 열어 순서대로 실행합니다.
# 터미널 1: REST 동기화 서버, http://localhost:5056
dotnet run --project src\OfflinePOS.Server\OfflinePOS.Server.csproj --launch-profile http# 터미널 2: TCP 모의 카드단말, 127.0.0.1:5055
dotnet run --project src\OfflinePOS.CardTerminalEmulator\OfflinePOS.CardTerminalEmulator.csproj -- --mode approved --port 5055# 터미널 3: .NET Framework 4.8 WPF 클라이언트
.\src\OfflinePOS.Client\bin\Release\net48\OfflinePOS.Client.exe지원서용 화면을 빠르게 재현하려면 Showcase 모드를 사용할 수 있습니다. 상품 3개와 오프라인 상태를 미리 표시하며, 실제 판매를 자동 확정하지는 않습니다.
$env:OFFLINEPOS_SHOWCASE="1"
.\src\OfflinePOS.Client\bin\Release\net48\OfflinePOS.Client.exe클라이언트는 %LOCALAPPDATA%\OfflinePOSDemo\offline-pos.db를 로컬 원장으로 사용하고, 영수증 텍스트 파일은 %LOCALAPPDATA%\OfflinePOSDemo\receipts에 기록합니다.
올리브 모이스처 크림을 세 번 담아 2+1 할인(54,000원 → 36,000원)을 확인합니다.- 상단의
연결 차단 전환을 눌러 POS 서버만 차단합니다. - 현금 또는 모의 카드 판매를 확정합니다. 판매는 SQLite에 남고 outbox는 재시도 상태가 됩니다.
동기화 대시보드에서 상태와 시도 횟수를 확인합니다.- 연결을 복구해
Completed전환을 확인합니다.
완성된 녹화 흐름은 로컬 전용 docs/demo-script.md를 따릅니다.
flowchart LR
subgraph Client["OfflinePOS.Client · WPF / .NET Framework 4.8"]
UI["판매 · 동기화 · 장치 진단"]
App["Application / Domain"]
DB[("SQLite\nSales + SaleItems + Outbox")]
Sync["lease 기반 SyncEngine"]
UI --> App --> DB
DB --> Sync
end
Sync -->|"REST · at-least-once"| API["ASP.NET Core .NET 10"]
API -->|"Serializable transaction\n멱등 키 + payload hash"| MSSQL[("SQL Server LocalDB")]
UI --> Raw["Win32 WM_INPUT"]
UI --> Printer["영수증 파일 프린터"]
UI -->|"4-byte length + JSON / TCP"| Terminal["모의 카드단말 .NET 10"]
- 클라이언트는 레거시 Windows 실행 환경을 실제로 검증하기 위해
net48을 사용합니다. - 도메인·계약·애플리케이션과 장치/저장소 경계는
netstandard2.0으로 공유합니다. - 서버와 카드단말 에뮬레이터, 테스트 러너는
net10.0입니다. - 판매·판매항목·outbox는 한 SQLite 트랜잭션으로 저장합니다. 인쇄는 커밋 이후 실행하므로 실패해도 판매를 지우지 않습니다.
- 전송은 at-least-once이며, 서버가
MessageId와 canonical SHA-256 payload hash로 중복 소비를 멱등 처리합니다.
상세 흐름과 데이터 소유권은 로컬 전용 docs/architecture.md, 결정의 배경은 docs/adr/README.md에 있습니다.
| 상황 | 기대 결과 | 자동 증거 |
|---|---|---|
| outbox insert 실패 | sale과 item도 rollback | Checkout_WhenOutboxInsertFails_RollsBackSaleAndItems |
| 앱/worker가 lease 중단 | 만료 후 다른 lease가 재처리 | ExpiredLease_IsClaimedAgain_AndCanComplete |
| 서버 커밋 후 응답 유실 | 재전송 후 중앙 sale은 1건 | ServerCommitThenResponseLoss_RetryCompletesOneCentralSale |
| 같은 키·같은 payload 동시 16회 | 1회 Applied, 15회 AlreadyApplied |
Concurrent_duplicates_create_one_sale_and_return_stable_results |
| 같은 키·다른 payload | HTTP 409, 기존 sale 불변 | Same_message_id_with_different_payload_returns_conflict_without_mutating_sale |
| TCP frame 분할·병합·절단 | 정확히 재조립하거나 명시적 실패 | LengthPrefixedJsonCodecTests |
| 카드단말 결과 저장 후 연결 종료 | 같은 RequestId 재요청으로 기존 결과 회수 |
AuthorizeAsync_DropAfterCommitCanBeRecoveredWithSameRequestId |
전체 표와 수동 검증 항목은 로컬 전용 docs/failure-matrix.md를 참고하세요.
| 상태 | 범위 |
|---|---|
| 구현·자동 검증 | N+M 프로모션, SQLite transactional outbox, lease/retry/dead-letter, REST 동기화, MSSQL 멱등 소비, 응답 유실 E2E, TCP framing/timeout/correlation, 프린터 에뮬레이터, 카드 승인 snapshot/payment attempt, poison outbox 격리, 서버 business invariant 검증 |
| 구현·로컬 1회 측정 | 100,000행 SQLite seed와 20,000회 indexed warm-cache 조회(231,514 lookups/s, p50 3.9μs, p95 4.7μs, p99 5.3μs); 5,000건 sync(3,414 sales/s, p50 88.706ms, p95 310.092ms, p99 310.150ms) |
| 구현·수동 검증 대기 | WPF 전체 사용자 흐름, 실제 Raw Input 스캐너 장치 구분, 앱 프로세스 재시작 데모, 프린터 실패 후 UI 재출력 |
| 미구현/미실행 | 실제 카드사/벤더 SDK와 COM 연동, 취소·반품 이벤트, 상품 master sync, CPU/memory/UI 측정, UI 자동화, 다중 단말 soak, lease heartbeat/circuit breaker, 현대 WPF 이전 spike |
| 산출물 대기 | 3분 데모 영상·반복 측정한 공개 성능 보고서 |
따라서 이 프로젝트가 증명하는 것은 중복 매출 등록을 막는 동기화 설계입니다. 실제 카드 결제의 중복 승인 방지나 운영 트래픽 경험을 주장하지 않습니다.
| 프로젝트 | 수 | 주요 범위 |
|---|---|---|
OfflinePOS.Domain.Tests |
37 | 도메인 불변식, 1+1/2+1, checkout 계약, sync 상태 분류 |
OfflinePOS.Sqlite.IntegrationTests |
7 | 실제 SQLite 원자성, rollback, lease, retry/dead-letter, payment attempt, poison payload |
OfflinePOS.Devices.Tests |
24 | TCP 단말, framing, timeout/cancel/correlation, 프린터, 바코드 버퍼 |
OfflinePOS.Server.IntegrationTests |
6 | 실제 LocalDB API, 순차·동시 멱등성, 409, validator, 정산 프로시저 |
OfflinePOS.EndToEndTests |
2 | 오프라인 5건 복구, 응답 유실 후 중복 없는 재전송 |
명령과 결과의 해석은 로컬 전용 docs/verification.md에 정리했습니다.
src/
OfflinePOS.Client net48 WPF UI, Raw Input
OfflinePOS.Domain 판매·상품·프로모션 불변식
OfflinePOS.Application checkout와 sync orchestration
OfflinePOS.Contracts REST/TCP DTO
OfflinePOS.Infrastructure.Sqlite 로컬 원장과 outbox
OfflinePOS.Infrastructure.Http REST transport와 network gate
OfflinePOS.Devices TCP/프린터/바코드 입력 경계
OfflinePOS.Server Minimal API와 MSSQL 멱등 소비자
OfflinePOS.CardTerminalEmulator TCP 단말 장애 모드
tests/ 단위·통합·E2E 테스트
sql/mssql/ 중앙 스키마와 정산 프로시저
tools/OfflinePOS.LoadGenerator 합성 sync/SQLite benchmark 도구
scripts/ 검증·DB 초기화·demo·benchmark 실행
docs/ 포트폴리오 근거 문서
상세 포트폴리오 Markdown 원고는 개인정보·지원서 맞춤 내용 보호를 위해 공개 저장소에서 제외하고 로컬에서만 관리합니다. 공개 저장소에는 README, PDF, 이미지 증거만 남깁니다.
- 구현 현황과 미구현 경계 ·
docs/implementation-status.md(로컬 전용) - CJ올리브영 공고 요구사항 추적표 ·
docs/requirements-traceability.md(로컬 전용) - 아키텍처 ·
docs/architecture.md(로컬 전용) - 장애·복구 매트릭스 ·
docs/failure-matrix.md(로컬 전용) - TCP 카드단말 프로토콜 ·
docs/card-terminal-protocol.md(로컬 전용) - ADR ·
docs/adr/README.md및docs/adr/*.md(로컬 전용) - 3분 데모 스크립트 ·
docs/demo-script.md(로컬 전용) - 1~2페이지 프로젝트 기술서 원고 ·
docs/project-sheet.md(로컬 전용) - 지원서 첨부용 프로젝트 기술서 PDF
- WPF 실행 화면 캡처
- WPF Showcase 화면 캡처
- 포트폴리오 보기 순서 ·
docs/portfolio-index.md(로컬 전용) - 상용 POS 대비 품질 기준 ·
docs/competitive-bar.md(로컬 전용) - 로컬 검증 기록 ·
docs/verification.md(로컬 전용) - 합성 성능 측정 방법 ·
docs/performance.md(로컬 전용) - 원래 구현 계획 ·
PLAN.md(로컬 전용)
.NET Framework 4.8 WPF POS 데모에서 판매 원장과 transactional outbox를 SQLite에 원자 저장하고, REST 기반 at-least-once 전송과 MSSQL 멱등 소비자를 결합해 네트워크 단절·응답 유실 상황에서도 중복 매출 등록 없이 복구하도록 구현·통합 테스트했습니다.