요즘 들어 많이 마주치는 단어 중 하나는 DI, Dependency Injection이다. 말 그대로 번역하면 "의존성 주입"이고, 좀 더 풀어서 해석하면 "의존관계를 외부에서 넣어주는"이라는 문장이 된다. 책이나 강의를 통한 예제를 통해서는 단순히 어떤 객체가 가지는 멤버변수가 있을 때, 자신 스스로 멤버변수를 만들어서 설정하는게 아니라 외부에서 이미 만들어진 녀석을 갖고 와서 쓰는 느낌으로만 알고 있었다. 그러나 DI를 나 스스로가 정말 무엇을 말하는지 알고 있는가?라고 물으면..음 글쎄다.

 

public class MemberService {
    private final MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

내가 아는 DI는 단순히 이렇게 "외부에서 만들어진 녀석을 내부로 갖고 와서 쓰게 해주는 것" 이다

 

이번 기회에 의존성 관계가 뭔지 확실히 정리해보자.


의존성(의존 관계)란

public class MemberService {
    private final MemberRepository memberRepository;
}

 

MemberService가 멤버변수로 MemberRepository를 가지는 모습이다. MemberService의 메서드들은 MemberRepository라는 멤버변수를 다루는 행동들을 할 것이다. 당연하게도, MemberRepository라는 놈이 변하면 그에 따라 MemberRepository를 다루는 MemberService에게도 영향이 미칠 것이다. 때문에 이런 관계에서, "MemberService는 MemberRepository에게 의존한다"라고 표현한다.

 

MemberService가 MemberRepository를 가진다

당연히 MemberService가 MemberRepository에 의한 영향을 받는다

→ MemberService가 MemberRepository에 의존한다

 

인 것이다. 하지만 나는 소프트웨어학과이므로 좀 더 추상적인 수준에서 "의존관계(의존성)"를 설명할 수 있어야 한다.

추상적은 수준에서의 "의존관계"란,

 

"두 객체 사이에서 한 객체가 변하면 다른 객체에게 영향이 가는 관계, 즉 변경에 의한 영향을 받는 관계"

 

라고 표현할 수 있을 것이다. 영향을 받는 객체가 영향을 발생시키는 객체를 의존한다고 표현하는 것이고. 구체적인 설명으로는 한 객체가 다른 객체의 메서드를 쓰는 관계, A가 B를 사용해야만 A의 역할을 수행할 수 있는 관계..라고도 볼 수 있다.

 

 

주입(Injection)이란

이건 단어 그대로 보면 된다. 외부에서 꽂아넣는거다.

 

public class MemberService {
    private final JofeMemberRepository memberRepository;
    
    public MemberService() {
        this.memberRepository = new JofeMemberRepository();
    }
}

 

위 코드는 JofeMemberRepository필드를 클래스 내부에서 스스로 찍어내고(?) 있다.

 

public class MemberService {
    private final JofeMemberRepository memberRepository;
    
    public MemberService(JofeMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

위 코드는 생성자를 통해(setter메서드 등으로 하는 것도 상관없음) 외부에서 JofeMemberRepository를 받아와서 자신의 필드로 세팅하는 모습이다. 외부에서 가져온다 → 외부에서 "주입"한다.

 

이것을 주입이라고 하는 것이다. 

근데 굳이 왜 주입하는거지? 어차피 내부에서 찍어내나 외부에서 주입하나 어차피 다른 거 없지 않아?

 

그것에 대한 답은 "역할을 분리하기 위해서"이다.

생성과 사용에 대한 관심을 분리하여 생성에 대한 책임은 다른 누군가에게 위임하고, 나는 사용하는 역할을 맡겠다는 것. 쉽게 말해서

 

"만드는 건 너가 해. 쓰는 건 내가 할게"

 

라는 거다. 또한 이렇게 책임을 짬때리는위임하는 동시에 필요에 따라 객체가 생성되는 방식을 선택할 수 있게 된다! 결국엔 의존하는 관계에서 가지던 일종의 강한 결합을 느슨하게 만들게 되고 이것을 통해 설계의 유연성을 가질 수 있기 때문에 굳이 주입하는 식으로 만드는 것.

 

 

이렇게 의존관계를 주입하는 것, 즉 외부에서 끼워넣는 것을 의존성 주입이라고 한다. 그러나! 이렇게 단순히 의존관계를 주입하는 것은 진정한 의존성 주입(DI)이 아니다.

(아니 의존관계를 주입하는 것을 의존성 주입이라고 부르지 않는다니,,이게 뭔말일까?)

 

즉 단순히 외부에서 만들어진 녀석을 내부로 가져온다고 해서 죄다 DI라고 하진 않는 것.

하나 더 알아둬야 할 것이 있다. 바로, 의존성 분리다.

 

 

의존성 분리란

앞서 설명했듯, 의존성(의존관계)란 변경에 의한 영향을 받는 관계를 말한다. 이것을 분리한다는 것은, 한마디로 변경에 의한 영향을 안 받게 하겠다! 라는 거다.

 

물론 A가 B에게 의존하는 관계에서 B가 b라는 메서드를 가지고 있다고 할 때, b메서드의 기능을 바꾼다고 하면 당연히 A에게도 영향이 간다. 여기서 말하고자 하는 영향은 이런 영향이 아니라, 바로 A가 이제부터는 B가 아니라 C라는 모듈을 사용하기로 변경한 경우를 의미한다고 보면 된다. 이 경우 A는 B를 사용하던 코드를 싹 다 바꿔야 한다. 

 

이는 A와 B가 강하게 결합돼있기 때문에 발생한다. 다르게 표현하자면 A가 B를 강하게 의존하기 때문, 즉 A가 B를 너무나도 잘 알기 때문에(?) 발생한다. 둘의 의존관계의 끈끈한 정도가 상당히 센 것이다.(너 없음 난 아무것도 못해~의 느낌..)

 

의존성을 분리한다는 것은 이러한 관계를 끊어낸다는 것으로, 아예 손절친다는게 아니라 그 관계의 끈끈한 정도(?)를 느슨~하게 만든다는 의미다. (강한 결합 느슨한 결합이 되게 추상적인 표현이라고는 생각하는데, 이 말 말고 더 느낌있는 말이 없는 것 같다)

 

이에 대한 전통적인 예시로 연극팀 예시가 있다. 어떠한 배역이 있고 그 배역을 맡은 배우가 있을 때, 내가 연극팀을 운영하는 입장이라면 배우가 아니라 배역에 집중해서 기획을 해야 한다. 배우에 집중한다고 하면(이 경우가 강한 결합!!) 그 배우가 아파서 연극에 못 온다고 하면 그냥 망하는거다. 그러나 특정 배우가 아닌 배역에 집중한다면(이 경우가 느슨한 결합!!) 실제로 이 일을 맡은 배우가 누군지 몰라도 "이걸 누가 하는지는 모르겠는데, 이 역할 맡은 배우는 이렇게 연기하면 돼!"를 알고 있는 거니까 상관없는거다. 즉 배우가 바뀌거나 아프거나 뭐 그런 것에 영향을 받지 않게 되는 것. 

 

연극팀 인원들이 원래는 배우에게 의존하는 관계를 더 이상 배우가 아니라 배역에게 의존함으로써, 연극팀과 배우간의 사이가 상당히 느슨해진다! 라고 표현가능하다. 이렇게 의존관계가 느슨해지는 것을 다른 관점에서 보면 연극팀이 배우가 아닌 배역에 의존함으로써 기존에 가지던 배우에게 의존하던 관계가 분리되기 때문에 "의존성이 분리된다"라고 볼 수 있는 거다. 

 

이렇게 특정 구현체(배우)가 아니라 인터페이스(배역)에 의존함으로써 의존성 분리가 가능하다. 이를 "의존관계 역전 원칙(의존성 뒤집기 원칙, Dependency Inversion Principle)"을 적용함으로써 의존성을 분리한다고 표현한다.

 

 

의존관계 역전 원칙

일단 이름은 간지난다. 처음부터 직접적으로 와닿게 설명하자면 "구상 클래스가 아닌 추상 클래스에 의존하게 만드는" 원칙을 말한다. 조금 더 파헤치면, 고수준 모듈이 저수준  모듈에 의존하면 안 된다는 뜻이 담겨 있다. 

 

구상 클래스가 아닌 추상 클래스에 의존하게 함으로써, 고수준 모듈가 저수준 모듈에게 의존하는 전통적인 의존관계를 뒤집히고 고수준 모듈이 저수준 모듈의 구현체들로부터 독립되게 할 수 있게 된다. 즉 의존성을 분리시킬 수 있다!

 

한마디로 저수준 모듈이 고수준 모듈에게 의존하도록 뒤집으라는건데..이걸 바로 구상 클래스가 아닌 추상 클래스에 의존하게 함으로써 뒤집으라는 얘기다.

 

원래 이랬던 거를
이렇게 역전, 즉 뒤집으라는 거

 

 

※ 여기서 고수준/저수준은 프로그래밍 언어를 말할 때의 고수준(사람에 가까운)/저수준(컴퓨터에 가까운)과 같은 의미다. 고수준 모듈이 어떤 의미있는 기능을 제공하는 모듈이라 한다면, 저수준 모듈은 고수준 모듈의 그 기능을 구현하는 실제적인 역할을 맡는 애들이라고 보면 된다. MemberService의 기능이 실제적으론 MemberRepository의 기능들을 통해 이루어진다면 MemberService가 고수준, MemberRepository가 저수준이 된다. 반대로 MemberRepository의 메서드들은 실제적으로 그들을 구현하는 구현체(ex: JofeMemberRepository)에서 구현되므로 이들사이는 MemberRepository가 고수준, JofeMemberRepository가 저수준이 된다.

 

MemberService는 이제 추상화된 MemberRepository에만 의존한다. 뿐만 아니라 다양한 MemberRepository구현체들도 역시 추상화된 MemberRepositiry클래스에 의존하게 된다. MemberService입장에서는 MemberRepository가 저수준 모듈들이지만, 기존에 의존하던 JofeMemberRepository입장에서는 MemberRepository라는 추상 클래스가 자신보다 고수준이다. 왜냐하면 그 놈의 인터페이스를 실제로 구현하는 역할을 JofeMemberRepository가 하니까. 

 

public class MemberService {
    private final MemberRepository memberRepository;
    
    public MemberService(MemberRepository concreteMemberRepository) {
        this.memberRepository = concreteMemberRepository;
    }
}

 

실제 memberRepository필드(= 배우)로 어떤 놈이 들어올지는(= 주입될지는) 모른다. 그래도 그 놈이 MemberRepository(= 배역)라는 역할을 맡은 것은 알고 있다. 때문에 실제 배우가 누가 되든 나는 상관없다.

 

이렇게 의존성 분리를 해주면서, 외부에서 의존관계를 주입받는 것. 이게 진정한 Dependency Injection이다. 단순히 외부에서 때려넣는다고 죄다 dependency injection이 아님!!

 

마지막으로 이 DI의 장점을 훑고 가면 다음과 같다.

 

  • 의존성이 줄어든다 : 의존성 분리를 해줬기 때문에, 의존대상이 바뀌어도 그로 인한 코드의 수정을 안 해도 된다. 즉 배우가 아닌 배역에 의존하니까 배우로 누가 들어오든 변하는 건 없다. 어차피 얘가 할 일은 정해져있으니까. 변경에 유연해진다고도 표현할 수 있겠다.
  • 재사용성이 높아진다 : 의존관계를 가지는 객체를 내부에서 만드는게 아니라 외부에서 만들어진 애를 받는거니까, 다른 클래스에서도 이 놈을 쓸 수 있다
  • 테스트하기 좋아진다 : 외부에서 주입받는 거니까, 주입되는 놈의 테스트를 주입받는 애의 테스트와 분리해서 가능하다. 
  • 가독성이 좋아진다 : DI를 하는 이유가 아까 말했듯 역할의 분리니까, 기능들이 분리되므로 자연스레 가독성이 높아진다.

 

 

 

의존성 주입(dependency injection) : 어떤 함수나 클래스가 자신들이 내부적으로 사용할 기능을 자신들 내부에서(즉 스스로가) 만들어내지 않고, 외부에서 이 기능들을 만든 뒤 주입시키는 것.

 

예를 들어, app.jsx에서 유튜브 api를 호출하는 기능을 쓴다고 할 때 이 기능을 app.jsx 내에서 만들 수도 있겠지만, 외부에서 유튜브 api를 호출하는 기능을 만든 다음 app.jsx에게 주입(by props)하여 이를 쓰게 할 수도 있다. 이를 의존성 주입이라 한다. 찾아보니까 객체 지향 프로그래밍(OOP)에서 코드의 재사용성을 목적으로 잘 사용되는 패턴이라고 한다.  (MVC패턴 등에서 view에 해당하는 애들이 비즈니스 로직도 처리할 수 있고 ~~도 할 수 있고~ 그러면 곤란..) 리액트에서는 컴포넌트들의 테스트를 하는 것에도 DI를 쓰는 게 좋다고 한다. 

 

리액트에선 index.js에서 사용할 함수 등을 불러와 app.jsx에 집어넣고, 그 다음은 이를 쓰는 컴포넌트로 계속해서 전달전달하는 식으로 활용한다. 예를 들면 다음과 같다.

const authService = new AuthService();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App authService={authService}/> 
  </React.StrictMode>
);

// authService객체는 login등의 메소드를 사용가능한 객체이다

그러면, App컴포넌트 안에 B컴포넌트 안에 C컴포넌트 안에 D컴포넌트 안에 E컴포넌트가 있다고 해보자. 또한 E컴포넌트는 login과 logout이라는 기능을 써야 한다. 이 때 방금 한 것처럼 index.js에서 login, logout메소드를 쓸 수 있는 객체(authService라고 가정)를 만들어 app.jsx에 넣어주고, app이 이를 계속 전달전달해주는 방식을 쓸 수 있다. 

// index.js

const authService = new AuthService();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App authService={authService}/> 
  </React.StrictMode>
);


// app.jsx

function App({authService}){
	return <B authService={authService} />;
}


// B .jsx

function B({authService}){
	return <C authService={authService} />;
}


// C.jsx

function C({authService}){
	return <D authService={authService} />;
}


// D.jsx

function D({authService}){
	return <E authService={authService} />;
}


// E.jsx

function E({authService}){
	return(
    	<>
           <button onClick={authService.login}>로그인</button>
           <button onClick={authService.logout}>로그아웃</button>
        </>
    );
}

이 때 E에서 다른 기능들도 이용해야 한다면? 그래서 index.js에서 다른 객체를 만들어 E에 전달해야 한다면? app.jsx, B.jsx, C.jsx, D.jsx에 하나하나 직접 코딩을 해줘야 한다. 이 작업? 너무 귀찮다.

 

그래서 여기서 꼼수(?)가 하나 있다. index.js에서 애시당초에 필요한 의존성들을 집어넣은 E라는 컴포넌트를 만들어 이걸 전달하는 것이다. 다음과 같다.

const authService = new AuthService();
const youtube = new Youtube();

const Jofe = props => {
  return(
    <E {...props} authService={authService} youtube={Youtube}/>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App Jofe={Jofe}/> 
  </React.StrictMode>
);

return <E authService={authService} youtube={youtube} /> 를 해도 되지만 확장성을 위해 저렇게 한 번 감싼 형태의 컴포넌트로 만들어서 전달할 수도 있다. 이렇게 하면 장점은, E에 새로 전달할 객체가 있어도 index.js에서 한 번만 수정하면 된다. 

+ Recent posts