- Bean들을 Wiring하는 방법
- 서.블.릿.
객체 지향 프로그래밍에서, 프로그램 기능은 객체들의 의존관계를 통한 협력으로 달성된다. 이를 달성하기 위해 객체들의 의존성을 관리하는 작업이 필요한데 스프링에선 이것을 "wiring bean"이라고 표현한다. 표현 방법이 중요한 것은 아니지만.. 하여튼 이렇게 표현한다.
스프링의 특징 중, IoC와 DI가 있었다. 스프링에서 사용되는 객체인 Bean을 생성하고 의존성을 주입하는 과정이 프로그램 코드 안에서 개발자에 의해 이뤄지는 것이 아니라, 설정 파일과 어노테이션의 정보를 토대로 IoC에 의해 진행된다는 특징이었다. 저번엔 빈의 생성을 살펴봤고, 의존성 주입은 어떻게 설정하는걸까?
Book
과 Person
클래스가 있다. 다음과 같이, Person
이 Book
을 필드 값으로 가지며 의존하고 있는 상황이다.
@Setter
@Getter
public class Book {
String name;
}
@Setter
public class Person {
Book book;
String name;
public void sayHello() {
System.out.println("내 이름은 " + name + "이고," +
" 내가 가진 책은 " + book.getName());
}
}
각 Bean이 만들어지고 어떻게 의존성이 연결될 수 있는지 확이하자.
@Bean
+ 메소드 인자@Configuration
클래스의 @Bean
메소드에는 해당 타입의 빈을 return하는 메소드가 존재한다. 이 메소드의 인자로 의존성을 연결할 빈 객체를 전달할 수 있다.
@Configuration
public class BeanParameter {
@Bean
public Book book() {
Book book = new Book();
book.setName("예쁜책");
return book;
}
@Bean
public Person person(Book book) {
Person person = new Person();
person.setName("부추");
person.setBook(book);
return person;
}
}
book()
는 이름이 "예쁜책"
인 book bean을 생성하는 메소드이다.person(Book book)
를 보면 인자로 Book
타입의 객체가 들어있다. 인자가 setter
을 통해 person
객체의 필드에 연결되고있다.그리고 이대로 main()
메소드를 돌려보면..
public class BeanParameterDriver {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(BeanParameter.class);
Person person = context.getBean(Person.class);
person.sayHello();
}
}
결과는 예상 가능한 sayHello()
출력문과 같이 나온다.
내 이름은 부추이고, 내가 가진 책은 예쁜책
@Bean
메소드를 통해 bean 객체를 생성할 때, 메소드 인자로 특정 타입의 객체가 필요하면 IoC 컨테이너는 특정 타입의 빈이 컨텍스트에 등록되어있는지 확인하고, 있다면 그 빈을 인자로 넣어주는 작업을 수행한다. getBean()
을 통해 불러온 결과를 injection했다고 생각하면 된다. 그렇기 때문에 명시적으로 '어떤 bean을 의존성으로 주입하라'고 설정할 필요가 없는 것이다.
@Component
+ @AutoWired
기존의 Person
, Book
클래스를 @Component
어노테이션을 붙여 다음과 같이 수정했다.
@Component
@Getter
public class Book {
String name;
public Book() {
this.name = "예쁜책";
}
}
@Component
public class Person {
@AutoWired
private Book book;
private String name;
public Person() {
this.name = "부추";
}
public void sayHello() {
System.out.println("내 이름은 " + name + "이고," +
" 내가 가진 책은 " + book.getName());
}
}
Person
클래스에 @AutoWired
가 붙은 book
필드가 중요하다. 필드에 @AutoWired
가 붙으면, application context에서 해당 필드와 같은 타입의 빈이 자동 주입된다. 똑같은 main 메소드를 실행해보면 실행 결과는 같다. (물론 실행하기 전, @ComponentScan
어노테이션을 붙인 @Configuration
빈을 추가하는 것을 잊지 말자!)
비슷한 방법으로 setter에 @AutoWired
를 붙일 수도 있다. Person
클래스가 아래와 같이 변하는 것이다.
@Component
public class Person {
private Book book;
private String name;
public Person() {
this.name = "부추";
}
@AutoWired
public void setBook(Book book) {
this.book = book;
}
public void sayHello() {
System.out.println("내 이름은 " + name + "이고," +
" 내가 가진 책은 " + book.getName());
}
}
결과는 100% 똑같게 나온다. IoC가 의존성을 주입해주기 위해 @AutoWired
가 붙은 메소들을 실행해주는 것이다. 이것은 @Bean
메소드에서 인자로 의존성을 주입할 빈이 자동으로 주입되는 과정과 같다.
아예 생성자 단계에서 의존성을 주입시킬 수도 있다. Person
클래스의 생성자 인자로 의존성을 주입할 Book
객체를 다음과 같이 구성한다.
@Component
public class Person {
@AutoWired
private Book book;
private String name;
public Person(Book book) {
this.name = "부추";
this.book = book;
}
public void sayHello() {
System.out.println("내 이름은 " + name + "이고, 내가 가진 책은 " + book.getName());
}
}
Spring 4.3부터 단일 생성자를 가진 클래스는 @Autowired 어노테이션을 생략 할 수 있다.
@AutoWired
와 관련한 docs 설명을 간단히 읽어보자.
@AutoWired
어노테이션을 붙이자. IoC 컨테이너가 빈을 생성할 때 해당 생성자를 호출할 것이고, 그 때 생성자 인자에 알맞은 빈이 주입된다.Spring 4.3부터 단일 생성자를 가진 클래스는 @Autowired 어노테이션을 생략 할 수 있다. 예컨데 위의 Person
코드에서 @AutoWired
어노테이션이 없어도 잘 동작한다. '단 하나의 생성자에 @AutoWired
를 붙여라'라는 제약이, 생성자가 단 하나라면 의미가 없으므로 편의를 이런 식으로 제공한 듯 하다.
웬만하면 Constructor를 통해 의존성을 주입해야한다. 그 이유는.
final
키워드를 붙일 수 있다.첫째로, 스프링에서 사용하는 Bean들은 immutable인 것이 좋다.(사실 스프링만이 아니라.. 어플리케이션에서 사용하는 객체들은 웬만하면 불변이 것이 좋다) 어플리케이션 동작 과정중 여러 스레드가 빈의 기능을 이용할텐데, 빈이 mutable이라면 race condition같은 예상치 못한 문제가 발생할 수도 있기 때문이다.
필드에 final
키워드가 붙으면, 생성자에 해당 필드의 값을 초기화해줘야하고 그 값은 프로그램의 종료 시까지 바뀌지 않는다. 빈 자체를 immutable로 만들 수 있다. 그렇기 때문에 constructor
둘째로, 상호 의존은 어플리케이션 코드의 수정을 매!!우!! 어렵게 만든다. 모듈이 서로를 참조하고 있으니 엄청나게 강한 결합이 이뤄지고 있는 것이고, 한 쪽을 수정했더니 다른쪽이 기능을 못하고 그 다른쪽을 수정했더니 다시 이쪽이 기능을 안하고... 결국 손댈 수 없이 엉킨 대참사 스파게티 코드가 탄생할 수 있다. 때문에 상호 참조는 객체 지향 프로그래밍에서 가장 피해야할 안티패턴중 하나인데, setter이나 @AutoWired
를 통한 빈 생성 후 의존 주입은 이를 가능하게 만든다.
하지만 constructor를 통한 DI는 어플리케이션 시작 전 이런 상호 참조, 즉 circular dependency를 막아낸다. 상호 의존을 만들기 위해 다음과 같이 Person
과 Book
을 간단하게 구성한 뒤 어플리케이션을 돌려보았다.
@Component
@RequiredArgsConstructor
public class Book {
private final Person person;
}
@Component
@RequiredArgsConstructor
public class Person {
private final Book book;
}
당연히 에러가 뜬다.
Is there an unresolvable circular reference?
라니, 꽤 똑똑한 놈.. 아무튼, 앞서 언급했듯 참사를 피하기 위해서 객체 간 의존성은 단방향으로 둬야한다. 이는 개발자의 개발 능력과 관련된 부분이기도 하니까.. 설계를 할 때부터 상호 의존이 생기지 않게 잘 구성할 것!
DI를 하려는데 동일 타입의 빈이 여러개라 ambiguous한 상황이라면, spring은 우선순위를 정해 빈을 주입한다.
@Primary
어노테이션이 붙은 빈을 주입@Qulifier("name")
의 name 문자열과 일치하는 빈 주입스프링을 사용한 자바 웹 어플리케이션은 서블릿 컨테이너가 요청-응답 과정에 일어나는 일을 처리한다. 서블릿 컨테이너는 그 자체로 웹 어플리케이션 서버이며, 서블릿들을 관리한다(서블릿이 무엇인진 아래에서 설명). 스프링 부트에선 기본적으로 (아파치) 톰캣을 서블릿 컨테이너로 사용한다.
아파치 톰캣은 아파치 소프트웨어 재단에서 개발한 서블릿 컨테이너만 있는 웹 애플리케이션 서버이다. 톰캣은 웹 서버와 연동하여 실행할 수 있는 자바 환경을 제공하여 자바서버 페이지와 자바 서블릿이 실행할 수 있는 환경을 제공하고 있다.
서블릿 컨테이너가 낀 웹 어플리케이션은 간단하게 다음 과정으로 동작한다.
여기서 서블릿 컨테이너 역할을 하는 것이 (아파치) 톰캣이다. 서블릿 컨테이너는 서블릿들을 관리하고, 어플리케이션에서 구성된 응답을 다시 반환하며 동적 컨텐츠를 제공하는데 쓰인다.
서블릿이 없는 채로 웹 서버만 두면.. HTTP 문자열을 파싱하고.. 객체화시키고.. 어플리케이션 로직 결과를 다시 HTTP 메세지로 구성하고.. 하는데 쓰잘데 없는 에너지를 소모하게 된다.
서블릿은 WAS에서 동적인 페이지 구성을 위해 어플리케이션 로직을 수행하는 프로그램이다. init()
을 통해 생성, service()
를 통해 로직 수행, destroy()
를 통해 파괴된다. 서블릿 컨테이너에 의해 관리되는 빈.. 정도로 이해하면 될 것 같다.
서블릿은 위 그림과 같은 생명주기를 가진다. 서블릿은 생성되지 않았다가, 서블릿에 맞는 최초의 사용자 요청이 들어오면
init()
을 통해 생성되고 service()
메소드가 실행된다. 그 뒤 서블릿은 싱글톤으로 관리되며 같은 요청이 들어왔을 때 재사용된다.
서블릿이 낀 웹 어플리케이션 동작을 조금 더 자세히 살펴보자?
1. 개발자가 spring 소스코드를 작성한다.
.java
2. 컴파일한다. .class
3. 톰캣과 같은 서블릿 컨테이너에 클래스 파일이 등록된다.
4. 사용자 요청이 들어온다.
5. 서블릿 컨테이너에서 path에 맞는 서블릿이 실행된다.
6. 비즈니스 로직이 실행된다.
7. @Repository
등과 관련해서 DB와 연동작업이 있으면 수행된다.
8. 응답 객체가 생성되고, 서블릿에 의해 응답 메세지로 변환된 뒤 반환된다.
요청 path의 개수만큼 서블릿을 구성하는 것은 번거롭고 귀찮다. /hi
, /hello
, /by
3개의 요청 path를 위해 hi서블릿, hello서블릿, by서블릿을 모두 두고 각각을 init()
, service()
, destroy()
로 관리하는 것은 귀찮다는 얘기다.
스프링에서는 "dispatcher servlet"이라는 것을 만들어, 서블릿 컨테이너에 해당 서블릿을 한개만 두고 핸들러를 여러개 두는 식으로 다양한 사용자 요청을 처리할 수 있도록 했다. dispatcher servlet은 서블릿으로서 서블릿 컨테이너에 의해 호출되어, 적절한 핸들러 메소드를 찾고 결과 데이터 / 뷰를 서블릿 컨테이너에게 다시 전달한다.
Dispatcher Servlet이 이용하는 Handler Mapping, Handler Adapter, View Resolver 등은 스프링의 IoC 컨테이너에 의해 주입이 된다. 결국 개발자가 신경써야할 부분은 Handler Adapter가 호출할 @Controller
객체들이라는 말이다.