백앤드(스프링)

IOC, DI 그리고 컨테이너에 대하여,,,

유승혁 2022. 1. 13. 23:15

저번에 백엔드 개발형태의 전반적인 개요를 다루면서 DI라는 것을 언급했다. OCP와 DIP라는 두 원칙을 위반하지 않기 위해서 한다고 했는데 이 말들이 도대체 무엇일까? 궁금했는데 오늘 아주 자알 배웠다 ㅎㅎ

일단 간단하게 OCP란 확장에는 열러있으나 코드 수정에는 닫혀있다라는 의미이고 DIP는 A라는 클래스가 B라는 것에 대해 구현 클래스와 인터페이스가 있으면 인터페이스만 의존하라는 것이다.... 그렇다묜 의존이란?! 그냥 그거 갖다 쓴다는 의미 별거 아니다 ㅋㅋ

1. 배경 지식
일단 기존에 아무것도 모르고 단지 자바 코드로만 작성을 한 기획 코드를 예로 들어보장. 어떤 쇼핑몰 회사에서 나보고 할인 정책을 만들어 달라고 했다. 근데 아직 할인 정책이 확정이 아직 안되었고 심지어 고정 할인으로 할지 비율 할인으로 할지 아무것도 정해지지 않은 채 내게 던져 버렸다. 그래서 난 일단 고정 할인이라 가정하고 각종 API들을 작성했다고 하자.

private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

이제 할인을 해주고 할인 가격을 알려주는 API (OrderService)에 저 코드를 이용한다. 간단히 설명하자면 DiscountPolicy는 인터페이스이고 FixDIscountPolicy는 구현 객체이다. (인터페이스가 있는 이유는 고정 할인이라고 확정해주지 않았어서...)
그런데 이제 추후에 갑자기 할인 정책이 정해졌다... 바로 10%할인으로 바꾸어 달라는 것이다!! 이제 어떻게 해야 할까. 당연히..!!

private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

이렇게 바꾸면 되는거 아냐?! 라고 생각하기 쉽다. 하지만 잘 생각해보면.. 지금 현재 RateDiscountPolicy도 만들어야 하고 OrderService의 코드 또한 변경 해야 한다. 여기서 바로 두 원칙을 위배했다는 것이 낱낱이 밝혀졌다!!!!! 현재 이 API에서 이미 구현 클래스와 인터페이스 둘 다 의존해버려서 DIP원칙을 이미 위배한 상태였고, 10%할인 정책으로 바꾸려고 하니 API의 코드를 수정할려고 하여 OCP 또한 위배되었다. OCP개념이 위배되었다는 뜻이 이제 뒤에서 더 코드를 보면 알게 되겠지만, 10%할인으로 현재 이 API의 기능을 확장하려고 하는데 코드를 수정하지 않고 해야 OCP를 지킨 것이다. 아니 도대체 어떻게 한다는 거야?!?!

바로 여기서 Configuration이라는 개념이 등장한다. 즉, 설정이라는 말 그대로 위 처럼 클래스들의 의존관계를 관리해주는 하나의 저장소를 만들어 주는 것이다. 사실상 거의 뭐 말장난 처럼 느껴지기도 하고 꼼수 처럼 느껴지기도 한다.
Config 클래스에 다음과 같은 생성자 함수를 만들어 준다.

public OrderService orderService(){
    return new OrderServiceImpl(new MemoryMemberRepository(),new RateDiscountPolicy());
}

코드를 보면 알 수 있듯이 OrderService 객체는 두 개의 객체 Memory~와 Rate~를 의존하고 있었고 이를 API에서 명시해 주었지만 이제 Config클래스에 다음과 같이 생성자 함수를 만들어 주어 OrderService API 에서 그냥 가져다 쓰면 된다. 아래 코드와 같이!

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

(여기서 Impl가 붙어서 의아해 할 수 있지만 사실 OrderService 는 클래스가 아니라 인터페이스였고 이를 구현한 클래스는 OrderServiceimpl였어서 위와 같이 코드가 나온다.) 아무튼 가장 중요한 것은 만약에 변덕이 심한 쇼핑몰 회사 양반이 다시 고정환율로 해달라고 때를 쓴다면

public OrderService orderService(){
    return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}

아까 Config의 생성자 함수 코드의 함수 인자를 FixDiscountPolicy 로 다시 바꾸어 주기만 하면 끝! 그렇다면 이제 OrderService API코드를 수정하지 않아도 되고!! 게다가 이 API는 저 생성자 함수 덕에 구현 클래스를 자신의 코드 부분에 명시하지 않아도 된다! 즉, OCP와 DIP 모두 지켜졌다고 생각하면 된다.

1-1) 그렇다면 main에서는?

AppConfig appConfig = new AppConfig();
OrderService orderService = appConfig.orderService();

바아로오 이렇게 하면 된다. 역시 말 보단 코드가 나으니깐 ㅎㅎ. 그냥 똑같이 객체를 생성하고 그 안에 있는 생성자 함수를 사용하면 된다. 그냥 뭐 우회한 느낌?

느낀점)
솔직히 거의 뭐 말장난인데 아무튼 이렇게 접근한게 어찌 보면 참신하기도 하고 코드 수정을 번거롭게 하지 않아도 된다는게 장점이지 않을까? 게다가 협업을 하게 되면 인터페이스 이름과 함수명들 만 맞춰 놓으면 내 친구가 어떻게 구현 했는지 이름은 무엇인지 신경쓰지 않아도 된다는 장점이 있는 것 같다.
또한 역할과 구현의 분리라는 개념이 있다. OrderService API는 자신이 의존하는 객체들이 어떠한 모양으로 생겼는지 어떤 방식으로 동작하는지 이름이 무엇인지와 무관하게 자신의 로직을 실행하기만 하면 된다. 이게 어찌보면 로직이 이 녀석의 역할이고 Config를 통해 연결된 두 MemoryMemberRepositiry와 FixDiscountPolicy의 구현을 신경쓰지 않아도 되니 상당히 역할과 구현코드들이 잘 분리 되었다고 여길 수 있겠다.

2. 그렇다면 DI란?
DI는 의존관계 주입이라고 해석이 되는데 위에서 의존이란 그냥 그 클래스라던지 인터페이스라던지 가져다가 쓰는 것을 의미한다고 했다. 지금 보면 Config클래스를 통해 외부에서 OrderService와 MemoryMemberRepositiry, FixDiscountPolicy 각각의 의존관계를 주입했다. 이게 바로 의존관계 주우이이입~~ 너무 간단하죠? 근데 이 DI도 여러 종류가 있다고 기억하는데 지금 한 것이 바로 생성자 DI 이다. 다른 방식도 있다는 것인데 참으로 궁금하죠잉~ 아 그리고 한가지 놓친 것이 있는데 이 DI는 사실살 런타임에서 의존관계가 주입 된다는 특징이 있다. (당연한 이야기~)

3. 이번엔 IoC
제어의 역전을 의미하기도 하는 IoC는 내가 백엔드라고 구글에 검색하고 그 정보 속에서 헤엄치면서 DI와 함께 계속 마주쳤던 단어다.. 도대체 무엇이길래? 이미 배경 지식에서 거의 다 말했다. 외부의 Config클래스를 통해서 각 클래스들은 단지 자신의 로직만을 실행한다. 이렇게 되면 Config클래스가 이 프로그램의 제어 흐름을 가져온다. 위에서 보여준 OrderService API는 어떤 객체가 자신에게 넘어오는지 알 방법이 없다. 게다가 자신을 이용해주지 않고 다른 API를 이용하여 기존의 API를 이용하지 않을 수도 있고(ㅠㅠ) NULL값을 넘겨주어 버려 이 API를 바보로 만들어 버릴 수도 있다. 정말 무시무시 하다. 즉, 이렇게 프로그램의 제어권을 외부에서 관리하게 하는 것을 IoC라고 한다.

4. 프레임 워크 vs 라이브러리
이제 IoC를 하는 친구들은 크게 두 부류가 있는데 내가 직접 작성한 코드(위의 Config 처럼) 일 경우 라이브러리이고 그 외의 모든 것은 프레임 워크라고 생각하면 된다. 프레임 워크의 예시 중에 하나가 바로 Spring이다!! 드디어 스프링이 등장했다..!!! 여지껏 자바 코드로만 놀았었잖아~ (또다른 프레임 워크로서 JUnit이 있는데 Test code부분을 담당한다.. 이 이야기는 다음 기회에)

5. 마지막으로 컨테이너란?
위의 Config클래스들의 이름이 바로 IoC 컨테이너 또는 DI 컨테이너라고 불린다. 객체를 생성하고 관리하고 의존관계를 연결해 주는 녀석들 말이다.

6. 정리
그래서 도대체 이게 어떻게 된 일이지? OCP와 DIP.. 즉, 코드 수정을 하는 과정을 편리하게 하기 위하여 저 두 원칙을 지키려고 노력을 하다보니 이렇게 까지 오게 되었다. 무엇인가 배보다 배꼽이 더 커진 느낌이지만 사실 우리가 실제 프로그램을 만들려고 하다 보면 어떤 총괄 책임자가 있으면 더 좋을 거 같기도 하고 위 느낀점에서 말했 듯이 분업에 상당히 효과적으로 보인다.
그리고 이제 중요한 것은 지금처럼 자바 코드로만 DI하고 IoC 를 해낼 수도 있지만 스프링이라는 프레임 워크를 이용해서 자바코드가 아닌 다른 도구를 통해 확장하고 수정하고 지지고 볶고 놀아 보겠다는 거다. 난 아직 스프링이 도대체 얼마나 훌륭해서 자바코드가 아닌 스프링을 이용하는지 모르겠지만 배우다 보면 놀라게 되지 않을까. 꽤 기대된다 ㅎㅎ
그래서 아마 다음 글은 이제 Bean을 시작으로 스프링의 이야기를 할 것 같다.(이미 저번 글에서 언급하기는 했지만 좀 자세히 다루어 보아야 겠지?!?!)