[F-Lab 모각코 챌린지 42일차] DI, 서블릿

부추·2023년 7월 12일
0

F-Lab 모각코 챌린지

목록 보기
42/66

TIL

  1. Bean들을 Wiring하는 방법
  2. 서.블.릿.



1. Wiring Beans

객체 지향 프로그래밍에서, 프로그램 기능은 객체들의 의존관계를 통한 협력으로 달성된다. 이를 달성하기 위해 객체들의 의존성을 관리하는 작업이 필요한데 스프링에선 이것을 "wiring bean"이라고 표현한다. 표현 방법이 중요한 것은 아니지만.. 하여튼 이렇게 표현한다.

스프링의 특징 중, IoC와 DI가 있었다. 스프링에서 사용되는 객체인 Bean을 생성하고 의존성을 주입하는 과정이 프로그램 코드 안에서 개발자에 의해 이뤄지는 것이 아니라, 설정 파일과 어노테이션의 정보를 토대로 IoC에 의해 진행된다는 특징이었다. 저번엔 빈의 생성을 살펴봤고, 의존성 주입은 어떻게 설정하는걸까?


BookPerson 클래스가 있다. 다음과 같이, PersonBook을 필드 값으로 가지며 의존하고 있는 상황이다.

@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이 만들어지고 어떻게 의존성이 연결될 수 있는지 확이하자.

1) @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을 의존성으로 주입하라'고 설정할 필요가 없는 것이다.


2) @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 메소드에서 인자로 의존성을 주입할 빈이 자동으로 주입되는 과정과 같다.


3) Constructor 주입

아예 생성자 단계에서 의존성을 주입시킬 수도 있다. 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 설명을 간단히 읽어보자.

  • constructor : IoC가 의존성을 주입해줬으면 하는 constructor 하나에만 @AutoWired 어노테이션을 붙이자. IoC 컨테이너가 빈을 생성할 때 해당 생성자를 호출할 것이고, 그 때 생성자 인자에 알맞은 빈이 주입된다.
  • field : 빈 생성 직후 어노테이션이 붙은 필드에 의존성 주입이 일어난다.
  • method : method 인자에 매칭되는 bean이 있으면 주입된다.

Spring 4.3부터 단일 생성자를 가진 클래스는 @Autowired 어노테이션을 생략 할 수 있다. 예컨데 위의 Person 코드에서 @AutoWired 어노테이션이 없어도 잘 동작한다. '단 하나의 생성자에 @AutoWired를 붙여라'라는 제약이, 생성자가 단 하나라면 의미가 없으므로 편의를 이런 식으로 제공한 듯 하다.


# 뭘 써야할까? : Constructor!

웬만하면 Constructor를 통해 의존성을 주입해야한다. 그 이유는.

  1. 의존성 필드에 final 키워드를 붙일 수 있다.
  2. 스프링 어플리케이션의 시작 전에 circular dependency를 찾아낼 수 있다.

첫째로, 스프링에서 사용하는 Bean들은 immutable인 것이 좋다.(사실 스프링만이 아니라.. 어플리케이션에서 사용하는 객체들은 웬만하면 불변이 것이 좋다) 어플리케이션 동작 과정중 여러 스레드가 빈의 기능을 이용할텐데, 빈이 mutable이라면 race condition같은 예상치 못한 문제가 발생할 수도 있기 때문이다.

필드에 final 키워드가 붙으면, 생성자에 해당 필드의 값을 초기화해줘야하고 그 값은 프로그램의 종료 시까지 바뀌지 않는다. 빈 자체를 immutable로 만들 수 있다. 그렇기 때문에 constructor

둘째로, 상호 의존은 어플리케이션 코드의 수정을 매!!우!! 어렵게 만든다. 모듈이 서로를 참조하고 있으니 엄청나게 강한 결합이 이뤄지고 있는 것이고, 한 쪽을 수정했더니 다른쪽이 기능을 못하고 그 다른쪽을 수정했더니 다시 이쪽이 기능을 안하고... 결국 손댈 수 없이 엉킨 대참사 스파게티 코드가 탄생할 수 있다. 때문에 상호 참조는 객체 지향 프로그래밍에서 가장 피해야할 안티패턴중 하나인데, setter이나 @AutoWired를 통한 빈 생성 후 의존 주입은 이를 가능하게 만든다.

하지만 constructor를 통한 DI는 어플리케이션 시작 전 이런 상호 참조, 즉 circular dependency를 막아낸다. 상호 의존을 만들기 위해 다음과 같이 PersonBook을 간단하게 구성한 뒤 어플리케이션을 돌려보았다.

@Component
@RequiredArgsConstructor
public class Book {
    private final Person person;
}
@Component
@RequiredArgsConstructor
public class Person {
    private final Book book;
}

당연히 에러가 뜬다.
Is there an unresolvable circular reference?라니, 꽤 똑똑한 놈.. 아무튼, 앞서 언급했듯 참사를 피하기 위해서 객체 간 의존성은 단방향으로 둬야한다. 이는 개발자의 개발 능력과 관련된 부분이기도 하니까.. 설계를 할 때부터 상호 의존이 생기지 않게 잘 구성할 것!


# 만약 동일 타입 Bean이 여러개라면?

DI를 하려는데 동일 타입의 빈이 여러개라 ambiguous한 상황이라면, spring은 우선순위를 정해 빈을 주입한다.

  1. 생성자의 인자 이름, 혹은 field의 이름과 일치하는 빈을 주입
  2. 동일 타입 중에선 @Primary 어노테이션이 붙은 빈을 주입
  3. 메소드 파라미터에 @Qulifier("name")의 name 문자열과 일치하는 빈 주입




2. Web Application

  • 웹 서버(WS) : 정적 컨텐츠 제공, 로드밸런싱
  • 웹 어플리케이션 서버(WAS) : 동적 컨텐츠 제공

# 서블릿 컨테이너

스프링을 사용한 자바 웹 어플리케이션은 서블릿 컨테이너가 요청-응답 과정에 일어나는 일을 처리한다. 서블릿 컨테이너는 그 자체로 웹 어플리케이션 서버이며, 서블릿들을 관리한다(서블릿이 무엇인진 아래에서 설명). 스프링 부트에선 기본적으로 (아파치) 톰캣을 서블릿 컨테이너로 사용한다.

아파치 톰캣은 아파치 소프트웨어 재단에서 개발한 서블릿 컨테이너만 있는 웹 애플리케이션 서버이다. 톰캣은 웹 서버와 연동하여 실행할 수 있는 자바 환경을 제공하여 자바서버 페이지와 자바 서블릿이 실행할 수 있는 환경을 제공하고 있다.

서블릿 컨테이너가 낀 웹 어플리케이션은 간단하게 다음 과정으로 동작한다.

  1. 사용자가 HTTP 요청을 날린다.
  2. 서블릿 컨테이너가 HTTP 요청에 맞는 서블릿을 호출한다. 서블릿은 HTTP 메세지를 자바에서 사용 가능한 object로 변환한 뒤 설정된 로직을 수행한다.
  3. 서블릿에 의해 로직이 수행되고 Reponse Object가 구성되면, 다시 서블릿이 객체를 HTTP 메세지로 변환하여 응답을 날린다.

여기서 서블릿 컨테이너 역할을 하는 것이 (아파치) 톰캣이다. 서블릿 컨테이너는 서블릿들을 관리하고, 어플리케이션에서 구성된 응답을 다시 반환하며 동적 컨텐츠를 제공하는데 쓰인다.
서블릿이 없는 채로 웹 서버만 두면.. HTTP 문자열을 파싱하고.. 객체화시키고.. 어플리케이션 로직 결과를 다시 HTTP 메세지로 구성하고.. 하는데 쓰잘데 없는 에너지를 소모하게 된다.


# 서블릿

서블릿은 WAS에서 동적인 페이지 구성을 위해 어플리케이션 로직을 수행하는 프로그램이다. init()을 통해 생성, service()를 통해 로직 수행, destroy()를 통해 파괴된다. 서블릿 컨테이너에 의해 관리되는 빈.. 정도로 이해하면 될 것 같다.
서블릿은 위 그림과 같은 생명주기를 가진다. 서블릿은 생성되지 않았다가, 서블릿에 맞는 최초의 사용자 요청이 들어오면 init()을 통해 생성되고 service() 메소드가 실행된다. 그 뒤 서블릿은 싱글톤으로 관리되며 같은 요청이 들어왔을 때 재사용된다.


서블릿이 낀 웹 어플리케이션 동작을 조금 더 자세히 살펴보자?
1. 개발자가 spring 소스코드를 작성한다. .java
2. 컴파일한다. .class
3. 톰캣과 같은 서블릿 컨테이너에 클래스 파일이 등록된다.
4. 사용자 요청이 들어온다.
5. 서블릿 컨테이너에서 path에 맞는 서블릿이 실행된다.
6. 비즈니스 로직이 실행된다.
7. @Repository 등과 관련해서 DB와 연동작업이 있으면 수행된다.
8. 응답 객체가 생성되고, 서블릿에 의해 응답 메세지로 변환된 뒤 반환된다.


# Dispatcher Servlet

요청 path의 개수만큼 서블릿을 구성하는 것은 번거롭고 귀찮다. /hi, /hello, /by 3개의 요청 path를 위해 hi서블릿, hello서블릿, by서블릿을 모두 두고 각각을 init(), service(), destroy()로 관리하는 것은 귀찮다는 얘기다.

스프링에서는 "dispatcher servlet"이라는 것을 만들어, 서블릿 컨테이너에 해당 서블릿을 한개만 두고 핸들러를 여러개 두는 식으로 다양한 사용자 요청을 처리할 수 있도록 했다. dispatcher servlet은 서블릿으로서 서블릿 컨테이너에 의해 호출되어, 적절한 핸들러 메소드를 찾고 결과 데이터 / 뷰를 서블릿 컨테이너에게 다시 전달한다.

  1. 서블릿 컨테이너가 사용자 요청을 받고 dispatcher servlet을 호출한다.
  2. dispatcher servlet은 Handler Mapping에게 어떤 컨트롤러의 핸들러를 호출해야할지 질의하고 응답을 받는다.
  3. 응답에 따라 적절한 핸들러 메소드를 호출한 뒤 결과를 받는다.
  4. 핸들러 메소드에게 반환받은 결과에 View가 포함될 경우, View Resolver에게 뷰 객체를 받은 뒤 서블릿 컨테이너에게 전달한다.

Dispatcher Servlet이 이용하는 Handler Mapping, Handler Adapter, View Resolver 등은 스프링의 IoC 컨테이너에 의해 주입이 된다. 결국 개발자가 신경써야할 부분은 Handler Adapter가 호출할 @Controller 객체들이라는 말이다.



REFERENCE

https://velog.io/@dahye4321/Constructor-%EC%A3%BC%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90#1-%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99-%EA%B4%80%EC%A0%90

https://velog.io/@jakeseo_me/%EC%9E%90%EB%B0%94-%EC%84%9C%EB%B8%94%EB%A6%BF%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90.-%EA%B7%BC%EB%8D%B0-%ED%86%B0%EC%BA%A3%EA%B3%BC-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%84-%EC%82%B4%EC%A7%9D-%EA%B3%81%EB%93%A4%EC%9D%B8#%EC%84%9C%EB%B8%94%EB%A6%BF%EC%9D%98-%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D

https://www.youtube.com/watch?v=calGCwG_B4Y

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글