<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://myoungsoo7.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://myoungsoo7.github.io/" rel="alternate" type="text/html" /><updated>2026-05-04T11:11:58+00:00</updated><id>https://myoungsoo7.github.io/feed.xml</id><title type="html">푸른영혼의 별 | Tech Blog</title><subtitle>Java Backend Engineer의 기술 블로그</subtitle><author><name>푸른영혼의 별</name><email></email></author><entry><title type="html">백엔드 면접 Q&amp;amp;A 50문항 — 프로젝트·Java/Spring·시스템설계·행동</title><link href="https://myoungsoo7.github.io/2026/05/04/interview-qa-50/" rel="alternate" type="text/html" title="백엔드 면접 Q&amp;amp;A 50문항 — 프로젝트·Java/Spring·시스템설계·행동" /><published>2026-05-04T07:00:00+00:00</published><updated>2026-05-04T07:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/interview-qa-50</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/interview-qa-50/"><![CDATA[<hr />

<h2 id="1-프로젝트-기술-면접-20문항">1. 프로젝트 기술 면접 (20문항)</h2>

<h3 id="q1-모놀리스에서-msa로-전환한-이유와-분리-기준은">Q1. 모놀리스에서 MSA로 전환한 이유와 분리 기준은?</h3>

<p>주문/결제와 정산은 Bounded Context가 명확히 다릅니다. 주문/결제는 실시간 사용자 요청 중심이고, 정산은 배치/비동기 처리 중심이라 배포 주기와 스케일링 요구가 다릅니다. 분리 기준은 DDD의 Bounded Context로 삼았고, order-service(주문/결제/상품/유저)와 settlement-service(정산/대사/송금/리포트)로 나눴습니다. 코드 의존성을 0으로 만들기 위해 Read-only Projection 패턴을 도입하여 settlement-service가 order-service의 테이블을 <code class="language-plaintext highlighter-rouge">@Immutable</code> 엔티티로 직접 매핑하되 코드 import는 하지 않는 방식을 택했습니다. <code class="language-plaintext highlighter-rouge">settings.gradle.kts</code>에서 <code class="language-plaintext highlighter-rouge">implementation(project(":order-service"))</code>가 없는 것으로 확인할 수 있습니다.</p>

<h3 id="q2-read-only-projection-패턴을-선택한-이유는-api-호출-방식과-비교하면">Q2. Read-only Projection 패턴을 선택한 이유는? API 호출 방식과 비교하면?</h3>

<p>MSA에서 서비스 간 데이터 조회는 보통 동기 API 호출, 이벤트 기반 데이터 복제, 공유 DB 세 가지 선택지가 있습니다. 동기 API는 settlement-service가 order-service에 런타임 의존성을 갖게 되어 장애 전파 위험이 있고, 이벤트 기반 복제는 Eventually Consistent하므로 정산 정합성에 리스크가 있습니다. Read-only Projection은 같은 PostgreSQL 인스턴스를 공유하되 settlement-service에 별도 <code class="language-plaintext highlighter-rouge">@Immutable</code> JPA 엔티티를 정의해서 payments/orders 테이블을 읽기 전용으로 매핑합니다. 코드 의존성 0, 런타임 API 호출 0, Strong Consistency를 모두 확보할 수 있습니다. 단, 진정한 MSA로 DB까지 분리하려면 이벤트 기반 복제로 전환해야 하는데, 현 단계에서는 정합성 우선으로 이 패턴을 선택했습니다.</p>

<h3 id="q3-transactional-outbox-패턴의-동작-원리와-도입-배경은">Q3. Transactional Outbox 패턴의 동작 원리와 도입 배경은?</h3>

<p>결제 CAPTURED 시점에 Kafka로 이벤트를 발행해야 하는데, DB 커밋과 Kafka 발행을 같은 트랜잭션에 묶을 수 없습니다. 커밋 후 발행하면 발행 실패 시 이벤트 유실, 발행 후 커밋하면 커밋 실패 시 고스트 이벤트가 발생합니다. Outbox 패턴은 도메인 트랜잭션 안에서 <code class="language-plaintext highlighter-rouge">outbox_events</code> 테이블에 PENDING 상태로 INSERT하고, 별도 폴러(2초 주기)가 이를 읽어 Kafka로 발행 후 PUBLISHED로 전이합니다. DB 커밋 성공 = 이벤트 영속화 성공이므로 원자성이 보장됩니다. At-least-once 보장이므로 컨슈머 측 멱등 처리가 필수입니다.</p>

<h3 id="q4-3단-멱등-방어란-무엇이고-왜-3단계나-필요한가요">Q4. 3단 멱등 방어란 무엇이고, 왜 3단계나 필요한가요?</h3>

<p>1단계는 <code class="language-plaintext highlighter-rouge">outbox_events.event_id UUID UNIQUE</code>로 프로듀서 측 중복 발행을 방지합니다. 2단계는 <code class="language-plaintext highlighter-rouge">processed_events</code> 테이블의 <code class="language-plaintext highlighter-rouge">(consumer_group, event_id)</code> PK로 컨슈머가 같은 이벤트를 두 번 처리하지 않게 합니다. 3단계는 <code class="language-plaintext highlighter-rouge">settlements.payment_id UNIQUE</code> 제약으로 하나의 결제에 대해 정산이 중복 생성되지 않게 합니다. At-least-once 메시징에서는 네트워크 재시도, Kafka 리밸런싱, 폴러 재시작 등 다양한 시점에서 중복이 발생할 수 있어서 단일 레이어 멱등으로는 부족합니다. 각 계층에서 독립적으로 방어해야 어떤 장애 시나리오에서도 정확히 한 번의 비즈니스 효과를 보장할 수 있습니다.</p>

<h3 id="q5-분할결제에서-역순-환불-정책을-채택한-이유는">Q5. 분할결제에서 역순 환불 정책을 채택한 이유는?</h3>

<p>포인트+상품권+카드 같은 분할결제에서 환불 시 외부 PG(카드)부터 먼저 환불합니다. 만약 내부 잔액(포인트)을 먼저 환불했는데 PG 환불이 실패하면, 포인트는 복원됐지만 카드 거래는 살아있는 정합성 깨짐 상태가 됩니다. 역순으로 처리하면 PG 환불 실패 시 내부 잔액은 건드리지 않은 채 운영자 알람으로 수동 대응할 수 있고, PG 환불 성공 후 내부 잔액 복원 실패는 운영자가 잔액만 수동 복원하면 되므로 위험도가 훨씬 낮습니다. <code class="language-plaintext highlighter-rouge">PaymentDomain.planRefundFromTenders(amount)</code> 메서드가 sequence DESC 순서로 환불 계획을 생성합니다.</p>

<h3 id="q6-sku-재고-차감에-optimistic-lock을-선택한-이유는-pessimistic-lock과-비교하면">Q6. SKU 재고 차감에 Optimistic Lock을 선택한 이유는? Pessimistic Lock과 비교하면?</h3>

<p>Optimistic Lock은 실제 충돌 시에만 재시도 비용이 발생하므로 대부분의 SKU에서는 락 대기 없이 즉시 처리됩니다. Pessimistic Lock(<code class="language-plaintext highlighter-rouge">SELECT FOR UPDATE</code>)은 단순하지만 동시성이 낮은 환경에서도 항상 락 대기가 발생해 처리량이 제한됩니다. 현재 구현은 JPA <code class="language-plaintext highlighter-rouge">@Version</code> + 최대 5회 재시도(지수 백오프 10ms~160ms) + <code class="language-plaintext highlighter-rouge">REQUIRES_NEW</code> 트랜잭션(1차 캐시 stale 회피)입니다. <code class="language-plaintext highlighter-rouge">VariantStockConcurrencyIT</code>에서 100스레드/재고 50개 시나리오로 정확히 50건 성공, 50건 실패, 음수 재고 0건을 검증합니다. 만약 retry/success 비율이 1.0을 넘는 hot SKU가 발생하면 Redis 분산 락으로 격상하는 것을 메트릭 기반으로 판단합니다.</p>

<h3 id="q7-부분-환불에서-pessimistic-lock--idempotency-key를-사용하는-이유는">Q7. 부분 환불에서 Pessimistic Lock + Idempotency Key를 사용하는 이유는?</h3>

<p>부분 환불은 동일 결제에 대해 여러 번 호출될 수 있고, 환불 금액 누적이 원 결제 금액을 초과하면 안 됩니다. <code class="language-plaintext highlighter-rouge">RefundPaymentUseCase</code>는 <code class="language-plaintext highlighter-rouge">REPEATABLE_READ</code> 격리 수준으로 트랜잭션을 열어 결제 레코드를 읽고, <code class="language-plaintext highlighter-rouge">refundableAmount</code>를 검증한 뒤 PG 환불을 호출합니다. Idempotency Key는 전액 환불 시 <code class="language-plaintext highlighter-rouge">payment-{id}-full</code>로 자동 생성되고, 부분 환불 시에는 호출자가 반드시 지정해야 합니다(없으면 <code class="language-plaintext highlighter-rouge">MissingIdempotencyKeyException</code>). <code class="language-plaintext highlighter-rouge">loadRefundPort.findByPaymentIdAndIdempotencyKey</code>로 이미 COMPLETED된 동일 키의 Refund가 있으면 PG 재호출 없이 현재 상태를 반환합니다.</p>

<h3 id="q8-다중-pg-라우팅-전략은-어떻게-구현했나요">Q8. 다중 PG 라우팅 전략은 어떻게 구현했나요?</h3>

<p><code class="language-plaintext highlighter-rouge">PgRouter</code>가 결제수단, 거래금액, PG 건강 상태를 기반으로 PG를 선택합니다. 고액 거래(100만원 이상)는 NICE 우선, 결제수단별 1순위(카드→TOSS, 카카오페이→NICE, 계좌이체→KCP)가 있고, 1순위 PG가 unhealthy면 fallback chain(TOSS→NICE→KCP→INICIS) 순회합니다. 거래 ID에 PG prefix(<code class="language-plaintext highlighter-rouge">TOSS:xxx</code>)를 붙여서 환불 시 동일 PG로 자동 라우팅합니다. PG별 독립 Resilience4j CircuitBreaker(50% 실패율/30초 OPEN)를 적용해 한 PG 장애가 다른 PG로 전파되지 않는 Bulkhead 격벽을 구현했습니다.</p>

<h3 id="q9-outbox-비동기-경계에서-분산-트레이싱이-끊기는-문제를-어떻게-해결했나요">Q9. Outbox 비동기 경계에서 분산 트레이싱이 끊기는 문제를 어떻게 해결했나요?</h3>

<p>일반적인 Outbox 구현은 DB 커밋과 폴러 사이, Kafka send와 receive 사이 두 곳에서 trace context가 끊깁니다. 도메인 트랜잭션 시점의 W3C Trace Context(<code class="language-plaintext highlighter-rouge">00-{traceId}-{spanId}-{flags}</code>)를 <code class="language-plaintext highlighter-rouge">outbox_events.trace_parent</code> 컬럼에 영속화하고, 폴러가 Kafka 발행 시 <code class="language-plaintext highlighter-rouge">ProducerRecord.headers()</code>에 <code class="language-plaintext highlighter-rouge">traceparent</code>로 복원합니다. 컨슈머 측 spring-kafka가 이 헤더를 읽어 같은 traceId로 새 span을 시작합니다. 비활성 환경에서는 <code class="language-plaintext highlighter-rouge">TraceContextCapture</code>가 null을 반환하여 기존 동작과 호환됩니다. Tempo에서 결제→정산까지 단일 trace로 추적 가능합니다.</p>

<h3 id="q10-셀러-등급별-tn-정산-주기와-holdback은-어떻게-설계했나요">Q10. 셀러 등급별 T+N 정산 주기와 Holdback은 어떻게 설계했나요?</h3>

<p><code class="language-plaintext highlighter-rouge">SellerTier</code>(NORMAL/VIP/STRATEGIC)별로 기본 정산 주기와 보류 정책이 다릅니다. NORMAL은 T+7/보류 30%/30일, VIP는 T+3/보류 10%/14일, STRATEGIC는 T+1/보류 0%입니다. <code class="language-plaintext highlighter-rouge">BusinessDayCalculator</code>가 한국 고정 공휴일 8개와 주말을 건너뛰어 영업일 기준으로 정산일을 계산합니다. 보류금은 정산 확정 시 <code class="language-plaintext highlighter-rouge">Settlement.applyHoldback(rate, releaseDate)</code>로 적용되고, 매일 03:00 KST 배치가 <code class="language-plaintext highlighter-rouge">releaseDate</code> 도달한 보류건을 자동 해제합니다. 환불 발생 시 <code class="language-plaintext highlighter-rouge">consumeHoldbackForRefund()</code>로 보류금에서 우선 차감하여 셀러 실수령액에 영향을 최소화합니다.</p>

<h3 id="q11-pg-대사reconciliation는-어떤-불일치를-감지하나요">Q11. PG 대사(Reconciliation)는 어떤 불일치를 감지하나요?</h3>

<p>PG사에서 받은 정산 파일과 내부 결제 데이터를 대조하여 5가지 유형의 불일치를 분류합니다: PG에만 있는 거래(내부 누락), 내부에만 있는 거래(PG 누락), 금액 불일치, 상태 불일치, 기타입니다. <code class="language-plaintext highlighter-rouge">PgReconciliationMatcher</code>가 PG 파일의 <code class="language-plaintext highlighter-rouge">PgTransactionRow</code>와 내부 <code class="language-plaintext highlighter-rouge">InternalPaymentRow</code>를 매칭하고, 불일치는 <code class="language-plaintext highlighter-rouge">ReconciliationDiscrepancy</code> 레코드로 기록됩니다. <code class="language-plaintext highlighter-rouge">ReconciliationRun</code>이 전체 대사 실행의 생명주기(시작→완료/실패)를 관리합니다. 일일 대사 배치(03:05)가 전일자를 자동 검증하고, 불일치 발견 시 Alertmanager로 알림을 발송합니다.</p>

<h3 id="q12-헥사고날-아키텍처의-패키지-의존-방향을-어떻게-강제하나요">Q12. 헥사고날 아키텍처의 패키지 의존 방향을 어떻게 강제하나요?</h3>

<p>ArchUnit 테스트(<code class="language-plaintext highlighter-rouge">HexagonalArchitectureTest</code>)로 CI에서 강제합니다. 규칙은 네 가지입니다: (1) domain은 application/adapter를 import하지 않는다, (2) application은 domain과 자기 포트만 참조한다, (3) adapter는 자기 도메인의 포트만 구현한다, (4) 교차 도메인 조회가 필요하면 자기 도메인에 신규 아웃바운드 포트를 두고 어댑터가 다른 도메인 테이블을 읽는다. 이 규칙 덕분에 도메인 모델이 순수 POJO로 유지되어 Spring 없이 단위 테스트가 가능하고, 어댑터 교체 시(예: <code class="language-plaintext highlighter-rouge">OutboxBackedEventPublisher</code> → <code class="language-plaintext highlighter-rouge">KafkaOutboxPublisher</code>) 도메인 코드 수정이 필요 없습니다.</p>

<h3 id="q13-정산-상태-머신state-machine은-어떤-전이를-허용하나요">Q13. 정산 상태 머신(State Machine)은 어떤 전이를 허용하나요?</h3>

<p><code class="language-plaintext highlighter-rouge">REQUESTED → PROCESSING → DONE</code>이 정상 흐름이고, <code class="language-plaintext highlighter-rouge">PROCESSING → FAILED → REQUESTED</code>로 재시도가 가능합니다. 각 전이 메서드(<code class="language-plaintext highlighter-rouge">startProcessing</code>, <code class="language-plaintext highlighter-rouge">complete</code>, <code class="language-plaintext highlighter-rouge">fail</code>, <code class="language-plaintext highlighter-rouge">retry</code>)가 현재 상태를 검증하고, 잘못된 전이 시 <code class="language-plaintext highlighter-rouge">IllegalStateException</code>을 던집니다. DONE 상태의 정산은 immutable로, 금액 변경이 필요하면 <code class="language-plaintext highlighter-rouge">SettlementAdjustment</code> 별도 레코드로 기록하여 원장 정합성을 유지합니다. <code class="language-plaintext highlighter-rouge">confirm()</code> 메서드는 레거시 호환용으로 REQUESTED→PROCESSING→DONE을 한 번에 수행하지만, 신규 코드에서는 각 단계를 명시적으로 호출하도록 권장합니다.</p>

<h3 id="q14-asat-프로젝트에서-web-audio-api의--5ms-타이밍-정확도를-어떻게-달성했나요">Q14. ASAT 프로젝트에서 Web Audio API의 +-5ms 타이밍 정확도를 어떻게 달성했나요?</h3>

<p>Web Audio API의 <code class="language-plaintext highlighter-rouge">AudioContext.currentTime</code>은 하드웨어 클럭 기반으로 JavaScript의 <code class="language-plaintext highlighter-rouge">setTimeout</code>/<code class="language-plaintext highlighter-rouge">setInterval</code>보다 훨씬 정밀합니다. 음원 재생 시점을 <code class="language-plaintext highlighter-rouge">AudioBufferSourceNode.start(when)</code>으로 스케줄링하여 OS 스레드 스케줄링과 무관하게 정확한 타이밍을 보장합니다. 응답 시간 측정은 <code class="language-plaintext highlighter-rouge">AudioContext.currentTime</code> 기준으로 음원 시작 시점과 사용자 입력 시점의 차이를 계산합니다. 브라우저별 오디오 레이턴시 보정값을 적용하고, 측정 신뢰도가 낮은 시행은 데이터 신뢰도 등급(A/B/C/F)으로 분류하여 분석에서 제외합니다.</p>

<h3 id="q15-asat의-적응적-계단법2-down-1-up-알고리즘은-어떤-문제를-해결하나요">Q15. ASAT의 적응적 계단법(2-down 1-up) 알고리즘은 어떤 문제를 해결하나요?</h3>

<p>청각 재활 훈련에서 고정 난이도는 너무 쉽거나 너무 어려워 훈련 효과가 떨어집니다. 2-down 1-up 계단법은 2회 연속 정답이면 난이도를 올리고, 1회 오답이면 난이도를 내려 피검자의 70.7% 정답률 수준에 수렴합니다. 이 수렴점이 청각 역치(threshold)를 나타냅니다. Trial 동시성은 Optimistic Lock으로 처리하여 같은 세션에서 중복 응답이 기록되지 않도록 합니다. 각 세션의 데이터 신뢰도를 reversal 횟수, 응답 시간 변동성, 수렴 안정성으로 평가하여 A~F 등급으로 분류합니다.</p>

<h3 id="q16-goods-online-프로젝트의-래플raffle-해시-체인은-어떤-목적인가요">Q16. goods-online 프로젝트의 래플(Raffle) 해시 체인은 어떤 목적인가요?</h3>

<p>래플(추첨) 결과의 무결성과 사전 조작 불가능성을 보장합니다. 추첨 전에 시드값의 해시를 공개하고, 추첨 후 시드값을 공개하면 누구나 해시를 검증할 수 있어 운영자가 결과를 사후 조작할 수 없습니다. 해시 체인은 각 추첨 라운드의 결과를 이전 라운드의 해시와 연결하여 중간 라운드 조작도 탐지 가능하게 합니다. 블록체인의 원리를 경량화하여 적용한 것으로, 별도 인프라 없이 SHA-256 해시만으로 투명성을 확보합니다.</p>

<h3 id="q17-global-seat-ticketing의-redis-분산-락은-어떤-시나리오에서-필요한가요">Q17. global-seat-ticketing의 Redis 분산 락은 어떤 시나리오에서 필요한가요?</h3>

<p>만석 콘서트 좌석 예매에서 동일 좌석에 대한 동시 예매를 방지해야 합니다. DB Pessimistic Lock은 단일 DB 인스턴스에서는 동작하지만 다중 인스턴스 환경에서는 부족합니다. Redis 분산 락(Redisson 기반)으로 좌석별 락 키(<code class="language-plaintext highlighter-rouge">seat:{eventId}:{seatNo}</code>)를 사용하여 클러스터 전체에서 단 하나의 요청만 예매를 진행할 수 있게 합니다. 락 TTL을 설정하여 프로세스 크래시 시에도 락이 자동 해제되고, 대기 중인 요청은 타임아웃 후 실패 응답을 받습니다. SKU Optimistic Lock과 달리 좌석 예매는 재시도의 의미가 없으므로(이미 다른 사람이 예매) 분산 락이 적합합니다.</p>

<h3 id="q18-sns-프로젝트에서-kafkasse-조합을-선택한-이유는">Q18. SNS 프로젝트에서 Kafka+SSE 조합을 선택한 이유는?</h3>

<p>SNS 피드에서 실시간 알림을 구현할 때, 폴링은 불필요한 요청이 많고, WebSocket은 양방향이 불필요한데 서버 리소스를 많이 소비합니다. SSE(Server-Sent Events)는 단방향 스트림으로 알림 전달에 적합하고, HTTP 기반이라 로드밸런서/프록시 호환성이 좋습니다. Kafka는 알림 이벤트의 내구성(durability)과 다중 컨슈머(알림/이메일/푸시) 지원을 보장합니다. 각 사용자의 SSE 연결은 서버 인스턴스에 로컬이므로, Kafka 컨슈머가 이벤트를 받으면 해당 인스턴스에 연결된 사용자에게만 SSE로 push합니다.</p>

<h3 id="q19-spring-boot-40과-java-25를-실무-프로젝트에서-사용한-이유는">Q19. Spring Boot 4.0과 Java 25를 실무 프로젝트에서 사용한 이유는?</h3>

<p>최신 기술 스택에 대한 적응력을 보여주기 위해 선택했습니다. Java 25의 Virtual Thread는 Kafka 컨슈머/Outbox 폴러 같은 I/O 바운드 작업에서 플랫폼 스레드 대비 처리량을 크게 향상시킵니다. Sealed class/record는 도메인 모델(예: <code class="language-plaintext highlighter-rouge">HoldbackPolicy</code> record, <code class="language-plaintext highlighter-rouge">SellerTier</code> enum)을 더 간결하게 표현합니다. Spring Boot 4.0의 Spring Cloud Gateway 2025는 기존 Zuul/SCG MVC 대비 Reactive 기반 라우팅 성능이 개선되었습니다. 마이그레이션 과정에서 발생한 호환성 이슈들을 ADR 0009에 기록했습니다.</p>

<h3 id="q20-이-프로젝트에서-가장-어려웠던-기술적-결정은">Q20. 이 프로젝트에서 가장 어려웠던 기술적 결정은?</h3>

<p>MSA 분리 시 settlement-service와 order-service 간 코드 의존성을 끊는 것이었습니다. 처음에는 모놀리스에서 settlement 코드가 order/payment 엔티티를 직접 import하고 있었는데, 단순히 API 호출로 바꾸면 동기 의존성이 생기고, 이벤트 기반 복제는 정산 정합성에 리스크가 있었습니다. Read-only Projection 패턴으로 같은 테이블을 <code class="language-plaintext highlighter-rouge">@Immutable</code> 엔티티로 별도 매핑하는 방식을 선택했는데, 이것이 진정한 MSA인가에 대한 고민이 있었습니다. 결론적으로 코드 의존성 0 + Strong Consistency를 우선하되, DB 분리가 필요한 시점에 이벤트 기반으로 전환할 수 있도록 이미 Outbox+Kafka 파이프라인을 갖추어 놓은 것이 핵심 전략입니다.</p>

<hr />

<h2 id="2-javaspring-심화-15문항">2. Java/Spring 심화 (15문항)</h2>

<h3 id="q21-jpa-n1-문제란-무엇이고-어떻게-해결하나요">Q21. JPA N+1 문제란 무엇이고, 어떻게 해결하나요?</h3>

<p>N+1 문제는 연관 엔티티를 LAZY 로딩으로 조회할 때, 부모 1건 조회 후 자식 N건을 개별 쿼리로 가져오는 현상입니다. Settlement 프로젝트에서 주문 목록 조회 시 각 주문의 결제 정보를 가져올 때 발생할 수 있습니다. 해결 방법은 <code class="language-plaintext highlighter-rouge">@EntityGraph</code>나 <code class="language-plaintext highlighter-rouge">JOIN FETCH</code>로 한 번에 가져오기, <code class="language-plaintext highlighter-rouge">@BatchSize</code>로 IN 절 묶기, DTO Projection으로 필요한 컬럼만 조회하기가 있습니다. settlement-service의 Read-only Projection은 필요한 컬럼만 매핑한 <code class="language-plaintext highlighter-rouge">@Immutable</code> 엔티티를 사용하므로 N+1 위험이 근본적으로 줄어듭니다.</p>

<h3 id="q22-spring의-트랜잭션-전파propagation-유형-중-requires_new는-언제-사용하나요">Q22. Spring의 트랜잭션 전파(Propagation) 유형 중 REQUIRES_NEW는 언제 사용하나요?</h3>

<p>REQUIRES_NEW는 기존 트랜잭션과 독립적인 새 트랜잭션을 시작합니다. SKU 재고 차감에서 Optimistic Lock 재시도 시 매 시도마다 <code class="language-plaintext highlighter-rouge">REQUIRES_NEW</code>를 사용하는데, 이전 시도에서 <code class="language-plaintext highlighter-rouge">OptimisticLockException</code>이 발생하면 1차 캐시에 stale 데이터가 남아있기 때문입니다. 새 트랜잭션을 열면 새로운 영속성 컨텍스트에서 최신 version의 엔티티를 다시 읽어옵니다. 주의할 점은 REQUIRES_NEW 트랜잭션이 커밋되어도 외부 트랜잭션이 롤백되면 REQUIRES_NEW의 결과는 유지된다는 것입니다. 따라서 보상 트랜잭션이나 로깅 같은 독립적 작업에 적합합니다.</p>

<h3 id="q23-spring-security-필터-체인의-동작-순서를-설명해주세요">Q23. Spring Security 필터 체인의 동작 순서를 설명해주세요.</h3>

<p>요청이 들어오면 <code class="language-plaintext highlighter-rouge">SecurityFilterChain</code>의 필터들이 순서대로 실행됩니다. <code class="language-plaintext highlighter-rouge">CorsFilter</code> → <code class="language-plaintext highlighter-rouge">CsrfFilter</code> → <code class="language-plaintext highlighter-rouge">UsernamePasswordAuthenticationFilter</code>(또는 커스텀 JWT 필터) → <code class="language-plaintext highlighter-rouge">ExceptionTranslationFilter</code> → <code class="language-plaintext highlighter-rouge">FilterSecurityInterceptor</code>(Authorization) 순입니다. Settlement 프로젝트에서는 Gateway 서비스에서 JWT 인증 필터가 토큰을 검증하고, 인증 정보를 SecurityContext에 저장합니다. 각 서비스는 <code class="language-plaintext highlighter-rouge">shared-common</code>의 JWT 설정(<code class="language-plaintext highlighter-rouge">common.config.jwt</code>)을 공유하며, HS256 알고리즘으로 토큰을 검증합니다. Actuator 엔드포인트는 별도 SecurityFilterChain으로 인증 필수 설정되어 있습니다.</p>

<h3 id="q24-bean-생명주기lifecycle를-설명해주세요">Q24. Bean 생명주기(Lifecycle)를 설명해주세요.</h3>

<p>Spring 컨테이너가 빈을 생성하면: (1) 인스턴스화 → (2) 의존성 주입(<code class="language-plaintext highlighter-rouge">@Autowired</code>, 생성자) → (3) <code class="language-plaintext highlighter-rouge">@PostConstruct</code>/<code class="language-plaintext highlighter-rouge">InitializingBean.afterPropertiesSet()</code> → (4) 사용 → (5) <code class="language-plaintext highlighter-rouge">@PreDestroy</code>/<code class="language-plaintext highlighter-rouge">DisposableBean.destroy()</code> 순입니다. Settlement 프로젝트에서 <code class="language-plaintext highlighter-rouge">OutboxPublisherScheduler</code>는 <code class="language-plaintext highlighter-rouge">@PostConstruct</code>에서 초기 상태를 확인하고, <code class="language-plaintext highlighter-rouge">@PreDestroy</code>에서 진행 중인 폴링을 안전하게 종료합니다. <code class="language-plaintext highlighter-rouge">@Scope("prototype")</code>은 요청마다 새 인스턴스를 생성하고 컨테이너가 소멸을 관리하지 않으므로, 기본 singleton과 혼용할 때 주의해야 합니다.</p>

<h3 id="q25-aopaspect-oriented-programming의-실제-활용-사례를-설명해주세요">Q25. AOP(Aspect-Oriented Programming)의 실제 활용 사례를 설명해주세요.</h3>

<p>AOP는 횡단 관심사(로깅, 트랜잭션, 보안)를 비즈니스 로직에서 분리합니다. Settlement 프로젝트에서는 <code class="language-plaintext highlighter-rouge">shared-common</code>의 감사(Audit) 모듈이 AOP로 PII 마스킹 + 감사 로그를 처리합니다. <code class="language-plaintext highlighter-rouge">@Transactional</code>도 AOP 기반으로, 프록시가 메서드 호출을 가로채 트랜잭션을 시작/커밋/롤백합니다. 주의할 점은 같은 클래스 내부 메서드 호출 시 프록시를 거치지 않아 AOP가 동작하지 않는 것입니다. 이를 self-invocation 문제라 하며, 별도 빈으로 분리하거나 <code class="language-plaintext highlighter-rouge">AspectJ</code> 위빙으로 해결합니다. SKU 재고 차감의 재시도 로직에서 <code class="language-plaintext highlighter-rouge">REQUIRES_NEW</code>가 동작하려면 반드시 다른 빈에서 호출해야 합니다.</p>

<h3 id="q26-transactional의-isolation-level을-repeatable_read로-설정하면-어떤-효과가-있나요">Q26. <code class="language-plaintext highlighter-rouge">@Transactional</code>의 isolation level을 REPEATABLE_READ로 설정하면 어떤 효과가 있나요?</h3>

<p>REPEATABLE_READ는 트랜잭션 시작 시점의 스냅샷을 유지하여 같은 쿼리를 반복 실행해도 동일한 결과를 보장합니다(Phantom Read는 DB에 따라 다름). <code class="language-plaintext highlighter-rouge">RefundPaymentUseCase</code>에서 REPEATABLE_READ를 사용하는 이유는 결제 조회 → 환불 가능 금액 계산 → PG 호출 → 상태 업데이트 과정에서 다른 트랜잭션이 같은 결제를 수정하는 것을 방지하기 위해서입니다. PostgreSQL은 REPEATABLE_READ에서 실제로 Serializable Snapshot Isolation에 가까운 동작을 하며, 충돌 시 <code class="language-plaintext highlighter-rouge">SerializationFailure</code>를 던져 애플리케이션이 재시도할 수 있게 합니다.</p>

<h3 id="q27-jpa의-dirty-checking과-merge의-차이는">Q27. JPA의 Dirty Checking과 Merge의 차이는?</h3>

<p>Dirty Checking은 영속성 컨텍스트가 관리하는 엔티티의 변경을 트랜잭션 커밋 시 자동으로 감지하여 UPDATE 쿼리를 생성합니다. <code class="language-plaintext highlighter-rouge">merge()</code>는 준영속(detached) 엔티티를 다시 영속 상태로 만들 때 사용하며, 모든 컬럼을 UPDATE합니다. Settlement 도메인에서 <code class="language-plaintext highlighter-rouge">startProcessing()</code>, <code class="language-plaintext highlighter-rouge">complete()</code> 같은 상태 전이 메서드는 영속 엔티티의 필드를 변경하므로 Dirty Checking으로 자동 반영됩니다. <code class="language-plaintext highlighter-rouge">@DynamicUpdate</code>를 사용하면 변경된 컬럼만 UPDATE하여 불필요한 갱신을 줄일 수 있습니다. <code class="language-plaintext highlighter-rouge">merge()</code>는 SELECT 후 UPDATE가 발생하므로 Dirty Checking보다 비효율적일 수 있습니다.</p>

<h3 id="q28-spring-batch의-chunk-기반-처리와-tasklet의-차이는">Q28. Spring Batch의 Chunk 기반 처리와 Tasklet의 차이는?</h3>

<p>Chunk 기반은 Reader → Processor → Writer 파이프라인으로, chunk-size만큼 읽어서 한 번에 쓰기 처리합니다. Tasklet은 단순한 단일 작업(파일 삭제, 알림 발송 등)에 적합합니다. Settlement 프로젝트의 정산 배치는 Chunk 기반으로, 전일자 결제 데이터를 chunk 단위로 읽어 정산을 생성하고 DB에 쓰니다. Holdback 자동 해제 배치도 Chunk 기반으로, <code class="language-plaintext highlighter-rouge">releaseDate</code> 도달한 정산 건을 조회하여 <code class="language-plaintext highlighter-rouge">releaseHoldback()</code>을 호출합니다. Chunk 실패 시 해당 chunk만 롤백되므로 전체 배치가 실패하지 않고, skip/retry 정책으로 일시적 오류를 흡수합니다.</p>

<h3 id="q29-caffeine-캐시와-redis-캐시의-선택-기준은">Q29. Caffeine 캐시와 Redis 캐시의 선택 기준은?</h3>

<p>Caffeine은 JVM 로컬 캐시로 네트워크 지연이 없고 매우 빠르지만, 인스턴스 간 공유가 불가합니다. Redis는 분산 캐시로 인스턴스 간 공유 가능하지만 네트워크 왕복이 필요합니다. Settlement 프로젝트에서 Caffeine을 선택한 이유는, 정산 조회/리포트 데이터가 인스턴스별로 독립적으로 캐싱되어도 정합성 문제가 없고, 별도 Redis 인프라 없이도 충분한 성능을 얻을 수 있기 때문입니다. ASAT 프로젝트에서는 세션 관리와 분산 환경 지원을 위해 Redis를 사용합니다. 캐시 무효화 전략(TTL, 이벤트 기반)도 선택의 핵심 요소입니다.</p>

<h3 id="q30-resilience4j의-circuitbreaker-상태-전이를-설명해주세요">Q30. Resilience4j의 CircuitBreaker 상태 전이를 설명해주세요.</h3>

<p>CLOSED(정상) → OPEN(차단) → HALF_OPEN(탐색) 세 상태입니다. CLOSED에서 실패율이 임계값(Settlement에서는 50%)을 넘으면 OPEN으로 전이되어 요청을 즉시 실패시킵니다. OPEN에서 대기 시간(30초)이 지나면 HALF_OPEN으로 전이되어 일부 요청을 통과시킵니다. 성공하면 CLOSED로, 실패하면 다시 OPEN으로 돌아갑니다. Settlement의 PG별 독립 CircuitBreaker(<code class="language-plaintext highlighter-rouge">tossPg</code>, <code class="language-plaintext highlighter-rouge">kcpPg</code>, <code class="language-plaintext highlighter-rouge">nicePg</code>, <code class="language-plaintext highlighter-rouge">inicisPg</code>)는 한 PG의 장애가 다른 PG 호출에 영향을 주지 않는 Bulkhead 효과를 제공합니다.</p>

<h3 id="q31-version을-사용한-optimistic-lock에서-optimisticlockexception이-발생하면-어떻게-되나요">Q31. <code class="language-plaintext highlighter-rouge">@Version</code>을 사용한 Optimistic Lock에서 OptimisticLockException이 발생하면 어떻게 되나요?</h3>

<p>JPA가 UPDATE 쿼리에 <code class="language-plaintext highlighter-rouge">WHERE version = :currentVersion</code>을 추가하고, 영향받은 행이 0이면 <code class="language-plaintext highlighter-rouge">OptimisticLockException</code>을 던집니다. 이는 다른 트랜잭션이 먼저 해당 행을 수정하여 version이 증가했다는 의미입니다. Settlement의 SKU 재고 차감에서는 이 예외를 잡아서 최대 5회 재시도합니다. 재시도 시 <code class="language-plaintext highlighter-rouge">REQUIRES_NEW</code> 트랜잭션으로 새 영속성 컨텍스트를 열어 최신 데이터를 다시 읽습니다. 지수 백오프(10ms~160ms)를 적용하여 동시 충돌 확률을 줄이고, 한계 초과 시 <code class="language-plaintext highlighter-rouge">StockConcurrencyException</code>을 던져 <code class="language-plaintext highlighter-rouge">variant.stock.decrease.failure</code> 메트릭을 기록합니다.</p>

<h3 id="q32-flyway와-liquibase의-차이-그리고-마이그레이션-관리-전략은">Q32. Flyway와 Liquibase의 차이, 그리고 마이그레이션 관리 전략은?</h3>

<p>Flyway는 SQL 기반으로 단순하고, Liquibase는 XML/YAML/JSON으로 DB 독립적 마이그레이션을 지원합니다. Settlement에서 Flyway를 선택한 이유는 PostgreSQL 전용이므로 DB 독립성이 불필요하고, 순수 SQL로 작성하면 DBA 리뷰가 용이하기 때문입니다. V1~V43까지 43개 마이그레이션이 있으며, 테이블 생성, 인덱스 추가, 컬럼 변경이 모두 버전 관리됩니다. 주의할 점은 이미 적용된 마이그레이션은 수정하면 안 되고(체크섬 검증 실패), 롤백은 별도 마이그레이션으로 작성해야 합니다. CI에서 Testcontainers로 마이그레이션을 매번 처음부터 실행하여 무결성을 검증합니다.</p>

<h3 id="q33-virtual-threadproject-loom의-장점과-주의점은">Q33. Virtual Thread(Project Loom)의 장점과 주의점은?</h3>

<p>Virtual Thread는 OS 스레드가 아닌 JVM이 관리하는 경량 스레드로, I/O 블로킹 시 캐리어 스레드를 양보하여 적은 OS 스레드로 많은 동시 요청을 처리합니다. Settlement의 Outbox 폴러, Kafka 컨슈머 같은 I/O 바운드 작업에서 효과적입니다. 주의점은 <code class="language-plaintext highlighter-rouge">synchronized</code> 블록에서 캐리어 스레드를 pin하므로 <code class="language-plaintext highlighter-rouge">ReentrantLock</code>을 사용해야 하고, ThreadLocal 사용 시 수백만 개 Virtual Thread가 각각 ThreadLocal을 가지면 메모리 문제가 발생할 수 있습니다. 또한 CPU 바운드 작업에서는 이점이 없으므로, 정산 금액 계산 같은 순수 연산에는 기존 스레드풀이 적합합니다.</p>

<h3 id="q34-spring의-scheduled와-spring-batch의-차이는">Q34. Spring의 <code class="language-plaintext highlighter-rouge">@Scheduled</code>와 Spring Batch의 차이는?</h3>

<p><code class="language-plaintext highlighter-rouge">@Scheduled</code>는 단순 주기적 작업에 적합하고, 실패 시 재시도/보상이 없으며, 클러스터 환경에서 중복 실행 방지를 별도로 구현해야 합니다. Spring Batch는 대용량 데이터 처리에 특화되어 chunk 기반 처리, skip/retry, 재시작, 실행 이력 관리를 기본 제공합니다. Settlement에서 Outbox 폴러는 <code class="language-plaintext highlighter-rouge">@Scheduled</code>(2초 주기)로 가볍게 실행하고, 정산 생성/Holdback 해제 같은 대량 처리는 Spring Batch로 구현합니다. Batch Job의 <code class="language-plaintext highlighter-rouge">JobExecution</code> 이력으로 실패 지점부터 재시작이 가능하여 운영 안정성이 높습니다.</p>

<h3 id="q35-dto와-도메인-모델을-분리하는-이유는">Q35. DTO와 도메인 모델을 분리하는 이유는?</h3>

<p>도메인 모델은 비즈니스 규칙과 불변식을 캡슐화하고, DTO는 외부와의 데이터 전송에만 사용합니다. Settlement 도메인의 <code class="language-plaintext highlighter-rouge">Settlement</code> 클래스는 상태 전이 규칙, 수수료 계산, 환불 검증 로직을 가지지만, API 응답에는 필요한 필드만 담은 DTO를 반환합니다. 분리하지 않으면 API 스펙 변경이 도메인 로직에 영향을 주거나, 도메인 내부 필드(version, failureReason)가 외부에 노출됩니다. 헥사고날 아키텍처에서 adapter/in/web이 DTO↔도메인 변환을 담당하고, domain 패키지는 어떤 직렬화 어노테이션(<code class="language-plaintext highlighter-rouge">@JsonProperty</code> 등)도 갖지 않습니다.</p>

<hr />

<h2 id="3-시스템-설계-10문항">3. 시스템 설계 (10문항)</h2>

<h3 id="q36-이커머스-정산-시스템을-설계하라는-질문에-어떻게-답하나요">Q36. “이커머스 정산 시스템을 설계하라”는 질문에 어떻게 답하나요?</h3>

<p>핵심 요구사항을 먼저 확인합니다: 일 거래량, 정산 주기, 다중 PG 여부, 환불/분쟁 처리. 설계는 크게 4계층입니다. (1) 이벤트 수집: 결제 CAPTURED 이벤트를 Outbox+Kafka로 at-least-once 전달, 컨슈머 멱등 처리. (2) 정산 생성: 셀러 등급별 T+N 영업일 정산일 계산, 수수료 차등 적용, Holdback 보류. (3) 대사/검증: PG 파일과 내부 데이터 대조, 3대 불변식(결제-환불=정산net+수수료, 역정산=환불, Outbox발행수=정산생성수) 배치 검증. (4) 송금: 펌뱅킹 연동, 멱등 키로 이중 송금 방지, 일/셀러별 한도 검증. 실제 Settlement 프로젝트에서 이 4계층을 모두 구현했습니다.</p>

<h3 id="q37-만석-콘서트-좌석-예매-시스템을-설계하라">Q37. “만석 콘서트 좌석 예매 시스템을 설계하라”</h3>

<p>핵심 병목은 동일 좌석에 대한 동시 예매입니다. (1) 좌석 선택: Redis 분산 락(<code class="language-plaintext highlighter-rouge">seat:{eventId}:{seatNo}</code>)으로 동시 접근 직렬화, 락 TTL로 크래시 안전성 확보. (2) 결제: 락 획득 후 결제 진행, 결제 실패 시 락 해제하여 다른 사용자에게 기회 제공. (3) 대기열: 트래픽 폭주 시 Redis Sorted Set 기반 대기열로 유입량 제어. (4) 좌석 상태: DB는 최종 정합성 보장, Redis는 실시간 좌석 현황 표시. global-seat-ticketing 프로젝트에서 Redisson 기반 분산 락으로 구현했으며, SKU Optimistic Lock과 달리 좌석은 재시도 의미가 없으므로 즉시 실패(fail-fast) 전략이 적합합니다.</p>

<h3 id="q38-이벤트-드리븐-주문-파이프라인을-설계하라">Q38. “이벤트 드리븐 주문 파이프라인을 설계하라”</h3>

<p>주문 생성 → 결제 승인 → 재고 차감 → 배송 준비 → 정산 생성이 이벤트로 연결됩니다. 각 단계는 Transactional Outbox로 이벤트를 발행하여 DB 커밋과 이벤트 발행의 원자성을 보장합니다. 실패 처리는 두 가지 전략: 보상 트랜잭션(Saga)과 재시도입니다. 결제 실패는 주문 취소(보상), 재고 차감 실패는 재시도(Optimistic Lock), 정산 실패는 FAILED 상태 후 수동/자동 재시도. 멱등성은 각 컨슈머의 <code class="language-plaintext highlighter-rouge">processed_events</code> 테이블 + 도메인 UNIQUE 제약으로 보장합니다. Settlement 프로젝트의 결제→정산 파이프라인이 이 패턴의 실제 구현입니다.</p>

<h3 id="q39-실시간-알림-시스템을-설계하라">Q39. “실시간 알림 시스템을 설계하라”</h3>

<p>(1) 이벤트 소스: 각 서비스가 알림 이벤트를 Kafka 토픽에 발행. (2) 라우팅: 알림 서비스가 이벤트를 소비하여 사용자별 채널(인앱/이메일/푸시)로 분배. (3) 인앱 전달: SSE(Server-Sent Events)로 서버→클라이언트 단방향 스트림, 연결 끊김 시 마지막 이벤트 ID부터 재전송. (4) 스케일아웃: 사용자 SSE 연결은 특정 인스턴스에 로컬이므로, Kafka 파티셔닝(user_id 키)으로 같은 사용자의 이벤트가 같은 인스턴스로 라우팅. SNS 프로젝트에서 Kafka+SSE 조합으로 구현했으며, WebSocket 대비 서버 리소스 절약과 HTTP 인프라 호환성이 장점입니다.</p>

<h3 id="q40-결제-pg가-30분-다운되면-어떻게-대응하나요">Q40. “결제 PG가 30분 다운되면 어떻게 대응하나요?”</h3>

<p>(1) 감지: PG별 CircuitBreaker가 실패율 50% 초과 시 OPEN으로 전환, Prometheus 메트릭 + Alertmanager 알림. (2) 자동 대응: PgRouter의 fallback chain이 다른 건강한 PG로 자동 라우팅. (3) 복구: CircuitBreaker가 30초 후 HALF_OPEN으로 전환, 일부 요청을 장애 PG로 보내 복구 확인. (4) 보류 건 처리: OPEN 동안 실패한 결제는 사용자에게 재시도 안내 또는 다른 결제수단 제안. Settlement 프로젝트에서 4개 PG(TOSS/KCP/NICE/INICIS) 독립 CircuitBreaker + Bulkhead로 장애 격리를 구현했습니다.</p>

<h3 id="q41-대용량-데이터-마이그레이션-전략을-설명하라">Q41. “대용량 데이터 마이그레이션 전략을 설명하라”</h3>

<p>(1) 이중 쓰기(Dual Write): 새 스키마와 구 스키마에 동시 쓰기, 읽기는 구 스키마. (2) 백필(Backfill): 배치로 구 데이터를 새 스키마로 복사, chunk 단위로 진행하여 DB 부하 분산. (3) 전환: 읽기를 새 스키마로 전환, 검증 후 구 스키마 쓰기 중단. (4) 정리: 구 스키마 삭제. Settlement의 43개 Flyway 마이그레이션 중 SellerTier 도입(V32)이 이 패턴을 따랐습니다. <code class="language-plaintext highlighter-rouge">commissionRate</code> 컬럼 추가 시 기존 데이터는 기본 3%를 유지하고, 신규 생성분부터 등급별 rate를 적용하는 점진적 전환을 했습니다.</p>

<h3 id="q42-분산-트랜잭션을-어떻게-처리하나요">Q42. “분산 트랜잭션을 어떻게 처리하나요?”</h3>

<p>2PC(Two-Phase Commit)는 성능/가용성 비용이 크므로, 실무에서는 Saga 패턴이나 Outbox 패턴을 사용합니다. Saga는 각 서비스가 로컬 트랜잭션을 수행하고, 실패 시 보상 트랜잭션을 실행합니다. Outbox 패턴은 도메인 트랜잭션과 이벤트 발행을 같은 DB 트랜잭션에 묶어 원자성을 보장합니다. Settlement에서는 Outbox+Kafka+3단 멱등으로 결제→정산 간 분산 트랜잭션을 처리합니다. 핵심은 “최종적 일관성(Eventual Consistency)”을 수용하되, 대사(Reconciliation)로 불일치를 탐지하고 자동/수동 보정하는 안전망을 갖추는 것입니다.</p>

<h3 id="q43-검색-시스템을-어떻게-설계하나요">Q43. “검색 시스템을 어떻게 설계하나요?”</h3>

<p>RDBMS 전문 검색은 LIKE 쿼리로 인덱스를 타지 못해 느립니다. Elasticsearch를 도입하여 역인덱스(inverted index) 기반 전문 검색을 수행합니다. 데이터 동기화는 Change Data Capture(CDC)나 이벤트 기반으로 합니다. Settlement에서는 정산 데이터를 ES에 색인하여 기간/셀러/상태별 빠른 검색을 지원합니다. 주의점은 ES와 DB 간 데이터 지연(lag)을 감안해야 하고, ES 장애 시 DB 폴백 쿼리를 준비해야 합니다. 인덱스 설계 시 한국어 형태소 분석기(nori)를 적용하고, 집계(aggregation)는 ES의 강점을 활용합니다.</p>

<h3 id="q44-rate-limiting을-어떻게-구현하나요">Q44. “Rate Limiting을 어떻게 구현하나요?”</h3>

<p>Token Bucket 알고리즘이 가장 일반적입니다. 일정 속도로 토큰이 충전되고, 요청마다 토큰을 소비하며, 토큰이 없으면 429 Too Many Requests를 반환합니다. Settlement에서는 Bucket4j(<code class="language-plaintext highlighter-rouge">shared-common.common.ratelimit</code>)로 API별 rate limit을 적용합니다. 분산 환경에서는 Redis 기반 Token Bucket(또는 Sliding Window)으로 인스턴스 간 공유해야 합니다. Gateway에서 글로벌 rate limit, 각 서비스에서 API별 세밀한 rate limit을 이중으로 적용하면 DDoS와 개별 API 남용을 모두 방어할 수 있습니다.</p>

<h3 id="q45-모니터링관측성observability-스택을-설계하라">Q45. “모니터링/관측성(Observability) 스택을 설계하라”</h3>

<p>3대 축은 메트릭(Prometheus), 로그(Loki/ELK), 트레이스(Tempo/Jaeger)입니다. Settlement에서는 Micrometer로 30+ 커스텀 메트릭을 수집하고, Prometheus가 스크랩하여 Grafana 대시보드로 시각화합니다. 분산 트레이싱은 OTLP로 Tempo에 전송하며, Outbox 경계에서 traceparent를 영속화하여 결제→정산 단일 trace를 유지합니다. 알림은 Alertmanager로 PG 장애, 대사 불일치, Outbox 적체 등 핵심 지표에 대한 알림을 설정합니다. 핵심은 “무엇이 고장났는지”(메트릭) → “어디서 고장났는지”(트레이스) → “왜 고장났는지”(로그) 순으로 드릴다운하는 흐름입니다.</p>

<hr />

<h2 id="4-행동-면접-5문항">4. 행동 면접 (5문항)</h2>

<h3 id="q46-프로덕션-장애-상황을-경험한-적이-있나요-어떻게-대응했나요">Q46. 프로덕션 장애 상황을 경험한 적이 있나요? 어떻게 대응했나요?</h3>

<p>개발 환경에서 Outbox 폴러의 폴링 주기(2초)와 Kafka 컨슈머의 처리 속도 불균형으로 PENDING 이벤트가 적체되는 상황을 경험했습니다. 원인은 settlement-service의 정산 생성 로직에서 Read-only Projection 조회 시 N+1 쿼리가 발생하여 컨슈머 처리가 느려진 것이었습니다. <code class="language-plaintext highlighter-rouge">OutboxPendingBacklog</code> 메트릭이 알림 임계값을 넘어 감지했고, 즉시 컨슈머 측 쿼리를 JOIN FETCH로 최적화하여 해결했습니다. 이후 대사(Reconciliation)의 3번 불변식(Outbox 발행 수 == 정산 생성 수)으로 이벤트 누락을 자동 감지하는 안전망을 강화했습니다.</p>

<h3 id="q47-기술적-의견-충돌이-있었던-경험은">Q47. 기술적 의견 충돌이 있었던 경험은?</h3>

<p>MSA 분리 시 서비스 간 데이터 조회 방식에 대해 동기 API 호출 vs Read-only Projection 패턴으로 의견이 나뉘었습니다. API 호출 방식은 진정한 MSA 분리이지만 런타임 의존성과 장애 전파 위험이 있고, Read-only Projection은 DB 공유라는 제약이 있지만 정합성과 성능이 우수합니다. 트레이드오프를 정리하여 현 단계의 우선순위(정산 정합성 &gt; 완전한 MSA 분리)를 기준으로 Read-only Projection을 선택했습니다. 동시에 이벤트 기반 전환 경로(Outbox+Kafka)를 미리 구축하여 DB 분리가 필요한 시점에 전환할 수 있도록 했습니다. ADR 문서로 결정 근거를 기록했습니다.</p>

<h3 id="q48-가장-도전적이었던-기술적-문제는">Q48. 가장 도전적이었던 기술적 문제는?</h3>

<p>Outbox 비동기 경계에서 분산 트레이싱이 끊기는 문제였습니다. DB 커밋과 폴러 사이, Kafka send와 receive 사이 두 곳에서 trace context가 사라져 Tempo에서 결제→정산이 별개 trace로 보였습니다. W3C Trace Context를 <code class="language-plaintext highlighter-rouge">outbox_events.trace_parent</code> 컬럼에 영속화하고, 폴러가 Kafka 헤더로 복원하는 방식을 설계했습니다. 어려웠던 점은 Tracer가 없는 환경(로컬/CI)에서도 기존 동작과 호환되어야 한다는 것이었고, <code class="language-plaintext highlighter-rouge">TraceContextCapture</code>가 null을 반환하면 traceparent 없이 동작하도록 graceful degradation을 구현했습니다. 이 설계를 ADR 0012로 문서화했습니다.</p>

<h3 id="q49-코드-리뷰에서-중요하게-보는-기준은">Q49. 코드 리뷰에서 중요하게 보는 기준은?</h3>

<p>첫째, 도메인 불변식이 깨지지 않는가입니다. Settlement의 <code class="language-plaintext highlighter-rouge">adjustForRefund</code>에서 DONE 상태는 immutable로 금액 변경을 막고, 누적 환불이 결제 금액을 초과하지 못하게 하는 것 같은 규칙이 명시적인지 확인합니다. 둘째, 헥사고날 의존 방향이 지켜지는가입니다. domain이 adapter를 import하거나, 서비스 간 코드 의존이 생기면 즉시 지적합니다. 셋째, 동시성 처리가 안전한가입니다. 락 전략, 멱등 키, 트랜잭션 격리 수준이 적절한지 확인합니다. ArchUnit 테스트와 Testcontainers 통합 테스트를 CI 게이트로 두어 이런 기준이 자동으로 검증되게 합니다.</p>

<h3 id="q50-이-프로젝트를-통해-가장-크게-성장한-부분은">Q50. 이 프로젝트를 통해 가장 크게 성장한 부분은?</h3>

<p>“정합성”에 대한 깊은 이해입니다. 단순히 ACID 트랜잭션만으로는 분산 시스템의 정합성을 보장할 수 없다는 것을 체감했습니다. Outbox+멱등으로 이벤트 파이프라인의 정합성, 대사 3대 불변식으로 시스템 수준의 정합성, Holdback으로 비즈니스 수준의 안전장치를 층층이 쌓아야 한다는 것을 배웠습니다. 또한 16개의 ADR을 작성하면서 “왜 이렇게 설계했는가”를 명확히 기록하는 습관이 생겼고, 트레이드오프를 정량적으로 평가하는 능력이 크게 향상되었습니다. 정산이라는 도메인이 결제, 환불, 배송, 셀러 관리까지 모든 도메인의 교차점이라 전체 시스템을 설계하는 관점을 키울 수 있었습니다.</p>]]></content><author><name>푸른영혼의 별</name></author><category term="interview" /><category term="면접" /><category term="spring-boot" /><category term="msa" /><category term="system-design" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">코딩 테스트 핵심 패턴 10선 (Java) — 템플릿 + 예제 + 복잡도</title><link href="https://myoungsoo7.github.io/2026/05/04/coding-test-patterns/" rel="alternate" type="text/html" title="코딩 테스트 핵심 패턴 10선 (Java) — 템플릿 + 예제 + 복잡도" /><published>2026-05-04T06:00:00+00:00</published><updated>2026-05-04T06:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/coding-test-patterns</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/coding-test-patterns/"><![CDATA[<p>면접/코딩 테스트에서 반복적으로 출제되는 10가지 알고리즘 패턴을 정리한다.
모든 코드는 복사-붙여넣기 후 바로 실행 가능하다.</p>

<hr />

<h2 id="목차">목차</h2>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>패턴</th>
      <th>대표 문제</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td><a href="#1-투-포인터-two-pointer">투 포인터</a></td>
      <td>정렬된 배열에서 두 수의 합</td>
    </tr>
    <tr>
      <td>2</td>
      <td><a href="#2-슬라이딩-윈도우-sliding-window">슬라이딩 윈도우</a></td>
      <td>최대 부분 합, 중복 없는 최장 부분 문자열</td>
    </tr>
    <tr>
      <td>3</td>
      <td><a href="#3-이진-탐색-binary-search">이진 탐색</a></td>
      <td>Lower Bound (첫 등장 위치)</td>
    </tr>
    <tr>
      <td>4</td>
      <td><a href="#4-bfs-너비-우선-탐색">BFS</a></td>
      <td>미로 최단 경로</td>
    </tr>
    <tr>
      <td>5</td>
      <td><a href="#5-dfs--백트래킹">DFS + 백트래킹</a></td>
      <td>순열/조합 생성, 섬의 개수</td>
    </tr>
    <tr>
      <td>6</td>
      <td><a href="#6-동적-프로그래밍-dp">동적 프로그래밍</a></td>
      <td>계단 오르기, LIS</td>
    </tr>
    <tr>
      <td>7</td>
      <td><a href="#7-그리디-greedy">그리디</a></td>
      <td>회의실 배정, 동전 거스름돈</td>
    </tr>
    <tr>
      <td>8</td>
      <td><a href="#8-해시맵-활용">해시맵 활용</a></td>
      <td>두 수의 합 O(N), 아나그램 판별</td>
    </tr>
    <tr>
      <td>9</td>
      <td><a href="#9-스택큐-활용">스택/큐 활용</a></td>
      <td>유효한 괄호, 주식 가격</td>
    </tr>
    <tr>
      <td>10</td>
      <td><a href="#10-힙우선순위-큐">힙/우선순위 큐</a></td>
      <td>K번째 큰 수, 데이터 스트림 중앙값</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="1-투-포인터-two-pointer">1. 투 포인터 (Two Pointer)</h2>

<h3 id="어떤-문제에-사용하나">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>정렬된</strong> 배열에서 특정 조건을 만족하는 쌍(pair) 찾기</li>
  <li>연속 부분 배열의 합/곱 조건 탐색</li>
  <li>배열 내 중복 제거</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 양 끝(또는 같은 방향)에서 두 개의 포인터를 이동시키며 탐색 범위를 줄인다.</p>
</blockquote>

<h3 id="템플릿">템플릿</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 양끝 투 포인터 (opposite direction)</span>
<span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">right</span> <span class="o">=</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span> <span class="o">-</span> <span class="mi">1</span><span class="o">;</span>
<span class="k">while</span> <span class="o">(</span><span class="n">left</span> <span class="o">&lt;</span> <span class="n">right</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">arr</span><span class="o">[</span><span class="n">left</span><span class="o">]</span> <span class="o">+</span> <span class="n">arr</span><span class="o">[</span><span class="n">right</span><span class="o">];</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">sum</span> <span class="o">==</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 정답 처리</span>
        <span class="k">break</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">sum</span> <span class="o">&lt;</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">left</span><span class="o">++;</span>   <span class="c1">// 합이 작으면 왼쪽 포인터를 오른쪽으로</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
        <span class="n">right</span><span class="o">--;</span>  <span class="c1">// 합이 크면 오른쪽 포인터를 왼쪽으로</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-정렬된-배열에서-두-수의-합">예제: 정렬된 배열에서 두 수의 합</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TwoSum</span> <span class="o">{</span>
    <span class="cm">/**
     * 정렬된 배열에서 합이 target인 두 수의 인덱스를 반환한다.
     * 인덱스는 1-based.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span><span class="o">[]</span> <span class="nf">twoSum</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">numbers</span><span class="o">,</span> <span class="kt">int</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">right</span> <span class="o">=</span> <span class="n">numbers</span><span class="o">.</span><span class="na">length</span> <span class="o">-</span> <span class="mi">1</span><span class="o">;</span>

        <span class="k">while</span> <span class="o">(</span><span class="n">left</span> <span class="o">&lt;</span> <span class="n">right</span><span class="o">)</span> <span class="o">{</span>
            <span class="kt">int</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">numbers</span><span class="o">[</span><span class="n">left</span><span class="o">]</span> <span class="o">+</span> <span class="n">numbers</span><span class="o">[</span><span class="n">right</span><span class="o">];</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">sum</span> <span class="o">==</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
                <span class="c1">// 1-based 인덱스로 반환</span>
                <span class="k">return</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[]{</span><span class="n">left</span> <span class="o">+</span> <span class="mi">1</span><span class="o">,</span> <span class="n">right</span> <span class="o">+</span> <span class="mi">1</span><span class="o">};</span>
            <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">sum</span> <span class="o">&lt;</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">left</span><span class="o">++;</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">right</span><span class="o">--;</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[]{-</span><span class="mi">1</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">};</span> <span class="c1">// 답이 없는 경우</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">numbers</span> <span class="o">=</span> <span class="o">{</span><span class="mi">2</span><span class="o">,</span> <span class="mi">7</span><span class="o">,</span> <span class="mi">11</span><span class="o">,</span> <span class="mi">15</span><span class="o">};</span>
        <span class="kt">int</span> <span class="n">target</span> <span class="o">=</span> <span class="mi">9</span><span class="o">;</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">result</span> <span class="o">=</span> <span class="n">twoSum</span><span class="o">(</span><span class="n">numbers</span><span class="o">,</span> <span class="n">target</span><span class="o">);</span>
        <span class="c1">// 입력: [2, 7, 11, 15], target = 9</span>
        <span class="c1">// 출력: [1, 2]  (numbers[0] + numbers[1] = 2 + 7 = 9)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="n">result</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(1)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-슬라이딩-윈도우-sliding-window">2. 슬라이딩 윈도우 (Sliding Window)</h2>

<h3 id="어떤-문제에-사용하나-1">어떤 문제에 사용하나</h3>

<ul>
  <li>연속된 부분 배열/부분 문자열에서 최대/최소/조건 만족하는 구간 찾기</li>
  <li>고정 크기 또는 가변 크기 윈도우</li>
  <li>“중복 없는 최장 부분 문자열” 같은 문제</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 윈도우(구간)를 오른쪽으로 한 칸씩 밀면서, 불필요한 재계산을 피한다.</p>
</blockquote>

<h3 id="템플릿-1">템플릿</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 가변 크기 슬라이딩 윈도우</span>
<span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="kt">int</span> <span class="n">best</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="c1">// state: 윈도우 내부 상태를 관리하는 자료구조 (Map, Set 등)</span>

<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">right</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">right</span> <span class="o">&lt;</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">right</span><span class="o">++)</span> <span class="o">{</span>
    <span class="c1">// 1. 오른쪽 원소를 윈도우에 추가</span>
    <span class="c1">// 2. 조건 위반 시 왼쪽을 줄인다</span>
    <span class="k">while</span> <span class="o">(</span><span class="cm">/* 조건 위반 */</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 왼쪽 원소를 윈도우에서 제거</span>
        <span class="n">left</span><span class="o">++;</span>
    <span class="o">}</span>
    <span class="c1">// 3. 현재 윈도우로 정답 갱신</span>
    <span class="n">best</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">best</span><span class="o">,</span> <span class="n">right</span> <span class="o">-</span> <span class="n">left</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-a-크기-k인-부분-배열의-최대-합">예제 A: 크기 K인 부분 배열의 최대 합</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MaxSubarraySum</span> <span class="o">{</span>
    <span class="cm">/**
     * 크기 k인 연속 부분 배열의 최대 합을 구한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">maxSum</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">arr</span><span class="o">,</span> <span class="kt">int</span> <span class="n">k</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 첫 윈도우의 합 계산</span>
        <span class="kt">int</span> <span class="n">windowSum</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">k</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="n">windowSum</span> <span class="o">+=</span> <span class="n">arr</span><span class="o">[</span><span class="n">i</span><span class="o">];</span>
        <span class="o">}</span>

        <span class="kt">int</span> <span class="n">maxSum</span> <span class="o">=</span> <span class="n">windowSum</span><span class="o">;</span>

        <span class="c1">// 윈도우를 한 칸씩 오른쪽으로 슬라이드</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">k</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="n">windowSum</span> <span class="o">+=</span> <span class="n">arr</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">-</span> <span class="n">arr</span><span class="o">[</span><span class="n">i</span> <span class="o">-</span> <span class="n">k</span><span class="o">];</span> <span class="c1">// 새 원소 추가, 맨 왼쪽 제거</span>
            <span class="n">maxSum</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">maxSum</span><span class="o">,</span> <span class="n">windowSum</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">maxSum</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">arr</span> <span class="o">=</span> <span class="o">{</span><span class="mi">1</span><span class="o">,</span> <span class="mi">4</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">10</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">20</span><span class="o">};</span>
        <span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">4</span><span class="o">;</span>
        <span class="c1">// 입력: arr = [1,4,2,10,2,3,1,0,20], k = 4</span>
        <span class="c1">// 출력: 24  (부분 배열 [2,3,1,0,20] 중 [1,0,20]... 아니라 [10,2,3,1]? → 아님)</span>
        <span class="c1">//       실제로 [0,20] 포함 구간: arr[5..8] = [3,1,0,20] = 24</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">maxSum</span><span class="o">(</span><span class="n">arr</span><span class="o">,</span> <span class="n">k</span><span class="o">));</span> <span class="c1">// 24</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-중복-없는-최장-부분-문자열-leetcode-3">예제 B: 중복 없는 최장 부분 문자열 (LeetCode 3)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LongestSubstring</span> <span class="o">{</span>
    <span class="cm">/**
     * 중복 문자가 없는 가장 긴 부분 문자열의 길이를 반환한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">lengthOfLongestSubstring</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">Character</span><span class="o">&gt;</span> <span class="n">window</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashSet</span><span class="o">&lt;&gt;();</span>
        <span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
        <span class="kt">int</span> <span class="n">maxLen</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">right</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">right</span> <span class="o">&lt;</span> <span class="n">s</span><span class="o">.</span><span class="na">length</span><span class="o">();</span> <span class="n">right</span><span class="o">++)</span> <span class="o">{</span>
            <span class="kt">char</span> <span class="n">c</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">right</span><span class="o">);</span>

            <span class="c1">// 중복이 있으면 왼쪽을 줄여서 중복 제거</span>
            <span class="k">while</span> <span class="o">(</span><span class="n">window</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">c</span><span class="o">))</span> <span class="o">{</span>
                <span class="n">window</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">s</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">left</span><span class="o">));</span>
                <span class="n">left</span><span class="o">++;</span>
            <span class="o">}</span>

            <span class="n">window</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">c</span><span class="o">);</span>
            <span class="n">maxLen</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">maxLen</span><span class="o">,</span> <span class="n">right</span> <span class="o">-</span> <span class="n">left</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">maxLen</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 입력: "abcabcbb"</span>
        <span class="c1">// 출력: 3  ("abc")</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">lengthOfLongestSubstring</span><span class="o">(</span><span class="s">"abcabcbb"</span><span class="o">));</span> <span class="c1">// 3</span>

        <span class="c1">// 입력: "pwwkew"</span>
        <span class="c1">// 출력: 3  ("wke")</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">lengthOfLongestSubstring</span><span class="o">(</span><span class="s">"pwwkew"</span><span class="o">));</span>   <span class="c1">// 3</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-1">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(K) — K는 윈도우 내 고유 원소 수</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="3-이진-탐색-binary-search">3. 이진 탐색 (Binary Search)</h2>

<h3 id="어떤-문제에-사용하나-2">어떤 문제에 사용하나</h3>

<ul>
  <li>정렬된 배열에서 특정 값 찾기</li>
  <li><strong>Lower Bound</strong>: 특정 값 이상인 첫 위치</li>
  <li><strong>Upper Bound</strong>: 특정 값 초과인 첫 위치</li>
  <li>답의 범위가 단조(monotonic)일 때 <strong>매개변수 탐색</strong> (parametric search)</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 탐색 범위를 절반씩 줄인다. O(log N).</p>
</blockquote>

<h3 id="템플릿-2">템플릿</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Lower Bound: target 이상인 첫 인덱스</span>
<span class="kt">int</span> <span class="nf">lowerBound</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">arr</span><span class="o">,</span> <span class="kt">int</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">lo</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">hi</span> <span class="o">=</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
    <span class="k">while</span> <span class="o">(</span><span class="n">lo</span> <span class="o">&lt;</span> <span class="n">hi</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="n">lo</span> <span class="o">+</span> <span class="o">(</span><span class="n">hi</span> <span class="o">-</span> <span class="n">lo</span><span class="o">)</span> <span class="o">/</span> <span class="mi">2</span><span class="o">;</span>  <span class="c1">// 오버플로 방지</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">arr</span><span class="o">[</span><span class="n">mid</span><span class="o">]</span> <span class="o">&lt;</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">lo</span> <span class="o">=</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">hi</span> <span class="o">=</span> <span class="n">mid</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">lo</span><span class="o">;</span> <span class="c1">// target이 없으면 삽입 위치 반환</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-특정-값의-첫-등장-위치">예제: 특정 값의 첫 등장 위치</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">BinarySearchLowerBound</span> <span class="o">{</span>
    <span class="cm">/**
     * 정렬된 배열에서 target이 처음 등장하는 인덱스를 반환한다.
     * target이 없으면 -1을 반환한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">findFirst</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">arr</span><span class="o">,</span> <span class="kt">int</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">lo</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">hi</span> <span class="o">=</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>

        <span class="k">while</span> <span class="o">(</span><span class="n">lo</span> <span class="o">&lt;</span> <span class="n">hi</span><span class="o">)</span> <span class="o">{</span>
            <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="n">lo</span> <span class="o">+</span> <span class="o">(</span><span class="n">hi</span> <span class="o">-</span> <span class="n">lo</span><span class="o">)</span> <span class="o">/</span> <span class="mi">2</span><span class="o">;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">arr</span><span class="o">[</span><span class="n">mid</span><span class="o">]</span> <span class="o">&lt;</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">lo</span> <span class="o">=</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span><span class="o">;</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">hi</span> <span class="o">=</span> <span class="n">mid</span><span class="o">;</span>  <span class="c1">// arr[mid] &gt;= target이면 왼쪽으로</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="c1">// lo가 유효한 인덱스이고 값이 target인지 확인</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">lo</span> <span class="o">&lt;</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span> <span class="o">&amp;&amp;</span> <span class="n">arr</span><span class="o">[</span><span class="n">lo</span><span class="o">]</span> <span class="o">==</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">lo</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">arr</span> <span class="o">=</span> <span class="o">{</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">4</span><span class="o">,</span> <span class="mi">5</span><span class="o">};</span>

        <span class="c1">// 입력: arr = [1,2,2,2,3,4,5], target = 2</span>
        <span class="c1">// 출력: 1  (인덱스 1에서 처음 등장)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">findFirst</span><span class="o">(</span><span class="n">arr</span><span class="o">,</span> <span class="mi">2</span><span class="o">));</span> <span class="c1">// 1</span>

        <span class="c1">// 입력: target = 6</span>
        <span class="c1">// 출력: -1 (존재하지 않음)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">findFirst</span><span class="o">(</span><span class="n">arr</span><span class="o">,</span> <span class="mi">6</span><span class="o">));</span> <span class="c1">// -1</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-2">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(log N)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(1)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="4-bfs-너비-우선-탐색">4. BFS (너비 우선 탐색)</h2>

<h3 id="어떤-문제에-사용하나-3">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>최단 거리/최소 횟수</strong> 문제 (가중치가 동일할 때)</li>
  <li>미로 탈출, 체스 나이트 이동 횟수</li>
  <li>레벨 순서 탐색 (트리의 레벨 순회)</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 큐를 사용해 가까운 노드부터 탐색한다. 처음 도착한 시점이 최단 경로이다.</p>
</blockquote>

<h3 id="템플릿-3">템플릿</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 2D 그리드 BFS</span>
<span class="kt">int</span><span class="o">[][]</span> <span class="n">dirs</span> <span class="o">=</span> <span class="o">{{</span><span class="mi">0</span><span class="o">,</span><span class="mi">1</span><span class="o">},{</span><span class="mi">0</span><span class="o">,-</span><span class="mi">1</span><span class="o">},{</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">},{-</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">}};</span>
<span class="kt">boolean</span><span class="o">[][]</span> <span class="n">visited</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">boolean</span><span class="o">[</span><span class="n">rows</span><span class="o">][</span><span class="n">cols</span><span class="o">];</span>
<span class="nc">Queue</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">[]&gt;</span> <span class="n">queue</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedList</span><span class="o">&lt;&gt;();</span>

<span class="n">queue</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="k">new</span> <span class="kt">int</span><span class="o">[]{</span><span class="n">startR</span><span class="o">,</span> <span class="n">startC</span><span class="o">,</span> <span class="mi">0</span><span class="o">});</span> <span class="c1">// {행, 열, 거리}</span>
<span class="n">visited</span><span class="o">[</span><span class="n">startR</span><span class="o">][</span><span class="n">startC</span><span class="o">]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>

<span class="k">while</span> <span class="o">(!</span><span class="n">queue</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
    <span class="kt">int</span><span class="o">[]</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="na">poll</span><span class="o">();</span>
    <span class="kt">int</span> <span class="n">r</span> <span class="o">=</span> <span class="n">cur</span><span class="o">[</span><span class="mi">0</span><span class="o">],</span> <span class="n">c</span> <span class="o">=</span> <span class="n">cur</span><span class="o">[</span><span class="mi">1</span><span class="o">],</span> <span class="n">dist</span> <span class="o">=</span> <span class="n">cur</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">r</span> <span class="o">==</span> <span class="n">endR</span> <span class="o">&amp;&amp;</span> <span class="n">c</span> <span class="o">==</span> <span class="n">endC</span><span class="o">)</span> <span class="k">return</span> <span class="n">dist</span><span class="o">;</span> <span class="c1">// 도착</span>

    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">d</span> <span class="o">:</span> <span class="n">dirs</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">nr</span> <span class="o">=</span> <span class="n">r</span> <span class="o">+</span> <span class="n">d</span><span class="o">[</span><span class="mi">0</span><span class="o">],</span> <span class="n">nc</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">d</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">nr</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">nr</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="o">&amp;&amp;</span> <span class="n">nc</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">nc</span> <span class="o">&lt;</span> <span class="n">cols</span>
                <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">visited</span><span class="o">[</span><span class="n">nr</span><span class="o">][</span><span class="n">nc</span><span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="n">grid</span><span class="o">[</span><span class="n">nr</span><span class="o">][</span><span class="n">nc</span><span class="o">]</span> <span class="o">!=</span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">visited</span><span class="o">[</span><span class="n">nr</span><span class="o">][</span><span class="n">nc</span><span class="o">]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
            <span class="n">queue</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="k">new</span> <span class="kt">int</span><span class="o">[]{</span><span class="n">nr</span><span class="o">,</span> <span class="n">nc</span><span class="o">,</span> <span class="n">dist</span> <span class="o">+</span> <span class="mi">1</span><span class="o">});</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span> <span class="c1">// 도달 불가</span>
</code></pre></div></div>

<h3 id="예제-미로-최단-경로">예제: 미로 최단 경로</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MazeShortestPath</span> <span class="o">{</span>
    <span class="c1">// 방향: 상하좌우</span>
    <span class="kd">static</span> <span class="kt">int</span><span class="o">[][]</span> <span class="n">dirs</span> <span class="o">=</span> <span class="o">{{-</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">},{</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">},{</span><span class="mi">0</span><span class="o">,-</span><span class="mi">1</span><span class="o">},{</span><span class="mi">0</span><span class="o">,</span><span class="mi">1</span><span class="o">}};</span>

    <span class="cm">/**
     * 0은 통로, 1은 벽.
     * (0,0)에서 (rows-1, cols-1)까지 최단 거리를 반환한다.
     * 도달 불가하면 -1.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">shortestPath</span><span class="o">(</span><span class="kt">int</span><span class="o">[][]</span> <span class="n">maze</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">rows</span> <span class="o">=</span> <span class="n">maze</span><span class="o">.</span><span class="na">length</span><span class="o">,</span> <span class="n">cols</span> <span class="o">=</span> <span class="n">maze</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">length</span><span class="o">;</span>

        <span class="c1">// 시작점이나 도착점이 벽이면 불가</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">maze</span><span class="o">[</span><span class="mi">0</span><span class="o">][</span><span class="mi">0</span><span class="o">]</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">||</span> <span class="n">maze</span><span class="o">[</span><span class="n">rows</span><span class="o">-</span><span class="mi">1</span><span class="o">][</span><span class="n">cols</span><span class="o">-</span><span class="mi">1</span><span class="o">]</span> <span class="o">==</span> <span class="mi">1</span><span class="o">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span>

        <span class="kt">boolean</span><span class="o">[][]</span> <span class="n">visited</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">boolean</span><span class="o">[</span><span class="n">rows</span><span class="o">][</span><span class="n">cols</span><span class="o">];</span>
        <span class="nc">Queue</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">[]&gt;</span> <span class="n">queue</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedList</span><span class="o">&lt;&gt;();</span>

        <span class="n">queue</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="k">new</span> <span class="kt">int</span><span class="o">[]{</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">});</span>
        <span class="n">visited</span><span class="o">[</span><span class="mi">0</span><span class="o">][</span><span class="mi">0</span><span class="o">]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>

        <span class="k">while</span> <span class="o">(!</span><span class="n">queue</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="kt">int</span><span class="o">[]</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="na">poll</span><span class="o">();</span>
            <span class="kt">int</span> <span class="n">r</span> <span class="o">=</span> <span class="n">cur</span><span class="o">[</span><span class="mi">0</span><span class="o">],</span> <span class="n">c</span> <span class="o">=</span> <span class="n">cur</span><span class="o">[</span><span class="mi">1</span><span class="o">],</span> <span class="n">dist</span> <span class="o">=</span> <span class="n">cur</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span>

            <span class="c1">// 도착 지점에 도달</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">r</span> <span class="o">==</span> <span class="n">rows</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">c</span> <span class="o">==</span> <span class="n">cols</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">return</span> <span class="n">dist</span><span class="o">;</span>
            <span class="o">}</span>

            <span class="k">for</span> <span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">d</span> <span class="o">:</span> <span class="n">dirs</span><span class="o">)</span> <span class="o">{</span>
                <span class="kt">int</span> <span class="n">nr</span> <span class="o">=</span> <span class="n">r</span> <span class="o">+</span> <span class="n">d</span><span class="o">[</span><span class="mi">0</span><span class="o">],</span> <span class="n">nc</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">d</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">nr</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">nr</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="o">&amp;&amp;</span> <span class="n">nc</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">nc</span> <span class="o">&lt;</span> <span class="n">cols</span>
                        <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">visited</span><span class="o">[</span><span class="n">nr</span><span class="o">][</span><span class="n">nc</span><span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="n">maze</span><span class="o">[</span><span class="n">nr</span><span class="o">][</span><span class="n">nc</span><span class="o">]</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">visited</span><span class="o">[</span><span class="n">nr</span><span class="o">][</span><span class="n">nc</span><span class="o">]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
                    <span class="n">queue</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="k">new</span> <span class="kt">int</span><span class="o">[]{</span><span class="n">nr</span><span class="o">,</span> <span class="n">nc</span><span class="o">,</span> <span class="n">dist</span> <span class="o">+</span> <span class="mi">1</span><span class="o">});</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span> <span class="c1">// 도달 불가</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[][]</span> <span class="n">maze</span> <span class="o">=</span> <span class="o">{</span>
            <span class="o">{</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">0</span><span class="o">},</span>
            <span class="o">{</span><span class="mi">1</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">},</span>
            <span class="o">{</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">0</span><span class="o">},</span>
            <span class="o">{</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">}</span>
        <span class="o">};</span>
        <span class="c1">// 입력: 4x4 미로 (0=통로, 1=벽)</span>
        <span class="c1">// (0,0) → (3,3) 최단 경로</span>
        <span class="c1">// 출력: 6</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">shortestPath</span><span class="o">(</span><span class="n">maze</span><span class="o">));</span> <span class="c1">// 6</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-3">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(V + E) — 그리드에서는 O(rows * cols)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(V) — visited 배열 + 큐</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="5-dfs--백트래킹">5. DFS + 백트래킹</h2>

<h3 id="어떤-문제에-사용하나-4">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>모든 경우의 수</strong> 탐색: 순열, 조합, 부분집합</li>
  <li>그래프 탐색: 섬의 개수, 연결 요소</li>
  <li>제약 조건이 있는 탐색: N-Queen, 스도쿠</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 한 방향으로 끝까지 탐색 후, 선택을 취소(백트래킹)하고 다음 방향을 탐색한다.</p>
</blockquote>

<h3 id="템플릿-4">템플릿</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 조합/순열 백트래킹</span>
<span class="kt">void</span> <span class="nf">backtrack</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">result</span><span class="o">,</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">path</span><span class="o">,</span>
               <span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span><span class="o">,</span> <span class="kt">int</span> <span class="n">start</span><span class="o">,</span> <span class="kt">boolean</span><span class="o">[]</span> <span class="n">used</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">==</span> <span class="cm">/* 목표 크기 */</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">result</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="n">path</span><span class="o">));</span> <span class="c1">// 깊은 복사 필수</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">start</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">nums</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">used</span><span class="o">[</span><span class="n">i</span><span class="o">])</span> <span class="k">continue</span><span class="o">;</span> <span class="c1">// 순열일 때</span>
        <span class="n">path</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">nums</span><span class="o">[</span><span class="n">i</span><span class="o">]);</span>
        <span class="n">used</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
        <span class="n">backtrack</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">path</span><span class="o">,</span> <span class="n">nums</span><span class="o">,</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">,</span> <span class="n">used</span><span class="o">);</span>
        <span class="n">path</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">-</span> <span class="mi">1</span><span class="o">);</span> <span class="c1">// 되돌리기</span>
        <span class="n">used</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-a-조합-생성-ncr">예제 A: 조합 생성 (nCr)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Combinations</span> <span class="o">{</span>
    <span class="cm">/**
     * 1~n에서 k개를 뽑는 모든 조합을 반환한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="nf">combine</span><span class="o">(</span><span class="kt">int</span> <span class="n">n</span><span class="o">,</span> <span class="kt">int</span> <span class="n">k</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
        <span class="n">backtrack</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(),</span> <span class="n">n</span><span class="o">,</span> <span class="n">k</span><span class="o">,</span> <span class="mi">1</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">backtrack</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">result</span><span class="o">,</span>
                                   <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">path</span><span class="o">,</span> <span class="kt">int</span> <span class="n">n</span><span class="o">,</span> <span class="kt">int</span> <span class="n">k</span><span class="o">,</span> <span class="kt">int</span> <span class="n">start</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// k개를 모두 골랐으면 결과에 추가</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">==</span> <span class="n">k</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">result</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="n">path</span><span class="o">));</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="c1">// 남은 수가 부족하면 가지치기 (pruning)</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">start</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="n">n</span> <span class="o">-</span> <span class="o">(</span><span class="n">k</span> <span class="o">-</span> <span class="n">path</span><span class="o">.</span><span class="na">size</span><span class="o">())</span> <span class="o">+</span> <span class="mi">1</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="n">path</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
            <span class="n">backtrack</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">path</span><span class="o">,</span> <span class="n">n</span><span class="o">,</span> <span class="n">k</span><span class="o">,</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span> <span class="c1">// i+1: 중복 허용 안 함</span>
            <span class="n">path</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">-</span> <span class="mi">1</span><span class="o">);</span> <span class="c1">// 백트래킹</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 입력: n=4, k=2</span>
        <span class="c1">// 출력: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">combine</span><span class="o">(</span><span class="mi">4</span><span class="o">,</span> <span class="mi">2</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-섬의-개수-dfs">예제 B: 섬의 개수 (DFS)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">NumberOfIslands</span> <span class="o">{</span>
    <span class="kd">static</span> <span class="kt">int</span><span class="o">[][]</span> <span class="n">dirs</span> <span class="o">=</span> <span class="o">{{-</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">},{</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">},{</span><span class="mi">0</span><span class="o">,-</span><span class="mi">1</span><span class="o">},{</span><span class="mi">0</span><span class="o">,</span><span class="mi">1</span><span class="o">}};</span>

    <span class="cm">/**
     * '1'은 땅, '0'은 물. 연결된 땅 덩어리(섬)의 개수를 반환한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">numIslands</span><span class="o">(</span><span class="kt">char</span><span class="o">[][]</span> <span class="n">grid</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
        <span class="kt">int</span> <span class="n">rows</span> <span class="o">=</span> <span class="n">grid</span><span class="o">.</span><span class="na">length</span><span class="o">,</span> <span class="n">cols</span> <span class="o">=</span> <span class="n">grid</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">length</span><span class="o">;</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">r</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">r</span> <span class="o">&lt;</span> <span class="n">rows</span><span class="o">;</span> <span class="n">r</span><span class="o">++)</span> <span class="o">{</span>
            <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">c</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">c</span> <span class="o">&lt;</span> <span class="n">cols</span><span class="o">;</span> <span class="n">c</span><span class="o">++)</span> <span class="o">{</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">grid</span><span class="o">[</span><span class="n">r</span><span class="o">][</span><span class="n">c</span><span class="o">]</span> <span class="o">==</span> <span class="sc">'1'</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">count</span><span class="o">++;</span>
                    <span class="n">dfs</span><span class="o">(</span><span class="n">grid</span><span class="o">,</span> <span class="n">r</span><span class="o">,</span> <span class="n">c</span><span class="o">,</span> <span class="n">rows</span><span class="o">,</span> <span class="n">cols</span><span class="o">);</span> <span class="c1">// 연결된 모든 땅을 방문 처리</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">count</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">dfs</span><span class="o">(</span><span class="kt">char</span><span class="o">[][]</span> <span class="n">grid</span><span class="o">,</span> <span class="kt">int</span> <span class="n">r</span><span class="o">,</span> <span class="kt">int</span> <span class="n">c</span><span class="o">,</span> <span class="kt">int</span> <span class="n">rows</span><span class="o">,</span> <span class="kt">int</span> <span class="n">cols</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 범위 밖이거나 물이면 종료</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">r</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">r</span> <span class="o">&gt;=</span> <span class="n">rows</span> <span class="o">||</span> <span class="n">c</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">c</span> <span class="o">&gt;=</span> <span class="n">cols</span> <span class="o">||</span> <span class="n">grid</span><span class="o">[</span><span class="n">r</span><span class="o">][</span><span class="n">c</span><span class="o">]</span> <span class="o">==</span> <span class="sc">'0'</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="n">grid</span><span class="o">[</span><span class="n">r</span><span class="o">][</span><span class="n">c</span><span class="o">]</span> <span class="o">=</span> <span class="sc">'0'</span><span class="o">;</span> <span class="c1">// 방문 표시 (물로 바꿈)</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">d</span> <span class="o">:</span> <span class="n">dirs</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">dfs</span><span class="o">(</span><span class="n">grid</span><span class="o">,</span> <span class="n">r</span> <span class="o">+</span> <span class="n">d</span><span class="o">[</span><span class="mi">0</span><span class="o">],</span> <span class="n">c</span> <span class="o">+</span> <span class="n">d</span><span class="o">[</span><span class="mi">1</span><span class="o">],</span> <span class="n">rows</span><span class="o">,</span> <span class="n">cols</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">char</span><span class="o">[][]</span> <span class="n">grid</span> <span class="o">=</span> <span class="o">{</span>
            <span class="o">{</span><span class="sc">'1'</span><span class="o">,</span><span class="sc">'1'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">},</span>
            <span class="o">{</span><span class="sc">'1'</span><span class="o">,</span><span class="sc">'1'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">},</span>
            <span class="o">{</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'1'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">},</span>
            <span class="o">{</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'0'</span><span class="o">,</span><span class="sc">'1'</span><span class="o">,</span><span class="sc">'1'</span><span class="o">}</span>
        <span class="o">};</span>
        <span class="c1">// 입력: 4x5 그리드</span>
        <span class="c1">// 출력: 3 (섬 3개)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">numIslands</span><span class="o">(</span><span class="n">grid</span><span class="o">));</span> <span class="c1">// 3</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-4">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>조합/순열</th>
      <th>그리드 DFS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N! 또는 2^N)</td>
      <td>O(rows * cols)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(K) 재귀 깊이</td>
      <td>O(rows * cols) 최악</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="6-동적-프로그래밍-dp">6. 동적 프로그래밍 (DP)</h2>

<h3 id="어떤-문제에-사용하나-5">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>최적 부분 구조</strong> + <strong>중복 부분 문제</strong>가 있을 때</li>
  <li>“최소 비용”, “경우의 수”, “최대 길이” 같은 최적화 문제</li>
  <li>대표 문제: 피보나치, 계단 오르기, 배낭 문제, LIS, LCS</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 작은 문제의 답을 저장(메모이제이션)해서 큰 문제를 푼다.</p>
</blockquote>

<h3 id="dp-문제-풀이-순서">DP 문제 풀이 순서</h3>

<ol>
  <li><strong>상태 정의</strong>: dp[i]가 무엇을 의미하는지 정한다</li>
  <li><strong>점화식 도출</strong>: dp[i]를 이전 값으로 표현한다</li>
  <li><strong>초기값 설정</strong>: 기저 조건(base case) 설정</li>
  <li><strong>순서 결정</strong>: 어떤 방향으로 채울지 결정</li>
  <li><strong>답 도출</strong>: dp 배열에서 답 추출</li>
</ol>

<h3 id="예제-a-계단-오르기">예제 A: 계단 오르기</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ClimbStairs</span> <span class="o">{</span>
    <span class="cm">/**
     * 한 번에 1칸 또는 2칸 오를 수 있을 때,
     * n개의 계단을 오르는 방법의 수를 반환한다.
     *
     * dp[i] = i번째 계단에 도달하는 방법의 수
     * 점화식: dp[i] = dp[i-1] + dp[i-2]
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">climbStairs</span><span class="o">(</span><span class="kt">int</span> <span class="n">n</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">n</span> <span class="o">&lt;=</span> <span class="mi">2</span><span class="o">)</span> <span class="k">return</span> <span class="n">n</span><span class="o">;</span>

        <span class="c1">// 공간 최적화: 직전 두 값만 기억</span>
        <span class="kt">int</span> <span class="n">prev2</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span> <span class="c1">// dp[1]</span>
        <span class="kt">int</span> <span class="n">prev1</span> <span class="o">=</span> <span class="mi">2</span><span class="o">;</span> <span class="c1">// dp[2]</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">3</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="n">n</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="kt">int</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">prev1</span> <span class="o">+</span> <span class="n">prev2</span><span class="o">;</span>
            <span class="n">prev2</span> <span class="o">=</span> <span class="n">prev1</span><span class="o">;</span>
            <span class="n">prev1</span> <span class="o">=</span> <span class="n">cur</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">prev1</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 입력: n = 5</span>
        <span class="c1">// 출력: 8</span>
        <span class="c1">// 경로 예: 1+1+1+1+1, 1+1+1+2, 1+1+2+1, 1+2+1+1, 2+1+1+1,</span>
        <span class="c1">//          1+2+2, 2+1+2, 2+2+1</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">climbStairs</span><span class="o">(</span><span class="mi">5</span><span class="o">));</span> <span class="c1">// 8</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-최장-증가-부분-수열-lis">예제 B: 최장 증가 부분 수열 (LIS)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LIS</span> <span class="o">{</span>
    <span class="cm">/**
     * 최장 증가 부분 수열의 길이를 반환한다.
     *
     * 방법 1: O(N^2) DP
     *   dp[i] = nums[i]를 마지막으로 하는 LIS 길이
     *   점화식: dp[i] = max(dp[j] + 1) (j &lt; i, nums[j] &lt; nums[i])
     *
     * 방법 2: O(N log N) 이진 탐색 (아래 구현)
     *   tails 배열: tails[i] = 길이 i+1인 증가 수열의 마지막 원소 최솟값
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">lengthOfLIS</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// tails: 각 길이별 가능한 최소 마지막 값</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">tails</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">num</span> <span class="o">:</span> <span class="n">nums</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// num이 들어갈 위치를 이진 탐색</span>
            <span class="kt">int</span> <span class="n">pos</span> <span class="o">=</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">binarySearch</span><span class="o">(</span><span class="n">tails</span><span class="o">,</span> <span class="n">num</span><span class="o">);</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">pos</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">pos</span> <span class="o">=</span> <span class="o">-(</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span> <span class="c1">// 삽입 위치</span>
            <span class="o">}</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">pos</span> <span class="o">==</span> <span class="n">tails</span><span class="o">.</span><span class="na">size</span><span class="o">())</span> <span class="o">{</span>
                <span class="n">tails</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">num</span><span class="o">);</span>    <span class="c1">// 수열 길이 증가</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">tails</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">pos</span><span class="o">,</span> <span class="n">num</span><span class="o">);</span> <span class="c1">// 더 작은 값으로 교체</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">tails</span><span class="o">.</span><span class="na">size</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span> <span class="o">=</span> <span class="o">{</span><span class="mi">10</span><span class="o">,</span> <span class="mi">9</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">5</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="mi">7</span><span class="o">,</span> <span class="mi">101</span><span class="o">,</span> <span class="mi">18</span><span class="o">};</span>
        <span class="c1">// 입력: [10, 9, 2, 5, 3, 7, 101, 18]</span>
        <span class="c1">// 출력: 4  (LIS: [2, 3, 7, 101] 또는 [2, 5, 7, 101] 등)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">lengthOfLIS</span><span class="o">(</span><span class="n">nums</span><span class="o">));</span> <span class="c1">// 4</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-5">복잡도</h3>

<table>
  <thead>
    <tr>
      <th>문제</th>
      <th>시간</th>
      <th>공간</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>계단 오르기</td>
      <td>O(N)</td>
      <td>O(1)</td>
    </tr>
    <tr>
      <td>LIS (DP)</td>
      <td>O(N^2)</td>
      <td>O(N)</td>
    </tr>
    <tr>
      <td>LIS (이진 탐색)</td>
      <td>O(N log N)</td>
      <td>O(N)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="7-그리디-greedy">7. 그리디 (Greedy)</h2>

<h3 id="어떤-문제에-사용하나-6">어떤 문제에 사용하나</h3>

<ul>
  <li>매 순간 <strong>현재 최선의 선택</strong>이 전체 최적해가 되는 문제</li>
  <li>정렬 후 순차적으로 결정</li>
  <li>대표: 활동 선택 문제, 허프만 코딩, 최소 신장 트리</li>
</ul>

<blockquote>
  <p>핵심 아이디어: “지금 이 순간 가장 좋은 것을 고르면 전체적으로도 최적이다”를 증명할 수 있어야 한다.</p>
</blockquote>

<h3 id="예제-a-회의실-배정-활동-선택">예제 A: 회의실 배정 (활동 선택)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MeetingRoom</span> <span class="o">{</span>
    <span class="cm">/**
     * 회의 시간이 겹치지 않게 최대한 많은 회의를 배정한다.
     * 전략: 끝나는 시간 기준으로 정렬 → 가장 빨리 끝나는 회의부터 선택
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">maxMeetings</span><span class="o">(</span><span class="kt">int</span><span class="o">[][]</span> <span class="n">meetings</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// meetings[i] = {시작시간, 종료시간}</span>
        <span class="c1">// 종료 시간 기준 오름차순 정렬</span>
        <span class="nc">Arrays</span><span class="o">.</span><span class="na">sort</span><span class="o">(</span><span class="n">meetings</span><span class="o">,</span> <span class="o">(</span><span class="n">a</span><span class="o">,</span> <span class="n">b</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">a</span><span class="o">[</span><span class="mi">1</span><span class="o">]</span> <span class="o">-</span> <span class="n">b</span><span class="o">[</span><span class="mi">1</span><span class="o">]);</span>

        <span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
        <span class="kt">int</span> <span class="n">lastEnd</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="c1">// 마지막으로 선택한 회의의 종료 시간</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">meeting</span> <span class="o">:</span> <span class="n">meetings</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// 현재 회의의 시작 시간이 이전 회의의 종료 시간 이후이면 선택</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">meeting</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span> <span class="o">&gt;=</span> <span class="n">lastEnd</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">count</span><span class="o">++;</span>
                <span class="n">lastEnd</span> <span class="o">=</span> <span class="n">meeting</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">count</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[][]</span> <span class="n">meetings</span> <span class="o">=</span> <span class="o">{</span>
            <span class="o">{</span><span class="mi">1</span><span class="o">,</span> <span class="mi">4</span><span class="o">},</span> <span class="o">{</span><span class="mi">3</span><span class="o">,</span> <span class="mi">5</span><span class="o">},</span> <span class="o">{</span><span class="mi">0</span><span class="o">,</span> <span class="mi">6</span><span class="o">},</span> <span class="o">{</span><span class="mi">5</span><span class="o">,</span> <span class="mi">7</span><span class="o">},</span> <span class="o">{</span><span class="mi">3</span><span class="o">,</span> <span class="mi">8</span><span class="o">},</span> <span class="o">{</span><span class="mi">5</span><span class="o">,</span> <span class="mi">9</span><span class="o">},</span>
            <span class="o">{</span><span class="mi">6</span><span class="o">,</span> <span class="mi">10</span><span class="o">},</span> <span class="o">{</span><span class="mi">8</span><span class="o">,</span> <span class="mi">11</span><span class="o">},</span> <span class="o">{</span><span class="mi">8</span><span class="o">,</span> <span class="mi">12</span><span class="o">},</span> <span class="o">{</span><span class="mi">2</span><span class="o">,</span> <span class="mi">13</span><span class="o">},</span> <span class="o">{</span><span class="mi">12</span><span class="o">,</span> <span class="mi">14</span><span class="o">}</span>
        <span class="o">};</span>
        <span class="c1">// 입력: 11개 회의</span>
        <span class="c1">// 출력: 4 (예: [1,4], [5,7], [8,11], [12,14])</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">maxMeetings</span><span class="o">(</span><span class="n">meetings</span><span class="o">));</span> <span class="c1">// 4</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-동전-거스름돈-최소-동전-수">예제 B: 동전 거스름돈 (최소 동전 수)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">CoinChange</span> <span class="o">{</span>
    <span class="cm">/**
     * 거스름돈을 최소 동전 수로 만든다.
     * 단, 그리디가 최적해를 보장하는 경우는 동전 단위가
     * 서로 배수 관계일 때뿐이다. (예: 500, 100, 50, 10)
     * 일반적인 경우에는 DP를 사용해야 한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">minCoinsGreedy</span><span class="o">(</span><span class="kt">int</span> <span class="n">amount</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">coins</span> <span class="o">=</span> <span class="o">{</span><span class="mi">500</span><span class="o">,</span> <span class="mi">100</span><span class="o">,</span> <span class="mi">50</span><span class="o">,</span> <span class="mi">10</span><span class="o">};</span> <span class="c1">// 한국 동전 단위</span>
        <span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">coin</span> <span class="o">:</span> <span class="n">coins</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">count</span> <span class="o">+=</span> <span class="n">amount</span> <span class="o">/</span> <span class="n">coin</span><span class="o">;</span> <span class="c1">// 현재 동전으로 낼 수 있는 최대 수</span>
            <span class="n">amount</span> <span class="o">%=</span> <span class="n">coin</span><span class="o">;</span>         <span class="c1">// 나머지</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="o">(</span><span class="n">amount</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">?</span> <span class="n">count</span> <span class="o">:</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span> <span class="c1">// 거슬러 줄 수 없으면 -1</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 입력: 1260원</span>
        <span class="c1">// 출력: 6 (500*2 + 100*2 + 50*1 + 10*1)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">minCoinsGreedy</span><span class="o">(</span><span class="mi">1260</span><span class="o">));</span> <span class="c1">// 6</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-6">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N log N) — 정렬이 지배적</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(1) (인플레이스 정렬 시)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="8-해시맵-활용">8. 해시맵 활용</h2>

<h3 id="어떤-문제에-사용하나-7">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>O(1) 조회</strong>가 필요한 문제</li>
  <li>두 수의 합 (정렬 안 된 배열)</li>
  <li>빈도수 세기, 아나그램 판별, 중복 찾기</li>
  <li>부분합(prefix sum)과 결합</li>
</ul>

<blockquote>
  <p>핵심 아이디어: “이전에 본 값”을 해시맵에 저장해서 현재 값과 짝을 맞춘다.</p>
</blockquote>

<h3 id="예제-a-두-수의-합-정렬-안-된-배열">예제 A: 두 수의 합 (정렬 안 된 배열)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TwoSumHash</span> <span class="o">{</span>
    <span class="cm">/**
     * 배열에서 합이 target인 두 수의 인덱스를 반환한다.
     * 정렬되지 않은 배열에서 O(N)으로 해결.
     *
     * 아이디어: nums[i]를 볼 때, target - nums[i]를 이전에 봤는지 확인
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span><span class="o">[]</span> <span class="nf">twoSum</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span><span class="o">,</span> <span class="kt">int</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 값 → 인덱스 매핑</span>
        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">map</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">nums</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="kt">int</span> <span class="n">complement</span> <span class="o">=</span> <span class="n">target</span> <span class="o">-</span> <span class="n">nums</span><span class="o">[</span><span class="n">i</span><span class="o">];</span> <span class="c1">// 짝이 되는 수</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">map</span><span class="o">.</span><span class="na">containsKey</span><span class="o">(</span><span class="n">complement</span><span class="o">))</span> <span class="o">{</span>
                <span class="k">return</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[]{</span><span class="n">map</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">complement</span><span class="o">),</span> <span class="n">i</span><span class="o">};</span>
            <span class="o">}</span>

            <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">nums</span><span class="o">[</span><span class="n">i</span><span class="o">],</span> <span class="n">i</span><span class="o">);</span> <span class="c1">// 현재 값 저장</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[]{-</span><span class="mi">1</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">};</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span> <span class="o">=</span> <span class="o">{</span><span class="mi">2</span><span class="o">,</span> <span class="mi">7</span><span class="o">,</span> <span class="mi">11</span><span class="o">,</span> <span class="mi">15</span><span class="o">};</span>
        <span class="kt">int</span> <span class="n">target</span> <span class="o">=</span> <span class="mi">9</span><span class="o">;</span>
        <span class="c1">// 입력: nums = [2,7,11,15], target = 9</span>
        <span class="c1">// 출력: [0, 1]  (nums[0] + nums[1] = 2 + 7 = 9)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="n">twoSum</span><span class="o">(</span><span class="n">nums</span><span class="o">,</span> <span class="n">target</span><span class="o">)));</span> <span class="c1">// [0, 1]</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-아나그램-판별">예제 B: 아나그램 판별</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Anagram</span> <span class="o">{</span>
    <span class="cm">/**
     * 두 문자열이 아나그램인지 판별한다.
     * 아나그램: 같은 문자를 재배열해서 만들 수 있는 문자열
     *
     * 방법: 각 문자의 빈도수가 동일한지 비교
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">isAnagram</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">,</span> <span class="nc">String</span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">s</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="na">length</span><span class="o">())</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>

        <span class="c1">// 알파벳 소문자만 있다면 int[26] 배열이 더 빠름</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">count</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">26</span><span class="o">];</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">s</span><span class="o">.</span><span class="na">length</span><span class="o">();</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="n">count</span><span class="o">[</span><span class="n">s</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">i</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">]++;</span> <span class="c1">// s의 문자는 +1</span>
            <span class="n">count</span><span class="o">[</span><span class="n">t</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">i</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">]--;</span> <span class="c1">// t의 문자는 -1</span>
        <span class="o">}</span>

        <span class="c1">// 모든 카운트가 0이면 아나그램</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">c</span> <span class="o">:</span> <span class="n">count</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">c</span> <span class="o">!=</span> <span class="mi">0</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 입력: "anagram", "nagaram"</span>
        <span class="c1">// 출력: true</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">isAnagram</span><span class="o">(</span><span class="s">"anagram"</span><span class="o">,</span> <span class="s">"nagaram"</span><span class="o">));</span> <span class="c1">// true</span>

        <span class="c1">// 입력: "rat", "car"</span>
        <span class="c1">// 출력: false</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">isAnagram</span><span class="o">(</span><span class="s">"rat"</span><span class="o">,</span> <span class="s">"car"</span><span class="o">));</span> <span class="c1">// false</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-7">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(N) — 해시맵 크기</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="9-스택큐-활용">9. 스택/큐 활용</h2>

<h3 id="어떤-문제에-사용하나-8">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>괄호 매칭</strong>: 유효한 괄호, 수식 계산</li>
  <li><strong>단조 스택(Monotone Stack)</strong>: 다음 큰 원소, 주식 가격 스팬</li>
  <li><strong>큐</strong>: BFS (4번 참고), 캐시 구현 (LRU)</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 스택은 “가장 최근 것부터 처리”, 큐는 “가장 오래된 것부터 처리”.</p>
</blockquote>

<h3 id="예제-a-유효한-괄호">예제 A: 유효한 괄호</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ValidParentheses</span> <span class="o">{</span>
    <span class="cm">/**
     * 괄호 문자열이 올바르게 짝지어져 있는지 확인한다.
     * '(', ')', '{', '}', '[', ']' 만 포함.
     *
     * 전략: 여는 괄호 → 스택에 push, 닫는 괄호 → pop해서 짝 확인
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">isValid</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Deque</span><span class="o">&lt;</span><span class="nc">Character</span><span class="o">&gt;</span> <span class="n">stack</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayDeque</span><span class="o">&lt;&gt;();</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">char</span> <span class="n">c</span> <span class="o">:</span> <span class="n">s</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">())</span> <span class="o">{</span>
            <span class="c1">// 여는 괄호면 대응하는 닫는 괄호를 push</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'('</span><span class="o">)</span> <span class="n">stack</span><span class="o">.</span><span class="na">push</span><span class="o">(</span><span class="sc">')'</span><span class="o">);</span>
            <span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'{'</span><span class="o">)</span> <span class="n">stack</span><span class="o">.</span><span class="na">push</span><span class="o">(</span><span class="sc">'}'</span><span class="o">);</span>
            <span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'['</span><span class="o">)</span> <span class="n">stack</span><span class="o">.</span><span class="na">push</span><span class="o">(</span><span class="sc">']'</span><span class="o">);</span>
            <span class="k">else</span> <span class="o">{</span>
                <span class="c1">// 닫는 괄호인데 스택이 비었거나 짝이 안 맞으면 false</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">stack</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">()</span> <span class="o">||</span> <span class="n">stack</span><span class="o">.</span><span class="na">pop</span><span class="o">()</span> <span class="o">!=</span> <span class="n">c</span><span class="o">)</span> <span class="o">{</span>
                    <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">stack</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">();</span> <span class="c1">// 스택이 비어야 모든 괄호가 매칭됨</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 입력: "()[]{}"  → 출력: true</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">isValid</span><span class="o">(</span><span class="s">"()[]{}"</span><span class="o">));</span> <span class="c1">// true</span>

        <span class="c1">// 입력: "(]"  → 출력: false</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">isValid</span><span class="o">(</span><span class="s">"(]"</span><span class="o">));</span> <span class="c1">// false</span>

        <span class="c1">// 입력: "{[]}"  → 출력: true</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">isValid</span><span class="o">(</span><span class="s">"{[]}"</span><span class="o">));</span> <span class="c1">// true</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-주식-가격-스팬-단조-스택">예제 B: 주식 가격 스팬 (단조 스택)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">StockSpan</span> <span class="o">{</span>
    <span class="cm">/**
     * 각 날의 주가 스팬을 계산한다.
     * 스팬 = 현재 날로부터 연속으로 현재 가격 이하인 날의 수 (당일 포함)
     *
     * 단조 스택 사용: 스택에 인덱스를 저장하고,
     * 현재 가격보다 작거나 같은 가격의 인덱스는 pop한다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span><span class="o">[]</span> <span class="nf">calculateSpan</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">prices</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="n">prices</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">span</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">n</span><span class="o">];</span>
        <span class="nc">Deque</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">stack</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayDeque</span><span class="o">&lt;&gt;();</span> <span class="c1">// 인덱스 저장</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="c1">// 현재 가격 이하인 이전 날들을 모두 pop</span>
            <span class="k">while</span> <span class="o">(!</span><span class="n">stack</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="n">prices</span><span class="o">[</span><span class="n">stack</span><span class="o">.</span><span class="na">peek</span><span class="o">()]</span> <span class="o">&lt;=</span> <span class="n">prices</span><span class="o">[</span><span class="n">i</span><span class="o">])</span> <span class="o">{</span>
                <span class="n">stack</span><span class="o">.</span><span class="na">pop</span><span class="o">();</span>
            <span class="o">}</span>

            <span class="c1">// 스택이 비었으면 처음부터 현재까지 전부 스팬</span>
            <span class="n">span</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="n">stack</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">()</span> <span class="o">?</span> <span class="o">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span> <span class="o">:</span> <span class="o">(</span><span class="n">i</span> <span class="o">-</span> <span class="n">stack</span><span class="o">.</span><span class="na">peek</span><span class="o">());</span>
            <span class="n">stack</span><span class="o">.</span><span class="na">push</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">span</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">prices</span> <span class="o">=</span> <span class="o">{</span><span class="mi">100</span><span class="o">,</span> <span class="mi">80</span><span class="o">,</span> <span class="mi">60</span><span class="o">,</span> <span class="mi">70</span><span class="o">,</span> <span class="mi">60</span><span class="o">,</span> <span class="mi">75</span><span class="o">,</span> <span class="mi">85</span><span class="o">};</span>
        <span class="c1">// 입력: [100, 80, 60, 70, 60, 75, 85]</span>
        <span class="c1">// 출력: [1, 1, 1, 2, 1, 4, 6]</span>
        <span class="c1">//   100 → 1 (자기 자신)</span>
        <span class="c1">//    80 → 1</span>
        <span class="c1">//    60 → 1</span>
        <span class="c1">//    70 → 2 (70, 60)</span>
        <span class="c1">//    60 → 1</span>
        <span class="c1">//    75 → 4 (75, 60, 70, 60)</span>
        <span class="c1">//    85 → 6 (85, 75, 60, 70, 60, 80)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="n">calculateSpan</span><span class="o">(</span><span class="n">prices</span><span class="o">)));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-8">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N) — 각 원소는 최대 1번 push, 1번 pop</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(N) — 스택 크기</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="10-힙우선순위-큐">10. 힙/우선순위 큐</h2>

<h3 id="어떤-문제에-사용하나-9">어떤 문제에 사용하나</h3>

<ul>
  <li><strong>K번째 크기</strong> 문제 (K번째 큰 수, K개 가장 빈번한 원소)</li>
  <li><strong>스트림에서 중앙값</strong> 유지</li>
  <li><strong>다익스트라</strong> 최단 경로 (가중치 그래프)</li>
  <li>우선순위가 있는 작업 스케줄링</li>
</ul>

<blockquote>
  <p>핵심 아이디어: 삽입/삭제가 O(log N)이면서 최소/최대를 O(1)에 조회한다.</p>
</blockquote>

<h3 id="java-힙-기본-사용법">Java 힙 기본 사용법</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 최소 힙 (기본)</span>
<span class="nc">PriorityQueue</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">minHeap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PriorityQueue</span><span class="o">&lt;&gt;();</span>

<span class="c1">// 최대 힙</span>
<span class="nc">PriorityQueue</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">maxHeap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PriorityQueue</span><span class="o">&lt;&gt;(</span><span class="nc">Collections</span><span class="o">.</span><span class="na">reverseOrder</span><span class="o">());</span>

<span class="n">minHeap</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="mi">5</span><span class="o">);</span>    <span class="c1">// 삽입: O(log N)</span>
<span class="n">minHeap</span><span class="o">.</span><span class="na">peek</span><span class="o">();</span>      <span class="c1">// 최솟값 조회: O(1)</span>
<span class="n">minHeap</span><span class="o">.</span><span class="na">poll</span><span class="o">();</span>      <span class="c1">// 최솟값 추출: O(log N)</span>
</code></pre></div></div>

<h3 id="예제-a-k번째-큰-수">예제 A: K번째 큰 수</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">KthLargest</span> <span class="o">{</span>
    <span class="cm">/**
     * 배열에서 K번째로 큰 수를 반환한다.
     *
     * 전략: 크기 K인 최소 힙을 유지한다.
     * 힙에 K개 초과 원소가 들어오면 가장 작은 것을 빼낸다.
     * 최종적으로 힙의 루트(최솟값)가 K번째로 큰 수이다.
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">findKthLargest</span><span class="o">(</span><span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span><span class="o">,</span> <span class="kt">int</span> <span class="n">k</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">PriorityQueue</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">minHeap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PriorityQueue</span><span class="o">&lt;&gt;();</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">num</span> <span class="o">:</span> <span class="n">nums</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">minHeap</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="n">num</span><span class="o">);</span>

            <span class="c1">// 힙 크기가 k를 초과하면 가장 작은 수 제거</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">minHeap</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">&gt;</span> <span class="n">k</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">minHeap</span><span class="o">.</span><span class="na">poll</span><span class="o">();</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">minHeap</span><span class="o">.</span><span class="na">peek</span><span class="o">();</span> <span class="c1">// k개 중 가장 작은 수 = 전체에서 k번째 큰 수</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span><span class="o">[]</span> <span class="n">nums</span> <span class="o">=</span> <span class="o">{</span><span class="mi">3</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">5</span><span class="o">,</span> <span class="mi">6</span><span class="o">,</span> <span class="mi">4</span><span class="o">};</span>
        <span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">2</span><span class="o">;</span>
        <span class="c1">// 입력: nums = [3,2,1,5,6,4], k = 2</span>
        <span class="c1">// 출력: 5  (정렬하면 [1,2,3,4,5,6], 2번째로 큰 수 = 5)</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">findKthLargest</span><span class="o">(</span><span class="n">nums</span><span class="o">,</span> <span class="n">k</span><span class="o">));</span> <span class="c1">// 5</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예제-b-데이터-스트림의-중앙값">예제 B: 데이터 스트림의 중앙값</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.*</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MedianFinder</span> <span class="o">{</span>
    <span class="c1">// 왼쪽 절반: 최대 힙 (작은 수들의 최댓값을 빠르게 접근)</span>
    <span class="kd">private</span> <span class="nc">PriorityQueue</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">maxHeap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PriorityQueue</span><span class="o">&lt;&gt;(</span><span class="nc">Collections</span><span class="o">.</span><span class="na">reverseOrder</span><span class="o">());</span>
    <span class="c1">// 오른쪽 절반: 최소 힙 (큰 수들의 최솟값을 빠르게 접근)</span>
    <span class="kd">private</span> <span class="nc">PriorityQueue</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">minHeap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PriorityQueue</span><span class="o">&lt;&gt;();</span>

    <span class="cm">/**
     * 새로운 수를 추가한다.
     *
     * 불변식:
     * 1. maxHeap.size() == minHeap.size() 또는 maxHeap.size() == minHeap.size() + 1
     * 2. maxHeap의 모든 수 &lt;= minHeap의 모든 수
     */</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">addNum</span><span class="o">(</span><span class="kt">int</span> <span class="n">num</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">maxHeap</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="n">num</span><span class="o">);</span> <span class="c1">// 일단 왼쪽에 추가</span>

        <span class="c1">// 왼쪽 최대가 오른쪽 최소보다 크면 밸런싱</span>
        <span class="n">minHeap</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="n">maxHeap</span><span class="o">.</span><span class="na">poll</span><span class="o">());</span>

        <span class="c1">// 크기 조정: 왼쪽이 오른쪽보다 같거나 1개 더 많아야 함</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">minHeap</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">&gt;</span> <span class="n">maxHeap</span><span class="o">.</span><span class="na">size</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">maxHeap</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="n">minHeap</span><span class="o">.</span><span class="na">poll</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="cm">/**
     * 현재까지의 중앙값을 반환한다.
     */</span>
    <span class="kd">public</span> <span class="kt">double</span> <span class="nf">findMedian</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">maxHeap</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">&gt;</span> <span class="n">minHeap</span><span class="o">.</span><span class="na">size</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">maxHeap</span><span class="o">.</span><span class="na">peek</span><span class="o">();</span> <span class="c1">// 홀수 개: 왼쪽 힙의 루트</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="o">(</span><span class="n">maxHeap</span><span class="o">.</span><span class="na">peek</span><span class="o">()</span> <span class="o">+</span> <span class="n">minHeap</span><span class="o">.</span><span class="na">peek</span><span class="o">())</span> <span class="o">/</span> <span class="mf">2.0</span><span class="o">;</span> <span class="c1">// 짝수 개: 두 힙의 루트 평균</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">MedianFinder</span> <span class="n">mf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MedianFinder</span><span class="o">();</span>

        <span class="n">mf</span><span class="o">.</span><span class="na">addNum</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
        <span class="c1">// 중앙값: 1.0</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">mf</span><span class="o">.</span><span class="na">findMedian</span><span class="o">());</span> <span class="c1">// 1.0</span>

        <span class="n">mf</span><span class="o">.</span><span class="na">addNum</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
        <span class="c1">// 중앙값: (1 + 2) / 2 = 1.5</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">mf</span><span class="o">.</span><span class="na">findMedian</span><span class="o">());</span> <span class="c1">// 1.5</span>

        <span class="n">mf</span><span class="o">.</span><span class="na">addNum</span><span class="o">(</span><span class="mi">3</span><span class="o">);</span>
        <span class="c1">// 중앙값: 2.0</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">mf</span><span class="o">.</span><span class="na">findMedian</span><span class="o">());</span> <span class="c1">// 2.0</span>

        <span class="n">mf</span><span class="o">.</span><span class="na">addNum</span><span class="o">(</span><span class="mi">4</span><span class="o">);</span>
        <span class="c1">// 중앙값: (2 + 3) / 2 = 2.5</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">mf</span><span class="o">.</span><span class="na">findMedian</span><span class="o">());</span> <span class="c1">// 2.5</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="복잡도-9">복잡도</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>K번째 큰 수</th>
      <th>데이터 스트림 중앙값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시간</td>
      <td>O(N log K)</td>
      <td>addNum O(log N), findMedian O(1)</td>
    </tr>
    <tr>
      <td>공간</td>
      <td>O(K)</td>
      <td>O(N)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="빠른-참조표">빠른 참조표</h2>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>패턴</th>
      <th>핵심 자료구조</th>
      <th>시간 복잡도</th>
      <th>사용 신호</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>투 포인터</td>
      <td>배열</td>
      <td>O(N)</td>
      <td>정렬된 배열, 쌍 찾기</td>
    </tr>
    <tr>
      <td>2</td>
      <td>슬라이딩 윈도우</td>
      <td>배열 + Set/Map</td>
      <td>O(N)</td>
      <td>연속 부분 배열/문자열</td>
    </tr>
    <tr>
      <td>3</td>
      <td>이진 탐색</td>
      <td>정렬 배열</td>
      <td>O(log N)</td>
      <td>정렬 + 탐색, 단조 조건</td>
    </tr>
    <tr>
      <td>4</td>
      <td>BFS</td>
      <td>큐</td>
      <td>O(V+E)</td>
      <td>최단 거리, 레벨 탐색</td>
    </tr>
    <tr>
      <td>5</td>
      <td>DFS + 백트래킹</td>
      <td>재귀 + 스택</td>
      <td>O(2^N) or O(N!)</td>
      <td>모든 경우의 수, 연결 요소</td>
    </tr>
    <tr>
      <td>6</td>
      <td>DP</td>
      <td>배열</td>
      <td>문제마다 다름</td>
      <td>최적 부분 구조 + 중복</td>
    </tr>
    <tr>
      <td>7</td>
      <td>그리디</td>
      <td>정렬</td>
      <td>O(N log N)</td>
      <td>현재 최선 = 전체 최선</td>
    </tr>
    <tr>
      <td>8</td>
      <td>해시맵</td>
      <td>HashMap</td>
      <td>O(N)</td>
      <td>O(1) 조회 필요, 빈도수</td>
    </tr>
    <tr>
      <td>9</td>
      <td>스택/큐</td>
      <td>Stack/Deque</td>
      <td>O(N)</td>
      <td>짝 매칭, 단조 스택</td>
    </tr>
    <tr>
      <td>10</td>
      <td>힙</td>
      <td>PriorityQueue</td>
      <td>O(N log K)</td>
      <td>K번째, 중앙값, 스케줄링</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="문제-유형별-패턴-선택-가이드">문제 유형별 패턴 선택 가이드</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"두 수의 합" 류
  └─ 정렬됨? → 투 포인터 (1)
  └─ 정렬 안 됨? → 해시맵 (8)

"연속 부분 배열/문자열" 류
  └─ 고정 크기? → 슬라이딩 윈도우 (2)
  └─ 가변 크기? → 슬라이딩 윈도우 (2) + 투 포인터 (1)

"최단 거리" 류
  └─ 가중치 동일? → BFS (4)
  └─ 가중치 다름? → 다익스트라 = BFS + 힙 (10)

"모든 경우의 수" 류
  └─ 순서 중요(순열)? → DFS + 백트래킹 (5)
  └─ 순서 무관(조합)? → DFS + 백트래킹 (5)
  └─ 경우의 수만 세기? → DP (6)

"최소/최대 비용" 류
  └─ 최적 부분 구조? → DP (6)
  └─ 현재 최선 = 전체 최선 증명 가능? → 그리디 (7)

"K번째" 류
  └─ → 힙 (10)

"괄호/짝 매칭" 류
  └─ → 스택 (9)

"정렬 배열에서 위치 찾기" 류
  └─ → 이진 탐색 (3)
</code></pre></div></div>]]></content><author><name>푸른영혼의 별</name></author><category term="algorithm" /><category term="코딩테스트" /><category term="java" /><category term="알고리즘" /><category term="자료구조" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">ASAT — Web Audio API로 ±5ms 정밀도의 청각 재활 훈련 시스템 만들기</title><link href="https://myoungsoo7.github.io/2026/05/04/asat-audio-training/" rel="alternate" type="text/html" title="ASAT — Web Audio API로 ±5ms 정밀도의 청각 재활 훈련 시스템 만들기" /><published>2026-05-04T05:00:00+00:00</published><updated>2026-05-04T05:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/asat-audio-training</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/asat-audio-training/"><![CDATA[<h2 id="프로젝트-소개">프로젝트 소개</h2>

<p>이명 환자·청각 재활 대상자를 위한 연구용 웹 애플리케이션입니다. Web Audio API 정밀 타이밍(±5ms) + 적응형 staircase 알고리즘 + 데이터 신뢰도 등급화(A/B/C/F)까지 고려한 임상 연구 지향 훈련 시스템입니다.</p>

<p><strong>Live</strong>: <a href="https://eln.lemuel.co.kr">eln.lemuel.co.kr</a></p>

<h2 id="왜-만들었는가">왜 만들었는가</h2>

<ul>
  <li>청각 재활 훈련은 “자극을 들려주는 것”이 아니라 <strong>밀리초 단위 타이밍·개인별 적응형 난이도·데이터 품질 보증</strong>이 전제되어야 함</li>
  <li>고정 난이도 훈련은 개인별 JND(최소변별차)를 측정할 수 없음 → 적응형 staircase 필수</li>
  <li>연구 데이터로 쓰려면 “reversal 몇 번 만에 수렴했는가, 정답률이 우연 수준인가, 헤드폰을 제대로 썼는가”까지 판정해야 함</li>
</ul>

<h2 id="핵심-설계-포인트">핵심 설계 포인트</h2>

<h3 id="1-적응형-알고리즘-2-down-1-up-staircase">1. 적응형 알고리즘 (2-down 1-up Staircase)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2연속 정답 → 난이도 UP (step down)
1 오답 → 난이도 DOWN (step up)
→ 70.7% 정답률에 수렴 = 청각 역치
</code></pre></div></div>

<ul>
  <li>TrainingSession / TrainingTrial / AdaptiveAlgorithmState를 별도 엔티티로 분리</li>
  <li>reversal 12회 도달 시 세션 자동 완료, 마지막 8개 reversal의 기하평균으로 JND 산출</li>
</ul>

<h3 id="2-동시성-제어-optimistic-lock">2. 동시성 제어 (Optimistic Lock)</h3>

<p>환불 도메인의 <code class="language-plaintext highlighter-rouge">PESSIMISTIC_WRITE</code>와 달리, 짧은 trial 트랜잭션 + 충돌 희귀 특성에 맞춰 <strong>낙관적 락</strong>을 선택했습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Version</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">version</span><span class="o">;</span>
<span class="c1">// 동일 세션에 동시 trial 기록 시 OptimisticLockException으로 차단</span>
</code></pre></div></div>

<h3 id="3-데이터-신뢰도-등급화-abcf">3. 데이터 신뢰도 등급화 (A/B/C/F)</h3>

<table>
  <thead>
    <tr>
      <th>등급</th>
      <th>조건</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>A</strong></td>
      <td>reversal ≥ 8 + 정답률 60~85% + 헤드폰 확인</td>
    </tr>
    <tr>
      <td><strong>B</strong></td>
      <td>fallback (A도 C도 아닌 경우)</td>
    </tr>
    <tr>
      <td><strong>C</strong></td>
      <td>reversal &lt; 8 / 정답률 비정상 / floor·ceiling 도달</td>
    </tr>
    <tr>
      <td><strong>F</strong></td>
      <td>reversal 0회 (데이터 무효)</td>
    </tr>
  </tbody>
</table>

<h3 id="4-환경-검증-calibration-선행-강제">4. 환경 검증 (Calibration) 선행 강제</h3>

<p>훈련 전 헤드폰 확인 + L/R 채널 테스트 + 볼륨·오디오 지연 측정을 <code class="language-plaintext highlighter-rouge">CalibrationRecord</code>로 영속화. <strong>“캘리브레이션 없이는 훈련 불가”</strong> 정책.</p>

<h2 id="아키텍처">아키텍처</h2>

<ul>
  <li>Backend: Spring Boot 4.0 / Java 25 / Hexagonal</li>
  <li>Frontend: Next.js 16 / React 19 / TypeScript 5</li>
  <li>Audio: Web Audio API (<code class="language-plaintext highlighter-rouge">AudioContext.currentTime</code> 스케줄링)</li>
  <li>DB: PostgreSQL 16 / Flyway V1~V36</li>
  <li>Cache: Redis</li>
  <li>Test: JUnit 5 + TestContainers</li>
</ul>

<h2 id="교훈">교훈</h2>

<blockquote>
  <p>측정 도메인은 “타이밍 정밀도 + 재현성”이 핵심 — 500ms 재생과 ±5ms RT 측정이 무너지면 JND는 쓰레기값</p>
</blockquote>

<blockquote>
  <p>데이터 품질 등급은 옵션이 아니라 필수 — 정산의 “CONFIRMED”처럼, 연구 데이터도 “A등급”이어야 논문에 쓸 수 있다</p>
</blockquote>]]></content><author><name>푸른영혼의 별</name></author><category term="project" /><category term="web-audio" /><category term="spring-boot" /><category term="staircase-algorithm" /><category term="hexagonal" /><summary type="html"><![CDATA[프로젝트 소개]]></summary></entry><entry><title type="html">Spring AI + RAG — 상품 데이터를 자연어로 검색하기</title><link href="https://myoungsoo7.github.io/2026/05/04/spring-ai-rag-pipeline/" rel="alternate" type="text/html" title="Spring AI + RAG — 상품 데이터를 자연어로 검색하기" /><published>2026-05-04T04:00:00+00:00</published><updated>2026-05-04T04:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/spring-ai-rag-pipeline</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/spring-ai-rag-pipeline/"><![CDATA[<h2 id="ragretrieval-augmented-generation란">RAG(Retrieval-Augmented Generation)란?</h2>

<p>LLM이 학습하지 않은 <strong>내부 데이터</strong>를 검색해서 답변에 활용하는 패턴입니다. 이커머스 상품 데이터를 자연어로 검색할 수 있게 만들었습니다.</p>

<p><strong>Live</strong>: <a href="https://chat.lemuel.co.kr">chat.lemuel.co.kr</a></p>

<h2 id="아키텍처">아키텍처</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>사용자 질문 → Intent 분석 → Vector 유사도 검색 (pgvector)
                                    ↓
                           관련 문서 추출 (Top-K)
                                    ↓
                    프롬프트 = 시스템 지시 + 문서 컨텍스트 + 질문
                                    ↓
                         Gemini LLM → 답변 생성
</code></pre></div></div>

<h2 id="핵심-구현">핵심 구현</h2>

<h3 id="1-벡터-임베딩">1. 벡터 임베딩</h3>

<p>상품 등록 시 이벤트 리스너가 자동으로 임베딩을 생성합니다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@EventListener</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onProductCreated</span><span class="o">(</span><span class="nc">ProductEmbeddingEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">text</span> <span class="o">=</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"%s %s 카테고리:%s 가격:%d"</span><span class="o">,</span>
        <span class="n">product</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">product</span><span class="o">.</span><span class="na">getDescription</span><span class="o">(),</span>
        <span class="n">product</span><span class="o">.</span><span class="na">getCategory</span><span class="o">().</span><span class="na">getName</span><span class="o">(),</span> <span class="n">product</span><span class="o">.</span><span class="na">getPrice</span><span class="o">());</span>
    
    <span class="nc">Document</span> <span class="n">doc</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Document</span><span class="o">(</span><span class="n">text</span><span class="o">,</span> <span class="n">metadata</span><span class="o">);</span>
    <span class="n">vectorStore</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">doc</span><span class="o">));</span>  <span class="c1">// pgvector에 저장</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="2-rag-검색--답변-생성">2. RAG 검색 + 답변 생성</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">AnswerResponse</span> <span class="nf">ask</span><span class="o">(</span><span class="nc">String</span> <span class="n">question</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 1. 벡터 유사도 검색</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Document</span><span class="o">&gt;</span> <span class="n">relevantDocs</span> <span class="o">=</span> <span class="n">vectorStore</span><span class="o">.</span><span class="na">similaritySearch</span><span class="o">(</span>
        <span class="nc">SearchRequest</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="n">question</span><span class="o">)</span>
            <span class="o">.</span><span class="na">topK</span><span class="o">(</span><span class="mi">5</span><span class="o">)</span>
            <span class="o">.</span><span class="na">similarityThreshold</span><span class="o">(</span><span class="mf">0.0</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">()</span>
    <span class="o">);</span>
    
    <span class="c1">// 2. 프롬프트 구성 (문서 + 질문)</span>
    <span class="nc">String</span> <span class="n">prompt</span> <span class="o">=</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="no">RAG_PROMPT_TEMPLATE</span><span class="o">,</span> 
        <span class="n">docsToText</span><span class="o">(</span><span class="n">relevantDocs</span><span class="o">),</span> <span class="n">question</span><span class="o">);</span>
    
    <span class="c1">// 3. LLM 답변 생성</span>
    <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">().</span><span class="na">user</span><span class="o">(</span><span class="n">prompt</span><span class="o">).</span><span class="na">call</span><span class="o">().</span><span class="na">content</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="3-intent-분석">3. Intent 분석</h3>

<p>자연어 질문의 의도를 분류하여 검색 전략을 최적화합니다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// "3만원 이하 가방" → PRICE_RANGE + CATEGORY</span>
<span class="c1">// "인기 있는 상품" → POPULARITY</span>
<span class="c1">// "빨간색 티셔츠" → COLOR + CATEGORY</span>
</code></pre></div></div>

<h2 id="기술-스택">기술 스택</h2>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>기술</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LLM</td>
      <td>Google Gemini (Spring AI)</td>
    </tr>
    <tr>
      <td>Vector DB</td>
      <td>PostgreSQL + pgvector</td>
    </tr>
    <tr>
      <td>Embedding</td>
      <td>Spring AI EmbeddingModel</td>
    </tr>
    <tr>
      <td>Framework</td>
      <td>Spring Boot 4.0</td>
    </tr>
  </tbody>
</table>

<h2 id="배운-점">배운 점</h2>

<ul>
  <li>RAG의 품질은 <strong>임베딩 텍스트 구성</strong>에 크게 좌우됨 — 상품명+설명+카테고리+가격을 포함해야 정확도 향상</li>
  <li>Top-K와 similarity threshold 조합이 중요 — 너무 엄격하면 결과 없음, 너무 느슨하면 노이즈</li>
  <li>프롬프트에 “문서에 없는 내용은 답변하지 마세요”를 명시해야 환각(hallucination) 방지</li>
</ul>]]></content><author><name>푸른영혼의 별</name></author><category term="ai" /><category term="spring" /><category term="spring-ai" /><category term="rag" /><category term="gemini" /><category term="pgvector" /><category term="embedding" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Spring AI Function Calling — LLM에게 도구를 쥐여주기</title><link href="https://myoungsoo7.github.io/2026/05/04/spring-ai-function-calling/" rel="alternate" type="text/html" title="Spring AI Function Calling — LLM에게 도구를 쥐여주기" /><published>2026-05-04T03:00:00+00:00</published><updated>2026-05-04T03:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/spring-ai-function-calling</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/spring-ai-function-calling/"><![CDATA[<h2 id="function-calling이란">Function Calling이란?</h2>

<p>LLM이 <strong>외부 함수를 직접 호출</strong>하여 실시간 정보를 가져오는 패턴입니다. 날씨, 계산, 현재 시간 등 LLM이 자체적으로 알 수 없는 정보를 도구를 통해 해결합니다.</p>

<h2 id="구현">구현</h2>

<h3 id="1-도구-정의">1. 도구 정의</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FunctionTools</span> <span class="o">{</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="s">"현재 날씨를 조회합니다"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">WeatherResponse</span> <span class="nf">getWeather</span><span class="o">(</span><span class="nc">WeatherRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 외부 API 호출</span>
        <span class="k">return</span> <span class="n">weatherClient</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getCity</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="s">"수학 계산을 수행합니다"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">CalculatorResponse</span> <span class="nf">calculate</span><span class="o">(</span><span class="nc">CalculatorRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">switch</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getOperation</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">case</span> <span class="no">ADD</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">CalculatorResponse</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getA</span><span class="o">()</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="na">getB</span><span class="o">());</span>
            <span class="k">case</span> <span class="no">MULTIPLY</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">CalculatorResponse</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getA</span><span class="o">()</span> <span class="o">*</span> <span class="n">request</span><span class="o">.</span><span class="na">getB</span><span class="o">());</span>
            <span class="c1">// ...</span>
        <span class="o">};</span>
    <span class="o">}</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="s">"현재 시간을 반환합니다"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">CurrentTimeResponse</span> <span class="nf">getCurrentTime</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">CurrentTimeResponse</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="2-llm에-도구-연결">2. LLM에 도구 연결</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">String</span> <span class="nf">chat</span><span class="o">(</span><span class="nc">String</span> <span class="n">userMessage</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">()</span>
        <span class="o">.</span><span class="na">user</span><span class="o">(</span><span class="n">userMessage</span><span class="o">)</span>
        <span class="o">.</span><span class="na">tools</span><span class="o">(</span><span class="n">functionTools</span><span class="o">)</span>  <span class="c1">// 도구 등록</span>
        <span class="o">.</span><span class="na">call</span><span class="o">()</span>
        <span class="o">.</span><span class="na">content</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="3-동작-흐름">3. 동작 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>사용자: "서울 날씨 어때?"
    ↓
LLM 판단: getWeather 호출 필요
    ↓
LLM → getWeather({city: "서울"}) 호출
    ↓
함수 실행 → {temp: 22, condition: "맑음"} 반환
    ↓
LLM: "서울은 현재 22도이고 맑은 날씨입니다."
</code></pre></div></div>

<p>LLM이 <strong>언제 어떤 도구를 쓸지 자율적으로 판단</strong>합니다. 개발자는 도구만 정의하면 됩니다.</p>

<h2 id="면접에서-이렇게-말하면-됩니다">면접에서 이렇게 말하면 됩니다</h2>

<blockquote>
  <p>“Function Calling은 LLM의 한계(실시간 데이터, 계산)를 외부 함수로 보완하는 패턴입니다. Spring AI의 <code class="language-plaintext highlighter-rouge">@Tool</code> 어노테이션으로 함수를 정의하면, LLM이 자율적으로 필요한 도구를 선택하여 호출합니다.”</p>
</blockquote>]]></content><author><name>푸른영혼의 별</name></author><category term="ai" /><category term="spring" /><category term="spring-ai" /><category term="function-calling" /><category term="gemini" /><category term="tools" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">AI Agent 패턴 비교 — ReAct, Plan&amp;amp;Execute, Self-Reflection</title><link href="https://myoungsoo7.github.io/2026/05/04/ai-agent-patterns/" rel="alternate" type="text/html" title="AI Agent 패턴 비교 — ReAct, Plan&amp;amp;Execute, Self-Reflection" /><published>2026-05-04T02:00:00+00:00</published><updated>2026-05-04T02:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/ai-agent-patterns</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/ai-agent-patterns/"><![CDATA[<h2 id="ai-agent란">AI Agent란?</h2>

<p>단순 질의응답을 넘어 <strong>목표를 달성하기 위해 자율적으로 도구를 사용하고, 판단하고, 행동하는 LLM 시스템</strong>입니다.</p>

<p>이커머스 고객 지원 AI Agent를 Strategy 패턴으로 구현하여 4가지 전략을 상황에 맞게 선택합니다.</p>

<h2 id="4가지-agent-전략">4가지 Agent 전략</h2>

<h3 id="1-react-reasoning--acting">1. ReAct (Reasoning + Acting)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Thought → Action → Observation 반복
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ReActStrategy</span> <span class="kd">implements</span> <span class="nc">AgentStrategy</span> <span class="o">{</span>
    
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">SYSTEM_PROMPT</span> <span class="o">=</span> <span class="s">"""
        당신은 자율적인 AI 에이전트입니다.
        사용자의 목표를 달성하기 위해 제공된 도구를 직접 호출하세요.
        도구 호출 결과를 바탕으로 최종 답변을 작성하세요.
        """</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">StrategyType</span> <span class="nf">type</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">StrategyType</span><span class="o">.</span><span class="na">REACT</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">execute</span><span class="o">(</span><span class="nc">String</span> <span class="n">goal</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">()</span>
            <span class="o">.</span><span class="na">system</span><span class="o">(</span><span class="no">SYSTEM_PROMPT</span><span class="o">)</span>
            <span class="o">.</span><span class="na">user</span><span class="o">(</span><span class="n">goal</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tools</span><span class="o">(</span><span class="n">functionTools</span><span class="o">)</span>
            <span class="o">.</span><span class="na">call</span><span class="o">().</span><span class="na">content</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="2-plan--execute">2. Plan &amp; Execute</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>전체 계획 수립 → 단계별 실행 → 결과 종합
</code></pre></div></div>

<p>복잡한 작업(주문 조회 → 환불 → 쿠폰 발급)을 먼저 계획한 후 순차 실행.</p>

<h3 id="3-self-reflection">3. Self-Reflection</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>실행 → 결과 평가 → 부족하면 재시도
</code></pre></div></div>

<p>답변 품질을 스스로 평가하고 개선.</p>

<h3 id="4-limited-tools">4. Limited Tools</h3>

<p>도구 제한 모드 — 특정 상황에서 위험한 도구(환불, 삭제)를 제외.</p>

<h2 id="strategy-패턴-적용">Strategy 패턴 적용</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AgentStrategyFactory</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">StrategyType</span><span class="o">,</span> <span class="nc">AgentStrategy</span><span class="o">&gt;</span> <span class="n">strategies</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="nc">AgentStrategy</span> <span class="nf">getStrategy</span><span class="o">(</span><span class="nc">StrategyType</span> <span class="n">type</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">strategies</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">type</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>상황에 따라 전략을 동적으로 선택:</p>
<ul>
  <li>단순 질문 → <strong>ReAct</strong> (빠른 응답)</li>
  <li>복잡한 요청 → <strong>Plan &amp; Execute</strong> (정확한 실행)</li>
  <li>중요한 작업 → <strong>Self-Reflection</strong> (품질 보장)</li>
  <li>제한 환경 → <strong>Limited Tools</strong> (안전성)</li>
</ul>

<h2 id="고객-지원-ai의-추가-기능">고객 지원 AI의 추가 기능</h2>

<ul>
  <li><strong>감성 분석</strong>: 고객 메시지에서 감정(긍정/부정/중립) 파악</li>
  <li><strong>우선순위 분류</strong>: 긴급도에 따라 자동 분류</li>
  <li><strong>Content Safety</strong>: 프롬프트 인젝션 방어 (<code class="language-plaintext highlighter-rouge">PromptSanitizer</code>)</li>
</ul>

<h2 id="기술-스택">기술 스택</h2>

<ul>
  <li>Spring AI + Google Gemini</li>
  <li>Strategy 패턴 (4가지 Agent 전략)</li>
  <li>Spring Boot 4.0 + PostgreSQL + pgvector</li>
</ul>]]></content><author><name>푸른영혼의 별</name></author><category term="ai" /><category term="architecture" /><category term="ai-agent" /><category term="react" /><category term="spring-ai" /><category term="strategy-pattern" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Kafka + SSE로 실시간 알림 시스템 구현하기</title><link href="https://myoungsoo7.github.io/2026/05/04/kafka-event-driven/" rel="alternate" type="text/html" title="Kafka + SSE로 실시간 알림 시스템 구현하기" /><published>2026-05-04T01:00:00+00:00</published><updated>2026-05-04T01:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/kafka-event-driven</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/kafka-event-driven/"><![CDATA[<h2 id="왜-kafka--sse인가">왜 Kafka + SSE인가?</h2>

<p>SNS 피드 시스템에서 좋아요/댓글 알림을 실시간으로 전달해야 했습니다.</p>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>폴링</td>
      <td>구현 간단</td>
      <td>불필요한 요청, 지연</td>
    </tr>
    <tr>
      <td>WebSocket</td>
      <td>양방향</td>
      <td>서버 리소스 큼, LB 복잡</td>
    </tr>
    <tr>
      <td><strong>SSE</strong></td>
      <td>단방향, HTTP 호환</td>
      <td>단방향만 가능</td>
    </tr>
  </tbody>
</table>

<p>알림은 서버→클라이언트 <strong>단방향</strong>이므로 SSE가 적합합니다. Kafka는 이벤트 내구성과 다중 컨슈머 지원을 위해 사용합니다.</p>

<h2 id="흐름">흐름</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[좋아요 이벤트] → Kafka 토픽 발행
                    ↓
              Kafka Consumer 수신
                    ↓
         SSE EmitterRepository에서 대상 유저 찾기
                    ↓
              SSE push → 브라우저 실시간 수신
</code></pre></div></div>

<h2 id="핵심-코드">핵심 코드</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Kafka Producer — 좋아요 시 이벤트 발행</span>
<span class="n">alarmProducer</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">AlarmEvent</span><span class="o">(</span>
    <span class="nc">AlarmType</span><span class="o">.</span><span class="na">NEW_LIKE_ON_POST</span><span class="o">,</span> 
    <span class="k">new</span> <span class="nf">AlarmArgs</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">postId</span><span class="o">),</span> 
    <span class="n">postOwnerId</span>
<span class="o">));</span>

<span class="c1">// SSE Emitter — 클라이언트 연결</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/subscribe"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SseEmitter</span> <span class="nf">subscribe</span><span class="o">(</span><span class="nd">@AuthenticationPrincipal</span> <span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">SseEmitter</span> <span class="n">emitter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SseEmitter</span><span class="o">(</span><span class="mi">60</span> <span class="o">*</span> <span class="mi">1000L</span><span class="o">);</span>
    <span class="n">emitterRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">emitter</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">emitter</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Live</strong>: <a href="https://sns.lemuel.co.kr">sns.lemuel.co.kr</a></p>]]></content><author><name>푸른영혼의 별</name></author><category term="backend" /><category term="messaging" /><category term="kafka" /><category term="sse" /><category term="spring-boot" /><category term="event-driven" /><summary type="html"><![CDATA[왜 Kafka + SSE인가?]]></summary></entry><entry><title type="html">Pessimistic vs Optimistic Lock — 도메인 특성에 맞는 동시성 전략</title><link href="https://myoungsoo7.github.io/2026/05/04/concurrency-control-patterns/" rel="alternate" type="text/html" title="Pessimistic vs Optimistic Lock — 도메인 특성에 맞는 동시성 전략" /><published>2026-05-04T00:00:00+00:00</published><updated>2026-05-04T00:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/concurrency-control-patterns</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/concurrency-control-patterns/"><![CDATA[<h2 id="금액-도메인--pessimistic-lock">금액 도메인 → Pessimistic Lock</h2>

<p>부분 환불에서는 동시 요청으로 인한 초과 환불이 절대 발생하면 안 됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Transactional</span><span class="o">(</span><span class="n">isolation</span> <span class="o">=</span> <span class="nc">Isolation</span><span class="o">.</span><span class="na">REPEATABLE_READ</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Refund</span> <span class="nf">refund</span><span class="o">(</span><span class="nc">Long</span> <span class="n">paymentId</span><span class="o">,</span> <span class="nc">BigDecimal</span> <span class="n">amount</span><span class="o">,</span> <span class="nc">String</span> <span class="n">idempotencyKey</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// SELECT FOR UPDATE — 다른 트랜잭션이 같은 결제를 수정하지 못하게 잠금</span>
    <span class="nc">Payment</span> <span class="n">payment</span> <span class="o">=</span> <span class="n">loadPaymentPort</span><span class="o">.</span><span class="na">loadForUpdate</span><span class="o">(</span><span class="n">paymentId</span><span class="o">);</span>
    
    <span class="k">if</span> <span class="o">(</span><span class="n">payment</span><span class="o">.</span><span class="na">getRefundableAmount</span><span class="o">().</span><span class="na">compareTo</span><span class="o">(</span><span class="n">amount</span><span class="o">)</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">RefundExceedsPaymentException</span><span class="o">();</span>
    <span class="o">}</span>
    <span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="재고-도메인--optimistic-lock">재고 도메인 → Optimistic Lock</h2>

<p>SKU 재고 차감은 짧은 트랜잭션 + 높은 동시성이라 재시도가 효율적입니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @Version 기반 Optimistic Lock + 최대 5회 재시도</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">attempt</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span> <span class="n">attempt</span> <span class="o">&lt;=</span> <span class="no">MAX_ATTEMPTS</span><span class="o">;</span> <span class="n">attempt</span><span class="o">++)</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">txTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">ProductVariant</span> <span class="n">v</span> <span class="o">=</span> <span class="n">loadPort</span><span class="o">.</span><span class="na">loadById</span><span class="o">(</span><span class="n">variantId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
            <span class="n">v</span><span class="o">.</span><span class="na">decreaseStock</span><span class="o">(</span><span class="n">quantity</span><span class="o">);</span>  <span class="c1">// stockQuantity &lt; quantity면 예외</span>
            <span class="n">savePort</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">v</span><span class="o">);</span>           <span class="c1">// UPDATE WHERE version = N</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">});</span>
        <span class="k">return</span><span class="o">;</span> <span class="c1">// 성공</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">OptimisticLockException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">10L</span> <span class="o">&lt;&lt;</span> <span class="o">(</span><span class="n">attempt</span> <span class="o">-</span> <span class="mi">1</span><span class="o">));</span> <span class="c1">// 지수 백오프</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="선택-기준">선택 기준</h2>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>Pessimistic</th>
      <th>Optimistic</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>충돌 빈도</td>
      <td>높음</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>트랜잭션 길이</td>
      <td>길어도 OK</td>
      <td>짧아야 효과적</td>
    </tr>
    <tr>
      <td>실패 비용</td>
      <td>높음 (금액)</td>
      <td>낮음 (재시도)</td>
    </tr>
    <tr>
      <td>처리량</td>
      <td>제한적</td>
      <td>높음</td>
    </tr>
  </tbody>
</table>

<p><strong>핵심</strong>: 도메인 특성에 따라 선택하되, 100스레드 동시성 테스트로 검증해야 합니다.</p>]]></content><author><name>푸른영혼의 별</name></author><category term="backend" /><category term="concurrency" /><category term="jpa" /><category term="spring-boot" /><category term="lock" /><category term="transaction" /><summary type="html"><![CDATA[금액 도메인 → Pessimistic Lock]]></summary></entry><entry><title type="html">Redis 분산 락으로 좌석 예매 동시성 제어하기</title><link href="https://myoungsoo7.github.io/2026/05/04/redis-distributed-lock/" rel="alternate" type="text/html" title="Redis 분산 락으로 좌석 예매 동시성 제어하기" /><published>2026-05-04T00:00:00+00:00</published><updated>2026-05-04T00:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/redis-distributed-lock</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/redis-distributed-lock/"><![CDATA[<h2 id="문제-상황">문제 상황</h2>

<p>만석 콘서트에서 같은 좌석에 동시 예매 요청이 들어오면?</p>

<p>DB Pessimistic Lock은 단일 인스턴스에서만 동작합니다. 다중 서버 환경에서는 <strong>Redis 분산 락</strong>이 필요합니다.</p>

<h2 id="구현-nodejs--redis">구현 (Node.js + Redis)</h2>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 좌석별 락 키</span>
<span class="kd">const</span> <span class="nx">lockKey</span> <span class="o">=</span> <span class="s2">`seat:</span><span class="p">${</span><span class="nx">eventId</span><span class="p">}</span><span class="s2">:</span><span class="p">${</span><span class="nx">seatNo</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>

<span class="c1">// Redis SETNX로 락 획득 (TTL 10분)</span>
<span class="kd">const</span> <span class="nx">acquired</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">redis</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">lockKey</span><span class="p">,</span> <span class="nx">oderId</span><span class="p">,</span> <span class="dl">'</span><span class="s1">NX</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">EX</span><span class="dl">'</span><span class="p">,</span> <span class="mi">600</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">acquired</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nx">HttpError</span><span class="p">(</span><span class="mi">409</span><span class="p">,</span> <span class="dl">'</span><span class="s1">SEAT_ALREADY_HELD</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">이미 선점된 좌석</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">try</span> <span class="p">{</span>
    <span class="c1">// 결제 진행</span>
    <span class="k">await</span> <span class="nx">processPayment</span><span class="p">(</span><span class="nx">orderId</span><span class="p">);</span>
    <span class="c1">// 좌석 상태 변경: Hold → Reserved</span>
    <span class="k">await</span> <span class="nx">updateSeatStatus</span><span class="p">(</span><span class="nx">seatNo</span><span class="p">,</span> <span class="dl">'</span><span class="s1">RESERVED</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
    <span class="c1">// 락 해제</span>
    <span class="k">await</span> <span class="nx">redis</span><span class="p">.</span><span class="nx">del</span><span class="p">(</span><span class="nx">lockKey</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="좌석-상태-전이">좌석 상태 전이</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Available → Hold (락 획득) → Reserved (결제 완료) → Issued (발권)
                ↓ (TTL 만료)
            Available (자동 해제)
</code></pre></div></div>

<h2 id="sku-optimistic-lock과의-차이">SKU Optimistic Lock과의 차이</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Redis 분산 락</th>
      <th>Optimistic Lock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상</td>
      <td>좌석 (1:1 배타적)</td>
      <td>재고 (수량 차감)</td>
    </tr>
    <tr>
      <td>재시도</td>
      <td>의미 없음 (fail-fast)</td>
      <td>최대 5회 재시도</td>
    </tr>
    <tr>
      <td>이유</td>
      <td>다른 사람이 이미 선택</td>
      <td>일시적 충돌, 재시도로 해결</td>
    </tr>
  </tbody>
</table>

<p><strong>GitHub</strong>: <a href="https://github.com/MyoungSoo7/global-seat-ticketing">global-seat-ticketing</a></p>]]></content><author><name>푸른영혼의 별</name></author><category term="backend" /><category term="concurrency" /><category term="redis" /><category term="distributed-lock" /><category term="node-js" /><category term="concurrency" /><summary type="html"><![CDATA[문제 상황]]></summary></entry><entry><title type="html">모놀리스에서 MSA로 — Settlement 정산 플랫폼 전환기</title><link href="https://myoungsoo7.github.io/2026/05/04/settlement-msa-architecture/" rel="alternate" type="text/html" title="모놀리스에서 MSA로 — Settlement 정산 플랫폼 전환기" /><published>2026-05-04T00:00:00+00:00</published><updated>2026-05-04T00:00:00+00:00</updated><id>https://myoungsoo7.github.io/2026/05/04/settlement-msa-architecture</id><content type="html" xml:base="https://myoungsoo7.github.io/2026/05/04/settlement-msa-architecture/"><![CDATA[<h2 id="왜-msa로-전환했는가">왜 MSA로 전환했는가</h2>

<p>주문/결제와 정산은 Bounded Context가 다릅니다. 주문은 실시간 트랜잭션, 정산은 배치/비동기 처리 중심이라 배포 주기와 스케일링 요구가 달랐습니다.</p>

<h2 id="read-only-projection-패턴">Read-only Projection 패턴</h2>

<p>MSA 분리의 핵심은 <strong>코드 의존성 0</strong>입니다. settlement-service가 order-service의 테이블을 조회해야 하지만 코드를 import하면 안 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">@Immutable</code> JPA Entity로 같은 테이블을 Read-only로 매핑하여:</p>
<ul>
  <li>코드 의존성 0</li>
  <li>런타임 API 호출 0</li>
  <li>Strong Consistency 확보</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// settlement-service에 정의된 Read-only Entity</span>
<span class="nd">@Entity</span>
<span class="nd">@Table</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"payments"</span><span class="o">)</span>
<span class="nd">@Immutable</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SettlementPaymentReadModel</span> <span class="o">{</span>
    <span class="nd">@Id</span> <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">orderId</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">BigDecimal</span> <span class="n">amount</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">status</span><span class="o">;</span>
    <span class="c1">// order-service의 Payment 테이블을 읽기 전용으로 매핑</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="outbox--kafka-3단-멱등">Outbox + Kafka 3단 멱등</h2>

<p>결제 CAPTURED → 정산 생성 파이프라인에서 데이터 정합성을 보장하기 위해 3단 멱등 방어를 구현했습니다.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">outbox_events.event_id UUID UNIQUE</code> — 발행 측 중복 방지</li>
  <li><code class="language-plaintext highlighter-rouge">processed_events PK(group, event_id)</code> — 컨슈머 중복 수신 차단</li>
  <li><code class="language-plaintext highlighter-rouge">settlements.payment_id UNIQUE</code> — 비즈니스 레벨 중복 방지</li>
</ol>

<h2 id="프로젝트-규모">프로젝트 규모</h2>

<ul>
  <li>519개 소스 파일, 112개 테스트</li>
  <li>16개 ADR (Architecture Decision Record)</li>
  <li>83개 Flyway 마이그레이션</li>
  <li>4개 Gradle 모듈 (order-service, settlement-service, gateway-service, shared-common)</li>
</ul>

<p><strong>GitHub</strong>: <a href="https://github.com/MyoungSoo7/settlement">MyoungSoo7/settlement</a><br />
<strong>Live</strong>: <a href="https://jen.lemuel.co.kr">jen.lemuel.co.kr</a></p>]]></content><author><name>푸른영혼의 별</name></author><category term="architecture" /><category term="msa" /><category term="spring-boot" /><category term="kafka" /><category term="hexagonal" /><category term="outbox" /><summary type="html"><![CDATA[왜 MSA로 전환했는가]]></summary></entry></feed>