[Spring] IoC 간단정리
IoC (Inversion of Control, 제어의 역전) 란?
객체 생성과 의존성 관리를 개발자가 아닌, 프레임워크가 대신하는 설계 원칙
일반적으로 우리가 객체를 생성하고 의존 객체를 주입하여 제어 흐름을 직접 작성하는 대신, 프레임워크가 대신 생성해주고 주입해주는 구조이다. 개발자는 그저 객체 간의 관계만 설정해주면 된다.
스프링 역시 이를 위한 프레임워크 중 하나이다.
이게 무슨 말인가 싶었지만, 이해해보니 그리 어려운 말이 아니였다. 아래 코드를 보면,
public static void main(String[] args) throws Exception {
Steps steps = new Communication();
}
public class Communication implements Steps {
private final Greeting greeting;
private final Farewell farewell;
private final Sender sender;
public Communication() {
this.greeting = new EnglishGreeting();
this.farewell = new EnglishFarewell();
this.sender = new ConsoleSender();
}
...
}
위는 우리가 흔히 작성하는 코드이다. Steps 이라는 인터페이스를 구현한 클래스에서, 생성자를 통해 내부 필드를 초기화하고 있다.
이때, greeting 의 객체 EnglishGreeting 을 KoreanGreeting 으로 수정해야한다면 어떻게 해야할까?
public Communication() {
this.greeting = new KoreanGreeting();
this.farewell = new EnglishFarewell();
this.sender = new ConsoleSender();
}
물론, 어렵지 않게 수정할 수 있다. 그러나, 이 Communication() 과 같은 클래스들이 많아진다면 일일이 하나씩 다 수정해줘야 한다는 불편함이 생긴다.
이 과정을 개발자가 하지않게 프레임워크가 대신 하는 것을 IoC 라 하고, 스프링에서는 이를 @Autowired 나 @Component 같은 의존성주입(DI) 으로 해결한다.
Spring IoC : Spring Container
그렇다면 스프링은 어떻게 객체를 생성하고 주입할 수 있게 해주는 것인가?
결론부터 말하자면, Spring Container 라는 녀석이 '객체 정의'를 미리 읽고, 그 정의를 기반으로 객체를 생성하고, 의존성을 주입하며, 생성주기를 관리한다.
주로 ApplicationContext 를 이용해서 구현하는데, Bean Factory, IoC Container, Spring Container 등 여러 방법으로 불러질 수 있지만 모두 비슷한 개념으로 보면 된다.
이전에 서블릿을 통해 Context 에 대한 개념을 이해할 수 있었다. 그렇다면, 스프링에서도 Context(환경) 에서 객체를 관리하는 구조라는 것을 알 수 있다.
Spring Bean
위에서 말한 것처럼, Spring Container 에서 객체를 관리하는데 이때 관리되는 객체들을 스프링에서는 Bean 이라고 부른다.
애플리케이션을 실행시키면 컨테이너(ApplicationContext)가 실행되고, 이때 컨테이너가 Bean 을 찾아 스프링에서 관리할 객체로 인식시켜주는 것이다. 한마디로, Bean은 Spring이 관리하는 객체를 의미한다.
다른 말로 하면, 스프링에서 객체를 관리하게끔 하고 싶다면 객체를 Bean 으로 등록해야 한다는 소리이다.
[Bean 등록 방법]
- @Component 기반 자동 등록
- @Configuration + @Bean 기반 수동 등록
스프링에서 Bean 을 등록하는 방법은 2가지가 있다. 두 방법 모두 어노테이션을 이용해서 등록할 수 있다.
1. @Component 기반
@Component
public class BeanBean {
}
말 그대로 Bean 으로 등록하고 싶은 클래스에 @Component 어노테이션을 붙이기만 하면 자동으로 Bean 으로 등록된다.
2. @Configuration + @Bean 기반
@Configuration
public class AppConfig {
@Bean
Bean bean() {
return new BeanBean();
}
}
위 방법은 @Configuration 을 붙인 클래스 내부에 등록할 객체들을 @Bean 어노테이션을 이용해 반환해주어야 한다.
두 방법 모두 동일한 동작을 하나, @Component는 클래스 자체를 Bean으로 등록하고, @Configuration은 클래스 내의 @Bean이 붙은 메서드를 통해 Bean을 정의하고 생성한다는 차이점이 있다.
Spring Bean 객체 생명 주기 설정
이러한 Spring Bean 도 결국엔 객체이므로 생성과 반환의 주기를 가지고 있다. 그리고 이는 개발자가 설정할 수도 있다.
[Bean Scope (생명 주기)]
스프링 IoC 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → (초기화 콜백 메소드 호출) → 사용 → (소멸 전 콜백 메소드 호출) → 스프링 종료
- singleton: ApplicationContext가 시작될 때 초기화되고, ApplicationContext가 종료될 때 소멸됨. 이는 애플리케이션 전체에서 단 하나의 인스턴스만을 유지함을 의미
- prototype: 요청될 때마다 새로운 인스턴스가 생성. 생성된 각 인스턴스는 호출자에 의해 관리되며, Spring 컨테이너는 생성 이후에 이 인스턴스를 관리하지 않음
- request(web): HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
- session(web): HTTP Session과 동일한 생명주기를 가지는 스코프
- application(web): 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
- websocket(web): 웹 소켓과 동일한 생명주기를 가지는 스코프
[중간 개입 메서드]
- @PostConstruct, @PreDestroy : 자바 EE 스펙 (추천)
- afterPropertiesSet(), destroy()
- custom init() / destroy() method
DI (Dependency Injection) : 의존성 주입
위처럼 Bean 으로 등록한 객체들이 필요로 하는 의존성을 개발자가 아니라, 외부에서 직접 주입받는 것을 DI 라고 한다.
DI 라고 해서 특별한건 아니고, 앞서 설명한 IoC 의 패턴 중 하나일 뿐이다. 그리고 스프링에서는 @Autowired 라는 어노테이션을 이용해 객체 간의 의존성을 주입해준다.
[DI 방법]
- Constructor Injection - 생성자 주입
- Setter Injection - 세터 주입
- Field Injection - 필드 주입
스프링에서 의존성을 주입하는 데에는 3가지 방법이 있다.
1. Constructor Injection
@Component
public class AppStartupRunner implements ApplicationRunner {
private HighSchoolStudent highSchoolStudent;
private UniversityStudent universityStudent;
// @AutoWired (생략 가능)
public AppStartupRunner(HighSchoolStudent highSchoolStudent, UniversityStudent universityStudent) {
this.highSchoolStudent = highSchoolStudent;
this.universityStudent = universityStudent;
}
}
우선 주입 받을 객체를 선언한다. 그리고 생성자를 생성하여 해당 객체에 의존성을 주입해주면 된다. (이때, 롬복도 사용이 가능하다.)
* 롬복 생성자
@NoArgsConstructor : 인자를 포함하지 않는 생성자 자동 생성
@AllArgsConstructor : 모든 인자 포함하는 생성자 자동 생성
@RequiredArgsConstructor : 필요한 인자만 생성자 자동 생성 (private final)
생성자 주입에서, 생성자가 하나일 경우 @Autowired 생략이 가능하다.
2. Setter Injection
@Component
public class AppStartupRunner implements ApplicationRunner {
private Greeting greeting;
@Autowired
public void setGreeting(Greeting greeting) {
this.greeting = greeting;
}
@Override
public void run(ApplicationArguments args) {
greeting.sayHello();
}
}
주입 받고 싶은 객체를 선언한다. 그리고 주입 받고 싶은 객체의 세터를 생성하여, 마찬가지로 의존성을 주입해준다.
3. Field Injection
@Component
public class AppStartupRunner implements ApplicationRunner {
@Autowired
private Greeting greeting;
@Override
public void run(ApplicationArguments args) {
greeting.sayHello();
}
}
주입 받고 싶은 객체를 선언한다. 해당 객체에 바로 @Autowired 를 붙여준다.
정리
정리하자면,
IoC 는 개발자의 제어권이 프레임워크로 넘어간다는 개념이다.
스프링에서는 이를 Bean을 등록하여 컨테이너가 관리하는 구조인데
이 Bean을 등록하는 방법에는 2가지가 있다.(@Component, @Configuration + @Bean)
그리고 이때 DI 라는 구현 방식으로 의존성을 주입해주는데, 3가지가 있다.(생성자, 세터, 필드)