백엔드 개발/API · 아키텍처 설계

Procedural 구조에서 Use Case 구조로, 내가 구조를 다시 짠 이유

JeongPark 2025. 5. 8. 19:17
728x90

1. 서론

많은 Node.js 백엔드 프로젝트들은 처음에는 빠르게 기능 구현을 목표로 시작되며, 자연스럽게 "절차적 구조"를 가지게 됩니다. 하지만 프로젝트가 커지고 유지보수와 테스트가 복잡해질수록, 구조적인 설계에 대한 고민이 시작됩니다.

이 글에서는 Use Case 지향 서비스 구조가 무엇이며, 기존 절차적 구조와 어떤 차이가 있고, 실제 프로젝트에 어떻게 리팩토링해나갈 수 있는지 정리합니다.


2. 절차형(Procedural) 서비스 구조란?

절차형 구조는 함수 중심으로 코드를 구성하며, 하나의 함수가 여러 역할(DB 접근, 로직 처리, 응답 구성 등)을 동시에 수행하는 구조입니다.

예시 구조

export const registerUser = async (data) => {
  const user = await userRepository.findOne(...);
  const hashed = await bcrypt.hash(data.password);
  const saved = await userRepository.save({...});
  const token = jwt.sign(...);
  return token;
};

문제점

  • 단일 책임 원칙 위반: 하나의 함수에서 너무 많은 역할을 수행함
  • 유지보수 어려움: 로직이 길어질수록 변경에 민감해짐
  • 재사용성 낮음: 함수가 특정 흐름에 종속됨
  • 테스트 어려움: 외부 의존성(DB, Redis 등)을 끊기 힘듦

3. Use Case 지향 구조란?

Use Case 지향 구조는 사용자 행위 단위로 비즈니스 로직을 구분하여, 각 단위가 오직 하나의 책임만을 가지게 만드는 구조입니다.

핵심 철학

  • 핵심 비즈니스 흐름(Use Case)를 명확하게 구분
  • 모든 외부 의존성은 주입받아 테스트와 재사용이 쉬운 구조로 설계
  • Service 로직은 Domain 규칙을 다루고, 인프라와는 분리함

예시 구조

📦 use-cases
 ┣ 📜 RegisterUser.ts
 ┗ 📜 LoginUser.ts

📦 services
 ┗ 📜 TokenService.ts

📦 infra
 ┣ 📜 BcryptAdapter.ts
 ┗ 📜 RedisTokenStore.ts
export class RegisterUserUseCase {
  constructor(
    private readonly userRepo: UserRepository,
    private readonly tokenService: TokenService,
    private readonly passwordHasher: PasswordHasher
  ) {}

  async execute(dto: RegisterUserDto) {
    const user = await this.userRepo.findByEmail(dto.email);
    const hashed = await this.passwordHasher.hash(dto.password);
    const savedUser = await this.userRepo.save({ ...dto, password: hashed });
    return this.tokenService.issue(savedUser);
  }
}

장점

  • 역할 명확화: 유저 행동마다 하나의 유즈케이스로 표현됨
  • 테스트 용이: 각 클래스가 외부 의존성을 받기 때문에 mocking 쉬움
  • 확장성 높음: 새로운 시나리오 추가에 유리함 (e.g. OAuth, 관리자 계정 등)

4. 리팩토링 전략

절차적 구조에서 Use Case 구조로 전환하기 위해선 다음과 같은 단계를 거칩니다.

1) 서비스 계층 명확히 구분

  • 기존 loginUser, registerUser 등을 LoginUserUseCase, RegisterUserUseCase 클래스로 분리

2) 유틸리티와 인프라 분리

  • bcrypt, redis, email 등은 infra/ 또는 adapters/로 옮김

3) DI 컨테이너 도입 고려

  • 테스트와 확장성을 위해 의존성 주입 구조 고려 (e.g. tsyringe, awilix)

4) Express 컨트롤러는 유즈케이스만 호출

const registerController = async (req, res) => {
  const result = await registerUserUseCase.execute(req.body);
  return res.json(result);
};

5. 정리하며

당신의 서비스가 성장하고 있다면, 구조 또한 성장해야 합니다. 절차형 서비스 구조는 빠른 개발에는 좋지만, 테스트와 확장에 큰 제약을 주곤 합니다.

이번 글에서 소개한 Use Case 지향 구조는 그 해답이 될 수 있으며, 특히 인증, 결제, 주문 처리 같은 도메인 중심 로직에서 큰 장점을 발휘합니다.

반응형