최근 트럼프 정부 하의 정부효율부의 고문 일론 머스크가 화제가 되고 있다. 트위터를 인수 후 서버실 자체를 없애버렸다느니, 효율을 위해 공무원들을 정리해고 하는 과정에서 핵무기 관리자를 해고했다느니, 미국 정부의 기밀을 유출했다느니... 좋다거나 나쁘다고 말하고 싶은 것은 아니지만 스타트업에서 일해 온 개발자 입장에서 본다면 세상에 맞서서 결코 자신의 신념, 단순한 것이 최고이다를 굽히지 않는 것 만큼은 정말 멋지다고 생각한다. 그가 항상 강조하는 것 중 하나는
"The best part is no part. The best process is no process."
불필요한 것을 제거하고 핵심을 바라보라는 것이다. 개발 업계에서도 이와 통하는 명언이 있다.
"오버엔지니어링(over-engineering)하지 마라."
즉, 너무 앞서가서 쓸데없는 추상화나 복잡한 설계를 도입하지 말라는 것이다.
나도 한때, 누구보다 빠르게 기능을 추가하는 것이 내가 가장 잘하는 것이라고 생각했다.
하지만 최근 슬럼프를 겪으면서 다른 생각을 가지게 되었다.
그렇다면 개발자가 시간을 들여 유지보수를 고려한 코드를 짜는것과, 해당 기능의 데드라인을 맞추는 것, 이 두가지는 결코 양립할 수 없는 문제일까? 이것에 명확한 정답은 없다고 생각한다. 하지만, 우리는 적어도 앞으로 재사용될 것에 대해서는 기술 부채를 막기 위해 모두가 합의할 수 있는 하나의 구조를 만드려고 할 수는 있다.
머스크의 철학처럼 필요하지 않은 부분을 제거하는 것과 빠르게 기능을 개발하는 것은 무엇보다 중요하다. 하지만 단기적인 효율이 장기적인 기술 부채로 이어질 수 있다.
슬럼프를 겪으며 반성했던 것 중의 하나는 위에 대한 생각을 전혀 해보지 않았던 것이었다. 지금에서라도 내가 앞으로 계속해서 참조할 수 있을 하나의 원칙을 가진 코드베이스가 필요했다.
마침 유튜브에서 구독해오던 한 개발자가 Next.js 기반의 프로젝트 템플릿을 작성하고 비디오를 공개해놓은 것이 있어 이를 클로닝하고, 이해한 뒤 계속해서 발전시켜 보기로 하였다.
"클린 아키텍처는 Robert C. Martin(일명 Uncle Bob)이 제창한 개념으로,
비즈니스 로직과 프레임워크를 분리하여 유지보수성을 높이는 설계 방식이다.
핵심 원칙은 '의존성 방향을 뒤집어(DIP, Dependency Inversion Principle) 핵심 로직을 보호하는 것'이다."
처음 소규모 프로젝트를 개발한다면 생각할 것이 많지는 않을 것이다. Docs에 따라 api를 정의하고, 데이터베이스와 연결점을 만든 뒤 프론트엔드를 만들어나가는데... 점점 기능이 많아질 수록 이러한 문제들이 발생하게 될 것이다.
이 문제를 해결하기 위해 Clean Architecture가 필요해진다. 코드를 계층화함으로써
✅ Seperation of Concerns를 통해 유지보수가 쉽다.
✅ Scale 조정을 하기 용의하다.
✅ 비즈니스 로직을 최대한 고립하여 둠으로써 테스팅이 쉬워진다.
위의 사항을 염두에 두면서 위 프로젝트를 스터디해보기로 하였다.
위 다이어드램에 따라 프로젝트의 폴더 구조와 어떤 레이어가 있는지 확인해 보자.
src/ // app router를 사용하고 있지만 구분을 용이하게 하기 위해 사용
├─app
│ ├─api
│ │ └─auth
│ │ └─[...nextauth]
│ ├─dashboard
│ │ └─_actions
│ └─new
├─components
│ └─ui
├─data-access
│ └─items
├─db
├─lib
└─use-cases
└─items
으로 나누어져 있다.
만약 nextjs에서 db 연결을 하지 않고 react-query를 사용한다면 lib 폴더쪽에서 사용하게 될 듯 하다.
위 구조로부터 파생되는 데이터 플로우는 다음과 같다.
// app\(main)\(auth)\sign-in\email\actions.ts
import { getCurrentUser, setSession } from "@/lib/session"; // 개발자님은 세션 핸들링은 추후 deploy 시 외부 클라우드에서 핸들링할 가능성이 높으므로, lib에서 사용했다.
import { signInUseCase } from "@/use-cases/users"; // 비즈니스 로직 호출
...
export const signInAction = unauthenticatedAction
.createServerAction()
.input(
z.object({
email: z.string().email(),
password: z.string().min(8),
})
)
.handler(async ({ input }) => {
await rateLimitByKey({ key: input.email, limit: 3, window: 10000 });
const user = await signInUseCase(input.email, input.password);
await setSession(user.id);
redirect(afterLoginUrl);
});
// \use-cases\users.tsx
import {
getUserByEmail,
verifyPassword,
} from "@/data-access/users"; // session은 lib에서 다루지만 user는 자체 데이터이므로 data-access 레이어에서 선언되었다.
...
export async function signInUseCase(email: string, password: string) {
const user = await getUserByEmail(email);
if (!user) {
throw new LoginError();
}
const isPasswordCorrect = await verifyPassword(email, password);
if (!isPasswordCorrect) {
throw new LoginError();
}
return { id: user.id }; // use-case와 액션 간 DTO 형식으로 소통
}
...
// \data-access\users.ts
import { database } from "@/db";
import { users } from "@/db/schema";
...
export async function getUserByEmail(email: string) {
const user = await database.query.users.findFirst({
where: eq(users.email, email),
});
// data-access 레이어에서는 디비 호출하는 로직만을 선언한다. 유저 데이터 검사는 business layer인 유즈케이스 내부에서 실행한다.
return user;
}
...
유의할 점은 다음과 같다.
개발자 이 템플릿은 1인 개발로 Saas를 만들기 위해 설계되었다. 앞으로 해당 템플릿을 직접 사용하면서 실제 생산성에 어떤 영향을 미치는지 확인해보도록 할 예정이다.
https://github.com/webdevcody/wdc-saas-starter-kit
https://celepbeyza.medium.com/introduction-to-clean-architecture-acf25ffe0310
https://github.com/jihyeonjeong11/nextjs-starter-template