🧽

Clean Architecture 적용기


기존 구조

/src/api/index.js 파일에서 모든 API들을 관리했다. 개별 API들을 컴포넌트에서 끌어다 쓰다보니, 로직이 API를 사용하는 컴포넌트들로 산개됐다. 이런 상황에서 API에 뭔가 변경이 생겼을 때 문제가 발생했다. 변경된 API를 사용하는 컴포넌트를 찾아야하고, 그 컴포넌트에서 로직을 변경해야했다. 뭔가 맞지 않다... 변경이 어려우면 코드를 리팩터링 해야한다는 신호라고 최근 책(Refactoring 2rd edition)에서 읽었다.

그럼 변경하기 쉬운 코드는 뭘까? 검색을 해보다가 Uncle Bob's clean architecture를 만났다. 관심사의 분리로 소프트웨어를 여러 레이어로 나누는 설계 방식이다. 프레임워크, UI, DB 등으로부터 독립적인 시스템을 만들 수 있다는 소개를 보자마자 "이거다"라는 생각이 들었다.

clean-architecture.png|500

블로그에 따르면, 클린 아키텍쳐는 관심사에 따라 계층을 나누고, 세부 구현이 아닌 도메인을 중심으로 설계한다. 그리고 도메인은 프레임워크나 DB, UI 등 외부 요소에 의존하지 않도록 한다. 그리고 이 아키텍쳐가 동작하기 위해서는 의존성 규칙을 지켜야 한다.

의존성 규칙은 모든 소스코드의 의존성은 외부에서 내부로, 즉, 고수준에서 저수준으로 향해야 한다는 규칙이다. 업무의 로직에 해당하는 코드들이 React 같은 프레임워크에 의존해서는 안된다는 것이다.

내가 작성한 코드는 이 규칙을 완전히 무시하고 있었다. 컴포넌트에 로직이 합쳐졌다. UI에 UseCase와 Presenter가 합쳐진 것이다. 서로 강한 의존성을 가지고, 하나가 변경되면 나머지 모두가 영향을 받는다.

리팩터링

나는 클린 아키텍쳐와 여러 레퍼런스를 기준으로 리팩터링을 시작했다.

Reference

아래와 같은 역할을 하는 디렉토리를 생성했다.

  • core: Entities, usecases와 같은 도메인들을 포함한다.
  • data: Repositories, presenters와 같은 어뎁터들을 포함한다.
  • di: 코어에 데이터를 주입한다.

이 디렉토리 구조에 유저 아이디와 토큰에 관련된 Auth를 분리해보겠다.

  1. src/api/index.js에서 use case로 분리하기

먼저 auth와 관련된 api로는 login이라는 API가 있었다. 이 API는 login이라는 use case로 빼냈고, use case에서 제공할 데이터는 IAuthEntity로 정의했다.

그 다음으로는 auth와 관련된 데이터를 사용하는 컴포넌트들을 살펴봤다. 추가적으로 logout, getToken 유즈 케이스를 발견했다.

core/useCases/interfaces/iAuth.ts에 발견한 유즈 케이스들의 인터페이스를 정의했다. core/entities/interfaces/iAuth.ts에는 엔티티를 정의했다.

1// core/entities/interfaces/iAuth.ts
2export interface IAuthEntity {
3 id: string;
4 token: string;
5}
6
7// core/useCases/interfaces/iAuth.ts
8export default interface IAuthUseCase {
9 login(code: string): Promise<IAuthEntity>;
10 logout(): void;
11 getToken(): string | null;
12}
  1. Use case 구현하기

위에서 정의한 인터페이스를 core/useCases/Auth.ts에 구현했다. 구현하면서 auth 정보를 받을 repository가 필요해졌다. 필요한 repository의 인터페이스를 core/useCases/repository-interfaces에 정의해 사용했다.

1// core/useCases/Auth.ts
2export default class AuthUseCase implements IAuthUseCase {
3 constructor(private readonly authRepo: IAuthRepository) {}
4
5 getToken(): string | null {
6 return this.authRepo.getToken();
7 }
8
9 async login(code: string): Promise<IAuthDTO> {
10 return await this.authRepo.login(code);
11 }
12
13 logout() {
14 this.authRepo.logout();
15 }
16}
17// core/useCases/repository-interfaces
18export default interface IAuthRepository {
19 login(code: string): Promise<IAuthDTO>;
20 logout(): void;
21 getToken(): string | null;
22}

Repository interface를 사용하는 이유는 의존성 규칙을 지키기 위해서다. 유즈 케이스가 레포지토리를 직접참조하면 의존성 규칙을 위반하게 된다. 유즈케이스가 인터페이스를 참조, 구체적인 AuthRepository가 이 인터페이스를 구현하면 의존성을 역전시킬 수 있어 의존성 규칙을 지킬 수 있다.

  1. Repository 구현하기

IAuthRepository의 세부사항을 data/repositories/Auth.ts에 구현했다.

1export default class AuthRepository implements IAuthRepository {
2 constructor(private readonly storage: iStorage) {}
3
4 async login(code: string): Promise<IAutoDTO> {
5 const authDTO = await HTTP.get(`/user/login?code=${code}`).then(
6 ({ data }) => new AuthDTO(data)
7 );
8 this.storage.set(authDTO);
9
10 return authDTO;
11 }
12
13 logout() {
14 this.storage.remove();
15 }
16
17 getToken(): string | null {
18 const auth = this.storage.get();
19 return auth ? auth.token : null;
20 }
21}
  1. Presenter 구현하기

위에 했던 과정과 비슷하게 Presenter를 구현했다. UI는 presenter로 auth에 접근하게 된다.

1export default class AuthPresenter implements IAuthPresenter {
2 constructor(private readonly useCase: IAuthUseCase) {}
3
4 async login(code: string): Promise<IAuthEntity> {
5 return await this.useCase.login(code);
6 }
7
8 logout(): void {
9 this.useCase.logout();
10 }
11
12 getToken(): string | null {
13 return this.useCase.getToken();
14 }
15}
  1. 의존성 주입하기

이제 마지막 단계다. di/Auth.ts에서 Presenter에 필요한 모든 의존성을 주입한다. React 컴포넌트는 이제 이 presenter의 인스턴스를 사용한다.

1// ...
2const authRepo = new AuthRepository(new TypeStorage<IAuthDTO>("auth", localStorage));
3const authUseCase = new AuthUseCase(authRepo);
4const Auth = new AuthPresenter(authUseCase);
5
6export default Auth;

이렇게 UI로부터 auth를 분리해냄으로써 컴포넌트는 UI에만 신경쓸 수 있고, 도메인은 도메인에만 신경쓸 수 있게 됐다. 확실히 코드 가독성이 좋아졌다는게 느껴졌다. 이제 컴포넌트에서는 거의 UI에 관한 코드만 남게 됐다.

다음 프로젝트에서는 처음부터 이런 구조로 코드를 작성해보고 싶다. 위에 적은건 auth 하나 뿐이지만, 실제 코드에서는 4~5개 더 빼냈다. 아는게 많아야 몸이 덜 고생하는 것 같다. 처음 적용해봐서 뭐가 맞는지 틀린지 모르겠지만 일단은 좋다!

(뭔가 이상하거나 틀린 점이 보인다면 알려주시면 감사하겠습니다 🙌)