의존성 없이 Java만을 사용해서 Configurable한 WAS를 만들어보았습니다.
완성된 코드 미리보기
시작하기에 앞서 완성된 코드를 먼저 확인하고 가겠습니다!
말보다는 코드로
Main.java
public class Main {
public static void main(String[] args) throws IOException {
Server server = new Server(new ServerConfiguration());
server.start();
}
}
- Configuration class를 기반으로 서버를 시작합니다
ServerConfiguration.java
public class ServerConfiguration extends Configuration {
@Override
protected void addRequestHandlers(Adder<RequestHandler> requestHandlerAdder) {
requestHandlerAdder.add(new StaticResourceHandler());
requestHandlerAdder.add(new IndexPageHandler());
}
@Override
protected void addRouterFunctions(PairAdder<EndPoint, RouterFunction> routerFunctionAdder) {
routerFunctionAdder.add(EndPoint.of(HttpMethod.GET, "/luizy"), (request, response) -> "Hello, Luizy!");
}
@Override
protected void addRouters(Adder<Router> routerAdder) {
routerAdder.add(new UsersRouter());
}
}
- Configuration class는 서버에서 사용할
Handler
,RouterFunction
,Router
들을 추가합니다. StaticResourceHandler
는resources/static
아래의 파일들을 서빙합니다.IndexPageHandler
는 경로 아래에index.html
파일이 있으면 서빙합니다.
UsersRouter.java
public class UsersRouter extends Router {
@Override
protected String setBasePath() {
return "/users";
}
@Override
protected void addRouterFunctions(PairAdder<EndPoint, RouterFunction> routerFunctionAdder) {
routerFunctionAdder.add(getUsersEndPoint, this::getUsers);
routerFunctionAdder.add(getUserEndPoint, this::getUser);
routerFunctionAdder.add(createUserEndPoint, this::createUser);
}
private final EndPoint getUsersEndPoint = EndPoint.of(HttpMethod.GET);
private List<User> getUsers(HttpRequest request, HttpResponse response) {
// get all users
}
private final EndPoint getUserEndPoint = EndPoint.of(HttpMethod.GET, "/{id}");
private User getUser(HttpRequest request, HttpResponse response) {
// get userId from path parameter
}
private final EndPoint createUserEndPoint = EndPoint.of(HttpMethod.POST);
private Object createUser(HttpRequest request, HttpResponse response) {
// create user
}
}
UsersRouter
class는 요청을 처리하기 위한메서드
를 구합니다base path
랑EndPoint
에 메서드를 매핑합니다.
이렇게 완성된 서버는 아래와 같은 api를 응답할 수 있게 됩니다.
- Base URL
http://localhost:8080
- Endpoints
- Create User
- URL:
/users
- Method:
POST
- Content-Type:
application/x-www-form-urlencoded
- Request Body:
userId=string& password=string& name=string
- Response:
- Success:
- Code:
201 Created
- Content:
{ "userId": "string", "name": "string", "password": "string" }
- URL:
- Get All Users
- URL:
/users
- Method:
GET
- Response:
- Success:
- Code:
200 OK
- Content:
[ { "userId": "string", "name": "string", "password": "string" } ]
- URL:
- Get a Single User
- URL:
/users/{id}
- Method:
GET
- Parameters:
- Path Parameter:
id
(number) - 조회할 유저의 ID
- Path Parameter:
- Response:
- Success:
- Code:
200 OK
- Content:
{ "userId": "string", "name": "string", "password": "string" }
- URL:
- Create User
조금 긴 프리뷰였지만, 지금부터는 소켓 통신을 시작하는 부분부터 보겠습니다.
멀티 스레드로 소켓통신
- 멀티 스레드를 사용하기 위해서
ExcuterService
를 사용했습니다.
Server.java
public class Server {
private final Configuration configuration;
private final ExecutorService executorService;
private final int port;
public Server(Configuration configuration) {
this.configuration = configuration;
this.executorService = Executors.newFixedThreadPool(configuration.getThreadCount());
this.port = configuration.getPort();
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
Socket clientSocket = serverSocket.accept();
executorService.execute(new ClientHandler(clientSocket, configuration.getRequestHandlers()));
}
}
}
Configuration
class의setThreadCount()
메서드를 통해서 스레드의 개수를 정할 수 있습니다.- default로 10개로 설정되어 있습니다.
Configuration
class의setPort()
메서드를 통해서 포트를 설정할 수 있습니다.- main thread에서
serverSocket.accept()
하고 Socket을 생성합니다. - 생성된 Socket으로
Runnable
인터페이스가 구현된ClientHandler
생성합니다. ExcuterService
를 통해서 멀티 스레드로 요청처리를 합니다.
요청 처리
RequestHandler
인터페이스를 구현한 클래스를 만들어서 요청을 처리합니다.
ClientHandler.java
public class ClientHandler implements Runnable {
private final Socket clientSocket;
private final List<RequestHandler> requestHandlers;
public ClientHandler(Socket clientSocket, List<RequestHandler> requestHandlers) {
this.clientSocket = clientSocket;
this.requestHandlers = requestHandlers;
}
@Override
public void run() {
try (InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream()) {
HttpRequest httpRequest = HttpRequest.pharse(in);
RequestHandler requestHandler = getHandler(httpRequest);
HttpResponse httpResponse = requestHandler.handle(httpRequest);
httpResponse.write(out);
} catch (Exception e) {}
}
private RequestHandler getHandler(HttpRequest request) {
for (RequestHandler requestHandler : requestHandlers) {
if (requestHandler.canHandle(request.endPoint())) {
return requestHandler;
}
}
return NoHandler.getInstance();
}
}
- InputStream을 통해서
HttpRequest
객체로 파싱합니다. RequestHandler
List를 순회하며 처리 가능한handler
에서 요청 처리합니다.- List 순서에 따라 요청 처리 우선순위를 지정할 수 있습니다.
HttpResponse
객체를 생성하고OutputStream
을 통해서 응답을 보냅니다.
RequestHandler.java
public interface RequestHandler {
boolean canHandle(EndPoint endPoint);
HttpResponse handle(HttpRequest request) throws Exception;
}
HTTP Request
- rfc 규격에 맞게 HTTP 요청을 파싱할 수 있도록 합니다.
- request는 한번 들어오면 변하지 않기 때문에
HttpRequest
를 record class로immutable
하게 구현했습니다.
HttpRequest.java
public record HttpRequest(
HttpMethod method,
URI uri,
Map<String, String> headers,
String body,
Map<String, List<String>> queryParams,
EndPoint endPoint
) { }
Router
- HTTP 요청을 처리하는건, 어떤 리소스에 어떤 메서드로 요청을 했냐에 따라 요청 처리가 달라진다고 생각합니다.
- 따라서, Http Method와 리소스를 나타내는
EndPoint
객체를 추가합니다.
EndPoint.java
public record EndPoint(HttpMethod method, String path) {}
public record EndPoint(HttpMethod method, String path) {}
- 요청을 처리하기 위한 함수
RouterFunction
을 지정합니다.
RouterFunction.java
@FunctionalInterface
public interface RouterFunction {
/**
* Route the request and return the response.
* @param request: request to be routed
* @param response: response to be modified (ex. setHeader, setStatus)
* @return convertable response body
*/
Object route(HttpRequest request, HttpResponse response);
}
EndPoint
를 키로 하는 Router Map을 구현합니다.
RouterHandler.java
public class RouterHandler implements RequestHandler {
private final Map<EndPoint, RouterFunction> routerMap;
public RouterHandler(Map<EndPoint, RouterFunction> routerMap) {
this.routerMap = routerMap;
}
@Override
public boolean canHandle(EndPoint endPoint) {
return routerMap.containsKey(endPoint);
}
@Override
public HttpResponse handle(HttpRequest request) throws Exception {
RouterFunction routerFunction = routerMap.get(request.endPoint());
HttpResponse response = HttpResponse.create();
Object body = routerFunction.route(request, response);
response.setBody(convertBody(body));
return response;
}
private byte[] convertBody(Object body) throws Exception {
// body 타입에 따라 적절한 변환을 해준다.
}
}
Configurable 한 Router 만들기
- Router를 만들기 위해서는
Router
class를 상속받아서 구현합니다. - 이때, 상속받은 Router class에서는 Rouuter 동작을 수행할 RouterFunction을 만듭니다.
- 만든 RouterFunction은
addRouterFunctions()
메서드를 상속받아 매핑 정보를 추가하기만 하면 됩니다.
UserRouter.java
public class UsersRouter extends Router {
private final UserDao userDao;
public UsersRouter(UserDao userDao) {this.userDao = userDao;}
@Override
protected String setBasePath() {
return "/users";
}
@Override
protected void addRouterFunctions(PairAdder<EndPoint, RouterFunction> routerFunctionAdder) {
routerFunctionAdder.add(getUsersEndPoint, this::getUsers);
}
private final EndPoint getUsersEndPoint = EndPoint.of(HttpMethod.GET, "/users");
private List<User> getUsers(HttpRequest request, HttpResponse response) {
return userDao.findAll();
}
}
addRouterFunctions()
메서드의 매개변수PairAdder
는 Functional Interface로 다음과 같이 정의되어 있습니다.
PairAdder.java
@FunctionalInterface
public interface PairAdder<K, V> {
void add(K k, V v);
}
add()
메서드를 하나만 구현하게 되어 있고, 이에대한 구현체는 상위Router
class에서 구현되어 있습니다.
Router.java
public abstract class Router {
private final String basePath;
private Map<EndPoint, RouterFunction> routerFunctionMap = new HashMap<>();
public Router() {
basePath = setBasePath();
addRouterFunctions(this::routerFunctionAdder);
}
protected String setBasePath() {return "";}
public final String getBasePath() {
return basePath;
}
protected void addRouterFunctions(PairAdder<EndPoint, RouterFunction> routerFunctionAdder) {}
private void routerFunctionAdder(EndPoint endPoint, RouterFunction routerFunction) {
EndPoint endPointWithBasePath = EndPoint.of(endPoint.method(), basePath + endPoint.path());
routerFunctionMap.put(endPointWithBasePath, routerFunction);
}
}
- 객체가 생성될때
addRouterFunctions()
메서드를 호출하게 되어 있습니다. addRouterFunctions()
메서드는PairAdder
를 매개변수로 받아서routerFunctionAdder()
메서드를 호출합니다.- 따라서,
Router
를 상속받아 구현할때addRouterFunctions()
메서드를 통해서routerFunctionAdder.add()
메서드를 호출하면 됩니다. routerFunctionAdder.add()
메서드는EndPoint
와RouterFunction
을 매핑합니다.basePath
도setBasePath()
메서드를 통해서 설정할 수 있습니다.
Configuration
- Server에서 사용할 Configuration을 만드려면
Configuration.class
를 상속받아 구현하면 됩니다. - Configuration에서는
addRequestHandlers()
,addRouterFunctions()
,addRouters()
메서드를 구현하면 됩니다.addRequestHandlers()
메서드는 Server에서 사용할RequestHandler
를 추가합니다.addRouterFunctions()
메서드는 Router에서 사용할RouterFunction
을 추가합니다.addRouters()
메서드는 Router를 추가합니다.
ServerConfiguration.java
public class ServerConfiguration extends Configuration {
@Override
protected void addRequestHandlers(Adder<RequestHandler> requestHandlerAdder) {
requestHandlerAdder.add(new StaticResourceHandler());
requestHandlerAdder.add(new IndexPageHandler());
}
@Override
protected void addRouterFunctions(PairAdder<EndPoint, RouterFunction> routerFunctionAdder) {
routerFunctionAdder.add(EndPoint.of(HttpMethod.GET, "/luizy"), (request, response) -> "Hello, Luizy!");
}
@Override
protected void addRouters(Adder<Router> routerAdder) {
routerAdder.add(new UsersRouter(UserDao.getInstance()));
routerAdder.add(new RegisterRouter());
}
}
addRequestHandlers()
,addRouters()
의 매개변수는 Functional Interface로 다음과 같이 정의되어 있습니다.@FunctionalInterface public interface Adder<T> { void add(T t); }
addRequestHandlers()
,addRouterFunctions()
,addRouters()
메게변수의 구현체는 상위Configuration
class에서 구현되어 있습니다.
Configuration.java
public abstract class Configuration {
private List<RequestHandler> requestHandlers = new ArrayList<>();
private Map<EndPoint, RouterFunction> routerFunctionMap = new HashMap<>();
public Configuration() {
// setRouterFunctionMap
addRouterFunctions(this::routerFunctionAdder);
// setRouters
addRouters(this::routerAdder);
// setRequestHandlers
addRequestHandlers(this::requestHandlerAdder);
}
protected void addRequestHandlers(Adder<RequestHandler> requestHandlerAdder) {}
private void requestHandlerAdder(RequestHandler requestHandler) {
requestHandlers.add(requestHandler);
};
protected void addRouterFunctions(PairAdder<EndPoint, RouterFunction> routerFunctionAdder) {}
private void routerFunctionAdder(EndPoint endPoint, RouterFunction routerFunction) {
routerFunctionMap.put(endPoint, routerFunction);
};
protected void addRouters(Adder<Router> routerAdder) {}
private void routerAdder(Router router) {
router.getRouterFunctionMap().forEach((endPoint, routerFunction) -> {
routerFunctionMap.put(endPoint, routerFunction);
});
};
}
- 객체가 생성될때
addRequestHandlers()
,addRouterFunctions()
,addRouters()
메서드를 호출하게 되어 있습니다. addRequestHandlers()
,addRouterFunctions()
,addRouters()
메서드는 Functional Interface를 매개변수로 받아서requestHandlerAdder()
,routerFunctionAdder()
,routerAdder()
메서드를 호출합니다.- 따라서,
Configuration.calss
를 상속받아 구현할때addRequestHandlers()
,addRouterFunctions()
,addRouters()
메서드를 통해서requestHandlerAdder.add()
,routerFunctionAdder.add()
,routerAdder.add()
메서드를 호출하면 됩니다. requestHandlerAdder.add()
,routerFunctionAdder.add()
,routerAdder.add()
메서드는 각각RequestHandler
,EndPoint
,Router
에 추가합니다.
마무리
JAVA만을 사용하여 의존성(외부 라이브러리) 없이 Configurable한 WAS를 만들어 보았습니다.
제가 만든 Server를 모듈로 배포하더라도 누구라도 쉽게 설정하여 사용할 수 있도록 하는것을 목표로 코드를 작성헀었습니다.
의존성 없이 작성하는것을 목표로 해서 Spring의 의존성 주입과 같은 기능을 사용할 수 없었고, 누구라도 쉽게 사용하는것을 목표로 해서 Reflecrtion을 사용하고 싶었지만, 코드 추적이 어렵다는점에서 순수 JAVA의 기능만을 활용하여 Configuration.class만 상속받아 필요한 기능만 추가하면 사용 가능하게 작성해 보았습니다!
이번 경험을 계기로 순수 JAVA만 활용하더라도 record, Functional Interface와 같은 기능을 더 적극적으로 활용해볼 기회가 되었고, 앞으로도 더 효율적으로 JAVA를 사용해볼 계기가 되었습니다.
질문이나 피드백이 있다면 언제든지 환영입니다!