클린 아키텍쳐에 대하여 깊게 고민하고 생각해보며 공부하였습니다.
이해하기에는 쉽지 않았으나, 공부한 내용을 글로 작성합니다.
글의 내용이 길다고 생각하여 마지막 부분에 쉬운 이해를 위해  내용을 간단하게 정리하였습니다.

 

클린 아키텍쳐란 무엇인가?

소프트웨어 프로젝트를 진행하다 보면 프로젝트의 크기가 커져 코드가 점점 복잡해집니다. 처음에는 깔끔했던 코드가 기능이 추가될수록 서로 얽혀 나중에는 작은 기능 하나를 수정해도 예상치 못한 곳에서 버그가 터져 나올 수 있습니다. 이런 상태를 '소프트웨어가 부패했다' 또는 '아키텍처가 무너졌다'고 표현합니다.

클린 아키텍처는 이러한 문제에 대한 해결책으로 로버트 C. 마틴(Uncle Bob)이 제안한 설계 사상입니다. 아래의 내용은 클린 아키텍쳐의 핵심 철학입니다.

 "좋은 아키텍처는 시스템을 유연하고, 테스트하기 쉽고, 유지보수하기 좋게 만든다. 이를 위해 시스템의 핵심 비즈니스 로직과 외부 기술(세부 사항)을 철저히 분리해야 한다."

여기서 '세부 사항(Details)'이란 프레임워크(Next.js), 데이터베이스(PostgreSQL), UI, 외부 라이브러리 등을 의미합니다. 이것들은 언젠가 바뀔 수 있는 부차적인 요소입니다. 반면, '핵심 비즈니스 로직'은 이 서비스가 존재하는 이유(서비스의 규칙)이며 쉽게 변하지 않습니다.

클린 아키텍처는 이 변하지 않는 핵심을 변하기 쉬운 세부 사항들로부터 보호하는 '방어벽'을 만드는 설계 방법론입니다.


클린 아키텍쳐의 시스템

클린 아키텍처는 시스템을 크게 네 개의 계층으로 나누어 설명합니다. 가장 안쪽부터 바깥쪽으로 이동합니다. 

 

아래는 각 계층의 역할과 특징입니다.

Entities (엔티티)

  • 역할 : 시스템의 가장 핵심적인 비즈니스 규칙을 담는 영역입니다. 이 규칙은 특정 애플리케이션에 종속되지 않습니다.

  • 예시 : 쇼핑몰 시스템의 Product(상품)나 Order(주문) 객체. 이 객체들은 상품의 가격 계산 로직이나 주문의 상태 변경 규칙 같은 고유한 비즈니스 로직을 포함할 수 있습니다.

  • 특징
    엔티티는 변하지 않고 안정적입니다. 외부의 어떤 계층이 바뀌더라도 엔티티는 영향을 받지 않습니다. 예를 들어, 애플리케이션이 웹에서 모바일로 바뀌거나 데이터베이스 종류가 바뀌어도 엔티티는 그대로 유지되어야 합니다.
// 예시 코드

// /src/core/entities/reservation.entity.ts
export class Reservation {
  public readonly rsv_id: string;
  public rsv_user: string;
  public rsv_room: string;
  public rsv_start: Date;
  public rsv_end: Date;

  constructor(props: Omit<Reservation, 'isWithinBusinessHours'>) {
    Object.assign(this, props);
  }

  // 엔티티 스스로가 가질 수 있는 핵심 비즈니스 규칙
  public isWithinBusinessHours(): boolean {
    const startHour = this.rsv_start.getHours();
    const endHour = this.rsv_end.getHours();
    return startHour >= 9 && endHour <= 18;
  }
}

 Use Cases (유즈케이스)

  • 역할 : 애플리케이션에 특화된 비즈니스 규칙을 구현합니다. 이 계층은 엔티티를 사용하여 시스템이 사용자에게 제공하는 특정 기능(유스케이스)을 완성시킵니다.

  • 예시: RegisterUserUseCase는 사용자 등록에 필요한 모든 단계를 조율하지만, 실제 데이터베이스에 사용자 정보를 저장하는 코드는 직접 작성하지 않고, 저장소 인터페이스를 호출할 뿐입니다.

  • 특징
    "사용자를 등록한다", "상품을 장바구니에 담는다"와 같은 구체적인 기능를 코드로 표현합니다.
    이 계층은 데이터가 어떻게, 어디에 저장되는지 전혀 알지 못합니다. 대신 "사용자를 등록해줘" 또는 "상품 데이터를 가져와줘"라는 추상적인 규칙(인터페이스)에만 의존합니다.
// 예시 코드
// /src/core/repositories/reservation.repository.ts (인터페이스 정의)
export interface IReservationRepository {
  save(reservation: Reservation): Promise<void>;
  findById(id: string): Promise<Reservation | null>;
}

// /src/core/use-cases/create-reservation.use-case.ts
import { Reservation } from '../entities/reservation.entity';
import { IReservationRepository } from '../repositories/reservation.repository';

export class CreateReservationUseCase {
  // 실제 구현이 아닌, 인터페이스에 의존함
  constructor(private reservationRepo: IReservationRepository) {}

  async execute(input: { userId: string, roomId: string, start: Date, end: Date }) {
    const newReservation = new Reservation({ ... });

    if (!newReservation.isWithinBusinessHours()) {
      throw new Error("예약은 업무 시간 내에만 가능합니다.");
    }
    // ... 다른 비즈니스 규칙 검사

    await this.reservationRepo.save(newReservation);
  }
}

Interface Adapters (인터페이스 어댑터)

  • 역할 : 안쪽의 유스케이스/엔티티 계층과 바깥쪽의 프레임워크/드라이버 계층 사이에서 데이터를 변환하고 전달하는 '번역가' 역할을 합니다.

  • 예시
    Controller : 웹 요청(HTTP Request)을 받아 유스케이스가 이해할 수 있는 데이터 형식으로 변환한 뒤, 해당 유스케이스를 호출합니다.
    Repository 구현체 : 유스케이스 계층에 정의된 "데이터 저장 규칙(인터페이스)"을 실제 데이터베이스 기술(예: SQL)을 사용하여 구현합니다.

  • 특징
    이 계층의 코드는 안쪽의 핵심 로직과 바깥쪽의 외부 기술 모두를 알고 있습니다.
    Controllers, Presenters, Gateways(Repositories) 등이 여기에 속합니다.
// 예시 코드
// /src/infra/repositories/supabase-reservation.repository.ts
import { IReservationRepository } from '@/core/repositories/reservation.repository';
import { Reservation } from '@/core/entities/reservation.entity';
import { supabase } from '@/infra/db/supabase'; // Supabase 클라이언트

export class SupabaseReservationRepository implements IReservationRepository {
  async save(reservation: Reservation): Promise<void> {
    const { error } = await supabase.from('reservations').insert([
      { ...reservation } // 엔티티를 DB 스키마에 맞게 변환
    ]);
    if (error) throw new Error(error.message);
  }
  //... findById 등 구현
}

Frameworks & Drivers (프레임워크 & 드라이버)

  • 역할 : 시스템의 가장 바깥쪽 경계로, 우리가 사용하는 모든 외부 도구와 기술이 이 계층에 해당합니다.

  • 특징
    웹 프레임워크, 데이터베이스, UI 프레임워크, 외부 라이브러리 등 구체적인 기술들입니다.
    이 계층의 코드는 주로 다른 계층들을 연결하고 초기화하는 '접착제' 역할을 합니다.

  • 예시 : Next.js 프레임워크, PostgreSQL 데이터베이스, React UI 라이브러리, 웹 서버 등.

위와 같은 구조로 가장 중요한 엔티티유스케이스는 가장 불안정한 프레임워크데이터베이스 기술로부터 완벽하게 보호받게 됩니다.

중요한 것 (핵심 비즈니스 로직)
이 서비스가 존재하는 이유 그 자체입니다. 예를 들어, 프로젝트에서는 "회의실을 특정 규칙에 따라 예약한다"는 것이 핵심 로직입니다. 이 로직은 웹사이트로 만들든, 모바일 앱으로 만들든 변하지 않습니다.

중요하지 않은 것 (세부 사항)
 핵심 로직을 구현하기 위해 사용하는 외부 도구들입니다.

  • 웹 프레임워크: Next.js, Express 등
  • 데이터베이스: PostgreSQL, MySQL, Supabase 등
  • UI: React, Angular 등

 


의존성 규칙 (The Dependency Rule)

클린 아키텍쳐를 이해할 때, 가장 중요한 규칙입니다.

"소스 코드의 의존성(참조 관계)은 오직 안쪽을 향해야 한다."


시스템을 여러 개의 구조로 생각했을 때, 안쪽 원은 바깥쪽 원에 대해 아무것도 모릅니다.

  • 안쪽 원 (핵심 로직): 데이터베이스가 무엇인지, 웹 프레임워크가 무엇인지 알 필요가 없습니다.
  • 바깥쪽 원 (세부 사항): 안쪽 원의 규칙을 알고 있으며, 그 규칙에 따라 움직입니다.

이 규칙으로 인해 데이터베이스를 바꾸거나 웹 프레임워크를 교체하는 큰 변화에도 핵심 비즈니스 로직 코드는 단 한 줄도 수정할 필요가 없게 됩니다. 이것이 클린 아키텍처가 추구하는 유연성입니다.


클린 아키텍쳐를 사용하는 이유

  • 유연성 : 데이터베이스나 웹 프레임워크를 바꿔야 할 때, 핵심 비즈니스 로직은 건드리지 않고 해당 부분만 교체하면 됩니다.
  • 테스트 용이성 : 핵심 로직을 외부 기술과 분리하여 독립적으로 쉽게 테스트할 수 있습니다.
  • 유지보수성 : 코드가 역할별로 명확히 분리되어 있어 이해하고 수정하기 쉽습니다.

정리

 

클린 아키텍쳐를 공부하며 많은 시간을 쓰고, 깊은 고민을 했습니다. 이후, 이해하게 된 내용은 상당히 간단하게 요약할 수 있었습니다.

시스템의 중요한 부분은 (핵심 비즈니스 로직)과 중요하지 않은 부분(외부 기술, 세부사항)을 분리하는 것입니다.

시스템은 안쪽으로 갈수록 중요해지며 안쪽은 Entity, Use Case 등이 위치하며 바깥쪽에는 Framework, Data Base 등이 있습니다.

이 계층의 사이에는 내부의 핵심 로직과 외부 세계(웹, DB 등) 사이에서 데이터를 변환하고 전달하는 중간 계층인 Adapter가 존재합니다.

 

Entity는 비즈니스 규칙과 데이터 구조 등이 위치합니다.

Use Case는 애플리케이션 고유의 로직이며 엔티티를 조율하여 실제 기능을 완성합니다.
Adapter는 Controller, Repository 구현체 등이 속합니다.
Frameworks & Drivers는 웹 프레임워크(Next.js), 데이터베이스(PostgreSQL), UI 등 우리가 사용하는 모든 외부 기술입니다. 이들은 '세부사항'이며, 언제든 교체될 수 있습니다.

 

 

 

출처 : https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

728x90

'CS' 카테고리의 다른 글

세션(Session)이란?  (1) 2025.08.03
Prisma ORM에 대하여  (0) 2025.08.02
객체 지향이란?  (0) 2025.06.29
데이터 정규화(Normalization)란?  (0) 2025.06.26
동기화와 비동기화  (0) 2025.06.20