정의: Spring이란 무엇인가?
배경: Spring은 어떻게 탄생했을까?
필요성: Spring은 어떤 기능을 제공할까?
핵심 요소: Spring에서 DI나 AOP와 같은 핵심 요소는 무엇이 있을까?
동작 원리: Spring은 어떻게 동작하는 걸까?
이전 2개의 글에서 Spring이 무엇이고 어떤 핵심 요소를 가지는지 알아보았다. 마지막으로 Spring의 동작 원리를 이해하기 위한 스프링 컨테이너와 빈을 먼저 알아본 후, 스프링에서 제공하는 어노테이션 종류들에 대해서 학습하고 마무리하려 한다.
Spring에 대해서 학습하고 이해도를 높여가면서 Spring이 IoC 컨테이너로 불리는 것을 많이 접하게 되었다. Spring이 IoC를 강력하게 지원하는 것은 알고 있지만, 어째서 Spring을 IoC 컨테이너라고 부르는 것인지 궁금했다.
스프링 컨테이너를 설명하기 전에 컨테이너란 뭔지 짚고 넘어가자. 컨테이너의 정의를 구글에서 잘 설명하고 있는 것 같아 인용해왔다.
구글에서는 컨테이너를 컨테이너는 어떤 환경에서나 실행하기 위해 필요한 모든 요소를 포함하는 소프트웨어 패키지
라고 설명하고 있다.
이러한 컨테이너의 의미를 스프링 컨테이너에 접목하여 생각도 해보고 스프링 컨테이너의 정의에 대해서 많은 자료를 살펴보았다. 다만, 스프링 컨테이너의 의미가 다양한 의미로 통용되고 있었기에 나름대로 내가 이해한 스프링 컨테이너에 대해서 설명하겠다.
스프링 컨테이너는 Spring에서 빈(Bean)들의 생명주기를 관리하는 Spring Framework의 코어(Core)이다.
스프링 컨테이너가 Spring Bean(스프링 빈)들의 생명주기를 관리한다는 것은 스프링에서 빈들을 생성하고, 소멸하는 등의 역할을 담당한다는 것으로 이해했다.
스프링의 핵심 3요소에서 스프링의 한 요소 중 IoC에 대해서 이전에 배웠는데, 스프링 컨테이너는 제어의 역전 즉, IoC라는 핵심 요소를 통해 스프링 빈들의 생명주기를 관리할 수 있게 해준다.
이미 알고 있지만 스프링 빈이 무엇인지 간단하게 짚고 넘어가자.
빈(Bean)은 스프링 컨테이너에 의해 관리되는 재사용 가능한 소프트웨어 컴포넌트이다. 즉, 스프링 컨테이너가 관리하는 자바 객체를 뜻하며, 하나 이상의 빈(Bean)을 관리한다.
여기서 빈은 인스턴스화된 객체를 의미하며, 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 볼 수 있겠다.
스프링 컨테이너와 빈이 무엇인지는 알았다. 그런데 빈을 관리하는데 스프링 컨테이너가 왜 필요한 건지는 잘 와닿지 않았다. 그런데 IoC와 DI를 들여다보니 스프링 컨테이너의 목적과 필요를 조금이나마 알게 되었다.
스프링 빈은 스프링 컨테이너에 등록된 Java 객체를 뜻한다. Java 객체를 사용하기 위해서는 new 연산자 등으로 인스턴스를 만들어야 하는데, 이렇게 되면 객체지향 언어인 Java의 특성을 잘 활용할 수 없게 된다.
객체가 많이 생성되어 버린다면, 객체가 서로를 참조하는 상황이 많아질 테고, 그에 따라 객체 간 의존관계가 많아 의존성이 높아지게 된다는 치명적인 단점이 있다.
결국 스프링 컨테이너를 통해 빈을 관리하여 객체간의 의존성을 낮추기 위해서 스프링 컨테이너가 등장했고 사용되어진다고 이해하였다.
스프링 컨테이너가 추상적인 하나의 무언가를 지칭하는 것인줄 알았지만 많은 자료들과 김영한님의 강의를 살펴본 결과, BeanFactory
와 ApplicationContext
가 스프링 컨테이너의 종류라고 언급하고 있다.
스프링 컨테이너는 DI(의존성 주입)가 이루어진 빈을
BeanFactory
와ApplicationContext
라는 2개의 컨테이너로 제어하고 관리한다고 한다.
스프링 컨테이너라고 불리는 BeanFactory
, ApplicationContext
이 2가지에 주목해보자.
위 그림을 보면 ApplicationContext
인터페이스가 다른 인터페이스들을 다중으로 상속받은 인터페이스라는 것을 알 수 있다.
상속받고 있는 인터페이스들의 종류를 살펴보면 다음과 같다.
- MessageSource: 메시지 소스를 활용한 국제화 기능(다국어 메시지 처리 기능)을 제공한다.
- EnvironmentCapable: 로컬환경, 개발환경, 운영환경 등을 구분하여 처리할 수 있게 해주는 환경변수와 같은 기능들을 제공한다.
- BeanFactory: 스프링 빈을 관리하고 조회하는 역할을 담당한다.
- ApplicationEventPublisher: 이벤트 기반의 프로그래밍을 할 때 필요한 기능을 제공한다.
- ResourceLoader: 파일, 클래스패스 등 외부에서 리소스를 편리하게 조회할 수 있는 기능을 제공한다.
그리고 ApplicationContext
인터페이스는 BeanFactory
인터페이스의 기능을 상속받고 있다. 여기서 BeanFactory
는 스프링 컨테이너의 최상위 인터페이스이며, getBean()이라는 메서드를 제공한다.
정리해보면 ApplicationContext
이라는 스프링 컨테이너는 BeanFactory
기능을 모두 상속받아 빈을 관리하고, 메시지, 이벤트, 리소스 등과 같은 기능들을 추가적으로 제공할 수 있는 인터페이스라고 볼 수 있다.
💡 그렇다면 BeanFactory라는 스프링 컨테이너가 있는데 굳이 ApplicationContext라는 컨테이너를 왜 사용하는 걸까?
앞서 언급했듯이 스프링 컨테이너는
BeanFactory
,ApplicationContext
로 구분해서 이야기하는데 실제로BeanFactory
를 직접 사용하는 경우는 거의 없기에 보통은ApplicationContext
를 스프링 컨테이너라고 부른다.이떄,
BeanFactory
를 직접 사용하지 않는 이유는 단순히 스프링 빈을 관리하고 조회하는 것 외에 부가적으로 필요한 기능이 담긴ApplicationContext
가 더욱 실용적이기 때문이다.
그렇다면 스프링 컨테이너는 빈을 어떻게 생성하고 관리할 수 있는 것일까? 스프링 컨테이너가 생성되는 과정을 보면 스프링 컨테이너의 동작 원리를 자세히 들여다 볼 수 있다.
이번 장에서는 김영한님의 스프링 핵심 원리 - 기본편 강의에서 학습한 것을 주로 다루고 있으니 참고하기 바란다.
위 코드는 스프링 컨테이너라고 불리는 ApplicationContext
의 코드이다. 해당 코드를 들여다 보면 인터페이스 형태로 제공되고 있기 때문에 다형성이 적용되었다고 볼 수 있다.
다형성의 특징 덕분에 우리는 스프링 컨테이너를 XML 기반으로 만들거나 어노테이션 기반의 클래스로 만들 수도 있다.
이러한ApplicationContext
의 구현체는 무엇이 있는지 알아보자.
위 그림을 보면 BeanFactory
가 스프링 컨테이너의 최상위 인터페이스 임을 알 수 있다. 그리고 ApplicationContext
를 구현한 구현체들은 다음과 같은 것들이 있다.
FileSystemXmlApplicationContext
ClassPathXmlApplicationContext
AnnotationConfigApplicationContext
이번에는 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 생성하는 과정에 대해서 설명할 것이기에 AnnotationConfigApplicationContext
구현체를 위주로 다뤄볼 것이다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
위 코드는 ApplicationContext
인터페이스의 구현체를 통해 스프링 컨테이너를 만들기 위해 new AnnotationConfigApplicationContext()
구문을 통해 생성자를 호출하여 스프링 컨테이너를 생성한다. 여기서 해당 생성자의 인자값으로 구성 정보를 전달해줘야하는데, AppConfig
와 같은 구성정보 클래스를 전달해주면 된다.
이 때, 필자가 사용한 설정 클래스의 내용은 아래와 같다.
AppConfig.java
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
결국, 직접적으로 스프링 컨테이너를 생성하기 위해서는 ApplicationContext
의 구현체인 AnnotationConfigApplicationContext
와 @Configuration
어노테이션이 적용되어 있는 구성 정보가 작성된 설정 클래스가 필요하다는 것을 알 수 있었다.
해당 구성 클래스에서 사용된
@Configuration
,@Bean
과 같은 어노테이션은 다음 소주제 글에서 자세히 다룰 예정이다.
그러면 위와 같이 스프링 컨테이너가 생성되고 그 안에 빈 저장소가 지정된다.
다음으로 AnnotationConfigApplicationContext
구현체의 생성자 호출시 인자로 전달한 AppConfig
설정 클래스를 사용하여 스프링 빈을 등록하게 된다.
빈 저장소에 빈으로 등록될 때, 빈 이름은 설정 클래스의 메서드명을 사용하는데 어노테이션 속성(name)을 사용하면 임의로 지정해줄 수도 있다.
🤔 빈을 등록할 때 유의할 점!
빈을 등록할 때 빈 이름은 중복되면 안된다. 빈 이름이 중복될 경우 다른 빈이 무시되거나 기존 빈을 덮어버리는 등의 에러가 발생할 수 있기 때문이다. 그래서 빈 이름은 중복되지 않는 고유한 이름으로 설정하는 것이 가장 바람직하다.
정리하자면, 위처럼 AppConfig
설정 클래스에 등록된 빈 정보를 조회하여 빈으로 등록해주게 된다.
스프링 컨테이너를 생성하고 스프링 빈 저장소에 빈을 등록했다. 이제 등록된 빈들에게 의존관계를 설정해 주어야 한다.
AppConfig.java
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() { // 3
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() { // 4
return new RateDiscountPolicy();
}
}
설정 클래스에 작성된 MemberService
는 MemberRepository
를 필요로 하고, OrderService
는 MemberRepository
와 DiscountPolicy
모두를 필요로 하고 있다.
결국 이 두 클래스를 생성하기 위해서는 MemberRepository
와 DiscountPolicy
의 의존관계를 주입해주어야 한다.
AppConfig
설정 클래스에 MemberRepository
와 DiscountPolicy
의 정보를 작성해두었기에 스프링이 자동으로 MemberService
와 OrderService
를 생성할 때 의존관계를 주입(DI) 해준다.
이와 같이 스프링에서 스프링 컨테이너를 생성할 때, 스프링 빈을 생성하고 등록한 후 의존관계를 설정하는 단계에서 어노테이션 기반의 자바 설정 클래스를 이용한다면 간단하게 스프링 빈을 등록하고 의존관계를 주입할 수 있음을 알 수 있었다.
스프링 컨테이너를 학습하니 스프링 컨테이너를 통해 스프링 빈을 등록하고 관리하는 것이 객체지향원칙을 따를 수 있고, 스프링의 핵심요소인 IoC와 DI를 잘 지킬 수 있도록 도와준다는 것임을 배우고 이해할 수 있었다.
이번에는 스프링이 제공하는 어노테이션이 무엇이고 어노테이션 종류들에는 무엇이 있는지 살펴보려 한다.
그 이유는 스프링 컨테이너에서 보았던 @Configuration
이나 @Bean
도 그렇고 여태까지 스프링을 사용해오면서 @RestController
나 @Service
, @Repository
와 같은 어노테이션의 원리를 제대로 알고 넘어가기 위함이다.
자바나 스프링에서 제공하는 어노테이션을 살펴보기 앞서 어노테이션의 의미에 대해서 잘 몰랐던 터라 알아보고 넘어가려 한다.
Annotation(어노테이션)은 무슨 의미일까?
먼저 위키백과에서는 어노테이션을 다음과 같이 설명하고 있다.
어노테이션이란
주석
이라는 사전적 의미를 지닌다. 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종으로 @ 기호를 앞에서 붙여서 주석처럼 특수한 의미를 부여해준다. JDK 1.5 버전 이상에서 사용 가능하다. 자바 애너테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다.
스프링에서 많이 어노테이션을 사용했다는 이유만으로 어노테이션이 스프링의 기능 중 하나라고 생각했지만 어노테이션은 Java 5부터 등장한 Java 언어의 기능이다.
어노테이션을 사용해봐서 잘 알지만 어노테이션을 사용한다면 작성해야 할 코드 수가 대폭 줄어들어서 유지보수하기 쉬워진다. 이로 인해 어노테이션을 사용한다면 보다 개발 생산성을 높여준다는 이점이 있다.
기본적으로 어노테이션은 클래스와 메서드에 추가하여 다양한 기능을 부여해준다.
Java에서도 이러한 역할을 담당하지만 스프링에서는 해당 클래스가 어떤 역할인지 결정할 수도 있고, 빈을 주입하기도 하며, 자동으로 Getter 및 Setter를 생성해주기도 한다. 결국 스프링에서의 어노테이션도 마찬가지로 특별한 의미를 부여하거나 기능을 확장하여 사용할 수 있도록 해주는 역할을 담당한다고 볼 수 있다.
그렇다면 스프링에서 사용했고, 대표적으로도 사용되는 어노테이션들에 대해서 알아보자.
@Bean
어노테이션은 다음과 같을 때 사용하는 어노테이션이다.
- 개발자가 직접 제어할 수 없는 라이브러리를 사용할 경우
- 애플리케이션 전범위적으로 사용되는 클래스를 등록할 경우
- 다형성을 활용하여 여러 구현체를 등록해주어야 할 경우
@Configuration
public class AppConfig {
@Bean
public ArrayList<String> testList(){
return new ArrayList<String>();
}
}
위와 같이 ArrayList 같은 라이브러리 등을 빈으로 등록하기 위해서는 별도로 해당 객체를 반환하는 메서드를 만든 후, @Bean
어노테이션을 붙여주면 된다.
@Component
어노테이션은 개발자가 생성한 클래스를 스프링 빈으로 등록할 때 사용하는 어노테이션이다.
@Component
public class Hamburger {
public Hamburger() {
System.out.println("This is Hamburger!");
}
}
위와 같이 작성한 클래스에 @Component
어노테이션을 붙여주면 스프링이 자동으로 해당 어노테이션 유무를 확인하고 스프링 빈으로 등록해준다.
@ComponentScan
어노테이션은 기본적으로는 @Component
어노테이션이 적용된 클래스가 있다면 해당 클래스를 스프링 컨텍스트에 빈으로 등록하는 역할을 수행한다.
@Configuration
, @RestController
, @Controller
, @Service
, @Repository
과 같은 어노테이션이 적용된 클래스도 @Component
어노테이션이 적용된 클래스와 동일하게 @ComponentScan
가 적용되어 빈으로 등록해준다.
🤔
@Component
어노테이션이 없는데 어떻게@ComponentScan
어노테이션의 탐색 영역이 될까?
@Configuration
,@RestController
,@Controller
,@Service
,@Repository
과 같은 어노테이션들은@Component
어노테이션이 아닌데 어떻게 스프링 빈으로 등록될 수 있는 걸까?해당되는 어노테이션들은 모두
@Component
어노테이션이 포함되어 있기 때문이다.
@Configuration
어노테이션은 스프링 컨테이너에게 스프링 빈을 구성할 설정 클래스임을 설정할 수 있게 해주는 어노테이션이다.
앞서 작성했던 AppConfig 설정 클래스와 같이 설정 클래스로 사용할 클래스에 @Configuration
어노테이션을 붙이면 된다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
...
}
이때, 불러야할 빈이 있다면 @Bean
어노테이션을 원하는 클래스에 적용하여 빈을 조회할 수 있다.
스프링 부트를 통해 개발을 많이 한다면 메인 클래스에서 가장 많이 접하게 되는 어노테이션이다.
@SpringBootApplication
어노테이션은 스프링 부트의 가장 기본적인 설정을 선언해주는 어노테이션으로 @EnableAutoConfiguration
, @ComponentScan
어노테이션을 포함하고 있다.
@RestController
어노테이션은 Rest API 형태의 컨트롤러 믈래스를 개발할 때 많이 사용했던 어노테이션이다.
해당 어노테이션은 요청의 결과를 JSON 형태로 반환해준다.
@RestController
어노테이션 코드를 보면 @Controller
와 @ResponseBody
어노테이션을 포함하고 있는 것을 볼 수 있다.
이로 인해, 요청에 대한 응답을 객체 형식의 데이터로 반환하게 된다.
@RestController의 실행 흐름은 다음과 같다.
Client -> HTTP Request -> Dispatcher Servlet -> Handler Mapping -> RestController(자동 ResponseBody 추가)-> HTTP Response -> Client
@Controller
어노테이션은 Spring MVC 구조에서 컨트롤러 클래스로 사용할 수 있도록 해주는 어노테이션이다.
기본적으로 @Controller
어노테이션이 적용된 컨트롤러 클래스의 반환 타입이 String
이라면 jsp 파일명을 의미하는데, 이는 View에게 반환하기 위함이다.
@Controller
어노테이션의 실행 흐름은 다음과 같다.
Client -> Request -> Dispatcher Servlet -> Handler Mapping -> Controller -> View -> Dispatcher Servlet -> Response -> Client
✅ @RestController와 @Controller의 차이는 무엇일까?
@RestController
는 요청에 대한 응답을 주로 데이터 위주로 반환하는 Rest API 구조에서 많이 사용되고,@Controller
는 요청에 대한 응답을 View에게 전달해야 할 경우(jsp 등)와 같은 MVC 구조에서 사용된다.
@Service
어노테이션은 비즈니스 로직을 수행할 클래스임을 명시해주는 어노테이션이다.
주석에 달린 내용을 한번 읽어보자.
Indicates that an annotated class is a "Service", originally defined by Domain-Driven Design (Evans, 2003) as "an operation offered as an interface that stands alone in the model, with no encapsulated state."
May also indicate that a class is a "Business Service Facade" (in the Core J2EE patterns sense), or something similar. This annotation is a general-purpose stereotype and individual teams may narrow their semantics and use as appropriate.
This annotation serves as a specialization of @Component, allowing for implementation classes to be autodetected through classpath scanning.
DDD(Evans, 2003)에서 정의된 Service는 캡슐화된 상태 없이 모델에서 단독으로 독립된 인터페이스로 제공되는 작업
이라고 한다.
여기서는 DDD를 기반으로 @Service
어노테이션을 설명하고 있는 것이 신기했다.
@Repository
어노테이션은 데이터베이스에 접근하는 메소드를 가지고 있는 클래스에서 사용되며, 주로 DAO(Data Access Object) 클래스에서 사용한다.
또한, 스프링에서 지원하지 않는 Exception를 Spring Exception으로 전환하기 위해서 @Repository
어노테이션을 사용한다.
✅
@Repository
어노테이션에서는 Exception이 발생할 경우 Unchecked Exception을 DataAccessException으로 전환시켜준다. 트랜잭션이 적용된 메소드에서 데이터베이스 관련 오류가 발생해도 롤백이 가능한 이유가 이 때문이다.
여기서도 주석에 달린 내용을 읽어보면
Indicates that an annotated class is a "Repository", originally defined by Domain-Driven Design (Evans, 2003) as "a mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects".
Teams implementing traditional Java EE patterns such as "Data Access Object" may also apply this stereotype to DAO classes, though care should be taken to understand the distinction between Data Access Object and DDD-style repositories before doing so. This annotation is a general-purpose stereotype and individual teams may narrow their semantics and use as appropriate.
A class thus annotated is eligible for Spring DataAccessException translation when used in conjunction with a PersistenceExceptionTranslationPostProcessor. The annotated class is also clarified as to its role in the overall application architecture for the purpose of tooling, aspects, etc.
As of Spring 2.5, this annotation also serves as a specialization of @Component, allowing for implementation classes to be autodetected through classpath scanning.
DDD(Evans, 2003)에서 정의된 Repository는 저장, 검색, 객체 컬렉션을 에뮬레이트하는 검색 행위를 캡슐화하는 메커니즘
이라고 한다.
@Service
어노테이션과 마찬가지로 DDD를 기반으로 설명하고 있음을 알 수 있었다.
이와 같이 스프링에서 사용되는 몇가지 대표적인 어노테이션들에 대해서 알아보았다. 사실 스프링에서 사용하는 어노테이션의 종류는 이것보다 무수히 많지만, 그것들을 일일이 다 기록하는 것은 복사/붙여넣기에 불과할 것 같아 나에게 의미있어 보이는 어노테이션만 기록해보았다.
사실 가장 궁금했던 것은 @Controller
, @Service
, @Repository
어노테이션를 통한 사용자 요청에 대한 응답 처리이다.
명확하게 @Controller
, @Service
, @Repository
3가지 어노테이션의 흐름을 잡을 순 없었지만 각 어노테이션의 역할이 무엇인지 파악하니 스프링이 제공하는 MVC 구조에서의 역할을 적절히 수행하기 위한 어노테이션이라는 점으로 이해하였다.
또한 해당 어노테이션들을 설명하는 개념들이 DDD에서 비롯되었다는 사실이 신박했다. 추후 DDD도 학습할 계획이지만 스프링 어노테이션에서 이러한 관계가 있다는 사실이 흥미로웠다.
사실 이번 학습 시간을 통해 Spring의 동작원리를 완벽하게 이해했다고 보긴 어렵다. 오히려 Spring의 내부 기능 중 Spring의 컨테이너와 어노테이션이라는 하위 기능 단위만을 학습했고 Spring의 방대한 동작원리를 이해하기 까지 공부해야할 범위는 아직 많이 남았다고 느껴진다.
다만, 내가 현시점에서 스프링을 학습하려 했던 이유와 목적을 충족시킬 정도의 수준까지는 학습이 된 것 같다.
스프링 관련 학습은 꾸준히 이어나갈 것이지만, 스프링 부트까지 학습 한 후 이론만으로는 슬슬 재미가 없어지는 것 같아 진행하고 있던 사이드 프로젝트에 학습한 것을 녹여내는 시간을 가져보려 한다.
본 글은 학습하며 작성한 글이기에 틀리거나 잘못된 내용이 기록될 수 있습니다.
잘못된 내용이 있다면 언제든지 지적해주십시오. 다시 학습하여 정정하도록 하겠습니다.
참고자료 출처