들어가며😃


이전에 관심사의 분리에 대해 정리하였다. 그때 ‘소프트웨어는 변화에 유연하게 대응할 수 있어야한다’ 라는 것을 배웠는데 오늘의 주제 또한 이 이념과 맞닿아있다. 오늘은 개념적인 부분도 참 중요하지만 이것을 적용하는게 더 중요한 것 같다. 코드가 엉망이지만 아직 다른 개발 건이 많을 때, 혹은 다른 프로젝트가 있을 때, 에라 모르겠다 다음에 하지 뭐 가 아닌 처음부터 잘 개발할 수 있도록 이 원칙을 마음에 새기자는 뜻으로 오늘의 포스팅 시작해보겠다.



의존성 역전 원칙(Dependency Inversion Principle)이란?


쉽게 말하면 변화하기 쉬운 것에 의존하지 마라는 것 인데, 우선 의존성이란 특정한 모듈이 동작하기 위해서 다른 모듈을 필요로 하는 것을 의미한다. 음 의존성이라는 말을 useEffect 에서 많이 써서 익숙할 것 이다. 의존성 배열에 값을 넣어주면 해당 값에 의존하여 값이 변할 때 마다 그냥 리랜더링 하면 된다.
그런데 코드와 코드에 의존성이 있다면?? 내가 의존하고 있는 코드에 수정할 부분이 있다면 여기도 고쳐주고 저기도 고쳐주고 이지저리 해당 코드를 찾으러 다녀야한다. 그런 의미에서 의존성 역전 원칙은 “유연성이 극대화된 시스템”을 만들기 위한 원칙으로, 구체에 의존하고 있던 의존성을 추상에 의존하도록 바꾸는 것이다.


추상? 구체? 🤷🏻‍♀️

우선 위의 말을 이해하기 위해선 추상과 구체의 개념을 먼저 알아야한다.

  • 추상 : 구체적인 구현 방법이 포함되어 있지 않은 형태을 뜻한다. 즉, 내부가 어떻게 구현되어 있던 신경 쓸 필요 없이 내가 해줘야하는 일(함수에 인자로 뭘 넣을지/input)과 결과(output)만 신경 쓸 수 있다는 것이다. 결국 추상화란 내가 여기서 제일 필요한 핵심 작업들만 추출하는 것이라 할 수 있겠다. 예를 들어 우리는 빈번하게 사용하고 있는 console.log가 어떤 원리로 브라우저에 찍히는지 모른다. 우리는 그 내부 로직을 이해하지 않고 쓰고 있는데, 인자에 어떤값을 넣어주면 브라우저에 값이 찍힌다 까지만 아는 것, 이게 바로 추상이다.

  • 구체 : 실질적으로 해당 동작을 수행하기 위한 구체적인 일련의 동작과 흐름을 뜻한다. 위의 예와는 반대로 console.log 라는 함수가 실제로 브라우저에 찍히기까지의 하나하나 모든 과정이 구체이겠다. 하지만 이 구체의 동작들은 빈번하게 변경될 여지가 있다. 따라서 소프트웨어가 구체에 의존하게 된다면 구체가 변할 때 마다, 내 소프트웨어의 로직도 그에 맞추어 변화해야 되고 점점 더 자주 바꿔줘야 하고 그 과정이 힘들어진다. 그렇다면 결국 변화를 포기하게 되고 소프트웨어는 망가지게 되는 것이다. 그렇기 때문에 구체에 의존하지 않는 것이 좋다.


구체에 존재하지 않는 것은 어떤 것일까?? 🤷🏻‍♀️

구체와 추상


위의 좌측 구체 이미지를 보면, 기능 1~5 가 모두 직접적으로 ‘대표 기능’ 모듈 하나에 의존하고 있다. 그말은 대표 기능이 변경되면 기능 1~5 모두 코드를 수정해야한다는 것을 의미한다.
하지만 우측 추상 이미지를 보면 내가 의존하는 파트를 기능을 다른 모듈로 빼서, 그 밑에 하위 모듈을 두게한다. 하위 코드들은 대표로 의존하는 기능이라는 저 모듈에만 의존하게 되고 대표 기능이 수정되어야 할 경우에는 하나의 모듈만 수정해주면 되는 것이다.

이게 바로 우리가 원하는 목표인 ‘추상화’이다.



이제 의미는 좀 알겠다. 그런데 코드를 좀 봐야 와닿을 것 같다. 가장 쉽게 떠올릴 수 있는 localStorage 관련 로직으로 예를 들어보자. localStorage를 사용하는 로직, 특히 로그인의 경우 토큰정보를 localStorage에 저장하고(save), 값을 가져오고(get), 값을 삭제(delete)하는 로직이 일반적이다. 이 작업들을 추상화해보자.


const token = localStorage.getItem("ACCESS_TOKEN");

위의 코드는 사실 나에겐 너무 당연한 방식으로써,, 늘 저렇게 사용했던 것 같다. 하지만 이 코드는 아주아주 구체적인 사항에 의존하고 있다. 추후 토큰 정보를 localStorage가 아닌 다른 저장소에 저장한다고 하면?? 그때부터 숨바꼭질 시작인것이다. 이제 이 코드를 추상화하기 위해 인터페이스를 설정해줄것이다.

// LocalStorageInterface

//   save(token:string):void
//   get():string
//   delete():void

// 자바스크립트에는 추상적인 요소들을 정의할 수 있는 방법이 없기에 주석을 이용해서 표현해봄

위의 인터페이스를 정의하는 작업은 하나의 약속과도 같다. 정의된 input/output 값이 잘 지켜진다면 내부에 코드를 어떻게 구현하던 상관이 없다. 자 이제 위에서 정의한 기능들을 구체적으로 구현해보자.

import { useEffect } from "react";
import axios from "axios";

export const useLocalStorage = () => {
  const TOKEN_KEY = "ACCESS_TOKEN";

  const saveToken = (token) => {
    localStorage.setItem(TOKEN_KEY, token);
  };

  const getToken = () => {
    return localStorage.getItem(TOKEN_KEY);
  };

  const removeToken = () => {
    localStorage.removeItem(TOKEN_KEY);
  };

  return { saveToken, getToken, removeToken };
};

// 외부 컴포넌트
export default function OuterComponent() {
  const { getToken } = useTokenRepository();
  const token = getToken();
}

위와 같은 방식으로 코드를 변경하게 되면 localStorage는 useLocalStorage라는 훅을 통해 관리되기 때문에 우리가 통제할 수 있다. 그리고 여기서 중요한 건 useLocalStorage 훅은 위의 LocalStorageInterface에서 정의한 사항을 모두 구현해주어야할 ‘책임’이 있다. 즉, useLocalStorage 훅은 위의 LocalStorageInterface에 의존하는 것이다.

만약 여기서 localStorage에서 SessionStorage로 저장소를 바꿔보면 어떨까? 🤷🏻‍♀️

상관없다. 외부요소들이 변경되게 된다면 외부 요소들의 동작을 LocalStorageInterface에 맞춰서 다시 구현해주기만 하면 된다. sessionStorage로 변경되든, cookie로 변경되든 상관없이 외부요소들은 무조건 save, get, remove라는 LocalStorageInterface에 구현된 3가지 동작만 할 수 있으면 된다.



호출 흐름과 의존성의 방향


// 변경 전
const token = localStorage.getItem("ACCESS_TOKEN");
  1. 실행 흐름 : token -> localStorage(구체)
  2. 의존성의 방향 : token -> localStorage(구체)


변경 전의 코드를 보면 token -> localStorage에 의존하고 있다. 만약 이 코드를 쓰는 곳이 100군데라면,, OMG 그 코드를 모두 찾아 일일히 수정해줘야 하는 상황이 온다.

// 변경 후
const token = getToken();

LocalStorageInterface를 정의하여 다시 재구성한 로직이다. 이 코드는 아래와 같은 호출 흐름과 의존성 방향을 가지게 된다.

  1. 실행 흐름 : token -> LocalStorageInterface(추상) -> useTokenRepository(구체)
  2. 호출 방향 : token -> LocalStorageInterface(추상) <- useTokenRepository(구체)


보이는가. 의존성이 뒤바뀌었다.
구체가 아닌 추상에 대한 의존성을 중간에 추가하게 되면 특정 시점에서 코드의 실행 흐름(제어 흐름)과 의존성이 방향이 반대로 뒤집히기에 이를 의존성 역전 원칙(DIP)이라고 부르며 IoC(Inversion of Control)라고도 표현한다.



의존성 주입(Dependency Injection)


의존성 주입이란 특정한 모듈에 필요한 의존성을 내부에서 가지고 있는 것이 아니라 해당 모듈을 사용하는 입장에서 주입해주는 형태로 설계하는 것을 의미한다…라고 하는데 쉽게 말해서 그냥 함수에 인자 전달하는 것을 어렵게 말한거다. withAuth(title, onSubmitFunc) 처럼 props를 받아 외부에서 주입하는 식으로 변경하면, 코드를 유연하게 사용할 수 있다는 말이다.

그런데 리액트에서는 props를 받을 순 있지만, 여러단계에 걸쳐서 받는 경우에는 의존성을 주입하기가 어렵다. 그치만 우린 방법이 있다. props 를 넘기기 힘들땐? Context API 를 사용하여 컴포넌트에 의존성을 주입하면 된다.




해당 개념을 정리하며 돌이켜보니 실제로 localStorage 관련 코드를 돋보기로 찾아다니며 일일히 수정했던 경험이 떠올랐다. 어쩌다 보니 구체화의 부작용을 몸소 체험하게 되어버렸는데.. 이제 해당 원리와 개념을 잘 짚었으니 앞으로는 구체가 아닌 추상에 의존하여 코드를 짤 수 있는 사람이 되어야겠다. 일단 당장의 목표는 원티드 사전과제로 제출했었던 로그인/로그아웃/투두리스트의 로직들을 추상화 하는 것! 이걸로 확실히 개념을 잡아야겠다.🧐 ++ 사실 이전에 커스텀 훅은 리액트에서 제공하는 hook을 사용하여 구현한 것만 개념에 해당되는 줄 알았다. 하지만 위의 코드를 구현하며 커스텀 훅의 핵심 개념은 코드를 재사용 가능한 단위로 분리하고, 필요한 상태나 로직을 추상화하여 사용하는 것임을 알았다. 개념이 더 탄탄해졌다.💪🏻