카테고리 없음

Zero Dependency Java WAS 만들기

코딩루이지 2024. 7. 9. 15:17

의존성 없이 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들을 추가합니다.
  • StaticResourceHandlerresources/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 pathEndPoint에 메서드를 매핑합니다.

이렇게 완성된 서버는 아래와 같은 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"
            }
    • Get All Users
      • URL: /users
      • Method: GET
      • Response:
        • Success:
        • Code: 200 OK
        • Content:
            [
                {
                    "userId": "string",
                    "name": "string",
                    "password": "string"
                }
            ]
    • Get a Single User
      • URL: /users/{id}
      • Method: GET
      • Parameters:
        • Path Parameter:
          • id (number) - 조회할 유저의 ID
      • Response:
        • Success:
        • Code: 200 OK
        • Content:
            {
                "userId": "string",
                "name": "string",
                "password": "string"
            }

조금 긴 프리뷰였지만, 지금부터는 소켓 통신을 시작하는 부분부터 보겠습니다.

 


멀티 스레드로 소켓통신

  • 멀티 스레드를 사용하기 위해서 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() 메서드는 EndPointRouterFunction을 매핑합니다.
  • basePathsetBasePath() 메서드를 통해서 설정할 수 있습니다.

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를 사용해볼 계기가 되었습니다.

 

질문이나 피드백이 있다면 언제든지 환영입니다!