자바 웹 프로그래밍 Next Step - 3장 웹 서버 실습
기초적인 웹 서버 프레임워크 만들기
이번 실습에서는 프레임워크 없이 java의 기본적인 라이브러리로 이루어진 웹 서버를 바탕으로 책의 요구사항을 만들어보게 됩니다. 아무것도 없는 환경에서 하나씩 기능구현을 해보면서 자연스럽게 웹 프레임워크 혹은 java 웹 표준 기술들이 왜 필요하게 되었는지 느끼게 됩니다. 결국 여러 기능을 구현하다보면 자연스럽게 공통 처리 로직을 묶어서 관리하게 되는데 이게 프레임워크의 가장 기초 뼈대로 수렴하기 때문입니다!
요구사항 1 - http://localhost:8080/index.html로 접속시 응답
요구사항 2 - get 방식으로 회원가입
요구사항 3 - post 방식으로 회원가입
요구사항 4 - redirect 방식으로 이동
요구사항 5 - cookie
요구사항 6 - stylesheet 적용
우선 주어진 스켈레톤 코드부터 볼까요?
먼저 main 함수가 있는 Webserver 클래스입니다. java에서 제공하는 Serversocket으로 listen Socket을 열어두고 요청이 오면 해당 요청을 처리해주는 requestHandler 스레드를 생성해줍니다. 즉 서블릿 컨테이너 코드의 간단한 버전이라고 보면 될 것 같습니다.
requestHandler 코드입니다. 열린 connection의 InputStream과 OutputStream을 인자로 받고 outputStream에 결과를 hello world를 반환하는 기능만 구현되어 있습니다. 위 webserver에서 8080포트로 서버를 띄웠으니 url로 localhost:8080에 접속하면 hello world를 볼 수 있습니다.
서버 구조를 간단하게 도식화하면 다음과 같습니다.
여기서 Stream이란 - 데이터가 전송되는 통로를 추상화한 것이라 보면 됩니다.
그럼 output을 받는 코드만 구현되어 있으니 간단하게 input도 받아볼까요? 어떤 값이 출력되는지 간단하게 print만 해봅시다.
위와 같이 InputStreamReader와 BufferedReader를 통해 InputStream으로 들어온 값을 읽을 수 있습니다.
이때 BufferedReader까지 활용하는 이유는 InputStream은 1byte만 10진수 utf-16 형식으로 읽기 때문입니다. int 값으로 반환되는데 저희가 읽고 싶은 값은 web 요청이므로 http 프로토콜로 들어오게 됩니다. 즉 문자열 값을 읽고 싶은데 BufferedReader가 바로 그 InputStream 값을 buffer에 쌓아둔 뒤 한번에 문자열로 처리해주는 역할을 합니다.
출력된 값을 살펴보면 위와 같습니다. http 프로토콜에 맞춰 문자열이 들어오는 걸 볼 수 있네요!
http 프로토콜은 단지 서로 정보를 어떻게 주고받을지에 대한 규약일 뿐입니다. 지금처럼 문자열 형태로만 가지고 있어서는 활용하기 힘들겠네요. 규약인만큼 어떤 형식으로 들어오게 될 지는 예상하기 어렵지 않습니다. http 프로토콜에 따라 해당 Request를 parsing하고 mapping해서 HttpRequest 클래스를 만들어 관리하는 것이 좋을 것 같습니다.
해당 구현을 하면서 같이 스터디했던 인원들 간에 토론이 벌어진 주제가 있어 간략하게 적습니다. HttpRequest의 생성자에서 파싱과 매핑을 해줬던 사람과 따로 문자열 파싱과 매핑의 역할을 맡는 클래스를 만들어 역할을 맡기고 반환값으로 HttpRequest를 만들었던 사람이 있었습니다. 여기서 생성자에서 너무 많은 일을 하는 건 좋은 코드인가?에 대한 토론이 벌어졌었는데요. 결론은 다음과 같이 났습니다. 답변의 경우 다른 분들의 조언을 얻어 결론이 났습니다 :)
일반적으로 우리가 사용하는 생성자는 값을 할당해주는 수준에 그치도록 작업하기 때문에 이질감을 느낄 수 있습니다. 그리고 이는 우리가 빈약한 도메인 모델 기반으로 개발을 하는 영향이 있다고 생각합니다. 실제 비즈니스 모델을 고려해서 생각해보는 것이 좋을 것 같습니다.
예를 들어 Money라는 클래스가 있을 때, "Money(돈)은 절대 음수가 될 수 없다"라는 비즈니스 로직이 있다고 하면, 이 유효성 검사는 어디서 하는게 올바를 것인가?라고 예를 들어볼까요. 컨트롤러나 서비스 등 어디보다 객체가 만들어지는 생성자가 가장 적합해 보입니다. Money는 0이 될 수 없음에 대한 도메인 표현이 해당 클래스 안에서 진행되어 응집도가 높아지고, 어느 곳에서도 Money 클래스를 쓴다면 0보다 작지 않음을 보장받을 수 있습니다. 그리고 생성자 테스트는 도메인이 풍부하게 개발하다 보면 작성되는 경우가 많습니다.
다시 책 내용으로 돌아와서 보면 "Http요청은 입력 스트림으로부터 데이터를 읽어 만들어진다 그리고 해당 코드를 보면 첫 번째는 requestLine, 그 다음은 헤더 라인, 공백 이후는 body 부분이다"를 알 수 있기 때문에 엄청 부적절하지는 않은 것 같습니다.
다만 생성자가 너무 길어서 파악하기 어려운 부분은 맞는 것 같아서, 위에 적힌대로 적당히 메서드 추상화 혹은 클래스 분리를 통해 해결할 수 잇을 것 같습니다.
그렇게 각각 request와 response를 mapping하면 url과 method에 따라 요청을 분리하는 부분을 짜게 됩니다. 처음엔 단순히 url과 method를 보고 if문을 통해 실행할 controller를 정해주는 코드를 짰습니다.
문제는 이럴 때마다 새로운 요구사항이 들어오면 기존 코드를 변경해야한다는 문제가 있었습니다. 물론 controller를 mapping 해주는 클래스는 분리하여 해당 클래스만 수정하면 되었지만 코드의 가독성 면이나 유지보수 면에서 좋아보이지 않았습니다.
그래서 각각의 controller들이 abstractController를 implements하게 만든 뒤 hashMap으로 <url, controller 구현체>로 관리하도록 하였습니다. 문자열 2장에서 구현한 코드 구조와 유사합니다 :)
이렇게 자체 프레임워크를 만든 뒤 위의 요구사항을 구현하기 시작하면 정말 정말 쉽게 해결됩니다. 반복되는 코드 로직은 라이브러리화하여 만들어놓고 실제로 개발자가 주로 구현해야할 부분만 남겼기 때문입니다.
• 서버 TCP/IP 연결 대기, 소켓 연결
• HTTP 요청 메시지를 파싱해서 읽기
• method 구분
• Content-Type 확인
• HTTP 메시지 바디 내용 피싱
• username, age 데이터를 사용할 수 있게 파싱
• 저장 프로세스 실행
• 비즈니스 로직 실행 -> 필요한 로직만 구현
• 데이터베이스에 저장 요청 -> 필요한 로직만 구현
• HTTP 응답 메시지 생성 시작
• HTTP 시작 라인 생성
• Header 생성
• 메시지 바디에 HTML 생성에서 입력
• TCP/IP에 응답 전달, 소켓 종
나머지는 라이브러리화
그리고 우리가 만든 이 부분이 바로 Servlet의 HttpServletRequest, HttpServletResponse 등 Servlet의 간략한 버전이라고 할 수 있습니다. 직접 웹 프레임워크의 기초를 구현해보면서 웹 기술에 대한 기초를 익히면서 구현 능력을 길러볼 수 있는 좋은 실습이었습니다 :)
구체적으로 요구사항을 채우는 방법은 기본적인 웹 지식과 위 구현 사항을 만족했다면 어렵지 않기 때문에 생략하겠습니다 :)