3. 스프링 IoC, DI
"스프링부트 백엔드 프로그래밍"이라는 주제로 약 8주간 글을 작성할 예정입니다. 스터디가 잘못된 방향으로 가지 않도록, 의견 및 조언을 아낌없이 해주시길 부탁드립니다.
지난주에는 스프링부트란 무엇인지 소개하고, 간단한 API 서버 만들면서 스터디 커리큘럼에 대해서 소개하였습니다. 이번 글에서는 스프링 프레임워크의 기본 개념인 IoC, DI, AutoConfiguration 에 대해서 알아봅니다.
이 글은 스프링부트가 처음인 초보자를 대상으로 작성한 글입니다. 이해하기 쉽도록 글을 작성하고 있습니다만, 스터디 시간에 쫓겨서 글을 급하게 작성했습니다. 또한 제 필력이 많이 부족한 것 같습니다...
각자... 나머지 공부하시면서 부족한 부분을 추가로 정리하시길 바랍니다.
1주차 - 스프링부트란 무엇인가?, 간단한 API 서버 만들어보기
2. [지난글]간단한 API 서버 만들어보기 (커리큘럼 소개)
2주차 - 스프링 프레임워크 기본 개념, Rest API
3. [이번글]스프링 프레임워크 IoC, DI(의존성주입)
4. [다음글]스프링부트 AutoConfiguration
[미정]3주차 - 스프링부트 테스트 코드 작성하기, 예외 처리하기
5. HTTP 기본 개념, Rest API
6. 테스트 코드 작성하기
7. 예외 처리하기
[미정] 4주차 - 캐싱
[미정] 5주차 - MQ, Pub/Sub
[미정] 6주차 - 보안(인증)
[미정] 7주차 - 병렬, 비동기 프로그래밍
[미정] 8주차 - Spring Cloud
[미정] JPA, Spring Data, Spring Session 등
지난 글의 샘플 코드를, DI를 사용하지 않고 작성해보겠습니다. SearchController에서는 영화 검색 응용서비스를 제공하는 MovieService 객체가 사용합니다. (즉, MovieService에 의존합니다.) 의존하는 객체(필요한 객체)는 new 클래스() 구문으로 생성해서 사용하면 됩니다.
일반적인 자바 프로그래밍 방법입니다. 필요한 객체는 생성해서 사용하면 되니깐요...
영화 검색 응용 서비스를 제공하는 MovieService 클래스는, 영화에 대한 메타 데이터를 조회해서 가져온 다음에 응용을 합니다. 영화 메타 데이터를 가져오기 위해서 MovieRepository를 사용하며, MovieRepository 인터페이스의 구현체인 MovieRepositoryImpl 객체를 생성합니다. 지난 글의 샘플 코드와 다른 점은 @Service 어노테이션을 사용하지 않았습니다. 스프링에서 관리하는 Bean 이 아닙니다.
(나중에 다시 설명하겠지만, @Service를 선언하면 Bean으로 등록이 됩니다.)
만약에, 응용서비스인 MovieService 클래스에서 의존하는 MovieRepository의 구현체를 변경하고 싶다면 어떻게 하면 될까요? 즉, MovieRepositoryImpl를 다른 구현체로 변경하고 싶다면? 아래 코드와 같이 구현체를 직접 변경해주면 됩니다.
즉, 이 경우에는 MovieRepository의 구현체를 변경하고 싶다면, MovieService 클래스에서 직접 구현체가 무엇인지, 어떤 걸로 변경할지에 대해서 상세하게 수정해야 합니다. 즉, MovieRepository의 변경이 MovieService에 영향을 미치게 됩니다. 이 부분은 DI를 사용해서, 개선해 볼 예정입니다. 3-2 참고해주세요@
MovieRepositoryImpl 객체에서는 원본 데이터를 조회하기 위해서 RestTemplate를 사용해서 HttpClient 기능을 수행합니다만 (샘플 코드에서는 실제로 RestTemplate를 사용하진 않았습니다.) 영화 데이터는 하드코딩으로 전달하도록 구현했습니다. MovieRepositoryImpl은, MovieService와 마찬가지로 @Component, @Service, @Repository 등의 어노테이션을 사용하지 않았습니다. (즉, 스프링 프레임워크가 관리하는 Bean으로 등록되지 않습니다.)
샘플 코드를 간단하게 그리면 아래와 같습니다.
각 객체를 생성하기 위해서 new 클래스() 구문을 사용했습니다.
DI를 사용해서 소스를 수정해보겠습니다. 참고로, 스프링의 DI 가 아닙니다. 일반적인 자바 프로그래밍 관점에서의 DI입니다. 객체를 직접 생성하지 않고, DI를 사용하는 방법은 여러 가지입니다.
- 생성자 주입
- 수정자(Setter) 주입
등등
이 글에서는 생성자 주입 방법으로 설명합니다. 아래 샘플 코드를 보시면, 생성자에 필요한 의존 객체를 주입해줍니다. MovieRepository의 구현체인 MovieRepositoryImpl에서는 OpenAPI 를 호출하기 위한 HttpClient 기능을 위해서 RestTemplate 객체를 사용합니다. 그래서, 아래와 같이 필요한 의존성은 생성자에서 주입하도록 구현하였습니다.
MovieService 응용서비스에서도 영화에 대한 메타 데이터를 조회하기 위해서 MovieRepository의 구현체에 의존합니다. 단, MovieRepository의 구현체를 직접 정의하지 않습니다. MovieSeervice 클래스의 생성자에서는, MovieRepository 인터페이스를 주입해주며, 실제로 필요한 구현체는 주입하는 곳에서 결정합니다. 아래 샘플 소스와 같이 인터페이스를 정의합니다.
실제 구현체인 MovieRepositoryImpl를 주입하는 곳은 Controller에서 주입하도록 합니다.
그림으로 그리면 아래와 같습니다.
DI를 사용하지 않았던 객체를 직접 생성하는 방식에서는 왼쪽에서 오른쪽으로 화살표가 되었다면, 이번에 화살표는 역방향입니다.
DI를 사용하면 여러 가지 장단점이 있습니다. 이 글에서는 모든 내용을 설명하진 못합니다. 단편적으로 일부분만 설명을 하겠습니다. 나머지 공부를 해주세요. 그리고 저도 좀 알려주세요ㅠㅠ 어렵습니다.
DI를 사용하면 어떤 장점이 있을까요?
MovieRepository 인터페이스의 구현체를 변경하고 싶다면 3-1 에서는 MovieService에서 직접 의존 객체를 수정했습니다. (복습) 3-1 샘플 코드 다시 확인
위 코드는, MovieService 응용서비스에서 의존하는 구현체를 직접 정의해야 했습니다. 그래서, 의존성이 매우 강하며, 느슨하지 못한 구조입니다. 반면에, DI를 사용하게 되면, MovieService 클래스를 수정하지 않아도 됩니다. 생성자에서 인터페이스를 정의하였기 때문입니다. MovieRepository의 구현체 중인 둘 중에(MovieRepositoryImpl, CinemaRepositoryImpl) 둘 중에 어떤 구현체가 들어와도 MovieService 에는 영향을 받지 않습니다!!
그림으로 그려보면 아래와 같습니다.
화살표는 역방향입니다. 그리고, DI는 유연하며, 느슨한 연결을 할 수 있습니다.
글로 설명하기 너무 어렵네요... ㅠㅠ 이해가 잘 되실는지 모르겠습니다... 하아...
지금까지는, 스프링과는 크게 관련 없는 설명이었습니다. 일반적인 자바 프로그래밍 관점에서의 DI에 대해서 이야기를 했습니다.
이제 드디어, 스프링에서는 DI를 어떻게 제공하는지 알아보도록 하겠습니다.
책장에 고이 보관 중인 "토비의 스프링"을 다시 꺼내보겠습니다..
스프링 애플리케이션에서는 오브젝트의 생성과 관계 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 독립된 컨테이너가 담당한다. 이를 컨테이너가 코드 대신 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC라고 부른다. 그래서 스프링 컨테이너를 IoC 컨테이너라고도 한다. [토비의 스프링]
음.. 좀 어려운 말인데. 어떻게 설명을 하면 좋을지..
IoC 란 오브젝트의 생성, 사용 및 생명주기의 관리까지 제어권이 바뀌었다는 의미입니다. 즉, 제어권을 스프링 프레임워크가 담당한다는 의미입니다.
IoC 개념을 사용하지 않았을 때는 아래 그림과 같이 개발자가 객체를 직접 신규로 생성해야 합니다.
IoC에서는 개발자가 객체를 직접 생성하지 않습니다. 프레임워크가 객체를 생성, 관리합니다. 의존성 주입을 하게 되면, 아래 그림과 같이 역방향으로 객체를 주입이 됩니다.
스프링에서 IoC를 담당하는 컨테이너를 빈 팩토리 또는 애플리케이션 콘텍스트라고 부르며, 컨테이너는 단순한 DI 작업 외에 여러 가지 기능을 하게 됩니다.
스프링 프레임워크에서 관리하는 객체이다. 좀 더 정확히는, IoC 컨테이너에 저장되고 관리하는 객체이다.
IoC는 DI와 같은 개념이 아닙니다. IoC 에는 크게 두 가지 방법이 있습니다.
- DL (의존성 룩업)
- DI (의존성 주입)
DL 이란,
컨테이너가 관리하는 빈 객체에 접근하기 위해서, 컨테이너가 제공하는 메서드를 사용해서 직접 빈을 검색해서 찾는 방법입니다. 컨테이너를 직접 사용해야 한다는 단점이 있습니다.
DI 이란,
스프링 컨테이너가 관리하는 빈 객체를, 컨테이너가 자동으로 연결 및 주입해 주는 것입니다. 컨테이너가 흐름의 주체가 되어 애플리케이션 코드에 의존관계를 주입해주기 때문에, 코드가 단순해지고 컴포넌트 간의 결합도가 제거된다. 우리는 스터디에서 DI에 대해서만 공부합니다.
단, 스프링 프레임워크는 어떤 객체가 Bean으로 등록되어야 하는지는 모릅니다. 그래서, 개발자는 객체를 빈으로 등록될 수 있도록 설정을 해줘야 합니다. 위에서 작성한 샘플 소스코드 MovieService, MovieRepository에 별다른 설정을 하지 않았기 때문에, 빈으로 등록되지는 않습니다. 즉, 스프링 프레임워크가 관리하는 빈 객체가 아닙니다.
만약, 스프링에서 관리할 수 있도록 빈 객체로 등록하게 되면 어떻게 될까요? 아래 그림과 같이 IoC 컨테이너에 빈 객체가 존재하게 됩니다.
컨테이너에서는 빈 객체를 관리하고, 아래 그림과 같이 의존성에 맞게 빈 객체를 조립해줍니다.
개발자는 이런 컨테이너에 빈이 등록될 수 있도록 정의를 해줘야 합니다. 또한 빈과 빈 사이에 의존관계가 있다는 것을 정의해줘야 합니다. 정의만 잘해주면 IoC 컨테이너에서 알아서 관리하고, 의존성을 연결해줄 것입니다. 어쨌든, 빈을 정의하거나 의존관계를 정의한 것은 개발자의 역할이지만, 실제로 빈을 주입해주는 주체는 바로 스프링 컨테이너입니다. 스프링 컨테이너가 제어의 주체가 됩니다. 단, 의존되는 객체와 의존하는 객체 모두 빈 이어야 가능합니다. SearchController 클래스도 빈이었습니다. (@RestController 어노테이션에 의해서...)
Bean을 정의하는 방법은 세 가지입니다.
- @Configuration, @Bean 어노테이션
- <Bean> xml 정의
- @Service, @Component, @Repository, @Controller 등 어노테이션
이 글에서는 xml 방법에 대해서 설명하지 않습니다.
MovieService를 Bean 객체로 등록하고, DI를 해서 사용할 수 있도록 수정해봅시다.
@Component 어노테이션에 해당하는 모든 어노테이션은 Bean으로 등록이 된다. 지난 글의 샘플 소스를 보면 @Service, @Component, @Repository, @RestController 등의 어노테이션을 사용하였는데, 모두 @Componet 어노테이션이다.
@Service 어노테이션을 클릭하면, @Component 가 선언되어있는 것을 알 수 있습니다.
스프링 부트에서는 @Component, @Service, @Repository, @Controller, @RestController 등의 어노테이션 선언하면 Bean으로 등록이 된다. 하지만, 비밀이 숨어있다. 바로 @ComponentScan 어노테이션이다. @ComponentScan 은 @Component 가 붙은 클래스를 모두 찾아서 Bean으로 등록하는 역할을 수행한다. 스프링 부트를 처음 시작하면 @ComponentScan 이 선언이 되어있지는 않지만, @SpringBootApplication 어노테이션을 클릭해보면 숨어있는 것을 찾을 수 있다.
@ComponentScan 어노테이션이 선언되어 있는 것을 확인할 수 있다.
@Component 어노테이션은 스캔의 패키지 경로를 지정할 수 도 있다. 참고로 루트 디렉터리에 @SpringBootApplication 어노테이션이 선언이 되어있기 때문에, 별도로 수정하지 않는다면 애플리케이션 전체를 스캔하게 될 것입니다.
스프링에서 DI를 하는 방법은 4가지입니다.
- Autowired 필드 인젝션
- 추천) 생성자 주입
- Setter 주입
- Lombok 사용
샘플 코드에서는 생성자 주입을 사용했습니다. 앞으로도 생성자 주입을 주로 사용할 것입니다.
참고로, @Autowired 필드 인젝션으로 주입을 할 수도 있습니다만, 추천하진 않습니다.
IntelliJ에서도 친절하게 사용하지 말라고 알려주네요
Lombok의 @RequiredArgsConstructor 어노테이션을 사용하면 아주 심플하게 DI를 정의할 수도 있습니다.
생성자를 선언해줄 필요가 없습니다.
만약에 Bean을 정의하지 않고 주입을 하게 되면 어떻게 될까요?
Could not autowird. No beans of "MovieService" type found.
애플리케이션을 실행하면 아래와 같이 에러 로그가 발생합니다.
시간이 없어서 정리를 못했어요...
@Primary
@Qualifier
빈 라이프 사이클
빈 스코프
등등등.. 너무 많음.
스프링 부트가 도입되기 전에는, 개발자가 직접 애플리케이션 콘텍스트를 정의해야 했습니다. 하지만, 스프링부트에서는 ApplicationContext 가 자동으로 생성이 될 것입니다.
run 메서드를 따라가보겠ㅅ브니다.
자세한 내용은 생략
자세한 내용은 생략
이번 글에서는 스프링의 기본 개념인 Ioc, DI에 대해서 공부했습니다. 다음 글에서는 스프링부트 의 AutoConfiguration에 대해서 공부합니다.