- IoC(Inversioin of Control)/DI(Dependency Injection)
- IoC의 의미를 이해할 수 있다.
Framework는 애플리케이션 흐름의 주도권이 Framework에 있고 Library는 애플리케이션 흐름의 주도권이 개발자에 있는데, 여기서 말하는 애플리케이션 흐름의 주도권이 뒤바뀐 것을 IoC라고 한다.
Java 웹 애플리케이션에서 IoC가 적용되는 예
서블릿 기반의 애플리케이션을 웹에서 실행하기 위한 서블릿 컨테이너의 모습이다 Java콘솔 애플리케이션의 경우 main() 메서드가 종료되면 애플리케이션의 실행이 종료된다. 하지만 웹에서 동작하는 애플리케이션의 경우 클라이언트가 외부에서 접속해서 사용하는 서비스이기 때문에 main() 메서드가 종료되지 않아야 할 것이다.
그런데 서블릿 컨테이너에는 서블릿 사양에 맞게 작성된 서블릿 클래스만 존재하지 별도의 main()메서드가 존재하지 않는다
main() 메서드처럼 애플리케이션이 시작되는 지점을 엔트리 포인트(Entry Point)라고도 부른다
main()메서드가 없는데 어떻게 애플리케이션이 실행되나면, 서블릿 컨테이너의 경우, 클라이언트의 요청이 들어올 때마다 서블릿 컨테이너 내의 컨테이너 로직( service() 메서드)이 서블릿을 직접 실행시켜주기 때문에 main() 메서드가 필요 없다.
이 경우에는 서블릿 컨테이너가 서블릿을 제어하고 있기 때문에 애플리케이션의 주도권은 서블릿 컨테이너에 있다. 바로 서블릿과 웹 애플리케이션 간에 IoC(제어의 역전)의 개념이 적용되는 거 같다
그렇다면 Spring에는 IoC의 개념이 어떻게 적용돼 나면 바로 DI이다
- DI의 의미를 이해할 수 있다.
IoC(제어의 역전)는 서버 컨테이너 기술, 디자인 패턴, 객체지향 설계 등에 적용하게 되는 일반적인 개념인데 반해 DI(Dependency Injection)는 IoC 개념을 조금 구체화시킨 것이라고 볼 수 있다
Dependency는 '의존하는 또는 종속되는'이라는 의미를 가지고 있다. 그리고 Injection은 '주입'이라는 의미를 가지고 있다.
- What(의존성 주입은 무엇인가?)
객체지향 프로그래밍에서 의존성이라고 하면 대부분 객체 간의 의존성을 의미한다
A, B라는 두 개의 클래스 파일을 만들어서 A클래스에서 B클래스의 기능을 사용하기 위해 B클래스에 구현되어 있는 어떤 메서드를 호출하는 상황을 생각해보자.
A클래스가 B클래스의 기능을 사용할 때, 'A클래스는 B클래스에 의존한다'라고 한다
즉 A클래스의 프로그래밍 로직 완성을 위해 B 클래스에게 도움을 요청한다.(B 클래스에게 의지(의존)한다)
코드를 통해 의존성 주입이 무엇인지 알아보자
- 클래스 간의 의존 관계 성립
커피를 주문하는 Java콘솔 애플리케이션을 개발한다고 가정해보자
MenuController 클래스는 클라이언트의 요청을 받는 엔드포인트(Endpoint) 역할을 하고, MenuService클래스는 MenuController 클래스가 전달받은 클라이언트의 요청을 처리하는 역할을 한다
클라이언트 측면에서 서버의 엔드포인트란 클라이언트가 서버의 자원(리소스, Resource)을 이용하기 위한 끝 지점을 의미한다.
MenuController 클래스는 메뉴판에 표시되는 메뉴 목록을 조회하기 위해서 MenuService의 기능을 사용하고 있다
Java의 객체 생성 방법인 new 키워드를 사용해서 MenuService 클래스의 객체를 생성한 후, 이 객체로 MenuService의 getMenuList() 메서드를 호출하고 있다.
이처럼 클래스끼리는 사용하고자 하는 클래스의 객체를 생성해서 참조하게 되면 의존 관계가 성립하게 된다.
- 의존성을 주입해보자
두 클래스 간에 의존 관계는 성립되었지만 아직까지 의존성 주입은 이루어지지 않았다
그림 2-5에서 MenuService의 기능을 사용하기 위해 MenuController에서 MenuService의 객체를 new키워드로 직접 생성한 반면에 그림 2-6에서는 MenuController 생성자로 MenuService의 객체를 전달받고 있다
이처럼 생성자를 통해 어떤 클래스의 객체를 전달받는 것을 '의존성 주입'이라고 한다
생성자의 파라미터로 객체를 전달하는 것을 외부에서 객체를 주입한다라고 표현하는 것이다
여기서 의미하는 객체를 주입해주는 외부는 CAfeClient 클래스가 Menucontroller의 생성자 파라미터로 MenuService를 전달하고 있기 때문에 객체를 주입해주는 외부가 된다.
클래스의 생성자로 객체를 전달받는 코드가 있다면'아, 객체를 외부에서 주입받고 있구나. 의존성 주입이 이루어져 있구나'라고 생각하고 이해하자.
- Why(의존성 주입은 왜 필요할까?)
객체지향 언어인 Java에서 생성자를 통해 객체를 전달하는 일은 아주 흔한 일이다. 즉 의존성 주입이 필요한 건 어찌 보면 당연한 일이다.
그런데 의존성 주입을 사용할 때, 항상 염두에 두어야 하는 부분이 한가지 있다. 그것은 현재의 클래스 내부에서 외부 클래스의 객체를 생성하기 위한 new키워드를 쓸지 말지 여부를 결정하는 것이다.
일반적으로 Java에서 new 키워드를 사용해서 객체를 생성하는데, Reflection이라는 기법을 이용해서 Runtime시에 객체를 동적으로 생성할 수 있는 방법도 있다.
그런데 애플리케이션 코드 내부에서 직접적으로 new키워드를 사용할 경우 객체지향 설계의 관점에서 중요한 문제가 발생할 수 있다.
예를 들어) 프런트엔드와 백엔드 개발자가 같이 협업한다고 가정했을 때,
스텁(Stub)은 메서드가 호출되면 미리 준비된 데이터를 응답하는 것이다. 즉, 고정된 데이터이기 때문에 몇 번을 호출해도 동일한 데이터를 리턴한다 몇번을 호출해도 동일한 데이터를 리턴하는 것을 전문용어로 멱등성(idempotent)을 가진다라고 한다.
이제 백엔드 팀에서는 메뉴 데이터 조회 API를 위한 Stub을 준비하기 위해서 MenuServiceStub 클래스를 추가로 작성해서 코드 구성을 그림 2-7과 같이 수정한다.
메뉴 목록 조회 API로 Stub을 제공하기 위해서 MenuSerivceStub 클래스를 사용하는 것으로 변경되었다.
MenuServiceStub 클래스를 보면 getMenuList()에 Stub 데이터로 채워져 있는 것을 볼 수 있다.
그런데 MenuServiceStub클래스를 사용하려고 보니, 이 MenuService클래스를 의존하고 있는 CafeClient와 MenuController에서 MenuService를 MenuServiceStub 클래스로 불가피하게 변경해야 되는 상황이 발생한다.
만약 MenuServiceStub클래스를 사용할 대상이 수십, 수백 군데라면 시간과 버그 발생, 테스트 코드를 다시 짜야될 가능성이 높다.
결국 new키워드를 사용해서 객체를 생성하게 되면 참조할 클래스가 바뀌게 될 경우, 이 클래스를 사용하는 모든 클래스들을 수정할 수밖에 없다. 이처럼 new키워드를 사용해서 의존 객체를 생성할 때, 클래스들 간에 강하게 결합되어있다고 한다
결론적으로 의존성 주입을 하더라도 의존성 주입의 혜택을 보기 위해서는 클래스들 간의 강한 결합은 피하는 것이 좋다.
- How(느슨한 의존성 주입은 어떻게 할까?)
MenuService를 MenuServiceStub으로 변경하다 보니, MenuService클래스를 의존하고 있는 나머지 클래스들을 수정해야 했다.
Java에서 클래스들 간의 관계를 느슨하게 만드는 대표적인 방법은 인터페이스(Interface)를사용하는것이다.
MenuController가 MenuService라는 클래스를 직접적으로 의존하는 게 아니라 클래스 이름은 같지만 인터페이스를 의존하고 있다.
MenuController가 MenuService를 의존하고 있지만 MenuService의 구현체는 MenuServiceImpl인지 MenuServiceStub인지 알 필요가 없다(MenuContoller 입장에서는 그저 메뉴 목록 데이터를 조회할 수만 있으면 그만이니까)
이처럼 어떤 클래스가 인터페이스 같이 일반화된 구성 요소에 의존하고 있을 때, 클래스들 간에 느슨하게 결합되어있다고 한다.
그림 2-9를 2-87과 비교해보면 MenuController는 Stub데이터를 받을 수 있는데도 불구하고 아무런 변경이 발생하지 않았다.
그림 2-7d에서는 MenuController가 생성자로 MenuServiceStub 클래스를 주입받았지만 여기서는 주입받은 대상이 MenuService인터페이스이기 때문에 menuService인터페이스의 구현 클래스이면 어떤 클래스도 전부 주입을 받을 수 있다.
1)을 보면 new로 MenuServiceStub 클래스의 객체를 생성해서 MenuService인터페이스에 할당한다
이처럼 인터페이스 타입의 변수에 그 인터페이스의 구현 객체를 할당할 수 있는데 이를 업 캐스팅(Upcasting)이라고 한다
이제 업 캐스팅을 통한 의존성 주입으로 인해 MenuController와 MenuService는 느슨한 결합 관계를 유지하게 된다.
2-7 보다는 변경해야 될 곳이 조금 적어졌지만 걸리는 게 있다.
클래스들 간의 관계를 느슨하게 만들기 위해서는 new키워드를 사용하지 않아야 되는데, CafeClient클래스의 1)을 보면 MenuServiceStub의 객체와 MenuController 객체를 생성하기 위해 여전히 new를 사용하고 있다.
이 new키워드는 어떻게 제거하고 의존관계를 느슨하게 만들까 > 바로 Spring이 대신해준다.
Who(Spring기반 애플리케이션에서는 의존성 주입을 누가 해주나?)
2-9에서는 CafeClient 클래스에 MenuServiceStub과 MenuController 객체를 생성하기 위해 new키워드를 사용했었는데, 그림 2-10에서는 보이지 않다.
대신에 1)과 같이 알 수 없는 코드들이 보이는데 1)에 해당하는 코드는 모두 Spring에서 지원하는 API코드이다.
new키워드를 없앨 수 있었던 것은 제일 하단에 있는 Config라는 클래스이다
Config클래스에서 3)과 같이 MenuController객체 생성을 정의해 두면 1)을 이용해서 이 객체를 애플리케이션 코드에서 사용하게 된다.
한마디로 Config클래스에 정의해둔 MenuController객체를 Spring의 도움을 받아서 CafeClient클래스에게 제공을 하고 있는 것이다.
이처럼 Spring 기반의 애플리케이션에서는 Spring이 의존 객체들을 주입해주기 때문에 애플리케이션 코드를 유연하게 구성할 수 있다
만약 백엔드 팀에서 메뉴 목록 조회 기능을 완성했다면 이제 Stub데이터 대신에 실제 데이터를 데이터베이스에서 제공해주면 될 텐데 이럴 경우 애플리케이션 코드에서 변경할 내용들이 있나? > 4)와 같이 Spring Framework영역에 있는 MenuServiceStub 클래스를 MenuServiceImpl 클래스로 단 한 번만 변경해주면 된다.
Point
- 애플리케이션 흐름의 주도권이 사용자에 있지 않고 Framework이나 서블릿 컨테이너 등 외부에 있는 것, 즉 흐름의 주도권이 뒤바뀐 것을 IoC라고 한다
- DI는 IoC개념을 조금 구체화시킨 것으로 객체 간의 관계를 느슨하게 해 준다
- 클래스 내부에서 다른 클래스의 객체를 생성하게 되면 두 클래스 간에 의존관계가 성립하게 된다.
- 클래스 내부에서 new를 사용해 참조할 클래스의 객체를 직접 생성하지 않고, 생성자 등을 통해 외부에서 다른 클래스의 객체를 전달받고 있다면 의존성 주입이 이루어지고 있는 것이다.
- new키워드를 사용하여 객체를 생성할 때, 클래스 간에 강하게 결합되어 있다고 한다
- 어떤 클래스가 인터페이스 같이 일반화된 구성 요소에 의존하고 있을 때, 클래스들 간에 느슨하게 결합되어 있다고 한다
- 객체들 간의 느슨한 결합은 요구사항의 변경에 유연하게 대처할 수 있도록 한다
- 의존성 주입(DI)은 클래스들 간의 강한 결합을 느슨한 결합으로 만들어준다
- Spring에서는 애플리케이션 코드에서 이루어지는 의존성 주입(DI)을 Spring에서 대신해준다.
'TIL' 카테고리의 다른 글
[Spring MVC] [Spring API계층] (0) | 2022.10.20 |
---|---|
[SpringFramework][AOP] (0) | 2022.10.15 |
[SpringFramework][POJO] (0) | 2022.10.14 |
[Spring Framework][사용하는 이유] (0) | 2022.10.14 |
[Spring][DI] (0) | 2022.10.13 |