프로젝트 주제 : 웹 서버 구현하기
프로젝트 기간 : 3/21 ~ 4/1
개발 방식 : 페어 프로그래밍
이전 CS10 마지막 개인 미션인 HTTP 요청, 응답에서 작성했던 코드와 구조를 기반으로 페어인 @아더 와 미션을 수행해가며 프로젝트를 진행했다.
public class HttpRequest {
private static final String REQUEST_LINE_DELIMITER = " ";
private static final String QUERYSTRING_DELIMITER = "?";
private String method;
private String requestURI;
private String protocol;
private Parameters params;
private Headers headers;
private Map<String, String> cookies;
private final HttpSession session = HttpSession.getInstance();
public HttpRequest(String requestLine, Map<String, String> headers, String requestMessageBody) {
parseRequestLine(requestLine);
setHeadersAndCookies(headers);
parseRequestMessageBody(requestMessageBody);
}
private void parseRequestLine(String requestLine) {
String[] requestLineTokens = requestLine.split(REQUEST_LINE_DELIMITER);
this.method = requestLineTokens[0];
extractQuery(requestLineTokens[1]);
this.protocol = requestLineTokens[2];
}
private void extractQuery(String requestURI) {
int queryStringDelimiterIndex = requestURI.indexOf(QUERYSTRING_DELIMITER);
queryStringDelimiterIndex = queryStringDelimiterIndex != -1 ? queryStringDelimiterIndex : requestURI.length();
this.requestURI = requestURI.substring(0, queryStringDelimiterIndex);
if (queryStringDelimiterIndex != requestURI.length()) {
this.params = new Parameters(
HttpRequestUtils.parseQueryString(requestURI.substring(queryStringDelimiterIndex + 1))
);
}
}
private void setHeadersAndCookies(Map<String, String> headers) {
this.headers = new Headers(headers);
cookies = Strings.isNullOrEmpty(headers.get("Cookie")) ? Collections.emptyMap() : HttpRequestUtils.parseCookies(headers.get("Cookie"));
}
private void parseRequestMessageBody(String requestMessageBody) {
if (!Strings.isNullOrEmpty(requestMessageBody)) {
this.params = new Parameters(HttpRequestUtils.parseQueryString(requestMessageBody));
}
}
public String getMethod() {
return method;
}
public String getRequestURI() {
return requestURI;
}
public String getProtocol() {
return protocol;
}
public String getParameter(String name) {
return params.getValue(name);
}
public Map<String, String> getCookies() {
return cookies;
}
public HttpSession getSession() {
return session;
}
}
HttpRequest 클래스는 클라이언트가 전송한 Http 요청 메시지에 담긴 텍스트 정보들을 하나의 객체로 관리할 수 있도록 설계된 클래스이다. 아래와 같이 3개의 메서드로 역할을 분리하여 인스턴스를 생성하도록 했다.
method
, request url
, protocol
로 분리한다.request url
에서 extractQuery 메서드로 query string
을 추출한다.header
값들 저장하고, cookie
값이 존재한다면 별도로 저장하여 관리한다.request body
값이 존재한다면 파싱하여 저장한다.public class HttpResponse {
private final String protocol;
private HttpStatus status;
private final Headers headers = new Headers();
private final Cookies cookies = new Cookies();
private byte[] body = "".getBytes();
public HttpResponse(String protocol) {
this.protocol = protocol;
}
public HttpStatus getStatus() {
return status;
}
public void setHeader(String name, String value) {
headers.setHeader(name, value);
}
public void addCookie(Cookie cookie) {
cookies.add(cookie);
}
public void setStatus(HttpStatus status) {
this.status = status;
}
public void setBody(byte[] body) {
this.body = body;
}
public byte[] toByteArray() {
String httpRequestMessage = protocol + " " + status + System.lineSeparator() +
headers.getHeaderMessage() + System.lineSeparator() +
cookies.toSetCookieMessage() + Strings.repeat(System.lineSeparator(), 2) +
new String(Arrays.copyOf(body, body.length));
return httpRequestMessage.getBytes(StandardCharsets.UTF_8);
}
}
toByteArray
메서드를 사용하여 형식에 맞게 Response Message
를 구성하고 해당 문자열을 byte[]
로 변환하여 반환한다. HttpResponse 클래스에게 인스턴스 필드들로 메시지를 구조화하는 책임을 주는 것이다. 이에 따라 뒤에서 설명할 Handler 가 OutputStream
을 사용하여 클라이언트에게 응답할 때 HttpResponse 인스턴스에 대해 별도로 조작할 필요가 없어진다. setHeader, addCookie, setStatus, setBody 메서드로 Header, Status 등 응답 객체의 필드 값들을 외부에서 유연하게 변경할 수 있도록 해줬다.
@FunctionalInterface
public interface HandlerMethod {
HttpResponse service(HttpRequest request) throws IOException;
}
HandlerMethod 라는 함수형 인터페이스
에 HttpRequest 를 인자로 받고, HttpResponse 를 반환하는 메서드를 선언한다. 해당 함수형 인터페이스는 현재 웹 서버에서 스프링 프레임워크를 적용한 웹 어플리케이션처럼 특정 Url 에 매핑된 Controller 메서드의 역할을 담당할 객체가 필요했기 때문에 작성했다.
public class HandlerMethodMapper {
private static final Logger logger = LoggerFactory.getLogger(HandlerMethodMapper.class);
private static final Map<String, HandlerMethod> mapper = new HashMap<>();
private static final HandlerMethod resourceHandlerMethod = (request) -> {
HttpResponse response = new HttpResponse();
response.setStatusLine(request.getProtocol(), HttpStatus.OK);
FileReader resources = new FileReader("src/main/resources/env.properties");
Properties properties = new Properties();
properties.load(resources);
byte[] body = Files.readAllBytes(new File(properties.getProperty("webapp_path") + request.getRequestURI()).toPath());
response.setHeader("Content-Length", Integer.toString(body.length));
response.setBody(body);
return response;
};
static {
mapper.put("/user/create", (request) -> {
User user = new User(request.getParameter("userId"), request.getParameter("password"),
request.getParameter("name"), request.getParameter("email"));
DataBase.addUser(user);
logger.debug("user: {}", DataBase.findAll());
HttpResponse response = new HttpResponse();
response.setStatusLine(request.getProtocol(), HttpStatus.OK);
return response;
});
}
public static HandlerMethod getHandlerMethod(String uri) {
return mapper.getOrDefault(uri, resourceHandlerMethod);
}
}
HandlerMethodMapper 라는 일급객체에 '.html', '.css' 와 같은 정적 리소스
들을 반환해주는 HandlerMethod (resourceHandlerMethod
) 의 동작을 정의하여 할당해둔다. getHandlerMethod 메서드의 인자로 요청 uri 를 주어 mapper 에서 해당 uri 에 매핑된 HandlerMethod 를 찾아 반환하여 아래의 RequestHandler 에서 해당 메서드를 수행하도록 로직을 구성했다.
물론 이러한 방식은 핸들러를 새로 추가
하거나 삭제
시 외부에서 변경이 불가능하다. 따라서, 뒤에서 언급할 자체 WebServerContext, WebConfiguer 클래스들을 생성하여 전체적인 구조를 개선함과 동시에 HandlerMethodMapper (Version 2.0) 에서 static 블록을 제거했다.
public class RequestHandler extends Thread {
private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
private Socket connection;
public RequestHandler(Socket connectionSocket) {
this.connection = connectionSocket;
}
/**
* 1. `receiveRequest` 메서드를 사용하여 클라이언트가 보낸 데이터 스트림을 읽어 요청 객체로 변환한다.
* 2. `HandlerMethodMapper` 로 요청 URI 값으로 매핑된 `HandlerMethod` 를 찾아 해당 요청을 수행하도록 한다.
* 3. `HandlerMethod` 가 반환한 결과 값을 받아 클라이언트에게 응답한다.
*
*/
public void run() {
logger.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
HttpRequest request = receiveRequest(in);
HandlerMethod handlerMethod = HandlerMethodMapper.getHandlerMethod(request.getRequestURI());
HttpResponse response = handlerMethod.service(request);
sendResponse(out, response);
} catch (IOException e) {
logger.error(e.getMessage());
}
}
/**
* 클라이언트가 보낸 데이터 스트림을 `RequestLine`, `RequestHeaders`, (+ RequestMessageBody) 로 구분 지어 읽어들인다.
* 읽어들인 메시지들을 사용하여 HttpRequest 객체를 생성하고 이를 반환한다.
*
* @param in
* @return `InputStream` 에서 읽어온 데이터로 HttpRequest 객체를 생성하여 반환한다.
* @throws IOException
*/
private HttpRequest receiveRequest(InputStream in) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line = URLDecoder.decode(br.readLine(), StandardCharsets.UTF_8);
String requestLine = line;
logger.debug("request line : {}", line);
List<HttpRequestUtils.Pair> headers = new ArrayList<>();
while (!Strings.isNullOrEmpty(line)) {
line = URLDecoder.decode(br.readLine(), StandardCharsets.UTF_8);
HttpRequestUtils.Pair pair = HttpRequestUtils.parseHeader(line);
headers.add(pair);
logger.debug("header : {}", line);
}
return new HttpRequest(requestLine, headers);
}
/**
* 매칭시킨 `HandlerMethod` 가 반환한 결과 값을 OutputStream 을 통해 클라이언트에게 응답한다.
*
* @param out
* @param response
*/
private void sendResponse(OutputStream out, HttpResponse response) {
DataOutputStream dos = new DataOutputStream(out);
try {
dos.write(response.toByteArray());
dos.flush();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
작성해놓은 javaDocs
주석에 RequestHandler 의 동작과정이 다 설명되어있다. receiveRequest, sendResponse 와 같이 요청마다 공통적으로 사용되는 메서드를 Util 클래스로 따로 분리할까 고민하던 중, 리뷰어님의 리뷰 에서 영감을 받아,
public class WebServer {
private static final Logger log = LoggerFactory.getLogger(WebServer.class);
private static final int DEFAULT_PORT = 8080;
public static void main(String args[]) throws Exception {
int port = 0;
if (args == null || args.length == 0) {
port = DEFAULT_PORT;
} else {
port = Integer.parseInt(args[0]);
}
try (ServerSocket listenSocket = new ServerSocket(port)) {
log.info("Web Application Server started {} port.", port);
Socket connection;
while ((connection = listenSocket.accept()) != null) {
RequestHandler requestHandler = new RequestHandler(connection);
requestHandler.start();
}
}
}
}
이렇게 웹 서버 클래스가 클라이언트로부터 요청이 올 때마다 Thread 를 상속받은 RequestHandler 를 생성하여 처리하는 구조에서,
public class WebServer {
private static final Logger log = LoggerFactory.getLogger(WebServer.class);
private static final int DEFAULT_PORT = 8080;
private static final FrontHandler frontHandler = FrontHandler.getInstance();
public static void main(String[] args) throws Exception {
int port = 0;
if (args == null || args.length == 0) {
port = DEFAULT_PORT;
} else {
port = Integer.parseInt(args[0]);
}
WebServerConfig.getInstance();
try (ServerSocket listenSocket = new ServerSocket(port)) {
log.info("Web Application Server started {} port.", port);
Socket connection;
while ((connection = listenSocket.accept()) != null) {
frontHandler.assign(connection);
}
}
}
}
스프링 프레임워크의 DispatcherServlet 역할을 하는 FrontHandler 가 모든 요청을 받고, 각 요청들은 java.util.concurrent
패키지 내의 ExecutorService
을 사용하여 스레드를 할당받아 처리되도록 구현했다. java.util.concurrent.ExecutorService
의 사용으로 인해 다음과 같은 장점을 얻을 수 있다.
- 먼저 스레드를
미리 생성
해두고 요청이 올 때마다할당
하는 방식이기 때문에 스레드를 생성하고 종료하는 비용이 들지 않아 응답 속도가 빠르다. (+ 컨텍스트 스위칭 비용을 최소화할 수 있다.)
- 또한 스레드 풀에 스레드의 최대 개수를 이미 지정해 놓은 상태이기 때문에 아무리 많은 요청이 오더라도 서버가 크래쉬될 정도로 CPU 와 메모리를 사용하지 않을 수 있다. 다시 말해 최대 스레드를 넘어서는 요청이 들어오면
거절
하거나대기
하도록 설정할 수 있어 안전 하게 서비스를 운영 할 수 있다.
- 개발자 관점에서는 스레드 관련 로직은 concurrent 패키지의 ExecutorService 가 처리해주고 테스크 관련 로직만 신경써도 되기 때문에
생산성이 증가
한다.
public class FrontHandler {
private static final Logger logger = LoggerFactory.getLogger(FrontHandler.class);
private static volatile FrontHandler frontHandler;
private final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private final Handler handler = Handler.getInstance();
private FrontHandler() {
}
public static FrontHandler getInstance() {
if (frontHandler == null) {
synchronized (FrontHandler.class) {
if (frontHandler == null) {
frontHandler = new FrontHandler();
}
}
}
return frontHandler;
}
public void assign(Socket connection) {
CompletableFuture.runAsync(() -> process(connection), executor);
}
private void process(Socket connection) {
try (InputStream in = connection.getInputStream();
OutputStream out = connection.getOutputStream()) {
sendResponse(out, handler.service(receiveRequest(in)));
} catch (IOException e) {
logger.error(e.getMessage());
}
}
/**
* 클라이언트가 보낸 데이터 스트림을 'RequestLine', 'RequestHeaders', (+ RequestMessageBody) 로 구분 지어 읽어들인다.
* 읽어들인 메시지들을 사용하여 HttpRequest 객체를 생성하고 이를 반환한다.
*
* @param in
* @return 'InputStream' 에서 읽어온 데이터로 HttpRequest 객체를 생성하여 반환한다.
* @throws IOException
*/
public HttpRequest receiveRequest(InputStream in) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line = URLDecoder.decode(br.readLine(), StandardCharsets.UTF_8);
String requestLine = line;
logger.debug("request line : {}", line);
Map<String, String> headers = new HashMap<>();
while (true) {
line = URLDecoder.decode(br.readLine(), StandardCharsets.UTF_8);
if (Strings.isNullOrEmpty(line)) {
break;
}
Pair<String, String> pair = HttpRequestUtils.parseHeader(line);
headers.put(pair.getKey(), pair.getValue());
logger.debug("header : {}", line);
}
String requestMessageBody = URLDecoder.decode(IOUtils.readData(br, getContentLength(headers)), StandardCharsets.UTF_8);
return new HttpRequest(requestLine, headers, requestMessageBody);
}
private int getContentLength(Map<String, String> headers) {
return Integer.parseInt(Optional.ofNullable(headers.get("Content-Length")).orElse(String.valueOf(0)));
}
/**
* 매칭시킨 'HandlerMethod' 가 반환한 결과 값을 OutputStream 을 통해 클라이언트에게 응답한다.
*
* @param out
* @param response
*/
private void sendResponse(OutputStream out, HttpResponse response) {
DataOutputStream dos = new DataOutputStream(out);
try {
dos.write(response.toByteArray());
dos.flush();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
이러한 구조 변경으로 인해 receiveRequest
, sendResponse
메서드를 모든 요청을 받아서 처리하는 FrontHandler 클래스로 이동시킬 수 있었다.
public class Handler {
private static volatile Handler handler;
private final HandlerMethodMapper handlerMethodMapper = HandlerMethodMapper.getInstance();
private Handler() {
}
public static Handler getInstance() {
if (handler == null) {
synchronized (Handler.class) {
if (handler == null) {
handler = new Handler();
}
}
}
return handler;
}
/**
* 1. 'HandlerMethodMapper' 로 요청 URI 값으로 매핑된 'HandlerMethod' 를 찾아 해당 요청을 수행하도록 한다.
* 2. 'HandlerMethod' 가 반환한 결과 값을 받아 FrontHandler 에게 전달한다.
*/
public HttpResponse service(HttpRequest request) throws IOException {
HandlerMethod handlerMethod = handlerMethodMapper.getHandlerMethod(
new Pair<>(
request.getMethod(),
request.getRequestURI()
)
);
return handlerMethod.service(request);
}
}
작성 당시 요청 url 이 /user/*
뿐이 없었기에 HandlerMapper 를 다음 단계에서 작성하였고, 모든 요청이 /user
로 시작한다는 전제 하에 위와 같은 Handler 클래스만을 만들어두고, FrontHandler 에서 이를 사용하였다. (+ 마지막 제출 단계에선 ResourceHandler 의 역할과 구분하기 위해 UserHandler 로 변경되었다.)
클라이언트로부터 Http 요청이 오면 WebServer -> FrontHandler -> Handler -> HandlerMethod
순서로 전달되고 각 클래스들은
DispatcherServlet
Controller
Controller Method
위와 같은 스프링 프레임워크 계층들의 역할을 참고하며 구현했다. 지금까지의 코드에서는 Handler 가 하나였지만, 마지막 리뷰를 받을 당시의 코드는 HandlerMapper 클래스를 별도로 생성하고, Handler RequestMapping 와 HandlerMethod RequestMapping 를 분리 하여 Controller 클래스 레벨에서 @RequestMapping
설정을 해준 것과 같은 기능을 구현해봤다.
public class HandlerMapper {
private static volatile HandlerMapper handlerMapper;
private final HandlerRegistry handlerRegistry = HandlerRegistry.getInstance();
private final Map<String, Handler> mapper = new HashMap<>();
private HandlerMapper() {
}
public static HandlerMapper getInstance() {
if (handlerMapper == null) {
synchronized (HandlerMapper.class) {
if (handlerMapper == null) {
handlerMapper = new HandlerMapper();
}
}
}
return handlerMapper;
}
public Handler getHandler(String url) {
return mapper.get(url);
}
public void mappingHandler(String url, int handlerIndex) {
mapper.put(url, handlerRegistry.getHandler(handlerIndex));
}
}
HandlerMethodMapper (Version 1.0) 에선 아래와 같은 static 블록으로 직접 요청 Url 과 HandlerMethod 를 매핑해주었다.
static {
mapper.put("/user/create", (request) -> {
User user = new User(request.getParameter("userId"), request.getParameter("password"),
request.getParameter("name"), request.getParameter("email"));
DataBase.addUser(user);
logger.debug("user: {}", DataBase.findAll());
HttpResponse response = new HttpResponse();
response.setStatusLine(request.getProtocol(), HttpStatus.OK);
return response;
});
}
public class HandlerMethodMapper {
private static volatile HandlerMethodMapper handlerMethodMapper;
private final HandlerMethodRegistry handlerMethodRegistry = HandlerMethodRegistry.getInstance();
private final Map<Pair<String, String>, HandlerMethod> mapper = new HashMap<>();
private HandlerMethodMapper() {
}
public static HandlerMethodMapper getInstance() {
if (handlerMethodMapper == null) {
synchronized (HandlerMethodMapper.class) {
if (handlerMethodMapper == null) {
handlerMethodMapper = new HandlerMethodMapper();
}
}
}
return handlerMethodMapper;
}
public HandlerMethod getHandlerMethod(Pair<String, String> pair) {
return mapper.getOrDefault(pair, handlerMethodRegistry.getHandlerMethod(0));
}
public void mappingHandlerMethod(Pair<String, String> pair, int handlerMethodIndex) {
mapper.put(pair, handlerMethodRegistry.getHandlerMethod(handlerMethodIndex));
}
}
하지만 위의 코드에서는 해당 블록이 사라지고 mappingHandlerMethod 메서드를 호출하는 쪽
에서 요청 Url 과 HandlerMethod 를 매핑하도록 구조를 개선했다.
public class HandlerMethodRegistry {
private static volatile HandlerMethodRegistry handlerMethodRegistry;
private final List<HandlerMethod> registry = new ArrayList<>();
private HandlerMethodRegistry() {
}
public static HandlerMethodRegistry getInstance() {
if (handlerMethodRegistry == null) {
synchronized (HandlerMethodRegistry.class) {
if (handlerMethodRegistry == null) {
handlerMethodRegistry = new HandlerMethodRegistry();
}
}
}
return handlerMethodRegistry;
}
public void addHandlerMethod(HandlerMethod handlerMethod) {
registry.add(handlerMethod);
}
public HandlerMethod getHandlerMethod(int index) {
return registry.get(index);
}
}
조금 더 정확히는 요청 Url 을 HandlerMethodRegistry 에 등록된 HandlerMethod
중 하나와 매핑시킬 수 있도록 구현했다. 이러한 방식으로 변경할 수 있게 된 것은 아래의 클래스들 덕분이다.
public class BeanFactory {
public Object getBean(String beanName) throws NoSuchFieldException, ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
Class<?> context = Class.forName(this.getClass().getTypeName());
Field field = context.getDeclaredField(beanName);
Class<?> clazz = Class.forName(field.getType().getTypeName());
Method method = clazz.getDeclaredMethod("getInstance");
return method.invoke(null);
}
}
짧지만 가장 정성들여서 작성해본 클래스다. 스프링 프레임워크의 빈을 생성하고 관리하는 BeanFactory
를 어떻게하면 구현해볼 수 있을까? 구현 방식에 대해 고민을 하다가 현재 프로젝트에서 FrontHandler, ~~Mapper 등과 같이 하나의 기능을 담당하는 클래스들은 모두 싱글톤
으로 구현했고, 각 클래스마다 하나씩만 생성된 인스턴스들은 정적 메서드
인 getInstance
로 반환받아 사용할 수 있다는 점에 착안했다.
public class WebServerContext extends BeanFactory {
final FrontHandler frontHandler;
/*
* Registry
*/
final HandlerRegistry handlerRegistry;
final HandlerMethodMapperRegistry handlerMethodMapperRegistry;
/*
* Mapper
*/
final HandlerMapper handlerMapper;
/*
* Handler
*/
final ResourceHandler resourceHandler;
final UserHandler userHandler;
public WebServerContext() {
super();
frontHandler = FrontHandler.getInstance();
handlerRegistry = HandlerRegistry.getInstance();
handlerMethodMapperRegistry = HandlerMethodMapperRegistry.getInstance();
handlerMapper = HandlerMapper.getInstance();
resourceHandler = ResourceHandler.getInstance();
userHandler = UserHandler.getInstance();
}
}
스프링 컨텍스트
를 표현하고자 작성한 WebServerContext 클래스다. BeanFactory
를 상속받은 해당 클래스는 지금까지 작성해놓은 모든 싱글톤 패턴의 클래스들을 getInstance 메서드로 초기화하고, 각 클래스 타입의 필드가 참조하도록 값을 할당해준다. 참조 값이 할당되어 있는 각각의 멤버 필드 명이 빈 이름
이 되는 느낌을 주었다. 이에 따라 상속받은 BeanFactory 의 getBean
메서드로 마치 Context
에 등록된 빈을 가져오는 효과를 주었다.
public interface WebServerConfigurer {
/**
* 사용할 'Handler' 를 'HandlerRegistry' 에 등록합니다. 현재는 UserHandler 만 등록하고 있습니다.
* HandlerRegistry 에 등록된 순서에 따라 Handler 의 index 값이 결정됩니다.
*
* @param handlerRegistry
* @throws Exception
*
*/
default void addHandler(HandlerRegistry handlerRegistry) throws Exception {
}
/**
* HandlerMapper 에 'URL' 과 HandlerRegistry 에 저장된 Handler 의 'index' 값을 매핑합니다.
* ex. UserHandler 는 UserHandler 는 '/users' 과 매핑되며 HandlerRegistry 에 0 번째 index 에 저장되어있으므로 - handlerMapper.mappingHandler("/user", 0);
*
* @param handlerMapper
*
*/
default void configureHandlerMapper(HandlerMapper handlerMapper) {
}
/**
* Handler 와 연동될 'HandlerMethodMapper' 를 HandlerMethodMapperRegistry 등록합니다.
* Handler 와 마찬가지로 등록된 순서에 따라 index 값이 결정되므로 이 값을 사용하여 Handler 와 HandlerMethodMapper 를 바인딩할 수 있습니다.
*
* HandlerMethodMapper 는 특정 Handler 가 담당할 메서드들의 집합입니다.
* HandlerMethod 는 함수형 인터페이스로 구현되어있으며, 각자 매핑된 'HttpMethod' 와 'URL' 이 존재합니다.
* UserHandler 에 바인딩된 HandlerMethodMapper 에는 GET /create, POST /create, POST /login, GET /logout 으로 매핑된 HandlerMethod 가 존재합니다.
*
* @param handlerMethodMapperRegistry
*
*/
default void addHandlerMethodMapper(HandlerMethodMapperRegistry handlerMethodMapperRegistry) {
}
/**
* HandlerRegistry 에 등록된 Handler 를 모두 순회하며 index 값으로 HandlerMethodMapperRegistry 에서 각자의 HandlerMethodMapper 를 찾아 바인딩합니다. (setter 주입 방식 느낌)
* ResourceHandler 를 제외한 모든 Handler 는 Handler 인터페이스를 구현하고 있으며 HandlerMethod 로 동작을 결정합니다.
*
* @param handlerRegistry
* @param handlerMethodMapperRegistry
*
*/
default void bindMethodsToHandler(HandlerRegistry handlerRegistry, HandlerMethodMapperRegistry handlerMethodMapperRegistry) {
}
}
마지막으로 스프링의 WebMvcConfigurer 인터페이스의 형태를 흉내내보았다. 실제 WebMvcConfigurer 인터페이스에는 선언되어 있지 않지만, 현재 프로젝트에서 필요한 기능들을 구성하기 위해 임의로 작성해봤다.
public class WebServerConfig implements WebServerConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebServerConfig.class);
private static volatile WebServerConfig webServerConfig;
final WebServerContext webServerContext = new WebServerContext();
private WebServerConfig() throws Exception {
logger.debug("WebServerConfig() start");
addHandler((HandlerRegistry) webServerContext.getBean("handlerRegistry"));
configureHandlerMapper((HandlerMapper) webServerContext.getBean("handlerMapper"));
addHandlerMethodMapper((HandlerMethodMapperRegistry) webServerContext.getBean("handlerMethodMapperRegistry"));
bindMethodsToHandler(
(HandlerRegistry) webServerContext.getBean("handlerRegistry"),
(HandlerMethodMapperRegistry) webServerContext.getBean("handlerMethodMapperRegistry")
);
logger.debug("WebServerConfig() end");
}
public static WebServerConfig getInstance() throws Exception {
if (webServerConfig == null) {
synchronized (WebServerConfig.class) {
if (webServerConfig == null) {
webServerConfig = new WebServerConfig();
}
}
}
return webServerConfig;
}
@Override
public void addHandler(HandlerRegistry handlerRegistry) throws Exception {
logger.debug("addHandlerMethod() start");
handlerRegistry.addHandler((UserHandler) webServerContext.getBean("userHandler"));
logger.debug("addHandlerMethod() end");
}
@Override
public void configureHandlerMapper(HandlerMapper handlerMapper) {
logger.debug("configureHandlerMethod() start");
handlerMapper.mappingHandler("/user", 0);
logger.debug("configureHandlerMethod() start");
}
@Override
public void addHandlerMethodMapper(HandlerMethodMapperRegistry handlerMethodMapperRegistry) {
logger.debug("addHandlerMethodMapper() start");
handlerMethodMapperRegistry.addHandlerMethod(
new HandlerMethodMapper(
Map.of(
new Pair<>(HttpMethod.GET.name(), "/create"),
(request) -> {
User user = new User(
request.getParameter("userId"),
request.getParameter("password"),
request.getParameter("name"),
request.getParameter("email")
);
DataBase.addUser(user);
logger.debug("user: {}", DataBase.findAll());
HttpResponse response = new HttpResponse(request.getProtocol());
response.setStatus(HttpStatus.CREATED);
return response;
},
new Pair<>(HttpMethod.POST.name(), "/create"),
(request) -> {
if (DataBase.findUserById(request.getParameter("userId")) != null) {
HttpResponse response = new HttpResponse(request.getProtocol());
response.setHeader("Location", "/user/form.html");
response.setStatus(HttpStatus.FOUND);
return response;
}
User user = new User(
request.getParameter("userId"),
request.getParameter("password"),
request.getParameter("name"),
request.getParameter("email")
);
DataBase.addUser(user);
logger.debug("user: {}", DataBase.findAll());
HttpResponse response = new HttpResponse(request.getProtocol());
response.setHeader("Location", "/index.html");
response.setStatus(HttpStatus.FOUND);
return response;
},
new Pair<>(HttpMethod.POST.name(), "/login"),
(request) -> {
logger.debug("cookies: {}", request.getCookies());
User user = DataBase.findUserById(request.getParameter("userId"));
logger.debug("user: {}", user);
HttpResponse response = new HttpResponse(request.getProtocol());
if (!user.getPassword().equals(request.getParameter("password"))) {
response.setHeader("Location", "/user/login_failed.html");
response.setStatus(HttpStatus.FOUND);
return response;
}
HttpSession session = request.getSession();
session.setAttribute("sessionId", UUID.randomUUID().toString());
Cookie cookie = new Cookie("sessionId", (String) session.getAttribute("sessionId"));
cookie.setPath("/");
response.addCookie(cookie);
response.setHeader("Location", "/index.html");
response.setStatus(HttpStatus.FOUND);
return response;
},
new Pair<>(HttpMethod.GET.name(), "/logout"),
(request) -> {
logger.debug("cookies: {}", request.getCookies());
HttpSession session = request.getSession();
Cookie cookie = new Cookie("sessionId", (String) session.getAttribute("sessionId"));
cookie.setMaxAge(0);
cookie.setPath("/");
HttpResponse response = new HttpResponse(request.getProtocol());
response.addCookie(cookie);
response.setHeader("Location", "/index.html");
response.setStatus(HttpStatus.FOUND);
session.removeAttribute("sessionId");
logger.debug("cookies: {}", request.getCookies());
return response;
}
)
)
);
logger.debug("addHandlerMethodMapper() end");
}
@Override
public void bindMethodsToHandler(HandlerRegistry handlerRegistry,
HandlerMethodMapperRegistry handlerMethodMapperRegistry) {
int handlerCount = handlerRegistry.size();
for (int index = 0; index < handlerCount; ++index) {
Handler handler = handlerRegistry.getHandler(index);
HandlerMethodMapper handlerMethodMapper = handlerMethodMapperRegistry.getHandlerMethod(index);
handler.bindHandlerMethodMapper(handlerMethodMapper);
}
}
}
WebServerConfigurer
인터페이스의 메서드로 WebServerContext
에 등록된 빈들을 사용하여 웹 서버의 전체적인 환경을 구성할 수 있게 되었다. addHandlerMethodMapper 메서드에서 직접 람다식으로 HandlerMethod 를 등록하는 방식은 여전히 변경되어야할 사항이다.
잘 읽고 갑니다 선생님;; 역시👍🏻