티스토리 뷰
서비스 다음은 Repository다 — 데이터 접근을 어디서 끊어야 코드가 안 무너질까? FastAPI · Spring Boot · Node.js
octo54 2026. 4. 3. 11:31서비스 다음은 Repository다 — 데이터 접근을 어디서 끊어야 코드가 안 무너질까? FastAPI · Spring Boot · Node.js
서비스 계층까지 나누고 나면, 이제 거의 반드시 나오는 질문이 있어요.
“그럼 DB 조회는 어디서 하지?”
“서비스에서 바로 SQL 써도 되나?”
“repository를 꼭 따로 만들어야 하나?”
저는 이 질문이 되게 중요하다고 생각해요.
왜냐하면 여기서 한 번 방향을 잘못 잡으면, 나중에 서비스 코드가 슬금슬금 DB 구현 세부사항까지 다 알아버리거든요. 처음엔 별거 아닌데, 쿼리가 늘고 조건이 복잡해지고 저장 방식이 바뀌기 시작하면 그때부터 코드가 탁해집니다.
이번 글에서는 Repository 계층 이야기를 해보겠습니다.
정확히는,
- repository가 왜 필요한지
- 서비스와 repository를 어디서 끊는지
- FastAPI / Spring Boot / Node.js에서 어떻게 시작하면 덜 망가지는지
이걸 실무 감각으로 정리해볼게요.
FastAPI 공식 문서는 SQL 데이터베이스 예제에서 SQLModel/SQLAlchemy 세션을 의존성으로 분리해 주입하는 방식을 보여주고, 큰 애플리케이션은 여러 파일로 나누는 구조를 안내합니다. (FastAPI)
Spring Data JPA는 JPA 기반 repository 구현을 쉽게 하도록 설계되어 있고, Spring Data JPA 프로젝트 페이지와 공식 레퍼런스는 repository 지원을 핵심 가치로 설명합니다. (Home)
Express는 라우팅과 미들웨어 중심의 가벼운 프레임워크이고, 공식 FAQ에도 데이터베이스 통합은 별도 주제로 다뤄질 만큼 애플리케이션 구조를 개발자가 직접 조합하는 성격이 강합니다. (expressjs.com)
Repository는 뭘 하는 계층일까
저는 repository를 어렵게 말하면 오히려 감이 안 온다고 봐요.
실무적으로는 그냥 이렇게 이해하면 됩니다.
repository는 “데이터를 어디서, 어떻게 가져오고 저장하는지”를 감추는 계층입니다.
즉 서비스는
“이메일 중복이 있나?”
“사용자를 저장해야 하나?”
“특정 조건의 주문을 찾아야 하나?”
이런 업무 질문을 던지고,
repository는
“그걸 DB에서 어떤 방식으로 조회하고 저장할지”를 처리합니다.
Spring Data JPA의 공식 문서도 repository 인터페이스를 정의하고, 메서드 이름이나 명시적 쿼리로 데이터 접근을 구성하는 방식을 설명합니다. (Home)
왜 서비스에서 바로 DB를 만지면 안 될까
이건 사실 “절대 안 된다”기보다,
조금만 커져도 너무 불편해진다는 쪽에 가깝습니다.
예를 들어 서비스에서 이런 걸 직접 하기 시작한다고 생각해보죠.
- SQL 문자열 작성
- ORM 세션 열기/닫기
- 조회 결과를 도메인 객체로 변환
- 중복 조회 쿼리 직접 호출
- 저장/수정/삭제 로직 혼합
처음엔 빨라 보여요.
근데 나중엔 서비스가 비즈니스 규칙도 하고, 데이터 접근 세부 구현도 하고, 심하면 트랜잭션 처리까지 다 끌어안게 됩니다.
그럼 무슨 일이 생기냐면,
- 서비스 테스트가 DB 구현에 묶이고
- 같은 조회 로직이 여러 서비스에 중복되고
- DB가 바뀌거나 ORM이 바뀌면 서비스 코드까지 크게 흔들리고
- “업무 로직”과 “저장 로직”의 경계가 사라집니다
Spring Data JPA가 repository abstraction을 제공하는 이유도 바로 이런 데이터 접근 분리 때문이고, FastAPI의 SQL 데이터베이스 예제 역시 세션을 의존성으로 분리해 라우터/업무 코드와 연결하는 형태를 보여줍니다. (Home)
제가 추천하는 아주 현실적인 책임 분리
저는 보통 이렇게 끊습니다.
컨트롤러 / 라우터
- HTTP 요청 받기
- DTO/Schema 받기
- 서비스 호출
- 응답 반환
서비스
- 업무 규칙 처리
- 여러 repository 조합
- 예외 처리 기준 결정
- 트랜잭션 중심 흐름
repository
- DB 조회/저장/수정/삭제
- 조건 기반 조회
- ORM/SQL 세부 구현 숨기기
핵심은 이거예요.
서비스는 “무엇을 해야 하는지”를 알고,
repository는 “데이터를 어떻게 다루는지”를 안다.
이번 글에서 맞출 예제
이번에도 회원가입 흐름으로 가겠습니다.
이번에는 서비스와 repository의 역할 차이를 더 분명히 보이게 해볼게요.
요구사항은 아래처럼 잡겠습니다.
- 이메일, 비밀번호, 닉네임을 받는다
- 이메일 중복이면 가입 실패
- 비밀번호는 해시해서 저장
- 저장된 사용자 정보 중 응답에는 id, email, nickname만 반환
여기서 포인트는 이겁니다.
- 중복 검사 판단은 서비스
- 이메일로 사용자 찾기, 저장하기는 repository
1) FastAPI에서 Repository 계층 분리하기
FastAPI는 데이터베이스 자체를 강제하지 않지만, 공식 SQL 데이터베이스 가이드에서 SQLModel과 Session 의존성을 분리해 사용하는 예제를 제공합니다. 또 큰 애플리케이션 구조에서는 여러 파일, 여러 모듈로 나누는 방식을 안내합니다. (FastAPI)
이번 글에서는 원리 이해가 목적이라 실제 DB 대신 메모리 저장소 형태의 repository로 먼저 구조를 보여드릴게요. 이게 감 잡기엔 더 좋아요.
추천 구조
fastapi-backend/
├── app/
│ ├── api/
│ │ └── user.py
│ ├── repositories/
│ │ └── user_repository.py
│ ├── schemas/
│ │ └── user.py
│ ├── services/
│ │ └── user_service.py
│ ├── dependencies.py
│ └── main.py
└── requirements.txt
requirements.txt
fastapi
uvicorn[standard]
pydantic
email-validator
app/schemas/user.py
from pydantic import BaseModel, EmailStr, Field
class CreateUserRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=100)
nickname: str = Field(min_length=2, max_length=20)
class UserRecord(BaseModel):
id: int
email: EmailStr
password_hash: str
nickname: str
class UserResponse(BaseModel):
id: int
email: EmailStr
nickname: str
app/repositories/user_repository.py
from typing import Optional
from app.schemas.user import UserRecord
class UserRepository:
def __init__(self) -> None:
self._users: list[UserRecord] = []
self._next_id = 1
def find_by_email(self, email: str) -> Optional[UserRecord]:
return next((user for user in self._users if str(user.email) == email), None)
def save(self, email: str, password_hash: str, nickname: str) -> UserRecord:
user = UserRecord(
id=self._next_id,
email=email,
password_hash=password_hash,
nickname=nickname,
)
self._users.append(user)
self._next_id += 1
return user
app/services/user_service.py
import hashlib
from app.repositories.user_repository import UserRepository
from app.schemas.user import CreateUserRequest, UserResponse
class UserService:
def __init__(self, user_repository: UserRepository) -> None:
self.user_repository = user_repository
def create_user(self, request: CreateUserRequest) -> UserResponse:
existing_user = self.user_repository.find_by_email(str(request.email))
if existing_user is not None:
raise ValueError("이미 가입된 이메일입니다.")
password_hash = hashlib.sha256(
request.password.encode("utf-8")
).hexdigest()
saved_user = self.user_repository.save(
email=str(request.email),
password_hash=password_hash,
nickname=request.nickname,
)
return UserResponse(
id=saved_user.id,
email=saved_user.email,
nickname=saved_user.nickname,
)
app/dependencies.py
from app.repositories.user_repository import UserRepository
from app.services.user_service import UserService
user_repository = UserRepository()
def get_user_service() -> UserService:
return UserService(user_repository)
app/api/user.py
from fastapi import APIRouter, Depends, HTTPException, status
from app.dependencies import get_user_service
from app.schemas.user import CreateUserRequest, UserResponse
from app.services.user_service import UserService
router = APIRouter(prefix="/api/users", tags=["users"])
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
request: CreateUserRequest,
user_service: UserService = Depends(get_user_service),
) -> UserResponse:
try:
return user_service.create_user(request)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
app/main.py
from fastapi import FastAPI
from app.api.user import router as user_router
app = FastAPI(title="backend-series-fastapi-repository-layer")
app.include_router(user_router)
실행
uvicorn app.main:app --reload
테스트
curl -X POST http://127.0.0.1:8000/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"password": "password123",
"nickname": "alice"
}'
FastAPI에서 여기서 핵심
FastAPI는 repository 패턴을 프레임워크가 직접 강제하진 않습니다.
대신 dependency system이 있어서 repository나 DB session을 서비스에 주입하는 구조를 만들기 좋습니다. 공식 문서도 dependencies를 핵심 기능으로 설명하고, 큰 애플리케이션 구조에서도 분리된 모듈 구성을 권장합니다. (FastAPI)
즉 FastAPI에서는 보통 이렇게 가져갑니다.
- 라우터는 얇게
- 서비스는 업무 규칙
- repository는 DB 접근
- DB session은 dependency로 공급
개인적으로 FastAPI는 너무 빨리 개발되다 보니, repository 분리를 안 하고 서비스에서 세션을 막 만지기 시작하는 순간 코드가 금방 탁해지더라고요. 초반에 끊어두면 훨씬 낫습니다.
2) Spring Boot에서 Repository 계층 분리하기
Spring은 이 주제에서 가장 전형적인 구조를 제공합니다.
Spring Data JPA는 JPA 기반 repository를 쉽게 구현하도록 설계되어 있고, repository 인터페이스 정의와 query method, projections 같은 기능을 공식 문서에서 자세히 다룹니다. (Home)
이번 예제는 실제 JPA 엔티티까지 붙이면 글이 너무 커지니까, 먼저 구조 감각에 집중해서 메모리 기반 repository로 보여드릴게요. 그다음에 “실무에서는 여기서 JPA Repository로 바뀐다”는 식으로 이해하면 좋습니다.
추천 구조
springboot-backend/
├── src/main/java/com/example/backend/
│ ├── BackendApplication.java
│ ├── controller/
│ │ └── UserController.java
│ ├── dto/
│ │ ├── CreateUserRequest.java
│ │ ├── UserRecord.java
│ │ └── UserResponse.java
│ ├── repository/
│ │ └── UserRepository.java
│ └── service/
│ └── UserService.java
└── build.gradle
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
src/main/java/com/example/backend/BackendApplication.java
package com.example.backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}
src/main/java/com/example/backend/dto/CreateUserRequest.java
package com.example.backend.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreateUserRequest(
@NotBlank
@Email
String email,
@NotBlank
@Size(min = 8, max = 100)
String password,
@NotBlank
@Size(min = 2, max = 20)
String nickname
) {
}
src/main/java/com/example/backend/dto/UserRecord.java
package com.example.backend.dto;
public record UserRecord(
Long id,
String email,
String passwordHash,
String nickname
) {
}
src/main/java/com/example/backend/dto/UserResponse.java
package com.example.backend.dto;
public record UserResponse(
Long id,
String email,
String nickname
) {
}
src/main/java/com/example/backend/repository/UserRepository.java
package com.example.backend.repository;
import com.example.backend.dto.UserRecord;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class UserRepository {
private final List<UserRecord> users = new ArrayList<>();
private long nextId = 1L;
public Optional<UserRecord> findByEmail(String email) {
return users.stream()
.filter(user -> user.email().equals(email))
.findFirst();
}
public UserRecord save(String email, String passwordHash, String nickname) {
UserRecord user = new UserRecord(nextId++, email, passwordHash, nickname);
users.add(user);
return user;
}
}
src/main/java/com/example/backend/service/UserService.java
package com.example.backend.service;
import com.example.backend.dto.CreateUserRequest;
import com.example.backend.dto.UserRecord;
import com.example.backend.dto.UserResponse;
import com.example.backend.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserResponse createUser(CreateUserRequest request) {
userRepository.findByEmail(request.email()).ifPresent(user -> {
throw new IllegalArgumentException("이미 가입된 이메일입니다.");
});
String passwordHash = sha256(request.password());
UserRecord savedUser = userRepository.save(
request.email(),
passwordHash,
request.nickname()
);
return new UserResponse(
savedUser.id(),
savedUser.email(),
savedUser.nickname()
);
}
private String sha256(String raw) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashed = md.digest(raw.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hashed);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 해시 생성 실패", e);
}
}
}
src/main/java/com/example/backend/controller/UserController.java
package com.example.backend.controller;
import com.example.backend.dto.CreateUserRequest;
import com.example.backend.dto.UserResponse;
import com.example.backend.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.createUser(request);
}
}
실행
./gradlew bootRun
Spring Boot에서 여기서 핵심
Spring에서 repository는 진짜 “공식적인 길”이 있습니다.
Spring Data JPA는 repository 인터페이스 정의, query method, projection 같은 걸 아주 성숙하게 지원하고요. (Home)
실무에서는 보통 이렇게 이어집니다.
- UserRepository 인터페이스
- JpaRepository<UserEntity, Long> 상속
- findByEmail(...) 같은 query method
- 서비스에서 repository 호출
- 컨트롤러는 서비스만 호출
그러니까 Spring에서는 repository 분리가 그냥 스타일이 아니라,
거의 프레임워크의 강점과 바로 맞닿아 있는 구조라고 봐도 됩니다. (Home)
3) Node.js(Express)에서 Repository 계층 분리하기
Express는 정말 얇고 유연합니다.
공식 문서도 라우팅, Router, 미들웨어 같은 웹 계층 기능을 중심으로 설명합니다. 반대로 말하면 데이터 접근 계층은 프레임워크가 직접 잡아주지 않는다는 뜻이기도 해요. (expressjs.com)
그래서 Node.js에서는 repository를 더 의식적으로 분리해야 합니다.
추천 구조
node-backend/
├── src/
│ ├── repositories/
│ │ └── user.repository.js
│ ├── routes/
│ │ └── user.route.js
│ ├── services/
│ │ └── user.service.js
│ └── server.js
└── package.json
package.json
{
"name": "backend-series-node-repository-layer",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node src/server.js"
},
"dependencies": {
"express": "^5.1.0"
}
}
src/repositories/user.repository.js
export class UserRepository {
constructor() {
this.users = [];
this.nextId = 1;
}
findByEmail(email) {
return this.users.find((user) => user.email === email) ?? null;
}
save(email, passwordHash, nickname) {
const user = {
id: this.nextId++,
email,
passwordHash,
nickname,
};
this.users.push(user);
return user;
}
}
src/services/user.service.js
import crypto from "node:crypto";
export class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
createUser({ email, password, nickname }) {
const existingUser = this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error("이미 가입된 이메일입니다.");
}
const passwordHash = crypto
.createHash("sha256")
.update(password, "utf8")
.digest("hex");
const savedUser = this.userRepository.save(email, passwordHash, nickname);
return {
id: savedUser.id,
email: savedUser.email,
nickname: savedUser.nickname,
};
}
}
src/routes/user.route.js
import { Router } from "express";
export function createUserRouter(userService) {
const router = Router();
router.post("/api/users", (req, res) => {
const { email, password, nickname } = req.body;
if (!email || !password || !nickname) {
return res.status(400).json({ message: "필수값이 누락되었습니다." });
}
if (password.length < 8) {
return res.status(400).json({ message: "비밀번호는 8자 이상이어야 합니다." });
}
try {
const user = userService.createUser({ email, password, nickname });
return res.status(201).json(user);
} catch (error) {
return res.status(400).json({ message: error.message });
}
});
return router;
}
src/server.js
import express from "express";
import { UserRepository } from "./repositories/user.repository.js";
import { createUserRouter } from "./routes/user.route.js";
import { UserService } from "./services/user.service.js";
const app = express();
const PORT = 3000;
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
app.use(express.json());
app.use(createUserRouter(userService));
app.listen(PORT, () => {
console.log(`server running on http://localhost:${PORT}`);
});
실행
npm install
npm run dev
Node.js에서 여기서 핵심
Node.js는 프레임워크가 repository를 챙겨주지 않아요.
그 대신 Router와 middleware가 너무 유연해서, 개발자가 구조를 안 잡으면 라우터 파일에 비즈니스 로직, DB 접근, 검증 코드가 다 섞이기 쉽습니다. Express 공식 문서가 Router와 middleware를 중심으로 설명하는 것도 이 프레임워크의 성격을 잘 보여줍니다. (expressjs.com)
그래서 Node.js에서는 repository 계층이 더 중요해집니다.
- 라우터는 HTTP 처리
- 서비스는 업무 규칙
- repository는 DB/ORM 접근
이걸 의식적으로 지켜야 나중에 Prisma든 TypeORM이든 Mongoose든 바꾸기가 훨씬 편합니다.
진짜로요.
세 스택을 나란히 놓고 보면
셋 다 결국 같은 구조를 향합니다.
FastAPI
- repository 패턴을 강제하지는 않지만 dependencies와 모듈 분리가 잘 맞음
- DB session 주입 구조와 자연스럽게 연결됨 (FastAPI)
Spring Boot
- repository abstraction이 프레임워크 강점과 직접 연결됨
- Spring Data JPA가 아주 강력한 기반을 제공함 (Home)
Node.js
- 자유도가 높은 대신 구조를 직접 만들어야 함
- repository를 안 만들면 서비스나 라우터가 금방 DB 세부구현까지 떠안기 쉬움 (expressjs.com)
repository에 넣어야 할 것, 넣지 말아야 할 것
이건 진짜 중요합니다.
repository에 넣기 좋은 것
- findById
- findByEmail
- save
- delete
- 특정 조건의 조회
- ORM/SQL 호출
- DB 결과를 모델/레코드로 매핑하는 일
repository에 넣지 않는 게 좋은 것
- 비밀번호 정책 판단
- 가입 가능 여부 정책
- 할인 계산
- 주문 상태 변경 규칙
- HTTP status code 결정
즉, repository는
업무 판단보다 데이터 접근에 집중하는 게 좋습니다.
주니어 때 자주 하는 실수
이건 정말 많이 봤어요.
1. 서비스가 ORM 코드를 직접 다룸
처음엔 편한데, 서비스가 금방 DB 세부 구현에 묶입니다.
2. repository가 비즈니스 규칙까지 처리
예를 들어 “이메일 중복이면 가입 불가”까지 repository가 판단하기 시작하면 계층이 섞입니다.
repository는 “찾았다/못 찾았다” 정도를 알려주고, 판단은 서비스가 하는 편이 좋습니다.
3. 컨트롤러가 repository를 직접 호출
이렇게 되면 서비스 계층을 나눈 의미가 거의 사라집니다.
4. repository가 단순 CRUD wrapper 이상도 이하도 아님
이건 조금 미묘한데요.
처음엔 CRUD wrapper여도 괜찮아요.
다만 서비스와 repository의 역할이 명확히 나눠져 있어야 합니다.
실무적으로 가장 무난한 시작점
너무 거창하게 가지 않아도 됩니다.
초반에는 이 정도면 충분히 좋아요.
- 컨트롤러는 서비스만 호출
- 서비스는 repository를 사용해 업무 처리
- repository는 DB 접근만 담당
- DB 모델/엔티티와 응답 DTO는 분리
- 나중에 ORM이나 DB가 바뀌어도 서비스는 최대한 덜 흔들리게 설계
이렇게만 가도 정말 많이 달라집니다.
이번 글 핵심 정리
이번 글의 핵심은 딱 하나예요.
서비스는 데이터를 “왜” 다루는지 알고, repository는 데이터를 “어떻게” 다루는지 안다.
이 차이를 코드에서 지키기 시작하면,
백엔드가 훨씬 덜 지저분해집니다.
오늘 글의 감각을 다시 압축하면 이렇습니다.
- 서비스와 DB 접근 코드는 분리한다
- repository는 저장소 접근의 세부 구현을 감춘다
- 중복 여부 판단 같은 비즈니스 규칙은 서비스가 한다
- ORM/SQL 세부사항은 repository에 둔다
- 프레임워크가 달라도 이 원칙은 거의 그대로 간다
다음 글 예고
다음 글에서는 이제 자연스럽게
트랜잭션(Transaction)과 데이터 일관성으로 넘어가겠습니다.
repository까지 나누고 나면 꼭 이런 질문이 나와요.
- 여러 저장을 한 번에 묶어야 할 때는?
- 중간에 실패하면 어디까지 롤백해야 하지?
- 서비스 계층과 트랜잭션은 왜 같이 이야기될까?
다음 글에서는 FastAPI, Spring Boot, Node.js 각각에서
트랜잭션을 어떤 감각으로 이해하고 적용해야 하는지 정리해보겠습니다.
출처
- FastAPI 공식 문서 — SQL (Relational) Databases (FastAPI)
- FastAPI 공식 문서 — Bigger Applications / Project Generation / Dependencies (FastAPI)
- Spring Data JPA 공식 프로젝트 페이지 (Home)
- Spring Data JPA 공식 문서 — Defining Repository Interfaces / Query Methods / Projections (Home)
- Spring 가이드 — Accessing Data with JPA (Home)
- Express 공식 문서 — Using Middleware / Routing / API Reference / FAQ (expressjs.com)
백엔드개발, FastAPI, SpringBoot, Nodejs, Express, RepositoryPattern, 데이터접근계층, SpringDataJPA, API설계, 백엔드시리즈
'study > 백엔드' 카테고리의 다른 글
- Total
- Today
- Yesterday
- rag
- 개발블로그
- 압박면접
- DevOps
- fastapi
- Python
- Redis
- REACT
- LangChain
- seo 최적화 10개
- 쿠버네티스
- Express
- Next.js
- flax
- 딥러닝
- Docker
- SEO최적화
- node.js
- CI/CD
- 웹개발
- 백엔드개발
- nextJS
- kotlin
- llm
- ai철학
- JAX
- JWT
- Prisma
- NestJS
- PostgreSQL
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
