[JAVA] Custom Tomcat 개발

함형주·2024년 1월 7일
0

KUIT

목록 보기
2/2

질문, 피드백 등 모든 댓글 환영합니다.

개요

건국대학교 교내 IT 동아리 KUIT에서 진행한 3주차 미션에 관한 내용입니다.
서블릿, 스프링 등 서버 개발 프레임워크/라이브러리 등을 사용하지 않고 순수 자바코드로만 간단한 웹 서버를 개발합니다.
해당 과정을 통해 웹 서버 애플리케이션 핵심 동작 원리를 파악하는 것을 목표로 합니다.

요구사항

  1. HTTP 요청을 받고 응답할 수 있는 custom tomcat을 구현하여 웹 서버를 구축한다.
    • 자바의 Socket 라이브러리를 통해 통신한다.
    • Input/Output Stream 을 통해 요청/응답을 처리한다.
  2. GET 요청에서 정적 리소스인 html, css 등을 응답한다.
  3. POST 요청에서 회원 가입 및 로그인 기능을 처리한다.
    • 필요에 따라 302 응답을 통한 redirect 를 구현한다.
    • 로그인 시 쿠키를 발급한다.
  4. 로그인 회원만 접속 가능한 요청을 구분한다.
    • 비로그인 회원이 해당 url에 접속하면 로그인 화면으로 redirect 시킨다.

  1. 최대한 객체지향적으로 개발한다.

전체 코드 : github

개발

구조

WebServer

메인 클래스로, Executors, ExecutorService 으로 쓰레드풀을 생성하여 여러 요청을 병렬적으로 처리함.
ServerSocket(환영소켓)과 WelcomeSocker(연결소켓)을 사용하여 요청을 받음.

  • 환영소켓을 통해 통신을 받아들이고, 연결이 되었을 때 실질적으로 연결 소켓을 만들어 그 소켓을 통해 통신한다.
        int port = DEFAULT_PORT;
        ExecutorService service = Executors.newFixedThreadPool(DEFAULT_THREAD_NUM);

        // TCP 환영 소켓
        try (ServerSocket welcomeSocket = new ServerSocket(port)){

            // 연결 소켓
            Socket connection;
            while ((connection = welcomeSocket.accept()) != null) {
                // 스레드에 작업 전달
                service.submit(new RequestHandler(connection));
            }
        }

RequestHandler

InputStream, OutputStream 을 사용하여 HttpRequest, HttpResponse 객체 생성.
FrontController로 HttpRequest, HttpResponse 전달.

Buffer I/O 에 대한 작업 자체만으로 큰 책임과 역할을 가진다. 때문에 해당 작업은 HttpRequest, HttpResponse 객체에 위임하고 RequestHandler 에서는 해당 객체를 생성하여 FrontController로 넘겨주었다.

  • HttpRequest : 소켓 Buffer로 들어온 HTTP 요청 메시지를 읽어 인스턴스 변수로 변환
  • HttpRespons : HTTP 응답 메시지 형식에 따라 start line, header body 데이터 write
	Socket connection;
    private static final Logger log = Logger.getLogger(RequestHandler.class.getName());

    public RequestHandler(Socket connection) {
        this.connection = connection;
    }

    @Override
    public void run() {
        log.log(Level.INFO, "[RequestHandler] New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort());

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()){

            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            DataOutputStream dos = new DataOutputStream(out);

            HttpRequest httpRequest = HttpRequest.from(br);
            HttpResponse httpResponse = new HttpResponse(dos);

            FrontController frontController = FrontController.getInstance();
            frontController.service(httpRequest, httpResponse);


        } catch (IOException e) {
            log.log(Level.SEVERE,e.getMessage());
        }
    }

FrontController

RequestMapper를 통해 HTTP 요청을 처리할 수 있는 컨트롤러 조회 및 실행.

요청을 처리할 컨트롤러를 매핑하여 관리하고 찾아내는 작업만으로 충분한 책임을 가진다. 때문에 해당 작업은 RequestMapper 에 위임하였고 덕분에 FrontController는 단순히 요청에 맞는 컨트롤러를 실행하는 역할만 수행할 수 있다.

    public void service(HttpRequest request, HttpResponse response) throws IOException {
        getController(request).process(request, response);
    }

    private Controller getController(HttpRequest request) {
        return RequestMapper.getController(request.getRequestUri());
    }

RequestMapper

등록된 컨트롤러를 관리하고 요청에 따른 컨트롤러를 반환.

여러 개의 if문을 통해 찾아내는 작업은 가독성 측면에서나 유지보수 관점에서 매우 좋지 못한 코드다. 컨트롤러들은 모두 Controller를 implement 하므로, Map 자료구조로 관리하도록 하였고 단순 정적 리소스에 대한 요청일 경우 ForwardController로 대응하였다.

    private static final Map<String, Controller> handlerMappingMap = new HashMap<>();

    static {
        handlerMappingMap.put(SIGNUP.getValue(), new SignUpController());
        handlerMappingMap.put(LOGIN.getValue(), new LoginController());
        handlerMappingMap.put(LIST.getValue(), new UserListController());
    }

    public static Controller getController(String path) {
        Controller controller = handlerMappingMap.get(path);
        return controller != null ? controller :
                new ForwardController();
    }

Controller

모든 컨트롤러는 인터페이스로 정의된 Controller를 상속받는다.

public interface Controller {

    void process(HttpRequest request, HttpResponse response) throws IOException;
}

정적 리소스에 관한 요청은 ForwardController가 책임진다.
"/" 경로로 요청이 발생하면 index.html을 응답한다.

public class ForwardController implements Controller {

    @Override
    public void process(HttpRequest request, HttpResponse response) throws IOException {
        if (request.getRequestUri().equals("/")) {
            response.forward(Http.INDEX.getValue());
            return;
        }
        response.forward(request.getRequestUri());
    }
}

POST 방식의 로그인 요청을 처리한다.
파라미터로 넘어온 loginid, passward 값을 비교하여 로그인 성공 시 쿠키값을 설정하고 첫 화면으로 redirect 시키고, 실패 시 로그인 실패 화면으로 redirect 시킨다.

  • 주의 정밀한 로그인 로직을 구현하는 것이 목표가 아니므로 쿠키값을 임의의 값을 설정하였습니다. 로그인 로직은 보안에 매우 민감하므로 세션, 토큰 등의 방식을 고민해야 합니다.
public class LoginController implements Controller {

    private final MemoryUserRepository repository;

    public LoginController() {
        repository = MemoryUserRepository.getInstance();
    }

    @Override
    public void process(HttpRequest request, HttpResponse response) throws IOException {
        String paramUserId = request.getParamValue("userId");
        String paramPassword = request.getParamValue("password");

        User findById = repository.findUserById(paramUserId);
        if (!passwordCheck(paramPassword, findById)) {
            response.redirect(USER_LOGIN_FAILED.getValue());
            return;
        }

        response.addHeader(SET_COOKIE.getValue(), LOGINED_TRUE.getValue());
        response.redirect(INDEX.getValue());
    }

    private boolean passwordCheck(String paramPassword, User findById) {
        return findById != null && findById.getPassword().equals(paramPassword);
    }
}

유저 목록을 조회하는 화면은 쿠키값을 비교하여 로그인 회원만 접근이 가능하도록한다. 비로그인 사용자는 로그인 화면으로 redirect 시킨다.

  • 별도의 프레임워크나 라이브러리의 도움 없이 동적으로 html을 생성하는 것은 매우 고달픈 일이다. 다음 미션에서 서블릿, jsp, jdbc 등을 적용하여 리팩토링을 진행할 예정이므로 해당 예제에서는 정적 페이지만 응답한다.
    @Override
    public void process(HttpRequest request, HttpResponse response) throws IOException {
        String cookieValue = request.getHeader(COOKIE.getValue());
        if (cookieValue == null || !cookieValue.equals(LOGINED_TRUE.getValue())) {
            response.redirect(LOGIN_FORM.getValue());
            return;
        }
        response.forward(request.getRequestUri());
    }
profile
평범한 대학생의 공부 일기?

0개의 댓글