의존성 역전 원칙
유연성이 극대화된 시스템
추상에 의존하며 구체에는 의존하지 않는 것을 의미한다.
추상 (interface)
구체적인 구현 방법이 포함되어 있지 않은 형태를 의미한다.
구체
구체적인 일련의 동작과 흐름을 의미한다. 이런 구체적인 동작들은 굉장히 빈번하게 변경 될 여지가 많다.
결과
외부요소에 직접적으로 의존하는 코드를 최소화하고, 전체적인 제어권을 우리의 애플리케이션 안으로 가져 올 순 있다.
구체가 아닌 추상에 대한 의존성을 중간에 추가하게 되면 특정 시점에서 코드의 실행 흐름(제어 흐름)과 의존성이 방향이 반대로 뒤집히기에 이를 “의존성 역전 원칙(DIP)”이라고 부르며 IoC(Inversion of Control)이라고도 표현한다.
class LocalTokenRepository {
#TOKEN_KEY = "ACCESS_TOKEN";
save(token) {
localStorage.setItem(this.#TOKEN_KEY, token);
}
get() {
return localStorage.takeItem(this.#TOKEN_KEY);
}
remove() {
localStorage.removeItem(this.#TOKEN_KEY);
}
}
const tokenRepository = new LocalTokenRepository();
fetch("todos", {
headers:{
Authorization:tokenRepository.get();
}
}
fetch("todos", {
headers:{
Authorization:tokenRepository.get();
}
}
fetch("todos", {
headers:{
Authorization:tokenRepository.get();
}
}
호출흐름: fetch → tokenRepository Interface(추상) → tokenRepositry Class(구체) → localStorage
의존성방향: fetch → tokenRepository Interface(추상) ← tokenRepositry Class(구체) → localStorage
의존성 주입이란 특정한 모듈에 필요한 의존성을 내부에서 가지고 있는 것이 아니라 해당 모듈을 사용하는 입장에서 주입해주는 형태로 설계하는 것을 의미한다.
class LocalTokenRepository {
#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);
}
}
class HttpClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.tokenRepository = new LocalTokenRepository();
}
fetch(url, options = {}) {
return window.fetch(`${this.baseURL}${url}`, {
...options,
headers: {
Authorization: this.tokenRepository.get(),
...options.headers,
},
});
}
}
const httpClient = new HttpClient(process.env.BASE_URL)
class LocalTokenRepository {
#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);
}
}
class SessionTokenRepository {
#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);
}
}
class TestTokenRepository {
constructor() {
this.#token = null;
}
save(token) {
this.#token = token;
}
get() {
return this.#token;
}
remove() {
this.#token = null;
}
}
class HttpClient {
constructor(baseURL, tokenRepository) {
this.baseURL = baseURL;
this.tokenRepository = tokenRepository;
};
fetch(url, options = {}) {
return window.fetch(`${this.baseURL}${url}`, {
...options,
headers: {
Authorization: this.tokenRepository.get(),
...options.headers,
},
});
}
}
// ver1
const localTokenRepository = new LocalTokenRepository();
const httpClient = new HttpClient(process.env.BASE_URL, localTokenRepository);
// ver2
const sessionTokenRepository = new SessionTokenRepository()
const httpClient = new HttpClient(process.env.BASE_URL, sessionTokenRepository);
// ver3
const testTokenRepository = new TestTokenRepository()
const httpClient = new HttpClient(process.env.BASE_URL, testTokenRepository);
의존성 주입을 적용하면 좋은 점은 해당 모듈에서 직접적으로 의존성을 가지고 있지 않기 때문에 모듈 내부의 코드는 전혀 건드리지 않고 모듈 외부의 일부 코드만 수정함으로서 동작을 변경할 수 있게 된다는 점이다.
의존성 주입을 이용해서 필요한 모듈을 클래스 내부에서 가지고 있는 것이 아니라, 클래스를 생성할 때 외부에서 주입하는 식으로 변경하게 되면 추후에 HttpClient의 코드 수정 없이 HttpClient에서 사용하는 tokenRepository와 연관된 동작을 쉽게 변경해서 다양하게 사용할 수 있게 된다. (OCP: Open-Closed Principle)
이는 곧 프로그램의 유연성, 테스트의 용이성, mocking등을 쉽게 활용할 수 있게 된다는 의미입니다.
함수의 경우에는 인자를 통해서 내부에서 사용할 요소를 전달받을 수 있는데, 동작을 내부에서 전체 다 가지고 있는 것이 아니라, 외부에서 받을 수 있게 설정하면 훨씬 더 유용하게 사용할 수 있게 되는 것을 생각해보면 된다.
참고자료