sxngt

비영리 스타트업의 MSA 여정 — 아키텍처, 예이린은 어떻게 풀어나갔는가

· Architecture · 12 min

예이린 사회적협동조합 기술블로그 시리즈 EP.3 — Medium에 함께 게재된 글입니다.

지난 글에서는 예이린 플랫폼의 전체 아키텍처 설계를 공유했습니다. MSA로 분리한 세 개의 서비스, DDD의 4개 계층, TDD와 결합한 견고한 도메인 모델까지. 하지만 실제로 두 개의 서비스가 통신하기 시작하면, 첫 번째로 마주치는 문제가 있습니다: "이 요청, 누가 보낸 거야?"

인증(Authentication)과 인가(Authorization) 문제입니다.

1. 문제 정의: 두 서비스, 두 가지 사용자

Soul-E 서비스의 두 가지 사용자

Soul-E(소울이)는 AI 기반 심리상담 챗봇입니다. 하지만 이 서비스에는 두 종류의 사용자가 있습니다:

  1. 선생님(Teacher): 아동을 선택하고 채팅 세션을 시작하는 사람
  2. 아동(Child): 실제로 소울이와 대화하는 사람

선생님은 yeirin에 이미 가입된 사용자이며 JWT 토큰을 가지고 있습니다. 하지만 아동은 yeirin에 직접 가입하지 않고 보호자(선생님)가 등록한 "아동 정보"로만 존재합니다.

Soul-E 서비스의 두 가지 사용자

이 두 사용자를 어떻게 인증할 것인가? 이것이 첫 번째 질문이었습니다.

2. 결정 1: MSA 환경에서의 인증 공유 방식

고민했던 선택지들

방식장점단점
API Gateway 통합중앙 집중화SPOF, 복잡한 로직
OAuth2/OIDC업계 표준, 확장성구현 복잡도, 추가 인프라
JWT Shared Secret단순함, 빠른 구현Secret 관리, 회전 어려움
mTLS (Service Mesh)강력한 보안인프라 복잡도 높음

선택: JWT Shared Secret 방식

yeirin에서 발급한 JWT를 Soul-E에서도 동일한 secret으로 검증하는 방식입니다.

JWT Shared Secret 인증 흐름

왜 이 선택인가?

첫째, Netflix "Passport" 패턴을 참고했습니다.

Netflix는 내부 서비스 간 인증에 "Passport"라는 토큰을 사용합니다. 외부 인증(OAuth)으로 받은 토큰을 내부 토큰으로 변환하고, 이를 서비스 간에 전파합니다. yeirin JWT가 이 Passport 역할을 합니다.

둘째, 현재 규모에 적합합니다.

2개 서비스(yeirin + soul-e)만 존재하는 상황에서 OAuth 서버를 별도로 구축하는 것은 과도한 엔지니어링입니다. 인증 서버 운영, 토큰 교환 로직, 추가 네트워크 홉의 비용 대비 이점이 없습니다.

셋째, 빅테크 사례가 있습니다.

  • Spotify: 내부 서비스는 JWT shared secret 사용
  • SoundCloud: 마이크로서비스 간 토큰 전파 방식
  • Uber: 초기에 shared secret, 이후 점진적 고도화

중요한 것은 **"처음부터 완벽한 아키텍처"가 아니라 "현재 규모에 맞는 아키텍처"**입니다.

3. 결정 2: yeirin 데이터 접근 방식

Soul-E가 선생님을 인증했다고 칩시다. 이제 선생님이 담당하는 아동 목록을 가져와야 합니다. 아동 데이터는 yeirin 데이터베이스에 있습니다.

고민했던 선택지들

방식장점단점
API 호출완전한 분리, 캡슐화네트워크 지연, 의존성
읽기 전용 DB 접근빠른 조회, 단순함DB 스키마 결합
이벤트 동기화완전한 분리인프라 복잡도, 일관성 이슈
Shared Database단순함안티패턴, 강한 결합

선택: 읽기 전용 DB 접근 (CQRS 패턴)

Soul-E가 yeirin의 PostgreSQL에 직접 연결하되, 읽기만 합니다.

CQRS 패턴 적용

왜 이 선택인가?

첫째, CQRS 패턴의 정수입니다.

CQRS(Command Query Responsibility Segregation)의 핵심은 쓰기와 읽기를 분리하는 것입니다:

  • Command (쓰기): yeirin에서만
  • Query (읽기): Soul-E는 읽기만

같은 데이터베이스이지만, 책임이 명확히 분리됩니다.

둘째, 실용적인 이유가 있습니다.

  • yeirin API에 Soul-E 전용 엔드포인트를 추가할 필요가 없습니다
  • 네트워크 홉이 제거되어 성능이 향상됩니다
  • 구현 복잡도가 낮습니다

셋째, 안전장치를 마련했습니다.

# 별도 read-only 커넥션
YEIRIN_DATABASE_URL = "postgresql://readonly_user:..."

# SQLAlchemy 모델에 "ReadModel" suffix
class GuardianProfileReadModel(YeirinBase):
    __tablename__ = "guardian_profiles"
    # 절대 쓰기 작업 금지 - 코드 리뷰로 강제

모델 이름에 "ReadModel"을 붙여서 의도를 명시적으로 표현합니다.

4. 결정 3: 서비스 독립성

Netflix/Amazon 철학

"서비스는 독립적으로 배포 가능해야 한다."

Soul-E를 개발하면서 yeirin 코드를 단 한 줄도 수정하지 않았습니다. 왜냐하면:

  • yeirin에 Soul-E 전용 엔드포인트를 추가하면, yeirin 팀에 의존하게 됩니다
  • yeirin이 바쁘면 Soul-E 개발이 블로킹됩니다
  • 인터페이스 변경 시 양쪽 모두 수정해야 합니다

대신 저는 yeirin이 이미 제공하는 것만 사용했습니다:

  1. JWT 구조 (문서화된 계약)
  2. 데이터베이스 스키마 (읽기 전용 접근)

Soul-E가 필요한 데이터 조합은 Soul-E가 직접 합니다. yeirin은 변경할 필요가 없습니다.

5. 결정 4: 이중 토큰 전략

문제 상황

선생님은 yeirin JWT로 인증합니다. 그런데 아동은 어떻게 인증할까요?

아동은 yeirin에 계정이 없고 직접 로그인할 수도 없습니다. 하지만 채팅을 하려면 "이 채팅이 어떤 아동의 것인지" 알아야 합니다.

고민했던 선택지들

방식장점단점
단일 토큰단순함아동 컨텍스트 분리 어려움
토큰 교환명확한 분리추가 왕복 필요
토큰 확장한 번에 모든 정보yeirin 변경 필요
이중 토큰역할 분리복잡도 증가

선택: 이중 토큰 전략

저는 두 개의 토큰을 사용하기로 했습니다:

이중 토큰 흐름
yeirin JWTchild_session_token
발급: yeirin 백엔드발급: soul-e
용도: 선생님 인증용도: 아동 채팅 세션
대상: auth/* 엔드포인트대상: chat/* 엔드포인트
수명: yeirin 설정수명: 60분

왜 이 선택인가?

첫째, 관심사 분리(Separation of Concerns)입니다.

선생님 인증과 아동 세션은 완전히 다른 개념입니다:

  • 선생님 인증: "이 사람이 선생님 맞아?"
  • 아동 세션: "이 채팅이 어떤 아동의 것이야?"

각 토큰이 단일 책임을 가집니다. 토큰 수명/갱신 정책도 독립적으로 관리할 수 있습니다.

둘째, 보안이 강화됩니다.

  • 아동 채팅에는 필요한 최소 정보만 포함됩니다 (childId, sessionId)
  • yeirin JWT 노출 범위가 제한됩니다 (auth/*에서만 사용)
  • child_session_token은 Soul-E만 검증할 수 있습니다

셋째, UX가 최적화됩니다.

  • 선생님: yeirin JWT로 아동 선택
  • 아동: child_session_token으로 채팅 (더 긴 세션 유지)
  • 토큰 만료 시 영향 범위가 제한됩니다

넷째, 빅테크 사례가 있습니다.

  • Google: access_token + ID token 분리
  • AWS: IAM token + session token 분리
  • 역할별 토큰 분리는 업계 일반적 패턴입니다

6. 결정 5: 데이터베이스 모델 설계

Soul-E가 yeirin 데이터베이스를 읽는다고 했습니다. 그런데 어떻게 읽을까요?

고민했던 선택지들

방식장점단점
ORM 공유코드 재사용강한 결합, 버전 충돌
DTO 기반느슨한 결합네트워크 오버헤드
Read ModelCQRS 원칙 준수중복 코드
Auto-generate동기화 보장도구 의존성

선택: Read Model (별도 읽기 전용 모델)

저는 별도의 읽기 전용 모델을 정의했습니다.

Anti-Corruption Layer 패턴

왜 이 선택인가?

첫째, 반부패 계층(Anti-Corruption Layer)입니다.

DDD에서 말하는 ACL 패턴입니다. yeirin 스키마가 변경되어도 Soul-E에 직접 영향을 주지 않습니다. 필요한 필드만 선택적으로 매핑하고, 중간 변환 계층 역할을 합니다.

둘째, 의도를 명시적으로 표현합니다.

class GuardianProfileReadModel(YeirinBase):
    """
    yeirin의 guardian_profiles 테이블 읽기 전용 모델
    ⚠️ 절대 쓰기 작업 금지
    """
    __tablename__ = "guardian_profiles"

    id = Column(UUID, primary_key=True)
    userId = Column(UUID, ForeignKey("users.id"))
    guardianType = Column(String)

    @property
    def is_teacher(self) -> bool:
        """Soul-E 비즈니스 로직: 선생님 여부 판단"""
        return self.guardianType in [
            GuardianType.CARE_FACILITY_TEACHER,
            GuardianType.COMMUNITY_CENTER_TEACHER
        ]

"ReadModel" suffix로 읽기 전용임을 명확히 합니다. is_teacher 같은 Soul-E만의 비즈니스 로직을 추가할 수 있습니다.

7. 결정 6: 인증 의존성 주입 패턴

FastAPI에서 인증을 어떻게 구현할까요?

고민했던 선택지들

방식장점단점
미들웨어일관성유연성 부족
데코레이터명시적Python-specific
Depends프레임워크 표준매 엔드포인트 추가
Guard 패턴재사용성FastAPI에 미적합

선택: FastAPI Depends + Optional 패턴

# 필수 인증 (auth/* 엔드포인트)
async def get_current_teacher(
    credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer_scheme)]
) -> TeacherContext:
    if credentials is None:
        raise HTTPException(status_code=401, detail="인증 토큰이 필요합니다")
    # JWT 검증 로직...

# 선택적 인증 (chat/* 엔드포인트) - 하위 호환성
async def get_optional_child_session(
    credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(child_session_scheme)]
) -> ChildSessionContext | None:
    if credentials is None:
        return None  # 인증 없이도 동작 (개발 환경)
    # 토큰 검증 로직...

왜 이 선택인가?

첫째, FastAPI 철학을 준수합니다.

의존성 주입이 FastAPI의 핵심입니다. 타입 힌트로 자동 문서화되고, 테스트 시 쉽게 모킹할 수 있습니다.

둘째, 유연한 인증 정책이 가능합니다.

  • get_current_teacher: 필수 인증 (401 반환)
  • get_optional_child_session: 선택적 인증 (None 반환)

엔드포인트별로 다른 정책을 적용할 수 있습니다.

셋째, 하위 호환성을 유지합니다.

# 환경 변수로 인증 기능 on/off
AUTH_ENABLED=false  # 개발 환경에서는 인증 없이 테스트 가능

점진적 마이그레이션이 가능합니다.

8. 결정 7: 에러 처리 전략

마지막으로, 에러 처리입니다.

명확한 HTTP 상태 코드 분리

# 503: 인증 기능 자체가 꺼져있음
if not settings.auth_enabled:
    raise HTTPException(
        status_code=503,
        detail="Authentication is not enabled"
    )

# 401: 인증 필요/실패
if credentials is None:
    raise HTTPException(
        status_code=401,
        detail="인증 토큰이 필요합니다"
    )

# 403: 인증됐지만 권한 없음
if not guardian_profile.is_teacher:
    raise HTTPException(
        status_code=403,
        detail="선생님 계정만 소울이를 사용할 수 있습니다"
    )
HTTP 상태 코드 결정 트리

왜 이렇게 분리하는가?

REST 표준을 준수합니다:

  • 401 Unauthorized: "너 누구야?" (인증 실패)
  • 403 Forbidden: "너 여기 못 들어와" (인가 실패)
  • 503 Service Unavailable: "서비스 점검 중" (기능 비활성화)

프론트엔드 처리가 용이합니다:

  • 401 → 로그인 페이지로 리다이렉트
  • 403 → 권한 안내 메시지 표시
  • 503 → 서비스 점검 안내

한국어 에러 메시지: 최종 사용자(선생님)가 이해할 수 있는 메시지를 제공합니다.

9. 현재 아키텍처 정리

모든 결정을 종합하면 다음과 같은 아키텍처가 됩니다.

Soul-E MSA 인증 최종 아키텍처

핵심 결정 요약

결정 사항선택참고 패턴/사례
인증 공유 방식JWT Shared SecretNetflix Passport
데이터 접근 방식CQRS Read ModelNetflix/Uber
서비스 독립성yeirin 코드 변경 없음Amazon 철학
토큰 전략이중 토큰Google, AWS
DB 모델Anti-Corruption LayerDDD
의존성 주입FastAPI Depends프레임워크 표준
에러 처리HTTP 상태 코드 분리REST 표준

10. 앞으로의 과제: 예측되는 기술적 문제와 해결 전략

지금까지는 현재 해결한 문제를 다뤘습니다. 하지만 MSA를 운영하다 보면 반드시 마주치게 될 문제들이 있습니다. 미리 예측하고 준비하는 것이 CTO의 역할이라고 생각합니다.

과제 1: 분산 트랜잭션 — 상담 매칭 실패 시 어떻게 롤백할 것인가?

문제 상황

현재 예이린의 상담 매칭 플로우:

분산 트랜잭션이 필요한 상담 매칭 플로우
  1. 보호자가 상담기관 선택 (yeirin)
  2. 상담의뢰지 상태 변경: MATCHED (yeirin DB)
  3. 추천 AI에 매칭 완료 알림 (recommendation-ai)
  4. 기관에 매칭 알림 발송 (외부 알림 서비스)
  5. 상담 세션 생성 준비 (yeirin DB)

만약 3번에서 실패하면 어떻게 될까요?

  • yeirin DB에는 이미 MATCHED 상태로 변경됨
  • 하지만 추천 AI는 이를 모름
  • 기관도 알림을 못 받음
  • 데이터 불일치 발생!

이것이 바로 분산 트랜잭션(Distributed Transaction) 문제입니다.

왜 기존 방식이 안 되는가?

**2PC(Two-Phase Commit)**는 분산 트랜잭션의 고전적 해결책입니다. 하지만:

  • 모든 참여 서비스가 동시에 가용해야 함
  • 하나라도 응답이 없으면 전체가 블로킹
  • MSA의 독립 배포 철학과 충돌
  • 네트워크 지연에 취약

해결 전략: Saga 패턴

Saga 패턴 — Orchestration 방식

Saga 패턴은 분산 트랜잭션을 일련의 로컬 트랜잭션으로 분해합니다. 각 단계가 실패하면 **보상 트랜잭션(Compensating Transaction)**을 실행해 이전 상태로 되돌립니다.

정상 플로우:
T1 → T2 → T3 → T4 → 완료

T3에서 실패 시:
T1 → T2 → T3(실패) → C2 → C1 → 롤백 완료

T = 트랜잭션, C = 보상 트랜잭션

예이린에 적용할 Saga 설계

// Saga Orchestrator (yeirin 메인 백엔드에 구현)
class MatchCounselSaga {
  async execute(matchRequest: MatchRequest): Promise<Result<void, SagaError>> {
    const saga = new SagaBuilder()
      // Step 1: 상담의뢰지 상태 변경
      .addStep({
        name: 'updateCounselRequestStatus',
        execute: () => this.counselRequestService.updateStatus(
          matchRequest.counselRequestId,
          'MATCHED'
        ),
        compensate: () => this.counselRequestService.updateStatus(
          matchRequest.counselRequestId,
          'RECOMMENDATION_REQUESTED'  // 이전 상태로 복원
        ),
      })
      // Step 2: 추천 결과에 선택 표시
      .addStep({
        name: 'markRecommendationSelected',
        execute: () => this.recommendationService.markSelected(
          matchRequest.recommendationId
        ),
        compensate: () => this.recommendationService.unmarkSelected(
          matchRequest.recommendationId
        ),
      })
      // Step 3: 기관 알림 발송
      .addStep({
        name: 'notifyInstitution',
        execute: () => this.notificationService.sendMatchNotification(
          matchRequest.institutionId,
          matchRequest.counselRequestId
        ),
        compensate: () => this.notificationService.sendMatchCancelNotification(
          matchRequest.institutionId,
          matchRequest.counselRequestId
        ),
      })
      .build();

    return saga.run();
  }
}

두 가지 Saga 구현 방식

Choreography 방식:

  • 각 서비스가 이벤트를 발행/구독
  • 장점: 느슨한 결합
  • 단점: 흐름 파악 어려움, 순환 의존성 위험
  • 적합한 경우: 단순한 플로우 (2–3 단계)

Orchestration 방식:

  • 중앙 조정자가 흐름 제어
  • 장점: 명확한 흐름, 디버깅 용이
  • 단점: 조정자가 SPOF 가능
  • 적합한 경우: 복잡한 플로우 (4단계 이상)

예이린의 선택: Orchestration. 상담 매칭 플로우는 4단계 이상의 복잡한 비즈니스 로직입니다. Choreography로 구현하면 이벤트 흐름을 추적하기 어려워집니다. yeirin 메인 백엔드가 Saga Orchestrator 역할을 담당하는 것이 적합합니다.

구현 시 주의사항

멱등성(Idempotency) 보장:

// 같은 요청이 여러 번 와도 결과가 같아야 함
async markSelected(recommendationId: string): Promise<void> {
  const recommendation = await this.repository.findById(recommendationId);

  // 이미 선택된 상태면 skip (멱등성)
  if (recommendation.isSelected) {
    return;
  }

  recommendation.select();
  await this.repository.save(recommendation);
}

타임아웃과 재시도:

const sagaConfig = {
  timeout: 30000,  // 30초
  retryPolicy: {
    maxRetries: 3,
    backoff: 'exponential',
    initialDelay: 1000,
  },
};

과제 2: 분산 환경에서의 Observability — 요청이 어디서 실패했는지 어떻게 알 것인가?

문제 상황

서비스가 3개로 늘어났습니다. 사용자가 "상담 매칭이 안 돼요"라고 신고합니다.

어디서 문제가 생겼을까요?

  • yeirin에서 JWT 검증 실패?
  • Soul-E에서 세션 생성 실패?
  • 추천 AI에서 벡터 검색 타임아웃?
  • 네트워크 문제?
MSA에서의 요청 추적 문제

각 서비스의 로그를 하나씩 열어보는 것은 비효율적입니다. 더 큰 문제는 로그 간의 연결고리가 없다는 것입니다. yeirin 로그의 "사용자 A의 요청"과 Soul-E 로그의 "세션 생성 실패"가 같은 요청인지 알 수 없습니다.

해결 전략: 분산 추적(Distributed Tracing)

핵심 아이디어: Correlation ID (Trace ID). 하나의 사용자 요청이 시작될 때 고유 ID를 부여하고, 이 ID를 모든 서비스 호출에 전파합니다.

Correlation ID 전파 흐름

모든 서비스의 로그에 같은 abc-123-xyz가 찍히므로, 이 ID로 검색하면 하나의 요청이 거쳐간 모든 경로를 추적할 수 있습니다.

OpenTelemetry 도입 계획

OpenTelemetry는 분산 추적의 업계 표준입니다. Google, Microsoft, AWS 등이 공동으로 개발하고 있습니다.

# Soul-E (FastAPI)에 OpenTelemetry 적용
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

# 자동 계측 활성화
FastAPIInstrumentor.instrument_app(app)

# 수동으로 Span 추가 (비즈니스 로직 추적)
tracer = trace.get_tracer(__name__)

async def create_chat_session(child_id: str):
    with tracer.start_as_current_span("create_chat_session") as span:
        span.set_attribute("child.id", child_id)

        # LLM 호출 추적
        with tracer.start_as_current_span("llm_initial_greeting"):
            greeting = await llm_service.generate_greeting(child_id)

        # DB 저장 추적
        with tracer.start_as_current_span("save_session_to_db"):
            session = await session_repository.save(...)

        return session
// yeirin (NestJS)에 OpenTelemetry 적용
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  serviceName: 'yeirin-backend',
  traceExporter: new OTLPTraceExporter({
    url: 'http://jaeger:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

Observability 3가지 축

Traces (요청 흐름 추적):

  • 도구: Jaeger, Zipkin
  • 용도: 요청이 어떤 경로로 흘러갔는지 시각화

Metrics (시스템 상태 모니터링):

  • 도구: Prometheus, Grafana
  • 용도: CPU, 메모리, 응답 시간, 에러율 등 수치 모니터링

Logs (상세 이벤트 기록):

  • 도구: ELK Stack, Loki
  • 용도: 디버깅을 위한 상세 로그 수집/검색
Observability 통합 아키텍처

단계별 도입 계획

Phase 1: Correlation ID 전파 (즉시)

# 미들웨어로 Correlation ID 생성/전파
@app.middleware("http")
async def correlation_middleware(request: Request, call_next):
    correlation_id = request.headers.get("X-Correlation-ID") or str(uuid4())

    # 컨텍스트에 저장
    request.state.correlation_id = correlation_id

    # 로그에 포함
    logger.info(
        "Request started",
        extra={"correlation_id": correlation_id, "path": request.url.path}
    )

    response = await call_next(request)
    response.headers["X-Correlation-ID"] = correlation_id

    return response

Phase 2: 구조화된 로깅 (1개월 내)

{
    "timestamp": "2025-01-15T10:30:00Z",
    "level": "INFO",
    "service": "soul-e",
    "correlation_id": "abc-123-xyz",
    "span_id": "span-456",
    "message": "Chat session created",
    "context": {
        "child_id": "child-789",
        "session_id": "session-012"
    }
}

Phase 3: OpenTelemetry 통합 (3개월 내)

  • Jaeger로 분산 추적 시각화
  • Prometheus + Grafana로 메트릭 대시보드
  • 알림 설정 (에러율, 지연시간 임계치)

과제 대응 로드맵

시기과제구현 내용
즉시Correlation ID모든 서비스에 X-Correlation-ID 전파
1개월구조화된 로깅JSON 로그 포맷, 로그 수집기 설정
2개월Saga 패턴상담 매칭 플로우에 Orchestration 적용
3개월OpenTelemetry분산 추적, 메트릭 대시보드 구축
6개월알림 시스템이상 탐지, 자동 알림 설정

마치며: 결정의 기록이 자산이다

이번 글에서는 Soul-E MSA 인증 아키텍처의 의사결정 과정을 공유하고, 앞으로 마주할 기술적 과제에 대한 해결 전략을 미리 설계해보았습니다.

솔직히 말하면, 이 결정들이 "정답"인지는 모르겠습니다. 1년 후에는 "그때 왜 그랬지?"라고 후회할 수도 있습니다. 하지만 중요한 것은 왜 그렇게 결정했는지 기록해두는 것입니다.

ADR(Architecture Decision Record)이라는 개념이 있습니다. 아키텍처 결정을 문서화하는 것이죠. 이 글 자체가 예이린의 ADR입니다.

나중에 누군가(미래의 나 포함)가 "왜 JWT Shared Secret을 썼어?"라고 물으면, 이 글을 보여주면 됩니다. "이런 선택지들이 있었고, 이런 이유로 이걸 선택했어"라고요. 그리고 "분산 트랜잭션은 어떻게 처리할 거야?"라고 물으면, Saga 패턴 설계를 보여줄 수 있습니다.

문제를 예측하고 대비하는 것, 그것이 아키텍트의 역할이라고 생각합니다.

다음 편에서는 더 실질적인 이야기를 해보려 합니다. Claude Code와 LLM 도구들을 어떻게 활용해 이 모든 것을 1인 개발로 해냈는지, 그 구체적인 방법론과 시행착오를 공유하겠습니다.


예이린 사회적협동조합은 기술로 돌봄을 새롭게, 아이들의 내일을 따뜻하게 만들어갑니다.

다음 편: "1인 CTO의 생존 전략 — Claude Code와 함께한 개발 일지"