- WAS란
- Socket과 HTTP 구현
- Servlet Container & Spring Container 구현
- Servlet 구현
- MVC 구현
Servlet Container 구현
Servlet Container란
서블릿 컨테이너는 브라우저와 같은 클라이언트로부터 들어오는 요청을 받아서 서블릿을 동작시켜 주는 일을 맡는다.
서블릿은 웹 애플리케이션이 시작될 때 미리 만들어둔 웹 애플리케이션 컨텍스트에게 빈 오브젝트로 구성된 애플리케이션의 기동 역할을 해줄 빈을 요청해서 받아둔다.
그리고 미리 지정된 메소드를 호출함으로써 스프링 컨테이너가 DI 방식으로 구성해 둔 애플리케이션의 기능이 시작되는 것이다.
<토비의 스프링 3.1>
RequestHandler
앞서 구현한 WebServer에서 RequestHandler 인스턴스를 생성하여 연결 데이터를 넘겨주었다.
Runnable r = new RequestHandler(connection);
이 RequestHandler는 사실상 서블릿 컨테이너의 핵심 역할을 한다.
- 넘겨 받은 연결 데이터를 파싱하여 요청 정보를 확인
- 요청에 따라 적절한 서블릿에게 정보 전달
- 서블릿에서 처리한 응답 정보를 적절하게 가공하고 반환
0. run()
RequstHandler는 Runnable 인터페이스를 구현하므로 Runnable의 run() 메서드를 구현하여 할 일을 시킨다.
When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread.
Runnable 인터페이스를 구현하는 객체가 스레드를 만드는 데 사용될 때, 스레드를 시작하면 별도로 실행되는 스레드에서 객체의 run 메서드를 호출한다.
1. 요청 정보 확인
private final Socket connection;
public RequestHandler(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
try(BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
OutputStream out = new BufferedOutputStream(connection.getOutputStream());
DataOutputStream dos = new DataOutputStream(out)) {
HttpRequest req = HttpUtil.parseRequest(in);
HttpResponse res = new HttpResponse();
} catch (IOException | InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
logger.error(ex.getMessage(), ex);
}
}
a. BufferReader를 이용해 데이터를 읽어들인다
b. try-catch-resource를 사용하여 자원 반환 까먹는 걸 방지한다 (AutoCloseable 인터페이스를 구현한 객체만 가능)
c. HttpUtil의 parseRequest 메서드에 데이터를 넘겨주고 파싱된 정보가 담긴 HttpRequest 인스턴스를 받는다
파악하기 편하게 나중에 클라이언트에게 반환할 HttpResponse 인스턴스도 미리 생성한다
2. DispatcherServlet 호출
@Override
public void run() {
try(BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
OutputStream out = new BufferedOutputStream(connection.getOutputStream());
DataOutputStream dos = new DataOutputStream(out)) {
HttpRequest req = HttpUtil.parseRequest(in);
HttpResponse res = new HttpResponse();
// * * *
ApplicationContext ac = ApplicationContext.getApplicationContext();
Servlet servlet = DispatcherServlet.getDispatcherServlet(ac);
// * * *
} catch (IOException | InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
logger.error(ex.getMessage(), ex);
}
}
a. ApplicationContext 값을 가져온다. 아래에서 다시 설명하겠지만 Spring Container의 역할을 하는 객체이다
b. DispatcherServlet의 getDispatcherServlet 메서드를 호출하며 ac를 전달한다
c. DispatcherServlet으로부터 Servlet을 받는다
Spring Container 구현
Spring Container란
스프링 컨테이너는 IoC(Inversion of Control, 제어의 역전) 컨테이너라고도 한다.
제어의 역전이라는 말이 스프링 컨테이너를 잘 설명해 준다고 생각한다.
웹 애플리케이션은 끊임없이 변화하는 세상에 대응해야 할 필요가 있다.
요구 사항은 시도 때도 없이 변하고, 기존 기술을 대체해야 할 상황이 오기도 하고, 다양한 환경에서 테스트를 해야 하고...
그때마다 개발자가 코드를 a부터 z까지 고치면 비효율적일 뿐만 아니라 변화의 속도도 따라잡을 수도 없다.
이 문제를 DI(Dependency Injection, 의존성 주입)라는 기술이 해결해 준다.
이제 변화에 대응하기 위해 부품을 갈아 끼우기만 하면 된다.
스프링 프레임워크는 DI 기술을 제공하며 스프링 프레임워크의 핵심 요소인 스프링 컨테이너 담당한다.
개발자는 부품을 쉽게 갈아끼우기 위한 부분들의 제어권을 스프링 컨테이너에게 넘기면서 비즈니스 로직에 더욱 집중할 수 있게 되었다.
ApplicationContext
위에서 이미 RequestHandler의 run() 메서드에서 ApplicationContext 코드가 등장했다.
실제로 스프링 컨테이너 또는 IoC 컨테이너라고 말하는 것은 바로 이 ApplicationContext 인터페이스를 구현한 클래스의 객체다.
나는 ApplicationContext를 하나만 구현했지만 실제로는 ApplicationContext는 인터페이스로 추상화되어 있다.
ApplicationContext를 상속받은 인터페이스들과 그 인터페이스들의 구현체들이 있다.
결론은 내가 구현한 ApplicationContext는 DispatcherServlet에서 사용할 빈을 담은 WebApplicationContext(extends ApplicationContext)의 구현체인 셈이다.
0. 생성자와 초기화
public class ApplicationContext {
private final Map<String, Object> beanStorage = new HashMap<>();
private ApplicationContext() {
initBeanStorage();
}
private static class InnerApplicationContext {
private static final ApplicationContext ac = new ApplicationContext();
}
public static ApplicationContext getApplicationContext() {
return InnerApplicationContext.ac;
}
private void initBeanStorage() {
beanStorage.put("handler.UrlControllerHandlerMapping", new UrlControllerHandlerMapping());
beanStorage.put("handler.SimpleControllerHandlerAdapter", new SimpleControllerHandlerAdapter());
beanStorage.put("controller.HelloController", new HelloController());
beanStorage.put("controller.TimeController", new TimeController());
}
...
}
a. InnerApplicationContext이라는 static 이너 클래스를 만든다
애플리케이션이 구동되자마자 생성되도록 하기 위해 static 이너 클래스로 만들었다.
b. 빈 정보는 Map에 담아 관리한다.
c. 실제 ApplicationContext는 구성 정보를 넘겨받아 구성 정보를 읽고 빈을 등록하고 DI 한다.
나는 구성 정보를 생략하고 initBeanStorage 메서드에서 수동으로 작성해 줬다.
c-1. 수동으로 작성하다 보니 빈 등록하는 코드는 있지만 DI(의존성 주입)해주는 코드가 없다.
실제로 스프링 컨테이너는 DI 할 때 빈이 중복으로 등록되지 않게 싱글톤 패턴을 적용한다.
1. getBean()
public class ApplicationContext {
private final Map<String, Object> beanStorage = new HashMap<>();
...
public <T> T getBean(String beanName, Class<T> requiredType) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
Object bean = beanStorage.get(beanName);
if (bean == null) {
bean = createBean(beanName);
registerStorage(beanName, bean);
}
if (!requiredType.isInstance(bean)) {
throw new IllegalArgumentException("this class is not available for bean");
}
return (T) bean;
}
private Object createBean(String beanName) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class clazz = Class.forName(beanName);
return clazz.getDeclaredConstructor().newInstance();
}
private void initBeanStorage() {
beanStorage.put("handler.UrlControllerHandlerMapping", new UrlControllerHandlerMapping());
beanStorage.put("handler.SimpleControllerHandlerAdapter", new SimpleControllerHandlerAdapter());
beanStorage.put("controller.HelloController", new HelloController());
beanStorage.put("controller.TimeController", new TimeController());
}
public void registerStorage(String name, Object obj) {
beanStorage.put(name, obj);
}
public HandlerMapping getHandlerMapping() throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return getBean("handler.UrlControllerHandlerMapping", HandlerMapping.class);
}
public HandlerAdapter getHandlerAdapter() throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return getBean("handler.SimpleControllerHandlerAdapter", HandlerAdapter.class);
}
}
a. 빈 관리 Map에서 beanName이 있으면 반환하고 없으면 해당 beanName의 인스턴스를 생성하고 빈 관리 Map에 등록해 준다
b. requiredType, 즉 클래스의 타입과 beanName으로 만든 인스턴스의 타입일 일치하지 않을 경우 에러 발생
getBean() 호출 예시
public class UrlControllerHandlerMapping implements HandlerMapping {
...
private Controller getHandleBean(String path) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return ApplicationContext.getApplicationContext().getBean(path, Controller.class);
}
}
a. 다음 글에서 다룰 내용이므로 간략하게 설명하자면,
어떤 URL로 요청이 들어오면 URL과 매핑된 핸들러 정보를 이용하여 적절한 컨트롤러를 반환한다.
여기서의 핸들러는 우리가 아는 그 @Controller다.
b. public <T> T getBean(String beanName, Class<T> requiredType) throws ...
실제 스프링 컨테이너는 빈의 이름, url, 클래스 이름 등 다양한 매핑 전략이 있다.
이 클래스의 매핑 전략에 따른 결과인 path, 반환 타입인 Controller.class를 getBean 메서드의 파라미터로 넘겨준다.
이번 포스트는 지속적으로 업데이트할 예정이다.
구현한 코드는 스프링 프레임워크 코드를 참고하였다.
'Project' 카테고리의 다른 글
Spring Boot에 Redis를 적용하여 인기 메뉴 순위 구현 (0) | 2023.08.06 |
---|---|
2. JAVA로 아주 간단한 WAS와 Spring MVC Framework 만들기 (0) | 2023.01.07 |
1. JAVA로 아주 간단한 WAS와 Spring MVC Framework 만들기 (0) | 2023.01.04 |
커피 주문 서비스를 객체 지향으로 설계해보기 with Java (0) | 2022.10.19 |
4. AWS + Spring Boot + React 프로젝트 근데 이제 배포 자동화를 곁들인 (0) | 2022.03.27 |