티스토리 뷰

반응형

컨트롤러에 로직을 다 넣으면 왜 망가질까? 서비스 계층(Service Layer)과 비즈니스 로직 분리 제대로 이해하기 — FastAPI · Spring Boot · Node.js

백엔드 처음 만들 때는요, 진짜 자연스럽게 이렇게 됩니다.

  • 요청 받음
  • 값 꺼냄
  • 검증함
  • 계산함
  • 저장함
  • 응답 만듦

문제는… 이걸 전부 컨트롤러에서 해버리기 시작하면 코드가 아주 빠르게 무거워진다는 거예요.

처음엔 별거 아닌 것 같거든요.
API 하나쯤은 컨트롤러에서 다 처리해도 되지 않나 싶죠.
근데 기능이 3개, 10개, 20개로 늘어나는 순간부터 슬슬 보이기 시작합니다.

  • 같은 비즈니스 로직이 여러 API에 중복되고
  • 테스트하려면 HTTP 요청부터 만들어야 하고
  • 트랜잭션 경계가 애매해지고
  • 컨트롤러가 점점 “뭐든지 다 하는 클래스”가 됩니다

저는 이 시점이 백엔드 실력이 진짜 갈리는 지점이라고 생각해요.
“코드가 돌아간다”에서 끝나지 않고,
어디에 어떤 책임을 둘지를 고민하기 시작하는 지점이거든요.

이번 글에서는 그 이야기를 해보겠습니다.
서비스 계층(Service Layer) 이 왜 필요한지,
그리고 FastAPI / Spring Boot / Node.js 에서 어떻게 나누면 좋은지,
실행되는 최소 코드와 함께 정리해볼게요.

FastAPI 공식 문서는 큰 애플리케이션을 여러 파일과 모듈로 나누는 방식을 APIRouter와 dependency 모듈 중심으로 설명하고, 재사용 가능한 의존성 분리를 강조합니다. (FastAPI)
Spring Framework는 @Controller, @Service, @Repository 같은 stereotype annotation을 계층 역할에 맞게 사용하는 방식을 설명하고, @Service와 @Controller는 @Component의 특화 형태라고 안내합니다. (Home)
Express는 기본적으로 미들웨어와 라우팅 중심의 얇은 프레임워크라서, 서비스 계층은 프레임워크가 강제하지 않지만 구조적으로 분리해 가져가는 패턴이 일반적입니다. (Home)


서비스 계층이란 뭘까

저는 서비스 계층을 아주 어렵게 설명하고 싶진 않아요.
실무에서는 그냥 이렇게 이해하면 꽤 잘 맞습니다.

컨트롤러는 “입구”, 서비스는 “업무 처리”, 저장소는 “데이터 접근”

조금 더 풀면 이렇습니다.

컨트롤러가 할 일

  • HTTP 요청 받기
  • path/query/body 값 꺼내기
  • 요청 DTO/Schema 받기
  • 서비스 호출하기
  • 응답 반환하기

서비스가 할 일

  • 비즈니스 규칙 실행
  • 여러 저장소/외부 API 조합
  • 도메인 흐름 제어
  • 실패 조건 처리
  • 트랜잭션의 중심이 되기 쉬움

저장소(repository)가 할 일

  • DB 조회/저장
  • persistence 세부 구현

즉, “무엇을 해야 하는지”는 서비스,
“어떻게 HTTP로 주고받는지”는 컨트롤러 쪽에 둡니다.

Spring 문서에서 @Controller는 웹 컴포넌트 역할을 가진 stereotype로 설명되고, @Service는 서비스 계층에 더 적합한 stereotype으로 안내됩니다. (Home)


왜 컨트롤러에 다 넣으면 안 될까

이건 그냥 취향 차이 정도가 아닙니다.
코드 수명이 길어질수록 꽤 큰 차이를 만듭니다.

예를 들어 “사용자 생성” API가 있다고 해볼게요.

해야 할 일이 이런 식으로 늘어납니다.

  1. 입력값 검증
  2. 이메일 중복 확인
  3. 비밀번호 해시
  4. 사용자 저장
  5. 가입 축하 이벤트 발행
  6. 응답 DTO 생성

이걸 전부 컨트롤러에 넣으면, 컨트롤러가 HTTP 처리기이면서 동시에 업무 처리기, 저장 흐름 제어기, 이벤트 오케스트레이터가 됩니다.

그 상태가 되면 생기는 문제는 보통 이거예요.

  • 같은 “이메일 중복 확인” 로직이 웹/관리자 API/배치에 중복됨
  • 컨트롤러 테스트가 지나치게 커짐
  • HTTP 없이 비즈니스 로직 단위 테스트가 어려워짐
  • 나중에 GraphQL, gRPC, CLI, 배치로 확장하기 힘듦

FastAPI도 큰 애플리케이션 구조에서 공통 dependency와 라우터 분리를 권장하고, Spring도 stereotype 분리를 통해 계층 역할을 나누는 방향을 제공합니다. (FastAPI)


이번 글에서 맞출 공통 예제

반응형

이번에는 “회원가입” 흐름으로 맞춰보겠습니다.

요구사항은 단순하게 이렇게 둘게요.

  • 이메일, 비밀번호, 닉네임을 받는다
  • 이메일 중복이면 실패한다
  • 비밀번호는 해시해서 저장한다
  • 응답에는 비밀번호를 내보내지 않는다

여기서 중요한 포인트는
컨트롤러는 요청을 받고 서비스만 호출하고,
실제 핵심 로직은 서비스 계층 으로 빼는 겁니다.


1) FastAPI에서 서비스 계층 분리하기

FastAPI는 서비스 계층을 프레임워크 차원에서 강제하지는 않지만, 큰 애플리케이션을 APIRouter, dependencies, 여러 모듈로 나누는 공식 가이드가 있고, 의존성 주입 시스템이 강해서 서비스 객체를 주입해 쓰기 좋은 구조입니다. (FastAPI)

추천 구조

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 UserResponse(BaseModel):
    id: int
    email: EmailStr
    nickname: str

app/repositories/user_repository.py

from typing import Optional


class UserRepository:
    def __init__(self) -> None:
        self._users: list[dict] = []
        self._next_id = 1

    def find_by_email(self, email: str) -> Optional[dict]:
        return next((user for user in self._users if user["email"] == email), None)

    def save(self, email: str, password_hash: str, nickname: str) -> dict:
        user = {
            "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-service-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에서 여기서 봐야 할 핵심

여기서 제일 중요한 건 APIRouter 안에 비즈니스 로직이 거의 없다는 점이에요.

컨트롤러는 딱 이 정도만 합니다.

  • 요청 스키마 받기
  • 서비스 주입받기
  • 서비스 호출하기
  • 예외를 HTTP 예외로 바꾸기

FastAPI의 dependency system은 이런 구조를 만들기 꽤 좋습니다. 공식 문서도 의존성 시스템을 강력하면서도 단순하게 설계했다고 설명합니다. (FastAPI)

즉 FastAPI에서는 서비스 계층을 “반드시 이렇게 하라”고 강제하진 않지만,
프로젝트가 커질수록 거의 반드시 도움이 되는 구조입니다.


2) Spring Boot에서 서비스 계층 분리하기

Spring은 사실 이 주제에서 가장 전형적인 프레임워크입니다.
@Controller, @Service, @Repository 같은 stereotype이 아예 계층 역할 분리를 염두에 두고 제공됩니다. Spring 공식 문서도 @Service와 @Controller를 각각 더 구체적인 역할의 @Component로 설명합니다. (Home)

추천 구조

springboot-backend/
├── src/main/java/com/example/backend/
│   ├── BackendApplication.java
│   ├── controller/
│   │   └── UserController.java
│   ├── dto/
│   │   ├── CreateUserRequest.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/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 org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Repository
public class UserRepository {

    private final List<Map<String, Object>> users = new ArrayList<>();
    private long nextId = 1L;

    public Optional<Map<String, Object>> findByEmail(String email) {
        return users.stream()
                .filter(user -> user.get("email").equals(email))
                .findFirst();
    }

    public Map<String, Object> save(String email, String passwordHash, String nickname) {
        Map<String, Object> user = Map.of(
                "id", nextId++,
                "email", email,
                "passwordHash", passwordHash,
                "nickname", 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.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;
import java.util.Map;

@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());

        Map<String, Object> savedUser = userRepository.save(
                request.email(),
                passwordHash,
                request.nickname()
        );

        return new UserResponse(
                (Long) savedUser.get("id"),
                (String) savedUser.get("email"),
                (String) savedUser.get("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에서는 이 구조가 정말 자연스럽습니다.

  • @RestController 는 웹 요청 처리
  • @Service 는 비즈니스 로직
  • @Repository 는 데이터 접근

Spring이 계층형 아키텍처를 “강제”한다고 보긴 어렵지만, 공식 stereotype과 component scanning 구조 자체가 이런 분리를 아주 잘 지원합니다. (Home)

그리고 실무에서는 서비스 계층이 특히 중요한 이유가 하나 더 있어요.
나중에 트랜잭션, 외부 연동, 도메인 규칙이 몰리는 중심점이 대체로 서비스이기 때문입니다.

컨트롤러가 그걸 다 떠안기 시작하면 진짜 금방 무너집니다.


3) Node.js(Express)에서 서비스 계층 분리하기

Express는 얇은 웹 프레임워크라서, 서비스 계층이라는 개념을 프레임워크가 직접 제공하진 않습니다. 대신 라우팅과 미들웨어를 중심으로 조합하게 되어 있고, 그 위에 services, repositories 같은 구조를 애플리케이션 차원에서 올리는 게 일반적입니다. (Home)

추천 구조

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-service-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는 정말 자유로워요.
그래서 더더욱 조심해야 합니다.

Express 라우터에 비즈니스 로직을 막 넣기 시작하면,
나중엔 라우터 파일이 서비스 파일인지, 검증 파일인지, 저장 파일인지 구분이 안 가게 됩니다.

여기서 중요한 건 이거예요.

  • 라우터는 HTTP 계층
  • 서비스는 업무 규칙
  • 저장소는 데이터 계층

이걸 프레임워크가 강제해주지 않으니까,
개발자가 의식적으로 지켜야 한다는 게 Node.js 쪽의 포인트입니다.
Express의 공식 자료도 프레임워크의 핵심을 라우팅과 미들웨어로 설명하지, 서비스 계층 자체를 제공하진 않습니다. (Home)


세 스택을 나란히 놓고 보면

셋 다 같은 문제를 풀고 있는데, 체감은 좀 다릅니다.

FastAPI

  • 의존성 주입이 강해서 서비스 객체 분리가 자연스러움
  • 라우터를 얇게 만들기 좋음
  • 공식 문서의 bigger applications 구조와 잘 맞음 (FastAPI)

Spring Boot

  • @Controller, @Service, @Repository 역할 분리가 가장 전형적
  • 장기 운영, 팀 협업 구조와 잘 맞음
  • 계층형 구조의 정석 느낌이 강함 (Home)

Node.js

  • 가장 자유롭고 빠르게 시작 가능
  • 대신 구조를 안 잡으면 가장 빨리 무너짐
  • 서비스 계층 분리를 “직접” 가져가야 함 (Home)

서비스 계층에 넣어야 할 것, 넣지 말아야 할 것

이 부분이 꽤 중요합니다.

서비스 계층에 넣기 좋은 것

  • 중복 검사
  • 상태 변경 규칙
  • 비밀번호 해시
  • 외부 API 호출 조합
  • 여러 저장소를 묶는 처리
  • 트랜잭션 중심 흐름

컨트롤러에 남겨두기 좋은 것

  • HTTP 요청/응답 처리
  • status code 선택
  • path/query/body 파라미터 수신
  • 인증 사용자 정보 꺼내기
  • 서비스 결과를 응답 DTO로 바꾸는 얇은 작업

저장소에 넣기 좋은 것

  • find/save/update/delete
  • DB 조회 조건
  • persistence 세부사항

이 기준을 잘못 잡으면 흔히 이런 일이 벌어져요.

  • 컨트롤러가 비대해짐
  • 저장소가 비즈니스 로직까지 해버림
  • 서비스가 그냥 repository wrapper 수준으로 끝남

개인적으로 제일 경계하는 건 “서비스가 아무 의미 없이 repository만 호출하는 상태”예요.
그건 아직 비즈니스 로직이 제대로 모이지 않았다는 신호일 수 있습니다.


주니어 때 자주 하는 실수

이건 진짜 많이 봤습니다.

1. 컨트롤러에서 if문으로 모든 비즈니스 규칙 처리

처음엔 빨라요.
근데 재사용이 안 됩니다.

2. 서비스 없이 바로 repository 호출

간단한 조회 하나면 그럴 수도 있는데,
조금만 복잡해져도 로직이 흩어집니다.

3. 서비스가 DTO 검증까지 다 맡음

입력 형식 검증은 가급적 API 경계에서 빠르게 처리하는 게 좋습니다.
FastAPI의 Pydantic body model, Spring의 @Valid @RequestBody 같은 방식이 바로 그 예입니다. (FastAPI)

4. 서비스가 HTTP를 알아버림

예를 들어 서비스 안에서 status code 400 같은 표현을 직접 다루기 시작하면 계층이 섞입니다.
서비스는 HTTP보다 업무 규칙에 집중하는 편이 오래 갑니다.


제가 추천하는 아주 현실적인 기준

초반엔 너무 거창하게 가지 않아도 됩니다.
진짜 딱 이 정도만 지켜도 많이 좋아져요.

  • 컨트롤러는 짧게
  • 서비스는 업무 규칙 중심
  • repository는 저장만
  • DTO/Schema는 입출력 전용
  • 예외는 서비스에서 만들고, HTTP 변환은 컨트롤러/전역 핸들러에서

이렇게만 해도
“코드는 돌아가는데 유지보수가 안 되는 상태”를 꽤 많이 피할 수 있습니다.


이번 글 핵심 정리

이번 글의 핵심은 하나입니다.

컨트롤러는 웹 계층이고, 서비스는 비즈니스 계층이다.

이걸 머리로만 아는 것과
진짜 코드에 적용하는 건 차이가 커요.

오늘 예제에서 꼭 가져가면 좋은 감각은 이겁니다.

  • 컨트롤러는 요청을 받고 서비스 호출만 한다
  • 서비스는 실제 업무 규칙을 처리한다
  • repository는 데이터 접근에 집중한다
  • 프레임워크가 달라도 계층 분리 원칙은 거의 같다

이 원칙이 잡히기 시작하면
그다음부터 인증, DB 트랜잭션, 테스트, 외부 API 연동도 훨씬 정리된 상태로 붙일 수 있습니다.


다음 글 예고

다음 글에서는 이 흐름을 이어서
Repository 계층과 데이터 접근 분리를 다뤄보겠습니다.

이 글에서 서비스까지 나눴다면,
다음 단계는 자연스럽게 이런 질문으로 넘어갑니다.

  • 서비스가 직접 DB를 알아야 하나?
  • repository는 어디까지 책임져야 하나?
  • ORM/쿼리 코드와 비즈니스 로직은 어떻게 끊어야 하나?

FastAPI, Spring Boot, Node.js 각각에서
실무적으로 무난한 repository 분리 기준을 이어서 정리해볼게요.


출처

  • FastAPI 공식 문서 — Bigger Applications / Multiple Files (FastAPI)
  • FastAPI 공식 문서 — Dependencies (FastAPI)
  • Spring Framework 공식 문서 — @Controller stereotype (Home)
  • Spring Framework 공식 문서 — Classpath Scanning and Managed Components (@Component, @Service, @Controller) (Home)
  • Spring Boot 공식 문서 — Spring MVC / @RestController handling (Home)


백엔드개발, FastAPI, SpringBoot, Nodejs, Express, ServiceLayer, Controller, Repository, 비즈니스로직분리, 백엔드시리즈

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
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
글 보관함
반응형