티스토리 뷰
외부 API 호출 코드는 왜 금방 지저분해질까? 결제·메일·AI API를 위한 HTTP 클라이언트 계층 설계법 — FastAPI · Spring Boot · Node.js
octo54 2026. 5. 26. 11:37외부 API 호출 코드는 왜 금방 지저분해질까? 결제·메일·AI API를 위한 HTTP 클라이언트 계층 설계법 — FastAPI · Spring Boot · Node.js
백엔드 만들다 보면 어느 순간부터 “우리 DB만 잘 다루면 되는 단계”를 지나갑니다.
그다음부터는 거의 반드시 외부 서비스를 붙이게 돼요.
- 결제 API
- 메일 발송 API
- 문자 API
- 지도/주소 API
- AI API
- 사내 다른 백엔드 API
이때 초반엔 보통 이렇게 시작합니다.
컨트롤러에서 바로 HTTP 호출.
서비스에서 fetch 한 번.
급하면 타임아웃도 없이 그냥 요청.
실패하면 try-catch로 한 줄 감싸기.
근데 이 방식이 진짜 빨리 무너져요.
왜냐하면 외부 API 호출은 생각보다 “호출 한 번”이 아니거든요.
- 타임아웃
- 재시도
- 에러 매핑
- idempotency
- 인증 헤더
- 로깅
- 서킷 브레이커나 백오프
- 응답 스키마 검증
이게 같이 따라옵니다. Spring Framework는 RestClient를 동기 HTTP 클라이언트로, WebClient를 비동기·리액티브 HTTP 클라이언트로 공식 제공하고 있고, HTTPX는 Python에서 sync/async API와 기본 타임아웃을 제공한다고 설명합니다. Node.js는 AbortSignal 기반 중단 모델을 공식 제공하고요. (Home)
저는 이 파트를 꽤 중요하게 봐요.
외부 API 호출 구조를 어떻게 잡느냐가, 나중에 “이 서비스가 운영 가능한가”를 크게 갈라버리거든요.
이번 글에서는 그걸 정리해보겠습니다.
- 외부 API 호출 코드를 어디 계층에 둘지
- 타임아웃과 재시도는 어디서 다룰지
- 멱등성(idempotency)은 왜 중요한지
- FastAPI · Spring Boot · Node.js에서 최소 구현을 어떻게 시작하면 좋은지
외부 API 호출은 왜 특별하게 다뤄야 할까
내 DB 조회는 보통 내가 통제할 수 있어요.
하지만 외부 API는 다릅니다.
- 느릴 수 있고
- 아예 죽어 있을 수 있고
- 429를 줄 수 있고
- 같은 요청을 두 번 보내면 사고가 날 수도 있고
- 응답 형식이 바뀔 수도 있어요
그래서 외부 API는 “그냥 함수 호출”처럼 다루면 안 됩니다.
HTTPX 문서는 기본적으로 네트워크 비활성 5초 타임아웃을 둔다고 설명하고, Spring 쪽은 retry와 resilience 기능을 별도로 제공합니다. Stripe는 POST 요청에 idempotency key를 써서 네트워크 오류 후 안전하게 재시도하라고 권장합니다. (python-httpx.org)
즉 외부 API 호출은 보통 이런 속성을 가진다고 보면 됩니다.
- 실패 가능성이 높음
- 느릴 수 있음
- 중복 호출이 위험할 수 있음
- 호출 정책이 필요함
제가 추천하는 기본 계층 구조
저는 보통 이렇게 나눕니다.
- Controller/Route: 요청 받기, 서비스 호출
- Service: 비즈니스 흐름 결정
- External Client: 실제 HTTP 호출
- DTO/Schema: 외부 요청/응답 모델
- Policy: 타임아웃, 재시도, 멱등성 규칙
핵심은 이거예요.
서비스는 “무엇을 요청할지”를 알고,
클라이언트는 “어떻게 HTTP로 보낼지”를 안다.
이걸 안 나누면 금방 생기는 문제가 있어요.
- 컨트롤러에서 외부 API 직접 호출
- 서비스마다 인증 헤더 복붙
- timeout 값이 파일마다 다름
- 에러 메시지가 제각각
- 테스트가 어려움
Spring의 RestClient는 fluent API 기반의 동기 REST 클라이언트이고, HTTPX는 클라이언트 인스턴스와 async 지원을 제공합니다. 이런 도구들은 결국 “호출 구현을 한곳에 모으라”는 방향과 잘 맞습니다. (Home)
타임아웃은 왜 꼭 넣어야 할까
이건 진짜 기본기예요.
외부 API 호출에 타임아웃이 없으면,
상대 서비스가 멈췄을 때 우리 요청도 같이 매달립니다.
HTTPX는 기본적으로 네트워크 비활성 5초 타임아웃을 둔다고 설명하고, quickstart에서도 같은 내용을 반복합니다. Node.js 쪽은 AbortSignal로 중단을 걸 수 있고, abort되면 ABORT_ERR가 날 수 있다고 설명합니다. (python-httpx.org)
제가 추천하는 감각은 이렇습니다.
- 외부 API는 항상 timeout
- 서비스별로 timeout 다를 수 있음
- 결제 같은 중요한 호출은 더 신중하게
- AI API처럼 길 수 있는 호출도 상한선은 둬야 함
재시도는 무조건 넣으면 좋을까
아니요. 여기서 실수 많이 합니다.
재시도는 강력하지만, 아무 요청이나 막 재시도하면 중복 실행 사고가 날 수 있어요.
그래서 “어떤 요청만 재시도 가능한가”를 먼저 봐야 합니다.
HTTPX는 transport 레벨에서 ConnectError나 ConnectTimeout에 대한 연결 재시도를 지원하지만, 더 일반적인 재시도 정책은 tenacity 같은 도구를 고려하라고 설명합니다. Spring 쪽은 @Retryable과 retry policy를 공식 제공하고요. Stripe는 POST 요청엔 idempotency key를 써야 안전하게 재시도할 수 있다고 설명합니다. (python-httpx.org)
즉 보통 이렇게 생각하면 됩니다.
- GET: 비교적 재시도 친화적
- POST: 멱등성 없으면 위험
- 결제/주문 생성: idempotency key 없으면 조심
idempotency는 왜 중요할까
이건 결제나 주문에서 특히 중요합니다.
예를 들어 결제 생성 요청을 보냈는데 응답 직전에 네트워크가 끊겼다고 해볼게요.
클라이언트는 “실패한 줄 알고” 다시 보낼 수 있죠.
그런데 실제론 첫 요청이 이미 성공했을 수도 있어요.
이때 멱등성이 없으면 같은 결제가 두 번 생길 수 있습니다.
Stripe는 idempotency key를 사용하면 네트워크 오류 후에도 같은 POST 요청을 안전하게 재시도할 수 있다고 설명합니다. 같은 API와 같은 계정 범위에서 같은 키를 쓰면 동일 작업으로 본다고도 안내합니다. (Stripe Docs)
그래서 저는 외부 API 호출 설계에서 이 질문을 꼭 던집니다.
“이 요청은 두 번 가면 안전한가?”
안전하지 않다면,
- idempotency key를 지원하는지 확인
- 우리 쪽에서 operation id를 묶는지 확인
- 재시도를 자동으로 할지 신중히 결정
이번 글에서 맞출 공통 예제
이번에는 결제 API 느낌의 예제로 맞추겠습니다.
실제 Stripe SDK를 쓰기보다, “외부 결제 API를 호출하는 우리 클라이언트 계층” 구조를 보여드릴게요.
흐름은 이렇습니다.
- PaymentService가 결제 생성 업무를 처리
- PaymentGatewayClient가 외부 결제 API 호출
- timeout 적용
- idempotency key 헤더 포함
- 외부 에러를 내부 에러로 변환
즉 오늘의 핵심은 이겁니다.
외부 API 호출은 서비스 안에 흩뿌리지 말고, 전용 클라이언트 계층으로 모아야 한다.
1) FastAPI에서 HTTPX로 외부 API 클라이언트 만들기
FastAPI는 async 웹 프레임워크라서, 외부 HTTP 호출도 보통 async 클라이언트로 맞추는 게 자연스럽습니다. HTTPX는 async client를 제공하고, async 웹 프레임워크와 함께 쓸 때 async client를 권장합니다. 또한 기본 타임아웃을 두고 있습니다. (python-httpx.org)
추천 구조
fastapi-backend/
├── app/
│ ├── api/
│ │ └── payment.py
│ ├── clients/
│ │ └── payment_gateway_client.py
│ ├── schemas/
│ │ └── payment.py
│ ├── services/
│ │ └── payment_service.py
│ └── main.py
app/schemas/payment.py
from pydantic import BaseModel, Field
class CreatePaymentRequest(BaseModel):
order_id: str = Field(min_length=1, max_length=100)
amount: int = Field(gt=0)
currency: str = Field(min_length=3, max_length=3)
class PaymentGatewayCreateRequest(BaseModel):
order_id: str
amount: int
currency: str
class PaymentGatewayCreateResponse(BaseModel):
payment_id: str
status: str
class CreatePaymentResponse(BaseModel):
payment_id: str
status: str
app/clients/payment_gateway_client.py
import httpx
from app.schemas.payment import (
PaymentGatewayCreateRequest,
PaymentGatewayCreateResponse,
)
class PaymentGatewayClient:
def __init__(self, base_url: str, api_key: str) -> None:
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = httpx.Timeout(5.0)
async def create_payment(
self,
request: PaymentGatewayCreateRequest,
idempotency_key: str,
) -> PaymentGatewayCreateResponse:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/payments",
json=request.model_dump(),
headers={
"Authorization": f"Bearer {self.api_key}",
"Idempotency-Key": idempotency_key,
},
)
response.raise_for_status()
return PaymentGatewayCreateResponse(**response.json())
app/services/payment_service.py
import uuid
from app.clients.payment_gateway_client import PaymentGatewayClient
from app.schemas.payment import (
CreatePaymentRequest,
CreatePaymentResponse,
PaymentGatewayCreateRequest,
)
class PaymentService:
def __init__(self, gateway_client: PaymentGatewayClient) -> None:
self.gateway_client = gateway_client
async def create_payment(
self,
request: CreatePaymentRequest,
) -> CreatePaymentResponse:
idempotency_key = str(uuid.uuid4())
gateway_response = await self.gateway_client.create_payment(
PaymentGatewayCreateRequest(
order_id=request.order_id,
amount=request.amount,
currency=request.currency,
),
idempotency_key=idempotency_key,
)
return CreatePaymentResponse(
payment_id=gateway_response.payment_id,
status=gateway_response.status,
)
app/api/payment.py
from fastapi import APIRouter, HTTPException
from app.clients.payment_gateway_client import PaymentGatewayClient
from app.schemas.payment import CreatePaymentRequest, CreatePaymentResponse
from app.services.payment_service import PaymentService
router = APIRouter(prefix="/api/payments", tags=["payments"])
gateway_client = PaymentGatewayClient(
base_url="https://example-payment-gateway.test",
api_key="demo-api-key",
)
payment_service = PaymentService(gateway_client)
@router.post("", response_model=CreatePaymentResponse)
async def create_payment(request: CreatePaymentRequest) -> CreatePaymentResponse:
try:
return await payment_service.create_payment(request)
except Exception as e:
raise HTTPException(status_code=502, detail=f"결제 게이트웨이 호출 실패: {str(e)}")
app/main.py
from fastapi import FastAPI
from app.api.payment import router as payment_router
app = FastAPI()
app.include_router(payment_router)
FastAPI에서 핵심 감각
여기서 중요한 건 PaymentService가 httpx.post(...)를 직접 모른다는 점이에요.
HTTP 호출은 PaymentGatewayClient가 전담합니다.
그리고 HTTPX는 기본적으로 timeout을 강제하므로, 외부 호출이 영원히 hang 되지 않게 하는 출발점이 좋습니다. async client도 공식 지원하고요. 연결 재시도는 transport 수준에서 제한적으로 가능하지만, 일반 재시도 정책은 별도 도구를 고려하는 게 맞습니다. (python-httpx.org)
2) Spring Boot에서 RestClient로 외부 결제 클라이언트 만들기
Spring Framework는 RestClient를 동기 HTTP 클라이언트로 공식 제공하고, fluent API로 요청을 구성할 수 있다고 설명합니다. 스프링 진영에서 외부 API 호출을 구조화할 때 꽤 좋은 출발점이에요. (Home)
추천 구조
springboot-backend/
├── src/main/java/com/example/backend/
│ ├── controller/
│ │ └── PaymentController.java
│ ├── dto/
│ │ ├── CreatePaymentRequest.java
│ │ ├── CreatePaymentResponse.java
│ │ ├── PaymentGatewayCreateRequest.java
│ │ └── PaymentGatewayCreateResponse.java
│ ├── client/
│ │ └── PaymentGatewayClient.java
│ └── service/
│ └── PaymentService.java
dto/CreatePaymentRequest.java
package com.example.backend.dto;
public record CreatePaymentRequest(
String orderId,
int amount,
String currency
) {
}
dto/PaymentGatewayCreateRequest.java
package com.example.backend.dto;
public record PaymentGatewayCreateRequest(
String orderId,
int amount,
String currency
) {
}
dto/PaymentGatewayCreateResponse.java
package com.example.backend.dto;
public record PaymentGatewayCreateResponse(
String paymentId,
String status
) {
}
dto/CreatePaymentResponse.java
package com.example.backend.dto;
public record CreatePaymentResponse(
String paymentId,
String status
) {
}
client/PaymentGatewayClient.java
package com.example.backend.client;
import com.example.backend.dto.PaymentGatewayCreateRequest;
import com.example.backend.dto.PaymentGatewayCreateResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class PaymentGatewayClient {
private final RestClient restClient;
public PaymentGatewayClient() {
this.restClient = RestClient.builder()
.baseUrl("https://example-payment-gateway.test")
.defaultHeader("Authorization", "Bearer demo-api-key")
.build();
}
public PaymentGatewayCreateResponse createPayment(
PaymentGatewayCreateRequest request,
String idempotencyKey
) {
return restClient.post()
.uri("/payments")
.contentType(MediaType.APPLICATION_JSON)
.header("Idempotency-Key", idempotencyKey)
.body(request)
.retrieve()
.body(PaymentGatewayCreateResponse.class);
}
}
service/PaymentService.java
package com.example.backend.service;
import com.example.backend.client.PaymentGatewayClient;
import com.example.backend.dto.*;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class PaymentService {
private final PaymentGatewayClient paymentGatewayClient;
public PaymentService(PaymentGatewayClient paymentGatewayClient) {
this.paymentGatewayClient = paymentGatewayClient;
}
public CreatePaymentResponse createPayment(CreatePaymentRequest request) {
String idempotencyKey = UUID.randomUUID().toString();
PaymentGatewayCreateResponse gatewayResponse =
paymentGatewayClient.createPayment(
new PaymentGatewayCreateRequest(
request.orderId(),
request.amount(),
request.currency()
),
idempotencyKey
);
return new CreatePaymentResponse(
gatewayResponse.paymentId(),
gatewayResponse.status()
);
}
}
controller/PaymentController.java
package com.example.backend.controller;
import com.example.backend.dto.CreatePaymentRequest;
import com.example.backend.dto.CreatePaymentResponse;
import com.example.backend.service.PaymentService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping
public CreatePaymentResponse createPayment(@RequestBody CreatePaymentRequest request) {
return paymentService.createPayment(request);
}
}
Spring Boot에서 핵심 감각
Spring에서는 외부 API 호출 코드를 서비스에 박아넣기보다
client 패키지로 빼는 게 진짜 중요해요.
그리고 Spring은 retry와 resilience 기능도 공식적으로 제공합니다. @Retryable로 재시도 정책을 붙일 수 있고, 최대 재시도 횟수 개념도 문서화돼 있습니다. 다만 결제 생성 같은 POST 요청은 멱등성 키 없이 무턱대고 재시도하면 안 됩니다. (Home)
3) Node.js에서 fetch + AbortSignal로 외부 API 클라이언트 만들기
Node.js는 요즘 fetch와 AbortSignal 조합으로 시작하기 좋습니다. Node 공식 문서는 AbortSignal과 AbortSignal.timeout()을 제공하고, abort 시 ABORT_ERR가 날 수 있다고 설명합니다. (Node.js)
추천 구조
node-backend/
├── src/
│ ├── routes/
│ │ └── payment.route.js
│ ├── services/
│ │ └── payment.service.js
│ └── clients/
│ └── payment-gateway.client.js
src/clients/payment-gateway.client.js
export class PaymentGatewayClient {
constructor({ baseUrl, apiKey }) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.apiKey = apiKey;
}
async createPayment({ orderId, amount, currency, idempotencyKey }) {
const response = await fetch(`${this.baseUrl}/payments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`,
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
orderId,
amount,
currency,
}),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
throw new Error(`payment gateway error: ${response.status}`);
}
return response.json();
}
}
src/services/payment.service.js
import crypto from "node:crypto";
export class PaymentService {
constructor(paymentGatewayClient) {
this.paymentGatewayClient = paymentGatewayClient;
}
async createPayment({ orderId, amount, currency }) {
const idempotencyKey = crypto.randomUUID();
const gatewayResponse = await this.paymentGatewayClient.createPayment({
orderId,
amount,
currency,
idempotencyKey,
});
return {
paymentId: gatewayResponse.paymentId,
status: gatewayResponse.status,
};
}
}
src/routes/payment.route.js
import { Router } from "express";
import { PaymentGatewayClient } from "../clients/payment-gateway.client.js";
import { PaymentService } from "../services/payment.service.js";
const router = Router();
const paymentGatewayClient = new PaymentGatewayClient({
baseUrl: "https://example-payment-gateway.test",
apiKey: "demo-api-key",
});
const paymentService = new PaymentService(paymentGatewayClient);
router.post("/", async (req, res) => {
const { orderId, amount, currency } = req.body;
if (!orderId || !amount || !currency) {
return res.status(400).json({ message: "필수값이 누락되었습니다." });
}
try {
const result = await paymentService.createPayment({
orderId,
amount,
currency,
});
return res.json(result);
} catch (error) {
return res.status(502).json({ message: error.message });
}
});
export default router;
src/server.js
import express from "express";
import paymentRouter from "./routes/payment.route.js";
const app = express();
const PORT = 3000;
app.use(express.json());
app.use("/api/payments", paymentRouter);
app.listen(PORT, () => {
console.log(`server running on http://localhost:${PORT}`);
});
Node.js에서 핵심 감각
Node.js는 외부 API 호출 구조를 안 나누면
라우터가 제일 먼저 더러워집니다.
그래서 저는 꼭 이렇게 나누는 편이에요.
- route: 요청/응답
- service: 비즈니스 흐름
- client: 실제 fetch
그리고 timeout은 AbortSignal.timeout()으로 꼭 주는 편이 좋습니다. Node는 abort 기반 중단 모델을 공식적으로 제공하고, abort 시 에러 코드도 문서화돼 있습니다. (Node.js)
그럼 재시도는 코드 어디에 둘까
제 기준엔 보통 이렇습니다.
- client 근처에 둔다
- 단, 무조건 자동 재시도는 안 한다
- 재시도 가능한 에러만 선별한다
- POST면 idempotency 먼저 확인한다
Spring은 retry를 프레임워크 차원에서 붙이기 쉽고, HTTPX는 transport나 외부 도구로 재시도를 설계할 수 있습니다. Stripe는 네트워크 오류 후 POST 재시도엔 idempotency key를 사실상 기본처럼 권합니다. (Home)
즉 “재시도 = 무조건 좋음”이 아니라,
재시도 가능한 요청만, 멱등성이 있을 때만
이 감각이 중요해요.
외부 API 에러를 그대로 사용자에게 보여주면 안 되는 이유
이것도 진짜 많이 하는 실수예요.
외부 결제 API가 이런 응답을 줬다고 해봅시다.
- invalid_request_error
- upstream_timeout
- authentication_failed
- 429 rate limit
이걸 프론트에 그대로 흘려보내면
우리 서비스의 에러 정책이 외부 벤더에 종속됩니다.
그래서 보통 이렇게 해야 합니다.
- 외부 에러는 client에서 1차 해석
- service에서 우리 비즈니스 에러로 변환
- controller는 최종 응답만 구성
즉 에러도 “계층 분리”가 필요합니다.
제가 추천하는 아주 현실적인 기본 규칙
외부 API 클라이언트를 붙일 때 저는 보통 이 6개를 먼저 봅니다.
- timeout 있다
- 인증 헤더 중앙화했다
- base URL 한곳에서 관리한다
- idempotency 필요한지 판단했다
- 응답 DTO 분리했다
- 에러를 그대로 노출하지 않는다
이 정도만 지켜도 코드가 훨씬 덜 망가집니다.
주니어 때 자주 하는 실수
1. 컨트롤러에서 바로 외부 API 호출
처음엔 빠르지만 금방 중복됩니다.
2. timeout 없음
이건 운영에서 진짜 무섭습니다. HTTPX는 기본 타임아웃이 있지만, 다른 곳은 직접 챙겨야 할 수 있습니다. (python-httpx.org)
3. POST 재시도를 멱등성 없이 자동화
결제/주문 생성에선 사고 납니다. Stripe가 idempotency key를 강하게 권하는 이유예요. (Stripe Docs)
4. 외부 응답 JSON을 서비스 전체에서 직접 파싱
응답 모델이 흩어집니다.
5. 벤더별 헤더/URL/에러 처리 코드가 서비스마다 복붙
나중에 가장 먼저 지저분해집니다.
이번 글 핵심 정리
이번 글의 핵심은 이겁니다.
외부 API 호출은 “HTTP 요청 한 번”이 아니라, 정책이 있는 인프라 의존성이다.
정리하면 이렇게 볼 수 있어요.
- 외부 호출은 전용 client 계층으로 분리
- 서비스는 비즈니스 흐름만 담당
- timeout은 기본
- 재시도는 선별적으로
- POST 재시도 전엔 idempotency 확인
- 에러는 우리 서비스 기준으로 번역
스택별 감각은 이렇습니다.
- FastAPI: async라면 HTTPX AsyncClient가 자연스럽고, 기본 타임아웃 감각이 좋다. (python-httpx.org)
- Spring Boot: RestClient와 retry/resilience 구성이 외부 API 계층화에 잘 맞는다. (Home)
- Node.js: fetch + AbortSignal.timeout() 조합으로 가볍고 명확하게 시작할 수 있다. (Node.js)
다음 글 예고
다음 글에서는 이어서
백엔드에서 비동기 작업과 큐를 언제 도입해야 하는지를 다뤄보겠습니다.
즉,
- 왜 메일 발송이나 이미지 변환을 요청-응답 안에서 다 처리하면 버거워지는지
- 동기 처리 vs 비동기 처리 기준은 뭔지
- 큐, 워커, 재시도, DLQ는 어떤 감각으로 이해하면 되는지
- FastAPI · Spring Boot · Node.js에서 어디서부터 비동기 작업을 분리하면 좋은지
이 흐름으로 이어가겠습니다.
출처
- HTTPX 공식 문서 — async support, default timeouts, transports/retries. (python-httpx.org)
- Spring Framework 공식 문서 — RestClient. (Home)
- Spring Framework 공식 문서 — resilience/retry (@Retryable). (Home)
- Node.js 공식 문서 — AbortSignal, abort behavior, abort-related error handling. (Node.js)
- Stripe 공식 문서 — idempotent requests, POST 요청에서의 idempotency key 사용 권장. (Stripe Docs)
백엔드개발, FastAPI, SpringBoot, Nodejs, Express, 외부API연동, HTTPClient, Idempotency, Timeout, 백엔드시리즈
'study > 백엔드' 카테고리의 다른 글
- Total
- Today
- Yesterday
- JWT
- PostgreSQL
- Python
- Express
- 주니어개발자
- nextJS
- seo 최적화 10개
- NestJS
- rag
- 쿠버네티스
- 웹개발
- flax
- nodejs
- 생성형AI
- JAX
- DevOps
- SEO최적화
- Prisma
- llm
- Next.js
- REACT
- SpringBoot
- fastapi
- CI/CD
- 딥러닝
- kotlin
- 개발블로그
- 백엔드개발
- node.js
- LangChain
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |

