먼저 프레임워크와 라이브러리의 차이를 알면, 제어역전에 관한 이해가 쉬워진다.
프레임워크란 무엇인가? 간단히 얘기하여 프레임워크는 개발자를 제한시킨다. 이 말은 개발자는 프레임워크의 법에 맞춰서 개발을 해야한다는 의미이다. 프레임워크가 허용하는 코드, 프레임워크가 허용하는 자원등을 이용하여야 프레임워크가 이해를 하게된다. Spring의 경우 Spring Framework가 이해할 수 있도록 Spring의 법칙에 맞게 개발을 해야한다. (ex NextJs, Django, Spring Framework, ...)
반면에, 라이브러리는 특정 메소드나 기능에 대한 모듈을 의미한다. 라이브러리는 어떠한 특정한 법칙을 요구하지 않는다. 그저 기능을 제공할 뿐이다. 그리고, 여러 라이브러리들과 공존이 가능하다. 프레임워크의 공존은 허용되지 않는다. 흔히 프레임워크로 착각하고 있는 라이브러리의 예제는 React, Flask 등이 있다.
하지만 이 차이가 어떻게 IoC의 이해에 도움을 주는가? 다음의 그림을 참고해 보자.
위 그림을 참고해보면, Application이 Framework에 종속적인 반면. Libarary는 Application에 종속적임을 확인할 수 있다. 이제 차이가 조금 분명하지 않은가? 말 그대로 제어 역전이란 이러한 종속성이 Application(개발자)에서 Framework(외부)로 역전되는 것을 의미한다.
라이브러리들만을 이용하여 개발할 때에는 객체의 생성, 설정, 초기화, 메소드 호출, 소멸(객체의 생명주기)를 개발자가 직접 관리하게 된다.
반면에, 제어 역전이 가능한 프레임워크는 개발자가 객체나 메서드의 제어를 외부에 위임하게 된다. 제어 역전 상태에서는 객체 스스로가 사용할 객체를 결정하지도 않고, 생성하지도 않는다. 즉, 객체 생명주기의 관리를 외부에 맡김으로써 개발자는 핵심 비즈니스 로직에 더욱 관심을 두고 개발이 가능하게 해준다.
Spring 에서는 이러한 제어 역전을 후에 설명할 Spring Container를 이용하여 실현한다. Spring은 Bean을 이용하여 객체를 관리함은 잘 알것이다. 위에서 설명한 것을 토대로 생각해보면, Spring에서 이러한 Bean의 생명주기를 관리함으로서 제어 역전을 실현함을 알 수 있다.
DI&DL는 IoC와 매우 밀접한 관련이 있다. DI는 IoC의 원칙을 실현하기 위한 여러 디자인 패턴중 하나이다. 또한, IoC와 DI&DL 모두 객체간의 결합을 느슨하게 만들어 유연하고 확장성이 높은 코드를 작성하기 위함도 밀접한 이유중 하나이다.
이 DI,DL은 외부에서 객체들의 관계를 결정해주는 디자인 패턴으로, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고, 런타임시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.
DL또한 IoC의 원칙을 실현하기 위한 디자인 패턴중 하나이다. DL은 의존성 검색이라고도 하며, DI는 IoC 컨테이너가 컴포넌트에 의존성을 주입하는 반면, 의존성 룩업에는 의존성 풀(Dependency Pool) 문맥에 따른 의존성 룩업(Contextualized Dependency Lookup)이 존재 하는데 의존성 풀이란 필요에 따라 레지스트리에서 의존성을 가져와 사용함을 의미한다.(즉, 설정된 전체 Bean을 모두 가져오게 된다.)
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring/app-context.xml"); //app-context에 등록된 모든 의존성 load
MessageRender mr = ctx.getBean("renderer", MessageRender.class);
mr.render();
}
이는 단점이 있는데, Bean 변경시 객체 내에 변경에 대한 부분을 일일이 설정파일에 수정해야하고, 따라서 테스트도 어렵게 되기 때문에 번거로운 단점이 있다.
문맥에 따른 의존성 룩업(CDL)은 의존성 풀과 유사하지만 의존성 풀처럼 특정 설정파일에서 가져오는것이 아닌, 자원을 관리하는 컨테이너에서 의존성을 가져온다. 또 늘 수행되는것이 아닌 몇 가지 정해진 시점에 수행되는데, 다음 코드와 유사한 인터페이스를 구현하는 컴포넌트를 기반으로 동작한다.
public interface ManagedComponent {
void performLookup(Container container);
}
이 때, performLookup 메소드를 구현하여, 필요할 때, 설정파일이 아닌 필요한 리소스를 관리하는 컨테이너로부터 의존성 룩업을 실행하도록 한다.
반면에, DI는 설정파일(레지스트리)로 부터 불러오는것이 아닌, IoC 컨테이너로 부터 직접 의존성 주입을 받는다. DI는 크게 3가지 방법이 있다.
|방식|
|:|
|생성자 주입(Constructor Injection)|
|수정자 주입(Setter Injection)|
|메소드 주입(Method Injection)|
생성자 주입은 객체 생성시에 주입하는 방법이다. 아래 코드를 참고하자
public class ConstructorInjection {
private Dependency dependency;
public ConstructorInjection(Dependency dependency) {
this.dependency = dependency;
}
@Override
public String toString() {
return dependency.toString();
}
}
다음과 같이 생성자에 의존성을 포함시켜 객체를 생성시키는 방법을 생성자 주입이라고 한다.
IoC 컨테이너는 이를 인식하여, 해당 컴포넌트를 초기화할 때 컴포넌트에 필요한 의존성을 전달한다.
수정자 주입은 객체에 public 메소드로 Java Bean처럼 Setter 메소드를 만들어 의존성을 주입한다.
반드시, setXXX 메서드 명칭 규약을 따라야 IoC 컨테이너가 이를 인식하고 의존성을 주입한다. 아래 코드를 참고하자.
@Service
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private PasswordEncode passwordEncode;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void setPasswordEncode(PasswordEncode passwordEncode) {
this.passwordEncode = passwordEncode;
}
@Override
public void register(User user, String rawPassword) {
if(this.userRepository.countByUsername(user.getUsername()) > 0) {
throw new UserAlreadRegisteredException();
}
user.setPassword(this.passwordEncode.encode(rawPassword));
this.userRepository.save(user);
}
public static void main(String[] args) {
UserRepository userRepository = new JdbcUserRepository(datasource);
PasswordEncod passwordEncoder = new PasswordEncod();
UserService userService = new UserServiceImpl(userRepository, passwordEncod);
}
}
메소드 주입은 Bean을 반환하는 메서드를 만들어 주입하는 방법이다. 이 주입 방식은 Spring에서는 Bean들이 Singletons(싱글톤, 한 객체당 인스턴스 하나)으로 관리되기 때문에 가능한 방식이기 때문에 제한이 있다. 하나의 이름을 가진 여러타입의 Bean을 띄운다면, DL을 추천하는 바이다. 다음 코드를 참고하자. 다음과 같은 방식으로 주입한다.
#exampleBean.java
public class exampleBean(){...constructor}
#Example.java
public exampleBean getExampleBean() {
return new exampleBean();
}
public class Example(){
public static void main(String ...args){
exampleBean bean = getExampleBean();
System.out.println(bean.toString()); //toString 메서드는 생략
}
}
의존성 주입법 중에는 생성자 주입이 권장되는 편인데, 수정자 주입과, 메서드 주입방식은 메서드로서 실행되기 때문에 빈 초기화 시에는 에러가 났음을 모른다. 이 메서드를 호출했을때 알게되는데, 생성자 주입은 초기화 시에 의존성을 강제시키므로 안정성 면에서 좋다고 할 수 있다.
하지만, 일반적으로는 수정자 주입을 많이 사용한다. 왜냐하면, 중간에 의존성을 변경할 수 있기 때문이다.
이 외에도 Spring의 Annotation을 이용한 필드 기반 의존성 주입이 존재한다.
필드 기반 의존성 주입은 기본적으로 생성자나, 수정자 메서드를 사용하지 않고, DI 컨테이너(Spring Container)의 힘을 빌려 의존성을 주입하는 방식이다.(Spring에서는 Spring Container)
@Service
public class TestService {
@Autowired
TestRepository testRepository;
}
유의해야할 점은 해당하는 객체 Type의 Bean이 여러개일 경우 Autowired는 이를 구별하지 못하기 때문에 @Qualifier나 @Primary와 같은 어노테이션을 활용하여 특정 bean을 찾아줘야한다. 만약 Autowired만 사용한다면 같은 타입의 bean을 하나 이상 발견했다는 오류를 내보낼 것이다.
이 외에도 Lombok 어노테이션을 활용한 @RequiredArgsConstructor가 있는데, 이는 클래스 내부에 final로 생성된 객체를 자동으로 의존성 주입을 해주는 편리한 의존성 어노테이션이다.
Spring에서도 필드 주입보다는 생성자나 수정자 주입을 권장하고 있기 때문에, Lombok을 활용한 주입이 가장 편리하다 생각 된다.
추가 이해를 돕기위해 작성한다. 필자는 예전에 이 부분을 이해할때, '아니 클래스 내에 이미 객체를 불러왔는데??.. 뭐가 주입이고 뭐가 결합도를 낮춘다는거지?' 라고 생각했엇는데, 선언만 하였지 할당을 안했음을 명시해야한다. 그리고 객체가 특정 인터페이스를 상속했다면, 인터페이스를 선언하고 상속한 객체를 주입해도 된다. 즉, 이 부분은 Spring에 대한 이해가 아닌 Java와 객체지향프로그래밍에 대한 공부를 더욱 해보길 바란다. 또한, DI Container에 대한 부분은 Google에서 발표한 DI 프레임워크인 Guice를 참고해도 좋다.
Spring Container는 Spring 내부에서 사용하는 IoC 컨테이너이다. Spring Container는 Bean의 생명주기를 관리하게 된다. Spring Container의 종류는 BeanFactory와 이를 상속한 ApplicationContext가 있다. 이 두개의 컨테이너로 의존성 주입된 빈들을 제어하고 관리할 수 있다.
스프링 웹 애플리케이션 동작 순서를 나열하면 다음과 같다.
(참고 출처:링크)
Spring WebApplication |
---|
1. 웹 애플리케이션이 실행되면, Tomcat(WAS, Sevlet Container)에 의해 web.xml이 로딩된다.(load-on-startup 으로 톰캣 시작시 servlet생성 가능하도록 설정 가능) |
2. web.xml에 등록되어 있는 ContextLoaderListener(Java class)가 생성된다. ContextLoaderListener 클래스는 ServletContextListener 인터페이스를 구현하고 있으며, ApplicationContext를 생성하는 역할을 수행한다. |
3. 생성된 ContextLoaderListener는 applicationContext.xml을 로딩한다. |
4. applicationContext.xml에 등록되어 있는 설정에 따라 Spring Container가 구동된다. 이 때 개발자가 작성한 비즈니스 로직에 대한 부분과 DAO(Data Access Object), VO(Value Object) 객체들이 생성된다. |
5. 클라이언트로부터 웹 애플리케이션 요청이 온다. |
6. DispatcherServlet(Servlet) 이 생성된다. DispatcherServlet은 FrontController의 역할을 수행한다. 클라이언트로부터 요청 온 메시지를 분석하여 알맞은 PageController에게 전달하고 응답을 받아 요청에 따른 응답을 어떻게 할지 결정만 한다. 실질적인 작업은 PageController에서 이뤄지기 때문이다. 이러한 클래스들을 HandlerMapping, ViewResolver 클래스라고 한다. |
7. DispatcherServlet은 servlet-context.xml(spring-mvc-xml)을 로딩한다. |
8. 두 번째 Spring Container가 구동되면 응답에 맞는 PageController들이 동작한다. 이 때 첫번째 Spring Container가 구동되면서 생성된 DAO, VO, Service 클래들과 협업하여 알맞은 작업을 처리하게 된다. |
다음 글에서는 Spring Container의 BeanFactory와 ApplicationContext에 대하여 공부 해 보겠다.