비영리 스타트업의 MSA 여정 — 아키텍처, 예이린은 어떻게 풀어나갔는가
· Architecture · 12 min
예이린 사회적협동조합 기술블로그 시리즈 EP.3 — Medium에 함께 게재된 글입니다.
지난 글에서는 예이린 플랫폼의 전체 아키텍처 설계를 공유했습니다. MSA로 분리한 세 개의 서비스, DDD의 4개 계층, TDD와 결합한 견고한 도메인 모델까지. 하지만 실제로 두 개의 서비스가 통신하기 시작하면, 첫 번째로 마주치는 문제가 있습니다: "이 요청, 누가 보낸 거야?"
인증(Authentication)과 인가(Authorization) 문제입니다.
1. 문제 정의: 두 서비스, 두 가지 사용자
Soul-E 서비스의 두 가지 사용자
Soul-E(소울이)는 AI 기반 심리상담 챗봇입니다. 하지만 이 서비스에는 두 종류의 사용자가 있습니다:
- 선생님(Teacher): 아동을 선택하고 채팅 세션을 시작하는 사람
- 아동(Child): 실제로 소울이와 대화하는 사람
선생님은 yeirin에 이미 가입된 사용자이며 JWT 토큰을 가지고 있습니다. 하지만 아동은 yeirin에 직접 가입하지 않고 보호자(선생님)가 등록한 "아동 정보"로만 존재합니다.

이 두 사용자를 어떻게 인증할 것인가? 이것이 첫 번째 질문이었습니다.
2. 결정 1: MSA 환경에서의 인증 공유 방식
고민했던 선택지들
| 방식 | 장점 | 단점 |
|---|---|---|
| API Gateway 통합 | 중앙 집중화 | SPOF, 복잡한 로직 |
| OAuth2/OIDC | 업계 표준, 확장성 | 구현 복잡도, 추가 인프라 |
| JWT Shared Secret | 단순함, 빠른 구현 | Secret 관리, 회전 어려움 |
| mTLS (Service Mesh) | 강력한 보안 | 인프라 복잡도 높음 |
선택: JWT Shared Secret 방식
yeirin에서 발급한 JWT를 Soul-E에서도 동일한 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(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이 이미 제공하는 것만 사용했습니다:
- JWT 구조 (문서화된 계약)
- 데이터베이스 스키마 (읽기 전용 접근)
Soul-E가 필요한 데이터 조합은 Soul-E가 직접 합니다. yeirin은 변경할 필요가 없습니다.
5. 결정 4: 이중 토큰 전략
문제 상황
선생님은 yeirin JWT로 인증합니다. 그런데 아동은 어떻게 인증할까요?
아동은 yeirin에 계정이 없고 직접 로그인할 수도 없습니다. 하지만 채팅을 하려면 "이 채팅이 어떤 아동의 것인지" 알아야 합니다.
고민했던 선택지들
| 방식 | 장점 | 단점 |
|---|---|---|
| 단일 토큰 | 단순함 | 아동 컨텍스트 분리 어려움 |
| 토큰 교환 | 명확한 분리 | 추가 왕복 필요 |
| 토큰 확장 | 한 번에 모든 정보 | yeirin 변경 필요 |
| 이중 토큰 | 역할 분리 | 복잡도 증가 |
선택: 이중 토큰 전략
저는 두 개의 토큰을 사용하기로 했습니다:

| yeirin JWT | child_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 Model | CQRS 원칙 준수 | 중복 코드 |
| Auto-generate | 동기화 보장 | 도구 의존성 |
선택: Read Model (별도 읽기 전용 모델)
저는 별도의 읽기 전용 모델을 정의했습니다.

왜 이 선택인가?
첫째, 반부패 계층(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="선생님 계정만 소울이를 사용할 수 있습니다"
)
왜 이렇게 분리하는가?
REST 표준을 준수합니다:
- 401 Unauthorized: "너 누구야?" (인증 실패)
- 403 Forbidden: "너 여기 못 들어와" (인가 실패)
- 503 Service Unavailable: "서비스 점검 중" (기능 비활성화)
프론트엔드 처리가 용이합니다:
- 401 → 로그인 페이지로 리다이렉트
- 403 → 권한 안내 메시지 표시
- 503 → 서비스 점검 안내
한국어 에러 메시지: 최종 사용자(선생님)가 이해할 수 있는 메시지를 제공합니다.
9. 현재 아키텍처 정리
모든 결정을 종합하면 다음과 같은 아키텍처가 됩니다.

핵심 결정 요약
| 결정 사항 | 선택 | 참고 패턴/사례 |
|---|---|---|
| 인증 공유 방식 | JWT Shared Secret | Netflix Passport |
| 데이터 접근 방식 | CQRS Read Model | Netflix/Uber |
| 서비스 독립성 | yeirin 코드 변경 없음 | Amazon 철학 |
| 토큰 전략 | 이중 토큰 | Google, AWS |
| DB 모델 | Anti-Corruption Layer | DDD |
| 의존성 주입 | FastAPI Depends | 프레임워크 표준 |
| 에러 처리 | HTTP 상태 코드 분리 | REST 표준 |
10. 앞으로의 과제: 예측되는 기술적 문제와 해결 전략
지금까지는 현재 해결한 문제를 다뤘습니다. 하지만 MSA를 운영하다 보면 반드시 마주치게 될 문제들이 있습니다. 미리 예측하고 준비하는 것이 CTO의 역할이라고 생각합니다.
과제 1: 분산 트랜잭션 — 상담 매칭 실패 시 어떻게 롤백할 것인가?
문제 상황
현재 예이린의 상담 매칭 플로우:

- 보호자가 상담기관 선택 (yeirin)
- 상담의뢰지 상태 변경: MATCHED (yeirin DB)
- 추천 AI에 매칭 완료 알림 (recommendation-ai)
- 기관에 매칭 알림 발송 (외부 알림 서비스)
- 상담 세션 생성 준비 (yeirin DB)
만약 3번에서 실패하면 어떻게 될까요?
- yeirin DB에는 이미 MATCHED 상태로 변경됨
- 하지만 추천 AI는 이를 모름
- 기관도 알림을 못 받음
- 데이터 불일치 발생!
이것이 바로 분산 트랜잭션(Distributed Transaction) 문제입니다.
왜 기존 방식이 안 되는가?
**2PC(Two-Phase Commit)**는 분산 트랜잭션의 고전적 해결책입니다. 하지만:
- 모든 참여 서비스가 동시에 가용해야 함
- 하나라도 응답이 없으면 전체가 블로킹
- MSA의 독립 배포 철학과 충돌
- 네트워크 지연에 취약
해결 전략: Saga 패턴

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에서 벡터 검색 타임아웃?
- 네트워크 문제?

각 서비스의 로그를 하나씩 열어보는 것은 비효율적입니다. 더 큰 문제는 로그 간의 연결고리가 없다는 것입니다. yeirin 로그의 "사용자 A의 요청"과 Soul-E 로그의 "세션 생성 실패"가 같은 요청인지 알 수 없습니다.
해결 전략: 분산 추적(Distributed Tracing)
핵심 아이디어: Correlation ID (Trace ID). 하나의 사용자 요청이 시작될 때 고유 ID를 부여하고, 이 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
- 용도: 디버깅을 위한 상세 로그 수집/검색

단계별 도입 계획
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 responsePhase 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와 함께한 개발 일지"