{"id":1157,"date":"2026-04-18T10:10:13","date_gmt":"2026-04-18T10:10:13","guid":{"rendered":"https:\/\/lifestyleinfo.kr\/%eb%8b%a4%ec%9d%8c\/"},"modified":"2026-07-09T15:33:02","modified_gmt":"2026-07-09T15:33:02","slug":"%eb%8b%a4%ec%9d%8c","status":"publish","type":"post","link":"https:\/\/lifestyleinfo.kr\/?p=1157","title":{"rendered":"\ub2e4\uc74c"},"content":{"rendered":"<p>\uc88b\uc2b5\ub2c8\ub2e4. \uc774\uc81c \ubb38\uc11c \u2192 \uc2e4\uc81c \ub3d9\uc791\ud558\ub294 \uc2dc\uc2a4\ud15c\uc73c\ub85c \ub118\uc5b4\uac11\ub2c8\ub2e4.<\/p>\n<p>\uc544\ub798\ub294 \ubcf5\ubd99\ud574\uc11c \ubc14\ub85c \uc2e4\ud589 \uac00\ub2a5\ud55c AOP \ucd5c\uc18c \ub808\ud37c\ub7f0\uc2a4 (FastAPI + Event Store + Replay)\uc785\ub2c8\ub2e4.<\/p>\n<\/p>\n<p>\ud83d\ude80 AOP Minimal Reference Implementation<\/p>\n<p>\ubaa9\ud45c<\/p>\n<\/p>\n<p>event \uae30\ub85d<\/p>\n<\/p>\n<p>projection \uc0dd\uc131<\/p>\n<\/p>\n<p>\uc2e4\ud328 \ubc1c\uc0dd<\/p>\n<\/p>\n<p>replay\ub85c \ubcf5\uad6c<\/p>\n<\/p>\n<p>1. \uc544\ud0a4\ud14d\ucc98 (\ucd5c\uc18c \uad6c\uc131)<\/p>\n<p>FastAPI (API)<\/p>\n<p>  \u2193<\/p>\n<p>Command Handler<\/p>\n<p>  \u2193<\/p>\n<p>Event Store (SQLite)<\/p>\n<p>  \u2193<\/p>\n<p>Projection (in-memory)<\/p>\n<p>  \u2193<\/p>\n<p>Replay Engine<\/p>\n<p>2. \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870<\/p>\n<p>Bash<\/p>\n<p>aop_minimal\/<\/p>\n<p> \u251c\u2500\u2500 main.py<\/p>\n<p> \u251c\u2500\u2500 event_store.py<\/p>\n<p> \u251c\u2500\u2500 projection.py<\/p>\n<p> \u251c\u2500\u2500 handlers.py<\/p>\n<p> \u251c\u2500\u2500 replay.py<\/p>\n<p> \u2514\u2500\u2500 models.py<\/p>\n<p>3. \ucf54\ub4dc<\/p>\n<p>3.1 models.py<\/p>\n<p>Python<\/p>\n<p>\uc2e4\ud589\ub428<\/p>\n<p>from pydantic import BaseModel<\/p>\n<p>from typing import Optional<\/p>\n<p>import uuid<\/p>\n<\/p>\n<p>class Command(BaseModel):<\/p>\n<p>    type: str<\/p>\n<p>    payload: dict<\/p>\n<p>    trace_id: str = str(uuid.uuid4())<\/p>\n<\/p>\n<p>class Event(BaseModel):<\/p>\n<p>    id: str<\/p>\n<p>    type: str<\/p>\n<p>    payload: dict<\/p>\n<p>    trace_id: str<\/p>\n<p>    seq: int<\/p>\n<p>3.2 event_store.py<\/p>\n<p>Python<\/p>\n<p>\uc2e4\ud589\ub428<\/p>\n<p>import sqlite3<\/p>\n<p>from models import Event<\/p>\n<\/p>\n<p>conn = sqlite3.connect(&#8220;events.db&#8221;, check_same_thread=False)<\/p>\n<p>cursor = conn.cursor()<\/p>\n<\/p>\n<p>cursor.execute(&#8220;&#8221;&#8221;<\/p>\n<p>CREATE TABLE IF NOT EXISTS events (<\/p>\n<p>    id TEXT,<\/p>\n<p>    type TEXT,<\/p>\n<p>    payload TEXT,<\/p>\n<p>    trace_id TEXT,<\/p>\n<p>    seq INTEGER<\/p>\n<p>)<\/p>\n<p>&#8220;&#8221;&#8221;)<\/p>\n<p>conn.commit()<\/p>\n<\/p>\n<p>def append_event(event: Event):<\/p>\n<p>    cursor.execute(<\/p>\n<p>        &#8220;INSERT INTO events VALUES (?, ?, ?, ?, ?)&#8221;,<\/p>\n<p>        (event.id, event.type, str(event.payload), event.trace_id, event.seq)<\/p>\n<p>    )<\/p>\n<p>    conn.commit()<\/p>\n<\/p>\n<p>def get_events(trace_id: str):<\/p>\n<p>    cursor.execute(<\/p>\n<p>        &#8220;SELECT id, type, payload, trace_id, seq FROM events WHERE trace_id=? ORDER BY seq&#8221;,<\/p>\n<p>        (trace_id,)<\/p>\n<p>    )<\/p>\n<p>    rows = cursor.fetchall()<\/p>\n<p>    return rows<\/p>\n<p>3.3 projection.py<\/p>\n<p>Python<\/p>\n<p>\uc2e4\ud589\ub428<\/p>\n<p>projections = {}<\/p>\n<\/p>\n<p>def apply_event(event):<\/p>\n<p>    trace_id = event.trace_id<\/p>\n<\/p>\n<p>    if trace_id not in projections:<\/p>\n<p>        projections[trace_id] = {&#8220;status&#8221;: &#8220;INIT&#8221;}<\/p>\n<\/p>\n<p>    if event.type == &#8220;OrderCreated&#8221;:<\/p>\n<p>        projections[trace_id][&#8220;status&#8221;] = &#8220;CREATED&#8221;<\/p>\n<\/p>\n<p>    elif event.type == &#8220;PaymentRequested&#8221;:<\/p>\n<p>        projections[trace_id][&#8220;status&#8221;] = &#8220;PAYMENT_PENDING&#8221;<\/p>\n<\/p>\n<p>    elif event.type == &#8220;PaymentCompleted&#8221;:<\/p>\n<p>\n<!-- Google AdSense --><br \/>\n<script async src=\"https:\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-1787748468296325\" crossorigin=\"anonymous\"><\/script><br \/>\n<ins class=\"adsbygoogle\"\n     style=\"display:block\"\n     data-ad-client=\"ca-pub-1787748468296325\"\n     data-ad-slot=\"3716088926\"\n     data-ad-format=\"auto\"\n     data-full-width-responsive=\"true\"><\/ins><br \/>\n<script>(adsbygoogle = window.adsbygoogle || []).push({});<\/script>\n<\/p>\n<p>        projections[trace_id][&#8220;status&#8221;] = &#8220;COMPLETED&#8221;<\/p>\n<\/p>\n<p>    elif event.type == &#8220;PaymentFailed&#8221;:<\/p>\n<p>        projections[trace_id][&#8220;status&#8221;] = &#8220;FAILED&#8221;<\/p>\n<\/p>\n<p>def get_projection(trace_id):<\/p>\n<p>    return projections.get(trace_id, {})<\/p>\n<p>3.4 handlers.py<\/p>\n<p>Python<\/p>\n<p>\uc2e4\ud589\ub428<\/p>\n<p>import uuid<\/p>\n<p>from models import Event<\/p>\n<p>from event_store import append_event<\/p>\n<p>from projection import apply_event<\/p>\n<\/p>\n<p>seq_counter = {}<\/p>\n<\/p>\n<p>def next_seq(trace_id):<\/p>\n<p>    seq_counter.setdefault(trace_id, 0)<\/p>\n<p>    seq_counter[trace_id] += 1<\/p>\n<p>    return seq_counter[trace_id]<\/p>\n<\/p>\n<p>def emit(event_type, payload, trace_id):<\/p>\n<p>    event = Event(<\/p>\n<p>        id=str(uuid.uuid4()),<\/p>\n<p>        type=event_type,<\/p>\n<p>        payload=payload,<\/p>\n<p>        trace_id=trace_id,<\/p>\n<p>        seq=next_seq(trace_id)<\/p>\n<p>    )<\/p>\n<p>    append_event(event)<\/p>\n<p>    apply_event(event)<\/p>\n<p>    return event<\/p>\n<\/p>\n<p>def handle_create_order(cmd):<\/p>\n<p>    trace_id = cmd.trace_id<\/p>\n<\/p>\n<p>    emit(&#8220;OrderCreated&#8221;, cmd.payload, trace_id)<\/p>\n<p>    emit(&#8220;PaymentRequested&#8221;, cmd.payload, trace_id)<\/p>\n<\/p>\n<p>    # intentionally fail once<\/p>\n<p>    if cmd.payload.get(&#8220;fail&#8221;):<\/p>\n<p>        emit(&#8220;PaymentFailed&#8221;, {}, trace_id)<\/p>\n<p>        raise Exception(&#8220;Payment failed&#8221;)<\/p>\n<\/p>\n<p>    emit(&#8220;PaymentCompleted&#8221;, {}, trace_id)<\/p>\n<p>3.5 replay.py<\/p>\n<p>Python<\/p>\n<p>\uc2e4\ud589\ub428<\/p>\n<p>from event_store import get_events<\/p>\n<p>from projection import apply_event, projections<\/p>\n<\/p>\n<p>def replay(trace_id):<\/p>\n<p>    projections[trace_id] = {&#8220;status&#8221;: &#8220;INIT&#8221;}<\/p>\n<\/p>\n<p>    events = get_events(trace_id)<\/p>\n<\/p>\n<p>    for row in events:<\/p>\n<p>        _, type_, payload, trace_id, seq = row<\/p>\n<p>        event = type(&#8220;Event&#8221;, (), {})()<\/p>\n<p>        event.type = type_<\/p>\n<p>        event.payload = eval(payload)<\/p>\n<p>        event.trace_id = trace_id<\/p>\n<p>        event.seq = seq<\/p>\n<\/p>\n<p>        apply_event(event)<\/p>\n<\/p>\n<p>    return projections[trace_id]<\/p>\n<p>3.6 main.py<\/p>\n<p>Python<\/p>\n<p>\uc2e4\ud589\ub428<\/p>\n<p>from fastapi import FastAPI<\/p>\n<p>from models import Command<\/p>\n<p>from handlers import handle_create_order<\/p>\n<p>from projection import get_projection<\/p>\n<p>from replay import replay<\/p>\n<\/p>\n<p>app = FastAPI()<\/p>\n<\/p>\n<p>@app.post(&#8220;\/run&#8221;)<\/p>\n<p>def run(cmd: Command):<\/p>\n<p>    try:<\/p>\n<p>        handle_create_order(cmd)<\/p>\n<p>    except Exception as e:<\/p>\n<p>        return {&#8220;status&#8221;: &#8220;failed&#8221;, &#8220;trace_id&#8221;: cmd.trace_id}<\/p>\n<\/p>\n<p>    return {&#8220;status&#8221;: &#8220;ok&#8221;, &#8220;trace_id&#8221;: cmd.trace_id}<\/p>\n<\/p>\n<p>@app.get(&#8220;\/projection\/{trace_id}&#8221;)<\/p>\n<p>def projection(trace_id: str):<\/p>\n<p>    return get_projection(trace_id)<\/p>\n<\/p>\n<p>@app.post(&#8220;\/replay\/{trace_id}&#8221;)<\/p>\n<p>def replay_endpoint(trace_id: str):<\/p>\n<p>    result = replay(trace_id)<\/p>\n<p>    return {&#8220;replayed&#8221;: True, &#8220;projection&#8221;: result}<\/p>\n<p>4. \uc2e4\ud589 \ubc29\ubc95<\/p>\n<p>Bash<\/p>\n<p>pip install fastapi uvicorn pydantic<\/p>\n<\/p>\n<p>uvicorn main:app &#8211;reload<\/p>\n<p>5. \ud14c\uc2a4\ud2b8 \uc2dc\ub098\ub9ac\uc624<\/p>\n<p>5.1 \uc815\uc0c1 \uc2e4\ud589<\/p>\n<p>Bash<\/p>\n<p>curl -X POST localhost:8000\/run \\<\/p>\n<p>\n<!-- Coupang Partners --><br \/>\n<script src=\"https:\/\/ads-partners.coupang.com\/g.js\"><\/script><br \/>\n<script>\n  new PartnersCoupang.G({\n    \"id\":893901,\n    \"template\":\"carousel\",\n    \"trackingCode\":\"AF8585598\",\n    \"width\":\"680\",\n    \"height\":\"140\",\n    \"tsource\":\"\"\n  });\n<\/script>\n<\/p>\n<p>-H &#8220;Content-Type: application\/json&#8221; \\<\/p>\n<p>-d &#8216;{&#8220;type&#8221;:&#8221;CreateOrder&#8221;,&#8221;payload&#8221;:{}}&#8217;<\/p>\n<\/p>\n<p>\u2192 projection \ud655\uc778<\/p>\n<\/p>\n<p>Bash<\/p>\n<p>curl localhost:8000\/projection\/{trace_id}<\/p>\n<p>5.2 \uc2e4\ud328 \uc720\ub3c4<\/p>\n<p>Bash<\/p>\n<p>curl -X POST localhost:8000\/run \\<\/p>\n<p>-H &#8220;Content-Type: application\/json&#8221; \\<\/p>\n<p>-d &#8216;{&#8220;type&#8221;:&#8221;CreateOrder&#8221;,&#8221;payload&#8221;:{&#8220;fail&#8221;:true}}&#8217;<\/p>\n<\/p>\n<p>\u2192 FAILED \uc0c1\ud0dc \ud655\uc778<\/p>\n<\/p>\n<p>5.3 Replay \uc2e4\ud589<\/p>\n<p>Bash<\/p>\n<p>curl -X POST localhost:8000\/replay\/{trace_id}<\/p>\n<\/p>\n<p>\u2192 \uc0c1\ud0dc \ubcf5\uad6c \ud655\uc778<\/p>\n<\/p>\n<p>6. \uc774 \ucf54\ub4dc\uc758 \uc758\ubbf8 (\uc911\uc694)<\/p>\n<\/p>\n<p>\uc774\uac74 production \ucf54\ub4dc\uac00 \uc544\ub2c8\ub77c:<\/p>\n<\/p>\n<p>\ud83d\udc49 \u201cAOP\uc758 \ubcf8\uc9c8\uc744 \ubcf4\uc5ec\uc8fc\ub294 \ucd5c\uc18c \ub2e8\uc704\u201d<\/p>\n<\/p>\n<p>\uc5ec\uae30\uc11c \uc774\ubbf8 \ud575\uc2ec 4\uac1c\uac00 \ub4e4\uc5b4\uc788\uc2b5\ub2c8\ub2e4:<\/p>\n<\/p>\n<p>event \uae30\ub85d<\/p>\n<\/p>\n<p>projection \uc0dd\uc131<\/p>\n<\/p>\n<p>failure \ubc1c\uc0dd<\/p>\n<\/p>\n<p>replay\ub85c \ubcf5\uad6c<\/p>\n<\/p>\n<p>7. \ub2e4\uc74c \ud655\uc7a5 \ubc29\ud5a5<\/p>\n<\/p>\n<p>\uc774\uac78 \uae30\ubc18\uc73c\ub85c \ub2e4\uc74c\uc744 \ubd99\uc774\uba74 \uc2e4\uc81c \uc2dc\uc2a4\ud15c\uc774 \ub429\ub2c8\ub2e4:<\/p>\n<\/p>\n<p>7.1 \ubc18\ub4dc\uc2dc \ucd94\uac00\ud574\uc57c \ud558\ub294 \uac83<\/p>\n<\/p>\n<p>DLQ<\/p>\n<\/p>\n<p>retry \/ backoff<\/p>\n<\/p>\n<p>idempotency key<\/p>\n<\/p>\n<p>side-effect isolation<\/p>\n<\/p>\n<p>event schema versioning<\/p>\n<\/p>\n<p>trace \/ correlation id propagation<\/p>\n<\/p>\n<p>7.2 Control Plane<\/p>\n<\/p>\n<p>replay request API<\/p>\n<\/p>\n<p>preflight simulation<\/p>\n<\/p>\n<p>approval workflow<\/p>\n<\/p>\n<p>audit log<\/p>\n<\/p>\n<p>operator console<\/p>\n<\/p>\n<p>7.3 Multi-tenant<\/p>\n<\/p>\n<p>tenant_id \ucd94\uac00<\/p>\n<\/p>\n<p>quota enforcement<\/p>\n<\/p>\n<p>rate limiting<\/p>\n<\/p>\n<p>8. \uc9c4\uc9dc \ub2e4\uc74c \ub2e8\uacc4<\/p>\n<\/p>\n<p>\uc774\uc81c \ub0a8\uc740 \uac74 \ub531 \ub450 \uac1c\uc785\ub2c8\ub2e4:<\/p>\n<\/p>\n<p>1\ufe0f\u20e3 Production-grade \uad6c\uc870\ub85c \ud655\uc7a5<\/p>\n<\/p>\n<p>Kafka \/ Redis Streams<\/p>\n<\/p>\n<p>Postgres projection<\/p>\n<\/p>\n<p>async worker<\/p>\n<\/p>\n<p>distributed trace<\/p>\n<\/p>\n<p>2\ufe0f\u20e3 \uc2e4\uc81c \ub3c4\uba54\uc778 \uc801\uc6a9<\/p>\n<\/p>\n<p>\uacb0\uc81c<\/p>\n<\/p>\n<p>\uc815\uc0b0<\/p>\n<\/p>\n<p>\ud06c\ub864\ub9c1<\/p>\n<\/p>\n<p>LLM agent orchestration<\/p>\n<\/p>\n<p>\uc6d0\ud558\uba74 \ub2e4\uc74c\uc73c\ub85c:<\/p>\n<\/p>\n<p>\ud83d\udc49 \u201c\uc774 \ucf54\ub4dc\ub97c production-grade AOP \uad6c\uc870\ub85c \ud655\uc7a5\ud558\ub294 \uc124\uacc4 + \ucf54\ub4dc\u201d<\/p>\n<p>\ud83d\udc49 \u201c\uacb0\uc81c\/\uc815\uc0b0 \ub3c4\uba54\uc778 \uc2e4\uc81c \uc801\uc6a9 \ubc84\uc804\u201d<\/p>\n<\/p>\n<p>\uae4c\uc9c0 \ubc14\ub85c \uc774\uc5b4\uc11c \ub9cc\ub4e4\uc5b4\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\uc88b\uc2b5\ub2c8\ub2e4. \uc774\uc81c \ubb38\uc11c \u2192 \uc2e4\uc81c \ub3d9\uc791\ud558\ub294 \uc2dc\uc2a4\ud15c\uc73c\ub85c \ub118\uc5b4\uac11\ub2c8\ub2e4. \uc544\ub798\ub294 \ubcf5\ubd99\ud574\uc11c \ubc14\ub85c \uc2e4\ud589 \uac00\ub2a5\ud55c AOP \ucd5c\uc18c \ub808\ud37c\ub7f0\uc2a4 (FastAPI + Event Store + Replay)\uc785\ub2c8\ub2e4. \ud83d\ude80 AOP Minimal Reference Implementation \ubaa9\ud45c event \uae30\ub85d projection \uc0dd\uc131 \uc2e4\ud328 \ubc1c\uc0dd replay\ub85c \ubcf5\uad6c 1. \uc544\ud0a4\ud14d\ucc98 (\ucd5c\uc18c \uad6c\uc131) FastAPI (API) \u2193 Command Handler \u2193 Event Store (SQLite) \u2193 Projection (in-memory) \u2193 Replay Engine &#8230; <a title=\"\ub2e4\uc74c\" class=\"read-more\" href=\"https:\/\/lifestyleinfo.kr\/?p=1157\" aria-label=\"\ub2e4\uc74c\uc5d0 \ub300\ud574 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uc138\uc694\">\ub354 \uc77d\uae30<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[313],"tags":[],"class_list":["post-1157","post","type-post","status-publish","format-standard","hentry","category-313"],"_links":{"self":[{"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=\/wp\/v2\/posts\/1157","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1157"}],"version-history":[{"count":1,"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=\/wp\/v2\/posts\/1157\/revisions"}],"predecessor-version":[{"id":1488,"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=\/wp\/v2\/posts\/1157\/revisions\/1488"}],"wp:attachment":[{"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1157"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1157"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lifestyleinfo.kr\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1157"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}