[Spring] IoC란 무엇인가

develemon·2025년 1월 12일

Spring

목록 보기
9/9
post-thumbnail

스프링을 시작하고 프로젝트의 구조를 갖춰가는 과정은 어느정도 정형적이어서 스프링에 대해 어느정도 잘 알고있는 것 같다는 착각을 심겨주기도 한다. 그런데 막상 IoC에 대해 설명해보라, 라고 누군가 물어온다면 이 또한 전형적인 답변으로밖에는 답하지 못할 것도 같다. 스프링으로 정형화된 구조의 프로젝트를 갖추는 건 경험적 지식만 쌓인다면 어렵지 않을 수 있겠지만, 스프링의 기본 개념은 여전히 모르고 지나갈 수 있는 것이다. 그래서, IoC란 무엇인가?

IoC란 무엇인가


우선 나는 이 글을 이일민 저, <토비의 스프링>의 일부를 인용 및 참고하여 작성할 것이다. 해당 저서에는 IoC 컨테이너에 대한 설명을 다음과 같은 문구로 시작한다.

스프링 애플리케이션에서는 오브젝트의 생성과 관계설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 독립된 컨테이너가 담당한다. 이를 컨테이너가 코드 대신 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC라고 부른다. 그래서 스프링 컨테이너를 IoC 컨테이너라고도 한다. <토비의 스프링 3.1 Vol.2> p.51

이는 IoC에 대한 전형적인 답변이 될 것이다. 그러나 본격적인 설명이 되기에는 너무 추상적인 답변이기도 하다. 위 인용글에서도 언급된 것처럼, 스프링 컨테이너를 IoC 컨테이너라고 말할 수 있는 건 ApplicationContext 인터페이스를 구현한 클래스의 오브젝트를 갖고 있기 때문인데, 이를 또 설명하자면 애플리케이션 컨텍스트가 무엇인지를 알아야 한다. IoC 컨테이너를 빈 팩토리 또는 애플리케이션 컨텍스트라고 부르기도 하는데, 보다 정확하게는 스프링 컨테이너는 단순한 DI 작업보다 더 많은 일을 하기에, DI를 위한 빈 팩토리에 보다 집중된 컨테이너 기능을 추가한 것이 애플리케이션 컨텍스트이다. 그러니까 애플리케이션 컨텍스트를 스프링 컨테이너라고 부르기도 하지만, '스프링 컨테이너' 자체는 빈 팩토리와 애플리케이션 컨텍스트를 포함하는 포괄적인 개념이라서 스프링 컨테이너 전체를 가리켜서 애플리케이션 컨텍스트라고 부르는 건 원론적으로는 정확하지 않다. 그럼에도 실질적으로는 대부분의 애플리케이션에서 애플리케이션 컨텍스트를 사용하기 때문에 스프링 컨테이너를 애플리케이션 컨텍스트로 이해해도 무방하다는 것이다. 그럼 애플리케이션 컨텍스트는 무엇일까?

ApplicationContext

우리가 처음 스프링부트 프로젝트를 시작할 때 main() 메소드 내에 존재하는 SpringApplicationConfigurableApplicationContext에 의존하고, ConfigurableApplicationContext는 애플리케이션 컨텍스트(ApplicationContext)를 상속받는다. 그리고 ApplicationContext는 다음과 같은 코드로 이루어져 있다.

public interface ApplicationContext extends 
		EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, 
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
        
    @Nullable
    String getId();

    String getApplicationName();

    String getDisplayName();

    long getStartupDate();

    @Nullable
    ApplicationContext getParent();

    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

그리고 ApplicationContextListableBeanFactoryHierarchicalBeanFactory를 상속받는데, 이들은 BeanFactory를 상속받는다. 즉, ApplicationContextBeanFactory를 확장한 상위 계층 인터페이스이고, 빈 팩토리는 스프링 IoC 컨테이너의 최상위 인터페이스로, 객체를 생성하고 의존성을 주입하는 기본 기능을 제공하고, 애플리케이션 컨텍스트는 빈 팩토리를 인터페이스를 상속받고 이외의 추가적 기능을 제공하는 확장 인터페이스이다.

그렇다면 IoC 컨테이너는 어떻게 동작하게 될까? 이를 위해서는 POJO 클래스설정 메타정보가 필요하다.

POJO 클래스

POJO(Plain Old Java Object)는 Java로 생성하는 순수한 객체로, 객체지향적인 원리에 충실하면서 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다. 그래서 이는 의존관계에 있는 다른 POJO와 느슨한 결합을 갖도록 만들기 위해 필요한 최소한의 인터페이스 정보만 공유하도록 설계된다. 그리고 각 오브젝트는 런타임에 IoC의 컨테이너의 도움으로 서로 연결된다.

설정 메타정보

만들어진 POJO 클래스들 중에 애플리케이션에서 사용할 것을 선정하고 이를 IoC 컨테이너가 제어할 수 있기 위해서는 적절한 메타정보를 만들어 제공해야 한다. 이때 IoC 컨테이너의 가장 기초적인 역할은 오브젝트를 생성하고 이를 관리하는 것이며, 스프링 컨테이너가 관리하는 이런 오브젝트를 빈(Bean)이라고 부른다. IoC 컨테이너가 필요로 하는 설정 메타정보는 바로 이 빈을 어떻게 만들고 어떻게 동작하게 할 것인가에 관한 정보다.

스프링의 설정 메타정보는 BeanDefinition 인터페이스로 표현되는 순수한 추상 정보다. 스프링 IoC 컨테이너, 즉 애플리케이션 컨텍스트는 바로 이 BeanDefinition으로 만들어진 메타정보를 담은 오브젝트를 사용해 IoC와 DI 작업을 수행한다. 만약 스프링의 설정 메타정보를 특정한 파일 포맷으로 작성하고자 한다면, BeanDefinition 오브젝트로 변환해주는 BeanDefinitionReader 인터페이스를 구현한 적절한 리더가 있으면 된다. 스프링 IoC 컨테이너는 각 빈에 대한 정보를 담은 설정 메타정보를 읽어들인 뒤에, 이를 참고해서 빈 오브젝트를 생성하고 프로퍼티나 생성자를 통해 의존 오브젝트를 주입해주는 DI 작업을 수행한다.

이렇게 POJO 클래스로 만들어지고, 설정 메타정보에 의해 DI로 연결되는 오브젝트들이 모여서 하나의 애플리케이션을 구성하고 동작하게 된다. 이것이 바로 IoC 컨테이너의 역할이다.

IoC 컨테이너 기동을 위한 서블릿 컨테이너


하지만 IoC 컨테이너를 기동시키는 데에 있어서 아직 이해가 부족하다. 스프링 IoC 컨테이너는 빈 설정 메타정보를 이용해 빈 오브젝트를 만들고 DI 작업을 수행하지만, 그것만으로는 애플리케이션이 동작하지 않는다. 마치 자바 애플리케이션의 main() 메소드처럼 어디에선가 특정 빈 오브젝트의 메소드를 호출함으로써 애플리케이션을 동작시켜야 한다. 보통 이런 기동 역할을 맡은 빈을 사용하려면 IoC 컨테이너에서 요청해서 빈 오브젝트를 가져와야 한다. 다만 이를 동작시키는 방식이 독립형 애플리케이션과는 근본적으로 다르다. 독립 자바 프로그램은 JVM에게 main() 메소드를 가진 클래스를 시작시켜 달라고 요청할 수 있다. 하지만 웹에서는 main() 메소드를 호출할 방법이 없다. 게다가 사용자도 여럿이며 동시에 웹 애플리케이션을 사용한다. 그래서 웹 환경에서는 main() 메소드 대신 서블릿 컨테이너가 브라우저로부터 오는 HTTP 요청을 받아서 해당 요청에 매핑되어 있는 서블릿을 실행해주는 방식으로 동작한다. 서블릿이 일종의 main() 메소드와 같은 역할을 하는 셈이다. 웹 애플리케이션에서 스프링 애플리케이션을 기동시키기 위해 main() 메소드 역할을 하는 서블릿을 만들어두고, 미리 애플리케이션 컨텍스트를 생성해둔 다음, 요청이 서블릿으로 들어올 때마다 getBean()으로 필요한 빈을 가져와 정해진 메소드를 실행해주면 된다.

이때 서블릿 컨테이너는 브라우저와 같은 클라이언트로부터 들어오는 요청을 받아서 서블릿을 동작시켜주는 일을 맡는다. 서블릿은 웹 애플리케이션이 시작될 때 미리 만들어둔 웹 애플리케이션 컨텍스트에게 빈 오브젝트로 구성된 애플리케이션의 기동 역할을 해줄 빈을 요청해서 받아둔다. 그리고 미리 지정된 메소드를 호출함으로써 스프링 컨테이너가 DI 방식으로 구성해둔 애플리케이션의 기능이 시작되는 것이다. 여기서 스프링은 이런 웹 환경에서 애플리케이션 컨텍스트를 생성하고 설정 메타정보로 초기화해주고, 클라이언트로부터 들어오는 요청마다 적절한 빈을 찾아서 이를 실행해주는 기능을 가진 디스패처 서블릿(DispatcherServlet)이라는 이름의 서블릿을 제공한다. 이 디스패처 서블릿에 대해서는 잠깐 뒤에 다시 살펴보자.

IoC 컨테이너 계층구조

그렇다면 이제 요청한 빈을 담아둘 IoC 컨테이너의 계층구조를 살펴보자. 보통 IoC 컨테이너는 애플리케이션마다 하나씩이면 충분하다. 빈의 개수가 많아져서 설정파일이 커지는 게 문제라면 파일을 여러 개로 쪼개서 만들고 하나의 애플리케이션 컨텍스트가 여러 개의 설정파일을 사용하게 하면 그만이다.

빈을 담아둘 IoC 컨테이너를 애플리케이션마다 하나씩 쓰기도 하지만, 계층구조를 이루게 될 때에는 한 개 이상의 IoC 컨테이너를 만들어두고 사용하게 된다. 계층구조 안의 모든 컨텍스트는 각자 독립적인 설정정보를 이용해 빈 오브젝트를 만들고 관리한다. 각자 독립적으로 자신이 관리하는 빈을 갖고 있긴 하지만 DI를 위해 빈을 찾을 때는 부모 애플리케이션 컨텍스트의 빈까지 모두 검색한다. 대신 자신의 부모와 그 부모 컨텍스트에게만 빈 검색을 요청하지 자식 컨텍스트나 같은 레벨에 있는 형제 컨텍스트에게는 요청하지 않는다.

이러한 계층구조를 이용하는 이유로는 미리 만들어진 애플리케이션 컨텍스트의 설정을 그대로 가져다가 사용하면서 그중 일부 빈만 설정을 변경하고 싶다면, 애플리케이션 컨텍스트를 두 개 만들어서 하위 컨텍스트에서 바꾸고 싶은 빈들을 다시 설정해줄 수 있도록, 기존 설정을 수정하지 않고 사용하지만 일부 빈 구성을 바꾸고 싶은 경우가 있다. 또한 각자 용도와 성격이 달라서 웹 모듈을 여러 개로 분리하긴 했지만 핵심 로직을 담은 코드는 공유하고 싶을 때, 여러 애플리케이션 컨텍스트가 공유하는 설정을 만들기 위한 경우도 있다(다만 애플리케이션 컨텍스트의 계층구조를 사용할 때, 자신이 만든 스프링 애플리케이션이 어떻게 컨텍스트가 만들어지고, 어느 것이 루트 컨텍스트이며, 어느 것이 그 자식 컨텍스트가 되는지를 분명히 알 필요가 있다).

웹 애플리케이션의 IoC 컨테이너 구성


자바 서버 기술이 막 등장했던 초기에는 URL당 하나의 서블릿을 만들어 등록하고 각각 독립적인 기능을 담당하게 했다. 이후에는 많은 웹 요청을 한 번에 받을 수 있는 대표 서블릿을 등록해두고, 공통적인 선행 작업을 수행하게 한 후에, 각 요청의 기능을 담당하는 핸들러라고 불리는 클래스를 호출하는 프론트 컨트롤러 패턴을 사용해왔다.

웹 애플리케이션에는 하나 이상의 스프링 애플리케이션의 프론트 컨트롤러 역할을 하는 서블릿이 등록될 수 있다. 이 서블릿에는 각각 독립적으로 애플리케이션 컨텍스트가 만들어진다. 이런 경우 각 서블릿이 공유하게 되는 공통적인 빈들이 있을 것이고, 이런 빈들을 웹 애플리케이션 레벨의 컨텍스트에 등록하면 된다. 그런데 하나의 서블릿이 웹 애플리케이션에 들어오는 모든 애플리케이션 요청을 처리할 수 있는 프론트 컨트롤러 역할을 하는데, 굳이 두 개 이상으로 나눠서 서블릿을 구성하고 요청을 분산해야 하는지 의문일 수 있다. 그렇다면 여러 개의 자식 컨텍스트를 두고 공통적인 빈을 부모 컨텍스트로 뽑아내서 공유하려는 게 아닌 이상 계층구조로 만들 필요가 없게 느껴진다. 하지만 전체 애플리케이션에서 웹 기술에 의존적인 부분과 그렇지 않은 부분을 구분할 필요가 있다. 스프링을 이용하는 웹 애플리케이션이라고 해서 반드시 스프링이 제공하는 웹 기술을 사용해야하는 건 아니기 때문이다. 따라서 스프링 서블릿을 사용하는 스프링의 웹 기술 외의 웹 기술을 고려 중이라면 계층 형태로 컨텍스트를 구분해두는 것이 바람직하다.

그런데 결국 웹 기술을 사용하기 위해서는 루트 애플리케이션 컨텍스트로 접근해야 하는데, 어떻게 접근할 수 있을까? 스프링은 웹 애플리케이션마다 하나씩 존재하는 서블릿 컨텍스트를 통해 루트 애플리케이션 컨텍스트에 접근할 수 있는 방법을 제공한다. 다음과 같은 스프링의 간단한 유틸리티 메소드를 통해 스프링 밖의 어디서라도 웹 애플리케이션의 루트 애플리케이션 컨텍스트를 얻을 수 있다. 그리고 getBean() 메소드를 사용하면 루트 컨텍스트의 어떤 빈이든 가져와 쓸 수 있다.

WebApplicationContextUtils.getWebApplicationContext(ServletContext sc)

ServletContext는 웹 애플리케이션마다 하나씩 만들어지는 것으로, 서블릿의 런타임 환경정보를 담고 있다. HttpServletRequestHttpSession 오브젝트를 갖고 있다면 간단히 ServletContext를 가져올 수 있다. 스프링과 연동돼서 사용할 수 있는 서드파티 웹 프레임워크는 바로 이 방법을 이용해서 스프링 빈을 가져와 사용한다.

그렇다면 애플리케이션 컨텍스트는 어떻게 만들어질까? 스프링에서는 서블릿 컨텍스트를 통해 IoC 컨테이너를 관리하게 되는데, 이때 ContextLoaderDispatcherServlet이 서블릿 컨텍스트와 애플리케이션 컨텍스트 간의 다리 역할을 하게 된다.

ContextLoaderListenerDispatcherServlet

스프링은 웹 애플리케이션의 시작과 종료 시 발생하는 이벤트를 처리하는 리스너인 ServletContextListener를 이용한다. ServletContextListener 인터페이스를 구현한 리스너는 웹 애플리케이션 전체에 적용 가능한 DB 연결 기능이나 로깅 같은 서비스를 만드는 데 유용하게 쓰인다. 이를 이용해서 웹 애플리케이션이 시작될 때(서블릿 컨텍스트 초기화 시) 루트 애플리케이션 컨텍스트를 만들어 초기화하고, 웹 애플리케이션이 종료될 때 컨텍스트를 함께 종료하는 기능을 가진 리스너를 만들 수 있다. 스프링은 이러한 기능을 가진 리스너인 ContextLoaderListener를 제공한다.

이어서 HTTP 요청을 처리하고 컨트롤러와 뷰 리졸버, 핸들러 매핑 등을 관리하는 DispatcherServlet을 통해 웹 애플리케이션 컨텍스트를 초기화한다. 서블릿 컨테이너에서 DispatcherServlet을 생성한 뒤, 이 서블릿이 자체적으로 스프링 애플리케이션 컨텍스트를 초기화하고 사용한다.

이처럼 둘의 초기화 시점이나 역할은 독립적이지만 스프링 애플리케이션에서 ContextLoaderListener는 전역 설정, DispatcherServlet은 웹 요청 처리에 초점을 맞춤으로써 계층적 컨텍스트 구조를 통해 상호보완적인 관계를 이룬다. 다만 이들은 스프링에서 빈으로 관리되는 게 아니라, ContextLoaderListener는 스프링 IoC 컨테이너와 서블릿 컨텍스트의 경계에서 동작하는 리스너로서 서블릿 컨텍스트 초기화 이벤트를 감지해 루트 애플리케이션 컨텍스트를 생성하고, DispatcherServlet은 서블릿 컨테이너에서 생성되어 애플리케이션 컨텍스트를 초기화하는 서블릿으로서 HTTP 요청에 따라 애플리케이션 컨텍스트를 생성하게 된다.

이로써 개발자가 직접 객체를 생성하고 의존성을 주입하던 방식에서 ApplicationContext가 IoC를 지원하는 덕분에 개발자를 대신하여 스프링 빈을 생성하고, 의존성을 주입하며, 객체 생명주기를 관리한다고 할 수 있는 것이다.

profile
유랑하는 백엔드 개발자 새싹 블로그

0개의 댓글