지난 원티드 프리온보딩 인턴쉽 강의에서 의존성에 대한 내용을 설명해주셨는데, 잘 이해가 되지 않아 다시 정리해보려고 한다. 내용의 일부분은 강의 자료를 참고해서 작성했음을 밝힌다.
✔️의존성
의존성이란 특정 모듈의 동작을 위해서 다른 모듈을 필요로 하는 것을 의미한다. 여기서 모듈이란 하나의 기능을 수행하는 함수 혹은 클래스를 의미한다. 예를 들어 함수A를 실행하기 위해 B라는 요소가 필요하다면 '함수 A는 B에 의존성을 갖고 있다' 혹은 'B는 A의 의존성이다' 라고 표현할 수 있다.
의존성에 대해 현재(내가..) 이해할 수 있는 가장 실질적인 예시로 useEffect 훅의 두 번째 매개변수인 의존성 배열이 있다.
const issues = useRecoilValue(issuesAtom);
useEffect(() => {
issuesAPI.setIssues(issues);}, [issues]);
위의 예시는 컴포넌트가 마운트 되거나, issues 라는 상태에 변화가 생길 때마다 setIssues 라는 함수를 실행하고 있다. 이 때 useEffect 안의 함수를 실행시키기 위해 issues 라는 상태값에 의존을 하고 있기 때문에, 의존성 배열에 값을 넘겨주어야 의도대로 동작하게 된다.
이 의존성이라는 개념을 왜 알아야 할까? OOP에서는 이 개념이 굉장히 중요하다고 한다. 의존성이라는 것은 두 모듈간의 관계라고도 볼 수 있는데, 의존성이 있는 두 모듈은 하나의 모듈에서 변경된 내용이 다른 모듈의 동작에 영향을 끼칠 수 있기 때문이다. 그래서 어떤 모듈- 함수나 클래스를 설계할 때 한 가지 목적만을 가지고 동작하도록 설계하는 것이 좋다. 그리고 가능하면 많은 의존성을 갖지 않도록 하는 것이 좋다.
좋다. 의존성이란 어떤 모듈을 동작시키기 위해 필요한 다른 요소들이라는 것을 알았다. 많은 의존성을 갖지 않는 것이 좋다고 했는데, 모듈을 만들면서 다른 요소에 의존성을 1도 갖지 않는 것은 불가능한 일일 것이다. 그러면 어떤 방식으로 모듈을 설계하는 것이 좋을까? 이에 대한 답은 객체지향 5원칙인 SOLID 의 D - Dependency Inversion Principle 즉 의존성 역전 원칙에 있다.
✔️의존성 역전 원칙 (Dependency Inversion Principle DIP)
이 원칙은 자주 변화하는 것, 변화하기 쉬운 것 보다는 거의 변화가 없는 것에 의존해야 한다는 것이다. 변화하기 쉬운 것 다시말하면 구체적인 것보다는 추상화 된 것에 의존해야 한다는 것이다. 그렇게 해야 관리가 쉽고 변화가 쉬워진다.
추상화란 OOP 뿐만 아니라 프로그래밍 전반적으로 중요한 개념 중 하나이다. 추상화란 단순하게 사용하는 대상이 그 구체적인 구현 내용을 몰라도 쓸 수 있도록 만드는 것이다.
일상 생활에서 찾아볼 수 있는 예시로는 전원 버튼이 있다. 전원은 버튼을 누른다 -> 켜진다 와 같은 매우 단순화된 사용 방법을 가지고 있다. 하지만 내부적으로는 수많은 구체적인 과정을 거쳐서 실행될 것이다. 만약 이런 추상화가 없이 전원을 켜야 한다면 일단 전원을 켜는 방법이 매우 복잡할 것이고, 기기의 제조사마다 내부적으로 생긴 모양도 다를 것이고 그에 맞게 일일히 대응을 해줘야 한다는 문제점이 있다.
따라서 내가 제어할 수 없는 외부의 구체적인 요소에 의존하게 만들기 보다는, 추상화된 클래스를 만들어서 High level 과 Low level 을 연결하는 중간 다리를 놓아주는 것이다.예시로 High level 모듈이 API 호출 모듈이고, Low level 모듈이 localStorage 라는 구체적인 요소일 때, 이 둘을 직접 연결하는 것은 극단적으로 비유하자면 전원 버튼 없이 전선을 직접 연결해서 전원을 켜는 것같은 느낌이다.
또한 localStorage 라는 구체적인 저장소에 의존하고 있다면, 이 저장소를 나중에 다른 저장소로 변경하기가 힘들어질 수 있다. 또 우리가 개발한 요소가 아닌 브라우저에서 제공하는 API 이기 때문에, 즉 외부의 요소이기 때문에 어떻게 변화할지 예측할 수 없고 만약 변화가 생긴다면 직접적으로 의존하고 있는 모든 부분에서 대응을 해주어야 한다.
두 모듈을 직접적으로 의존하게 하는 것과, 중간에 관련된 구체적인 일을 담당하는 추상화된 Interface 를 생성하여 둘을 연결하는 것을 비교해보자.
fetch("todos", {
headers:{
Authorization:localStorage.getItem("ACCESS_TOKEN");
}
}
어떤 프로그램에서 모든 API 호출에서 요청 헤더에 직접 localStorage 에서 가져온 액세스 토큰을 넣어 요청을 보내고 있다. 이 때의 의존관계는 API 호출 모듈 -> localStorage 가 된다. API 호출 모듈이 정상적으로 동작하기 위해서 localStorage에 의존하고 있기 때문이다. 만약 localStorage 에 대한 변경이 생긴다면 그에 따라 API를 호출하는 코드도 변경이 필요해진다.
이 과정에 추상화된 요소를 도입할 수 있다. 참고로 JavaScript에는 추상화된 요소를 표현할 수 있는 방법이 존재하지 않는다. Typescript 에는 class 앞에 abstract 를 붙여 추상클래스와 추상메서드를 정의할 수 있다. 토큰에 관련된 구체적인 동작을 Typescript 문법으로 추상화하여 표현한다면 아래와 같을 것이다. (사실 본인이 직접 사용해 본 적은 없으므로 이런 느낌이라는 것에 가까울지도ㅋㅋ)
abstract class TokenRepositoryInterface {
public abstract save(token:string):void; // string type의 토큰을 받고, 리턴 값은 없음
public abstract get():string;
public abstract remove():void;
}
위와 같이 정의된 추상 클래스를 바탕으로 tokenRepository 라는 클래스를 만들 수 있을 것이고, 여기에 해당하는 메서드를 사용해 위 API 호출 코드를 고쳐보면 아래와 같을 것이다.
fetch("todos", {
headers:{
Authorization:tokenRepository.get();
}
}
중요한 것은 tokenRepository 클래스의 get 이라는 메서드가 localStorage를 사용하든, sessionStorage를 사용하든 이 API 호출 코드에서는 중요하지 않다는 것이다. 내부의 구현을 추상화했기 때문에 변화를 주고싶다면 해당 부분이 구체적으로 구현된 tokenRepository 클래스에 가서 고치면 된다.
그리고 또 한가지 중요한 것은 tokenRepository 클래스는 상속받은 추상클래스인 tokenRepositoryInterface 에 정의된 내용을 구현해야할 의무가 있다. 따라서 tokenRepository 가 추상클래스 tokenRepositoryInterface에 의존성을 가지고 있다고 말할 수 있다.
이제 다시 돌아와서 두 방식의 의존성 방향을 비교해보자.
1. API 호출 모듈 -> localStorage
2. API 호출 모듈 -> tokenRepository Interface <- tokenRepository Class -> localStorage
두 가지 방식 다 실행 흐름은 똑같이 API 호출 ~ (중간에 클래스) ~ localStorge 로 방향이 같은데, 두 번째 방식에서 추상 클래스를 도입함으로써 High level 도 Low level 도 모두 Interface 에 의존성을 가지고 있다. 이렇게 실행 흐름과 의존성의 방향이 뒤집어졌기 때문에 이 원칙에 '의존성 역전 원칙' 이라는 이름이 붙었고, 이러한 방식을 제어의 흐름이 뒤집혔다고 하여 Inversion of Control (IOC) 라고도 한다.
✔️결론
개인적으론 이름이 의존성 역전 원칙이라서 도대체 이게 뭔소리인가... 싶은데 지금까지 정리된 내용으로 보자면 추상화를 도입하여 거기에 의존성을 갖게 한다는 의미인 것 같다. 추상화에 의존하고 있기 때문에 구체적인 것에 의존하는 것보다 더 유연하게 대처할 수 있다는 장점이 있다.
이렇게 추상화를 하여 유연하게 대처해야 하는 이유는, 소프트웨어는 Soft 라는 이름에 걸맞게 변화에 대응하고 기능을 확장해 나갈 수 있어야 하기 때문이다. 바꿔말하면 유지/보수가 쉬운 코드를 만들기 위해서라고 할 수 있다. 구체적인 것에 의존하느라 변화에 일일이 대응을 해줘야만 한다면 수정도 힘들고 결국은 멈추는 코드가 될 가능성이 높다.
자바스크립트 ES6 부터 도입된 class 문법으로 객체지향 프로그래밍을 할 수 있다는 것을 배우게 되었다. 지금까지는 그냥 재사용을 위해서 함수로 분리했던 기능들이 많은데, 변화나 유지보수를 고려한 추상화의 관점으로 접근할 수도 있다는 깨달음을 얻었다. 실질적으로 지금까지 class 문법을 프로젝트에 도입해 본 적이 없는데, 위와 같이 적용하면 확실히 수정도 쉽고 사용성이 좋은 모듈을 만들 수 있을 것 같다.
추가적으로, JavaScript에서의 객체지향 프로그래밍이 궁금하다면 아래의 글을 읽어보길 추천한다.
'TIL' 카테고리의 다른 글
[TIL] 2023-0112 : 3주차 과제 진행 과정 -2 디바운싱, 캐싱, 키보드 이벤트 (0) | 2023.01.12 |
---|---|
[TIL] 2023-0111 : useRef / 3주차 과제 진행 과정 (0) | 2023.01.12 |
[TIL] 2023-0106 : 프리온보딩) 2주차 과제 제출, Custom hook과 관심사 분리 (0) | 2023.01.07 |
[TIL] 2023-0104 : 프리온보딩) 1주차 과제 리뷰, 리액트 렌더링 최적화 (0) | 2023.01.04 |
[TIL] 2022-1222 : 프리온보딩 인턴십 1차 과제 ing (1) | 2022.12.23 |