Background 클린 아키텍처란? 의존성 방향Sentry 개발자의 NextJS 클린 아키텍처클린 아키텍처를 그대로 적용해보기완벽한 분리와 오버엔지니어링 사이의 고민각 레이어 별 역할 분담 및 네이밍구현하면서 깨달은 DI컨테이너가 싱글톤 패턴인 이유최종 아키텍처 완성결론참고
Background
현재 노션에 작성한 블로그들이 많아서 Notion Api를 사용해서 나만의 블로그 페이지로 만들어 보고 있다. 그리고 현재 NextJS를 더 깊이 파고 공부하기 위해서 Nextjs가 가진 다양한 렌더링 전략과 캐시전략을 적용해보고 싶었다. 물론 그렇게 큰 프로젝트라고 할 수는 없지만, 유지보수가 간편하고 확장가능한 형태의 아키텍처를 적용해보고 싶다는 고민을 해보기 시작했다. 그러다가 클린 아키텍처에 대해서 궁금해졌다.
이번 블로그에서는 클린 아키텍처에 대해 많은 블로그들이 다루고 있으므로 지식적으로 파고드는 것보다, 무엇이 핵심이고 나의 프로젝트에 어떻게 적용해 나가면서 가진 고민을 해결해 나가는 과정을 풀어보도록 하겠다.
클린 아키텍처란?
일단 클린 아키텍처를 검색하게 되면 제일 많이 보이는 그림이 아래와 같다. 일단 그림은 간편해보이지만 구현단계에서는 도대체 잘 이해가 되지 않는다. 특히
레이어 분리
와 단방향 의존성
이 강조되는 개념에서는 원으로 그려진 이 그림이 오히려 이해하는 데 나에게는 제일 큰 장벽이 되었다.
“원으로 그려져있으니까, DB함수를 사용할 때도 Controller를 통해서 UseCase에 접근하라는 말인가?”
저런 질문이 계속 머리속에 남아있엇고, 결국
클린 아키텍처
책을 구입해서 어떤 맥락으로 저런 그림을 그리게 된 것인지 이해하고 싶었다. 그리고 그것을 제가 이해한 방식으로 설명을 하자면 먼저 의존성 방향
이라는 것을 이해해야된다.의존성 방향
물론 코딩을 배우면서 웹이나 앱을 개발하신 분들이라면 클린 아키텍처를 몰라도, 누구나 코드를 의존성 있게 작성한다. 하지만 의존한다라는 말이 이해를 조금 어렵게 하는데, 제가 간단히 정리한 방식은 이렇다.
모르는 쪽이 독립적이고, 아는 쪽이 의존적이다.

그림을 보다 시피,
PostMetadata
는 아는 쪽
의 코드는 PostMetadata
를 불러와서 반환하는 함수를 사용하고 있다. 모르는 쪽인 PostMetadata
는 자신을 호출하는 쪽의 어떠한 정보도 알고 있지 않다. 이 상태가 아는 쪽이 모르는 쪽 방향으로 의존하고 있다
라고 말할 수 있다. 그럼 이 지식을 가지고 다시 NextJS 클린 아키텍처에 대해서 알아보자.
Sentry 개발자의 NextJS 클린 아키텍처
일단
Nextjs Clean Architecture
로 검색하면 Lazar Likolov
라는 개발자가 적용한 클린 아키텍처가 검색이 되는데, 그 아키텍처를 잘 설명한 블로그는 아래와 같다. Sentry 개발자의 아키텍처를 설명한 블로그 글과 실제 레파지토리
nextjs-clean-architecture
nikolovlazar • Updated Sep 9, 2025
이 아티클에서 구조를 확인하고 깃허브까지 연결되어있어서 코드를 확인할 수 있었다. 그리고 아무래도 글로는 잘 이해가 되지 않기 때문에 그림을 그려가면서 의존성의 흐름이 어떻게 되는 지 그려 보았다.

위의 그림은
/app/page.tsx
에서 getTodo
함수에서 부터 위에서 설명한 의존성 흐름대로 나열해본 것이다. 물론 Sentry개발자이기 때문에 내가 감히 이 코드가 좋다 나쁘다 판단할 짬밥은 안되지만, 나의 부족함 때문인지 봐도 너무 잘 이해가 되지 않고 어렵게 느껴진다. 다음은 어렵게 느껴지는 부분을 정리해 보았다.- 인터페이스와 구현체와의 중복된 이름때문에 오는 혼란 (
IRepository
,RepostoryImpl
)
- Interface Adapter의 역할
깃 허브를 내용을 보니 Interface Adapter로
module
파일을 사용하고 있는데, 거기서 controller
가 Interface Adapter 폴더에 속해있어서 역할과 책임이 헷갈림- 함수가 다르긴 하지만 레이어의 순환
Application
-Infrastructure
-Application
물론 내가 Sentry 개발자의 프로젝트를 따로 구동해본게 아니라서, 잘 이해가 안되는 것일 수도 있다. 하지만 위의 어렵게 느껴지는 부분을 개선을 해서 내 프로젝트에 적용하게 되는 과정과 결과를 공유해보고자 한다.
클린 아키텍처를 그대로 적용해보기
위에서 언급을 했었지만, 내가 이해한 클린 아키텍처의 핵심은
단방향 의존성
과 레이어 분리
라고 생각한다. 그리고 현재 나의 프로젝트에 클린 아키텍처를 적용할려면, 아래와 같은 레이어를 생각해 볼 수가 있다. 
위 그림. 처럼 클린 아키텍처를 적용해서 레이어를 분리해보면
UI / Presentation / Application / Domain / Infrastructure
레이어 정도로 나누어 볼 수 있다. 그리고 이 레이어들을 모두 분리하기 위해서는 추상화된 Interface가 필요해지고, 의존하게 되는 레이어에서는 Interface의 구현체를 구현하면서 레이어를 분리할 수 있게 된다. 그리고 그 폴더 구조는 다음과 같다. 클린 아키텍쳐를 교과서 적으로 적용한 구조
project ├── app ----------------------------------- │ ├── (about) │ │ └── about │ │ └── _components │ ├── (blog) │ │ ├──_components UI Layer │ │ └── blog │ │ └── [id] │ ├── (main) │ │ └── _components │ └── api ----------------------------------- │ ├── presentation ----------------------------------- │ ├── hooks │ ├── providers │ ├── stores Presenation │ ├── controllers Layer │ ├── view-model │ ├── use-cases-interface │ └── utils ----------------------------------- │ ├── application ----------------------------------- │ ├── data-cache Application │ ├── repositories-interface Layer │ └── use-cases-impl ----------------------------------- │ ├── domain ----------------------------------- │ ├── entities Domain Layer │ └── utils ----------------------------------- │ └── infrastructure ----------------------------------- ├── database │ ├── drizzle Infrastructure │ ├── external-api Layer │ └── supabase └── repositories-impl -----------------------------------
완벽한 분리와 오버엔지니어링 사이의 고민
그리고 누군가 나에게
클린 아키텍처를 구성하는 데 폴더와 파일이 늘어나는 것이 걱정이 된다면, 그건 아직 클린 아키텍처를 받아들일 준비가 되지 않은 것
이라고 한 적이 있다. 하지만 현재 구조에서는 controller의 역할을 정확히 파악할 수 없었다. 그저 useCas호출하고 UI에 필요한 데이터를 전달하는 역할밖에 하지 않았기 때문이다. 그리고 그러한 역할은 Presentation의 각 함수들이 충분히 감당할 수 있는 부분이라고 생각했다.그래서 클린 아키텍처에서 controller의 역할이
UI / Presentation
사이에서 추상화와 분리를 위한 목적으로 사용된 것 같았고, 현재 프로젝트에서 그것이 클린 아키텍처의 궁극적 목표와 방향( 유연성과 확장성 )에 맞는 건지 생각해보게 되었다. 사실 무엇보다 중요한 건 과연 내가 하고 있는 1인 개발에서 맞는 구조인가
하는 것이다.그렇게 고민을 해보다가,
Presentation layer
에 있는 hooks / store / utils 함수들이 useCase를 호출해서 UI에 맞는 데이터를 가공하도록 하면 따로 controller와 viewmodel이 필요할 것 같지 않아서 없애기로 했다. 그래서 현재 프로젝트에서는 UI Layer
는 Presentation Layer
를 직접 의존하는 방향으로 선택하였다 (사실 이게 기본적으로 다들 하는 방식).따라서 UI레이어에서는
조건부 렌더링 / 삼항 연산자 / 배열 함수
등 일부 단순 로직만 사용할 수 있도록 분리 하였다. Presentation에서는 hooks / utils / store 로직을 담당하고 UI에 레이어에서 필요한 데이터 형태로 가공하는 역할을 부여했다. .png?table=block&id=24e9c76c-6cb4-8029-a755-fabb65304621&cache=v2)
이제
UI Layer
와 Presentation Layer
는 강하게 결합되어있지만 어느정도의 역할을 분리하여 UI에서의 가독성을 더 높이고, Presentation 레이어에서 복잡한 UI로직을 처리하게 되었다. 그리고 Presentation 레이어에서 이 로직을 처리할 때 어떤 데이터가 필요한지 인식하게되고, 관련된 데이터를 앱의 핵심 레이어인 Application Layer
로 요청을 하게 되는 것이다.그리고
Application Layer
에는 UseCase
가 있다. 클린 아키텍처에서도 UseCase가 제일 중요한 부분이라고 한 만큼, 나는 내 프로젝트의 UseCase를 헬스키친의 고든램지
역할을 하는 레이어라고 이해했다. UI/Presentation에서 일하고 있는 지배인이 어떤 메뉴가 필요한지 주방보조에게 직접 요구하는 것이 아니라, 주방장에게 요청을 해야되는 것이기 때문이다. .png?table=block&id=24e9c76c-6cb4-8024-8769-c9cbf45b6085&cache=v2)
그래서
Presentation Layer
(지배인)는 Repository
에 직접 요청하지 않게하고, 반드시 UseCase에게 필요한 데이터가 뭔지만 요청하면 되는 것이다. 그러면 UseCase
는 Repository
에 도메인에 관련된 요청을 각각 요청하게 된다. 주방장이 마치 한식파트 양식파트 일식파트 요리사들에게 각각의 메뉴를 요청하는 것 같이 말이다. 그럼 UseCase는 각각 완성된 메뉴를 검증하고 조합해서 하나의 주문서에 해당하는 메뉴들을 출하(?)시킨다. 이렇게 UseCase는 다양한
repository
들과 소통하고, repository
는 다양한 곳에서 데이터를 가져올 것이기 때문에, 변경될 가능성이 높은 부분이라고 볼 수 있다. 그래서 강하게 결합하면 안되는 확고한 이유가 된다. 따라서 이 UseCase는 추상화된 레파지토리에게 이런 메뉴가 필요해
만 요청하고, 그걸 어떻게 만들어 올지는 신경쓰지 않게 해야했다.각 레이어 별 역할 분담 및 네이밍
따라서 1인 개발 블로그 프로젝트의 특성을 살려서 적용한 레이어의 역할 분담은 다음과 같다.
- UI Layer
- 스타일링 및 컴포넌트
- UI코드의 가독성을 방해하지 않는 선에서 최소한의 로직 코드 - map, 조건부 렌더링, 삼항연산자 등
- Presentation Layer
- UI 관련 로직 - 상태 관리, 리액트 훅 등
- 데이터가 없을 때의 에러 처리 - toast
- UseCase Interface 의존
- Application Layer
- UseCase Interface의 구현 → 비즈니스 로직
- 데이터 캐시 로직
- UI에 필요한 데이터를 가공해서 제공
- Repository Interface 의존
- Domain Layer
- 아무것도 의존하지 않는 레이어
- 데이터 스키마 정의
- 데이터 매핑 로직 정의
- Infrastructure Layer
- Repository Interface의 구현
- 상황에 맞는 데이터를 가져오고 조작하는 레이어 - notion API / supabase / local
- Result 유니언 타입으로 일관된 리턴 형태로 가공
- 데이터 베이스 에러 처리
- 데이터 직렬화 / 역직렬화
현재 구조에서는 나름 의존성흐름과 레이어의 분리가 의도한 부분 이외에는 나름 만족할 정도가 되었지만, 한가지 실제로 구현해보면서 불편한 점이 있었다. 바로 interface와 구현체의 네이밍이 비슷하니까 자주 헷갈리는 경우가 많았고, 오히려 폴더를 찾는 데 시간을 보내는 경우가 많았다. 때론 클린아키텍처를 구현하신 분들 폴더 구조를 보면, repository 같은 폴더가 각 레이어에 있는 경우가 많았다. 그래서 나는 뭔가 좀더 직관적인 구조가 필요함을 느꼇다.
그래서 분리가 필요한 레이어에는 interface를 port로 이름을 변경하고, 구현이 필요한 곳에는 adapter라는 이름을 명명하였더니 훨씬 관리가 편해짐을 느낄 수 있었다. 그냥 adapter파일을 찾아서 로직을 손보면 되었기 때문이다.

이제 각 레이어는 다른 레이어를 의존하는 것이 아니라, 자체 레이어의 port를 의존하고 그 뒤에 일들은 신경쓰지 않도록 했다. Adapter로서 구현되는 레이어는 Port에서 요구하는 양식 그대로 usb를 포트에 연결하듯이 리턴 값을 맞춰서 끼워주기만 하면 되는 것이다.

이렇게 사용하는 것이 interface를 중간 매개체라고 생각하지 않고 사용할 수 있어서 머릿 속에 개념정리가 더 잘될 수 있었던 것 같다. 그리고 Domain Layer는 아무것도 의존하지 않는 핵심 레이어인 동시에 어디에서든 참조당할 수 있는 레이어가 되기 때문에 현재 프로젝트에서는 원형 모양의 클린 아키텍처 그림 대신에, 위처럼 빌딩 블록같은 느낌이 더 맞을 것 같다는 생각이 들었다.
구현하면서 깨달은 DI컨테이너가 싱글톤 패턴인 이유
그리고 의존성 방향과 분리, 그리고 역할과 책임까지 명료하게 정리가 되어서 신나게 구현해 나가던 중에 약간 불편한 느낌을 가질 수 있었다. 그건 바로 Presentation레이어에서 UseCase를 사용할 때 계속 객체를 생성하면서 사용하고 있었기 때문이다. 함수를 사용할 때마다 useCase를 create하고 있었던 것이다.
그 불편함 때문에, 자바의 IOC같은 컨테이너가 필요함을 체감할 수 있었다. 이 프로젝트에서도 프로젝트 생명주기 동안 모든 의존성을 한번만 띄우고 관리해주는 Dependency Injection Container가 필요했다. node에서export된 함수는 자바처럼 static하게 띄우지 않고도 싱글톤으로 재사용이 가능하다고 한다.
그래서 각 도메인의 의존성 관계와 주입을 정의하고, DiContainer에서는 그 의존성들을 한데 묶어서 DiContainer를 export하고 앞으로 사용시에 DiContainer에서 useCase나 repository를 꺼내서 사용하면 된다.
각 관심사의 의존성을 관리하는 DiContainer
export const createDiContainer = (): DiContainer => { const postDependencies = createPostDependencies(); const tagInfoDependencies = createTagInfoDependencies(postDependencies.postRepository); return { tagInfo: tagInfoDependencies, post: postDependencies, }; }; // 싱글톤 export const getDiContainer = (): DiContainer => { if (!global.__diContainer) { global.__diContainer = createDiContainer(); } return global.__diContainer; }; export const diContainer = getDiContainer();
최종 아키텍처 완성
이제 이 모든 과정을 거쳐서 1인 개발에 적용할 수 있는 클린 아키텍처를 구성한 의존성 관계도와 폴더 구조는 다음과 같다.
최종 완성된 아키텍처
project ├── app ----------------------------------- │ ├── (about) │ │ └── about │ │ └── _components │ ├── (blog) │ │ ├──_components UI Layer │ │ └── blog │ │ └── [id] │ ├── (main) │ │ └── _components │ └── api ----------------------------------- │ ├── presentation ----------------------------------- │ ├── hooks │ ├── providers Presenation │ ├── stores Layer │ ├── port │ └── utils ----------------------------------- │ ├── application ----------------------------------- │ ├── data-cache Application │ ├── port Layer │ └── use-cases-adapter ----------------------------------- │ ├── domain ----------------------------------- │ ├── entities Domain Layer │ └── utils ----------------------------------- │ └── infrastructure ----------------------------------- ├── database │ ├── drizzle Infrastructure │ ├── external-api Layer │ └── supabase └── repositories-adatper -----------------------------------
결론
물론 현재 이게 완벽한 구조다라고는 말할 수 없는 것이, 왜 함수형 프로그래밍과 객체지향 프로그래밍을 같이 사용하는 것이 맞는가? 하는 생각이 든다. 아직 그 두 개념을 완벽히 머릿속에 그려지지 않기 떄문에 그런것 같다고 생각한다. 하지만 현재 내가 아는 지식선에서 어느 정도 정리가 되어 자리잡은 클린 아키텍처를 적용한 것이라 조금 뿌듯하다. 앞으로도 프로그래밍의 더 나은 아키텍처와 클린 코드를 위해 고민하는 풀스택 개발자로 성장하고 싶다.
깃허브 주소
notion-blog-nextjs
chugue • Updated Sep 9, 2025
배포된 사이트