1. 의존성 (Dependency) 이란?
의존성이란 특정한 모듈이 동작하기 위해서 다른 모듈을 필요로 하는 것
2. 의존성 역전 원칙(DIP)
“유연성이 극대화된 시스템"을 만들기 위한 원칙이다. 이 말은 곧 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 것을 의미한다.
- 추상
- 구체적인 구현 방법이 포함되어 있지 않은 형태를 의미
- 쉽게 말하면, 그 내부가 어떻게 구현되어있는지 신경쓰지 않고 그냥 내가 “해줘야 하는 일”과 “결과"만 신경쓸 수 있게 된다는 것 - 구체
- 실질적으로 해당 동작을 하기 위해서 수행해야 하는 구체적인 일련의 동작과 흐름을 의미
- 구체적인 동작들은 굉장히 빈번하게 변경될 여지가 많다.
- 이러한 구체에 애플리케이션이 점점 의존하게 된다면 결국 구체가 변할 때 마다, 내 애플리케이션도 그에 맞춰서 변화해야 한다는 의미
실생활의 예를 통해 추상과 구체의 개념을 알아보면, 우리 모두는 스마트폰을 활용한다. 그리고 스마트폰은 전화를 할 수 있으며, 우리는 스마트폰의 전화 앱을 실행하고
- 번호를 입력한다.
- 통화 버튼을 누른다.
의 과정을 거치면 통화가 이루어진다는 것을 알고 있다.
하지만 저 내부적인 과정에서는 우리의 요청을 통신사가 받아서, 기지국을 찾고, 상대방의 전화번호와 연결된 기지국을 찾고 두개의 음성을 연결해서 실시간으로 전달해주는 구체적인 과정이 발생한다.
우리가 어떤 스마트폰을 사용하든, 그리고 어떤 통신사를 사용하든 번호를 입력하고, 통화버튼을 누른다는 추상은 변하지 않는다. 하지만 통신사가 변경되면 통신사별로 통화를 연결할 때 사용하는 프로세스, 기지국 등은 미묘하게 달라질 것이다. 만약 우리가 통신사를 변경할 때 마다 이러한 모든 프로세스를 일일이 맞춰서 변경해야지만 통화기능이 동작하게 되어있다면 대부분의 사용자들은 결국 통신사를 변경하는 것을 포기하게 될 것입니다.
이처럼 변화가 자주 발생하는 추상에 의존하는 것은 애플리케이션 구조 상 기피해야 할 항목이다.
하지만, 우리가 일반적으로 코드를 작성하다보면 위와 같이 구체에 의존하는 경우가 자주 발생하게 된다.
fetch("todos", {
headers:{
Authorization:localStorage.getItem("ACCESS_TOKEN");
}
}
위 코드가 가진 두가지 문제
- localStorage라는 구체적인 사항에 의존
- 이는 storage를 추후에 다른 저장소로 변경하기 힘들다는 것을 의미 - localStorage는 “브라우저”라는 구체적인 사항에서 제공하는 API이다.
- 이런 애플리케이션 외부의 요소들은 변화가 발생할 수 있으며, 가장 큰 문제는 어떤식으로 변화할 지 우리가 컨트롤 할 수 없다는 점, 따라서 이런 요소들에 직접적으로 의존하는 것은 좋지 않다.
물론, 구체적인 요소에 하나도 의존을 하지 않고 애플리케이션을 만들 수는 없다. 실질적으로 브라우저에서 제공하는 기능을 이용해야 한다는 사실을 무시할 순 없기때문이다. 하지만, 이 외부 요소에 직접적으로 의존하는 코드를 최소화하고, 전체적인 제어권을 우리의 애플리케이션 안으로 가져올 순 있다.
class TokenRepository {
TOKEN_KEY = "ACCESS_TOKEN";
save(token) {
localStorage.setItem(this.TOKEN_KEY, token);
}
get() {
return localStorage.getItem(this.TOKEN_KEY);
}
remove() {
localStorage.removeItem(this.TOKEN_KEY);
}
}
const tokenRepository = new TokenRepository();
fetch("todos", {
headers:{
Authorization:tokenRepository.get();
}
}
위와 같은 방식으로 코드를 변경하게 되면 구체적인 요소인 localStorage는 TokenRepository Class에 의해서 관리된다. 그리고, 애플리케이션 내에서의 의존관계는 변경되게 된다. 이제 핵심 비지니스 로직들은 tokenRepositry에 의존하게 되었으며 실질적인 localStorage에 대한 의존성은 없어지게 되었다. 만약 이 상황에서 외부 요소들이 변경되게 된다면?
외부요소들이 변경되게 된다면 외부 요소들의 동작을 tokenRepository에 맞춰주게 되면 된다. sessionStorage로 변경되든, cookie로 변경되든 외부요소들이 어떻게 되든 상관없이 외부요소들은 무조건 save, get, remove라는 tokenRepositry에 구현된 3가지 동작을 할 수 있어야 한다.
class TokenRepository {
TOKEN_KEY = "ACCESS_TOKEN";
save(token) {
sessionStorage.setItem(this.TOKEN_KEY, token);
}
get() {
return sessionStorage.getItem(this.TOKEN_KEY);
}
remove() {
sessionStorage.removeItem(this.TOKEN_KEY);
}
}
이 상황에서 코드의 실행 흐름과 의존성의 방향
코드는 아래의 방향대로 실행
- API 호출 코드 → tokenRepositry → localStorage
기존의 구체적인 localStorage를 그대로 사용하고 있던 코드의 의존성 방향
- API 호출 코드 → localStorage
위와 같은 의존성이 설정되어있기에 localStorage가 변경되면 API 호출 코드 또한 변경되어야 한다.
하지만 tokenRepository를 이용해서 의존성을 관리한 코드는 아래와 같은 의존성 방향을 가진다.
- API 호출 코드 → tokenRepositry ← localStorage
이처럼 특정 시점에서 코드의 실행 흐름(제어 흐름)과 의존성이 방향이 반대로 뒤집혔기에 이를 “의존성 역전 원칙(DIP)”이라고 부르며 "IoC(Inversion of Control)"이라고도 표현합니다.
3. 의존성 주입
의존성 주입이란 특정한 모듈에 필요한 의존성을 내부에서 가지고 있는 것이 아니라 해당 모듈을 사용하는 입장에서 주입해주는 형태로 설계하는 것을 의미한다.
- 의존성 주입 X
import httpClient from "./httpClient";
import tokenRepository from "./tokenRepository";
class AuthService {
signup(email, password) {
httpClient
.fetch("auth/signup", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ access_token }) => tokenRepository.save(access_token));
}
singin(email, password) {
httpClient
.fetch("auth/signup", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ access_token }) => tokenRepository.save(access_token));
}
logout() {
tokenRepository.remove();
}
}
const authService = new AuthService();
- 의존성 주입 O
import httpClient from "./httpClient";
import tokenRepository from "./tokenRepository";
class AuthService {
constructor(httpClient, tokenRepository) {
this.httpClient = httpClient;
this.tokenRepository = tokenRepository;
}
signup(email, password) {
this.httpClient
.fetch("auth/signup", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ access_token }) => this.tokenRepository.save(access_token));
}
singin(email, password) {
this.httpClient
.fetch("auth/signup", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ access_token }) => this.tokenRepository.save(access_token));
}
logout() {
this.tokenRepository.remove();
}
}
const tokenRepository = new TokenRepositry();
const httpClient = new HttpClient(process.env.BASE_URL);
const authService = new AuthService(httpClient, tokenRepository)
- 의존성 주입을 적용하면 좋은 점
- 해당 모듈에서 직접적으로 의존성을 가지고 있지 않게 되는 것
예를들어 의존성 주입을 하지 않은 경우에는 AuthService 클래스에서 직접적으로 httpClient, tokenRepositry를 의존하고 있기에 관련된 동작을 변경하려면 AuthService를 직접 수정해야 한다.
하지만 의존성 주입을 이용해서 클래스 내부에서 가지고 있는 것이 아니라, 클래스를 생성할 때 외부에서 주입하는 식으로 변경하게 되면 추후에 AuthService의 코드 수정 없이 AuthService에서 사용하는 httpClient, tokenRepositry와 연관된 동작을 쉽게 변경해서 다양하게 사용할 수 있게 된다.
이는 곧 프로그램의 유연성, 테스트의 용이성, mocking등을 쉽게 활용할 수 있게 된다는 의미이다.
보통 Class 단위에서 많이 사용되는 용어이기에 어려움을 느낄 수 있지만 익숙한 함수로 생각하면 된다고 한다. 함수의 경우에는 인자를 통해서 내부에서 사용할 요소를 전달받을 수 있는데, 동작을 내부에서 전체 다 가지고 있는 것이 아니라, 외부에서 받을 수 있게 설정하면 훨씬 더 유용하게 사용할 수 있게 되는 것을 생각해보면 된다.
const log = (data) => console.log(data);
log("Hello, World");
// --------------------------
const log = (logger, data) => logger(data);
log(console.log, "Hello, World");
log(console.info, "Hello, World");
log(console.warn, "Hello, World");
log(console.error, "Hello, World");
log(customLogger, "Hello, World");
기본적으로 Class의 경우에는 constructor를 통해서, 함수의 경우에는 인자를 통해서 의존성을 주입하게 된다.
그런데 리액트 애플리케이션을 설계하다보면 컴포넌트에도 의존성을 주입하고 싶은 욕구가 생길 수 있다. 하지만 리액트는 props를 통해서 단방향으로만 데이터를 전달할 수 있기에 의존성을 주입하기가 쉽지 않다. 이를 해결하기 위해서 Context API를 컴포넌트에게 의존성을 주입하는 용도로 사용할 수 있다.
'Computer Sceience > etc.' 카테고리의 다른 글
TDD (Test Driven Development) - 테스트 주도 개발 (0) | 2022.11.03 |
---|---|
소프트웨어 테스트와 종류 (0) | 2022.10.31 |
Design Pattern (MVC & Flux) (0) | 2022.10.28 |
횡단 관심사 (Cross-cutting-concerns) (0) | 2022.10.26 |