애플리케이션 기획이 변경되면, 기존에 구현한 방식(예: localStorage 사용)을 그대로 유지하기 어려울 때가 많습니다. 이번 포스트에서는 기획 변경에 따른 유연한 대응을 위해 추상(기획 의도) 과 구체(구현 방법) 를 분리하는 접근법을 적용한 사례를 소개합니다.
먼저, Dependency Inversion Principle (의존성 역전 원칙) 을 되새겨 봅니다.
Dependency Inversion Principle (의존성 역전 원칙)
- 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 의존해서는 안 된다.
- 세부 사항이 정책에 의존해야 한다.
이 원칙을 따르면, 비즈니스 로직(예: 로그인 유지 관리)과 실제 저장소 구현(예: localStorage, sessionStorage)이 서로 독립적으로 변경될 수 있습니다.
기획 의도
한 번 로그인하면 로그아웃 버튼을 누를 때까지 계속 로그인 상태를 유지하는 서비스 제공
기존 구현
구현 방식: access, refresh token 및 사용자 정보를 localStorage에 저장
// 기존 AuthProvider 코드 일부
export function AuthProvider({ children }: AuthProviderProps) {
const [isLoggedIn, setIsLoggedIn] = useState(true);
const activateAuthSession = ({ access, refresh, user }: { access: string; refresh: string; user: UserAuthDetail; }) => {
setIsLoggedIn(true);
localStorage.setItem('auth', JSON.stringify({ access, refresh }));
localStorage.setItem('user', JSON.stringify(user));
};
// 생략...
}
하지만 기획이 변경되어 브라우저 창을 닫으면 로그아웃 되어야 한다는 요청이 들어왔습니다.
| 방법 1 : 기존 방식(localStorage) 변경 거부 | 방법 2 : localStorage 사용 부분을 전부sessionStorage로 교체 |
|---|---|
![]() | ![]() |
그런데 만약 기획이 또 수정되어 아예 다른 storage를 사용해야 한다면, 매번 수정해야 하는 상황에 빠지게 될까요?
🚨 구체적인 구현 방식(예: localStorage 사용)이 기획 의도(추상)에 종속되어서는 안 됩니다.
즉, 비즈니스 로직은 추상(기획 의도)에 의존해야 하며, 구체적인 구현에 의존해서는 안 됩니다.
비즈니스 로직은 구체적인 저장 방식에 의존하지 않고, 추상화된 인터페이스에 의존해야 합니다. 이를 위해 Auth Storage 인터페이스를 정의하고, 브라우저 환경에 맞는 구현체를 별도로 작성합니다.
export const AUTH_STORAGE_KEY = {
auth: 'auth',
user: 'user',
};
interface AuthStorage {
get(key: keyof AUTH_STORAGE_KEY): string | null;
set(key: keyof AUTH_STORAGE_KEY, value: any): void;
remove(key: keyof AUTH_STORAGE_KEY): void;
}
이렇게 정의해두면, 추후에 BrowserAuthStorage가 아닌 react native 의 async storage나 다른 형태의 storage를 사용해야 할 때에도, AuthStorage 인터페이스를 구현(implements)하여 쉽게 대체할 수 있습니다.
브라우저의 Storage API를 이용해 구체적인 구현을 제공하지만, 실제 비즈니스 로직에서는 이 구체 구현에 의존하지 않고 인터페이스만 사용합니다.
export class BrowserAuthStorage implements AuthStorage {
private storage: Storage;
constructor(storage: Storage) {
this.storage = storage;
}
get(key: keyof AUTH_STORAGE_KEY): string | null {
return this.storage.getItem(key);
}
set(key: keyof AUTH_STORAGE_KEY, value: any): void {
const valueString = typeof value === 'string' ? value : JSON.stringify(value);
this.storage.setItem(key, valueString);
}
remove(key: keyof AUTH_STORAGE_KEY): void {
this.storage.removeItem(key);
}
}
비즈니스 로직에서는 이제 authStorage 인터페이스에만 의존하게 됩니다.
기획이 변경되어 로그아웃 조건(브라우저 창 닫음 등)에 따라 필요한 경우, 쉽게 구현체를 교체할 수 있습니다.
'use client';
export function AuthProvider({ children }: AuthProviderProps) {
const queryClient = useQueryClient();
const [isLoggedIn, setIsLoggedIn] = useState(true);
// 기획 변경이 발생해도 쉽게 구현 수정 가능
const authStorage = new BrowserAuthStorage(localStorage);
const activateAuthSession = ({
access,
refresh,
user,
}: {
access: string;
refresh: string;
user: UserAuthDetail;
}) => {
setIsLoggedIn(true);
authStorage.set(AUTH_STORAGE_KEY.auth, { access, refresh });
authStorage.set(AUTH_STORAGE_KEY.user, user);
};
// 생략: inActiveAuthSession, getUserInfo 등
return (
<AuthContext.Provider
value={{
isLoggedIn,
activateAuthSession,
inActiveAuthSession,
setIsLoggedIn,
userInfo: getUserInfo(),
setUserInfo,
}}
>
{children}
</AuthContext.Provider>
);
}
대다수 애플리케이션에서 유지보수성(maintainability) 은 재사용성보다 훨씬 중요하다. 애플리케이션에서 코드가 반드시 변경되어야 한다면, 이러한 변경이 여러 컴포넌트 도처에 분산되어 발생하기보다는, 차라리 변경 모두가 단일 컴포넌트에서 발생하는 편이 낫다. 만약 변경을 단일 컴포넌트로 제한할 수 있다면, 해당 컴포넌트만 재배포하면 된다. 변경된 컴포넌트에 의존하지 않는 다른 컴포넌트는 다시 검증하거나 배포할 필요가 없다.
[📕 클린아키택처 - 110p]
추상(기획 의도)과 구체(구현 방법)의 분리
기획 변경에도 비즈니스 로직은 그대로 유지되고, 구현체만 교체하면 됩니다.
의도적 중복 허용
모든 로직을 하나의 공통 컴포넌트로 통합하기보다는, 기획에 맞게 적절히 분리해 두는 것이 변경 시 유지보수성을 높입니다.
공용 로직 추상화
비즈니스 로직과 구체적 구현을 혼합하지 않고, 일반적(추상적) 개념에 의존하도록 설계합니다.
이번 리팩토링 사례에서는 의존성 역전 원칙을 기반으로 기획 의도와 구체 구현을 분리하는 방법에 대해 생각해 보았습니다. 클린 코드와 유지보수성을 고려한 설계가 장기적인 프로젝트 관리에 큰 도움이 됨을 다시 한 번 느낄 수 있었습니다. 앞으로도 이러한 설계 원칙을 꾸준히 적용하여 보다 유연하고 확장 가능한 코드를 작성할 수 있도록 노력해야겠습니다.