[자바 웹 프로그래밍 Next-Step] 1주차 CH03, 04: 개발 환경 구축 및 HTTP 웹 서버 구현

이호석·2023년 2월 9일
1
post-thumbnail

자바 웹 프로그래밍 Next-Step - 박재성 저자 책으로 스터디를 하며 진행했던 내용들을 기록하고 있습니다.

1주차에 진행했던 Chapter 03, 04의 목표는 다음과 같습니다.

  • Chapter 03: 스스로 생각하여 구현한 웹 서버(실습)
  • Chapter 04: 박재성님을 따라서 실제 웹 서버 구현 따라가기(설명)

모든 코드들은 다음 저장소에서 확인할 수 있습니다.
https://github.com/Java-web-programming-Next-Step/next-step-web-programming/tree/HiiWee/3
프로젝트명: web-application-server-gradle

📌 1주차 3장, 4장


✅ 사전 공부

AWS 공부하기!


✅ 3.3 [배포] - AWS EC2 배포하기 with gradle

git을 통해 프로젝트 파일을 clone하고 프로젝트 파일로 이동한다.

책에서는 Maven을 이용하지만 본인은 Gradle을 사용함

✔︎ 배포순서 (이슈❗️)

  1. 서버 접속 및 GitHub 저장소 Clone
  2. clone한 디렉토리로 이동해 다음 명령어 실행
    • Gradle: ./gradlew clean build
    • Mavne: mvn clean package (그 전에 Maven을 깔아주어야 한다.)
  3. 빌드 끝나면 다음 명령어를 통해 WebServer를 실행한다.
    • Gradle: ./gradlew run —args=$PORT &
      • 실행시 문제 발생
        ./gradlew run --args=7070
        
        FAILURE: Build failed with an exception.
        
        * What went wrong:
        Task 'run' not found in root project 'web-application-server'.
        
        * Try:
        > Run gradlew tasks to get a list of available tasks.
        > Run with --stacktrace option to get the stack trace.
        > Run with --info or --debug option to get more log output.
        > Run with --scan to get full insights.
        
        * Get more help at https://help.gradle.org
        
        BUILD FAILED in 2s
        우선 run이라는 실행 task를 찾을 수 없다는 오류를 발견했다. 기존의 build.gradle 파일은 다음과 같고, 오류가 나지 않게 하려면 볼드 처리된 부분을 변경해 주어야 한다.
        /*
         * This file was generated by the Gradle 'init' task.
         */
        
        plugins {
            id 'java-library'
        		**/* 지우고 application으로 변경 */**
            id 'maven-publish'
        }
        
        **/ * mainClassName을 명시해주어야 함*/**
        
        repositories {
        /* mavenCentral()로 대체 가능 */
            mavenLocal()
            maven {
                url = uri('https://repo.maven.apache.org/maven2/')
            }
        }
        
        dependencies {
            api 'com.google.guava:guava:18.0'
            api 'ch.qos.logback:logback-classic:1.1.2'
            testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.22.0'
            testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
            testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
        }
        
        group = 'org.nhnnext'
        version = '1.0'
        description = 'web-application-server'
        java.sourceCompatibility = JavaVersion.VERSION_11
        
        /* 필요 없는 부분 */
        publishing {
            publications {
                maven(MavenPublication) {
                    from(components.java)
                }
            }
        }
        
        /* 필요 없는 부분 */
        jar {
            manifest {
                attributes 'Main-Class': 'webserver.WebServer'
            }
        }
        
        tasks.withType(JavaCompile) {
            options.encoding = 'UTF-8'
        }
        
        tasks.withType(Javadoc) {
            options.encoding = 'UTF-8'
        }
        
        tasks.named('test') {
            useJUnitPlatform()
        }
        따라서 다음과 같이 build.gradle 파일을 수정했다.
        /*
         * This file was generated by the Gradle 'init' task.
         */
        
        plugins {
            id 'java-library'
            **id 'application'**
        }
        
        **mainClassName = 'webserver.WebServer'**
        
        repositories {
            mavenCentral()
        }
        
        dependencies {
            api 'com.google.guava:guava:18.0'
            api 'ch.qos.logback:logback-classic:1.1.2'
            testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.22.0'
            testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
            testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
        }
        
        group = 'org.nhnnext'
        version = '1.0'
        description = 'web-application-server'
        java.sourceCompatibility = JavaVersion.VERSION_11
        
        tasks.withType(JavaCompile) {
            options.encoding = 'UTF-8'
        }
        
        tasks.withType(Javadoc) {
            options.encoding = 'UTF-8'
        }
        
        tasks.named('test') {
            useJUnitPlatform()
        }
        이후 정상적으로 빌드 이후 웹서버가 실행된다.
  4. 이후 curl http://localhost:8080을 통해 Hello World가 정상 출력되는지 확인한다.

✔︎ 배포 쉘 스크립트

본인은 배포에 관한 일련의 과정을 쉘 스크립트를 이용해 작성하였다.

#!/bin/bash

# 현재 브랜치명
BRANCH=HiiWee-3
REPOSITORY=/home/ubuntu/next-step-web-programming
PROJECT_NAME=web-application-server-gradle
PORT=8080

echo "> 기존 실행 웹서버 종료"
fuser -k $PORT/tcp

echo "> 디렉토리 이동"

cd $REPOSITORY/$PROJECT_NAME/

echo "> 브랜치 이동"

git checkout $BRANCH

echo "> Git Pull"

git pull

echo "> 프로젝트 Build 시작"

./gradlew build

echo "> 프로젝트 실행"

./gradlew run --args=$PORT &

✅ 3.4 웹 서버 실습

WebServer 클래스

기본 port(8080) 혹은 사용자 지정 포트를 이용해 서버 소켓을 열고 클라이언트의 입력이 오길 기다린다.
클라이언트의 입력이 들어오면(http://localhost:8080) RequestHandler에게 해당 연결에 대한 처리를 맡긴다.

RequestHandler 클래스

WebServer 클래스로부터 받아온 클라이언트의 요청을 분석하고, 해당 요청에 맞는 동작을 분기하여 실행하고 응답 body에 알맞은 응답을 보낸다.

✔︎ 요구사항 1 - index.html 응답하기

HTTP Header는 다음과 같이 request Header를 구성한다.

GET /index.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: ko,en-US;q=0.9,en;q=0.8,ja;q=0.7
Cookie: Idea-5ba2b974=570aea70-9ae8-494a-9e67-fac7949c07fc; Idea-5ba2b975=4d2b9ec8-6fb6-4ef5-b682-bb1b965ac1d0

최초 첫번째 줄은 항상 Request Line으로 ${HTTP Method} ${request URI} ${전송 방식}과 같이 요청이 오는데 이것의 의미는
어떤 웹서버로 접속(Host 부분)해서, 어떠한 방식(HTTP/1.1)으로, 어떠한 메소드(GET)를 통해 무엇을(/doc/test/.html) 요청했는지에 대한 메시지가 담겨있다.

구현 과정

  • 요구사항1 명세서

    • Request Line을 기준으로 Http Method, Request URI, 전송방식을 구분해야 한다.
    • Http Method는 상태값을 가진다.
    • Request URI를 이용해 webapp에 존재하는 index.html 정적 리소스를 요청해야 한다.
    • 전송 방식은 기본이 HTTP/1.1이다.
  • HTTP Request 한 줄 읽어오기

    Http Request의 제일 첫 라인은 Request Header로 구성된다. 이곳에서 사용자가 요청한 페이지(request URI)를 알 수 있다

    // ReuestHandler
    HeaderReader httpReader = new HeaderReader(in);
    RequestLine requestLine = HttpRequestUtils.parseRequestLine(httpReader.readLine());
    byte[] fileBytes = Files.readAllBytes(new File("./webapp" + requestLine.getUri()).toPath());

    우선 요청 핸들러에서 request line을 읽는다. 이후, parseRequestLine 메서드를 통해 RequestLine의 정보를 담은 객체를 반환받는다.

    이후 requestLine의 uri정보를 통해 파일을 호출하고 바이트로 변경하여 response body로 전송하여 응답할 수 있다.

✔︎ 요구사항 2 - GET 방식으로 회원가입하기

위에서 구현한 index.html에서 회원가입하기 버튼을 클릭하면 다음과 같이 url이 변경된다.

http://localhost:8080/user/form.html

이후 회원가입 정보를 입력하고 회원가입을 누르면 다음과 같은 url을 호출한다.

http://localhost:8080/user/create?userId=testID&password=1234&name=test&email=test%40naver.com

쿼리스트링을 통해 회원가입 정보를 전달하고 있으므로 회원가입을 위해 쿼리스트링을 파싱해야 한다. 이는 기존 HttpRequestUtils.parseQueryString() 메서드를 활용하면 된다.

  • FrontController를 통한 전체 요청 관리하기

    기존 RequestHandler와 달리 요청이 들어오면 모든 요청은 FrontController로 전달된다.
    또한 HttpRequest는 요청이 들어온 모든 정보(Request Line, Request Header, Message Body)의 정보를 가지고 있는다.

  • FrontController 내부 코드

    요청이 들어오면 다음과 같은 우선순위를 가진다.

    1. 우선 해당 url에 매핑된 컨트롤러가 있는지 살펴본다.
    2. 매핑된 url이 없다면 정적인 소스(webapp/)을 살펴본다.
    3. 그 마저도 없다면 예외 발생(자동발생)
  • 매핑된 Controller 인터페이스를 구현하는 UserController

    UserController에서 GET요청시 다음과 같이 doGet 메소드가 실행된다.

    쿼리 스트링으로 값이 전달되었으므로 HttpRequest에는 이미 쿼리스트링의 값이 저장되어 있으므로 getParameter를 통해 쿼리스트링의 값을 가져온다. 이후 null 체크 및 회원가입을 완료한다.

    true가 반환되면 FrontController에서 200신호를 내리도록 HttpResponse에게 응답 명령을 준다.

✔︎ 요구사항 3 - POST 방식으로 회원가입하기

GET은 url에 쿼리스트링으로 값을 전달하지만, POST 방식은 form data로 전송한다.
form data는 request header 다음 한 줄을 비우고 그 다음줄에 전송되므로 별도의 파싱이 필요하다.
IOUtils는 해당 데이터의 파싱을 도와주고 있으므로 해당 유틸 메소드를 사용해 구현해보자

  • HttpRequest 클래스에서 form 데이터 추출

기존 header 데이터는 전부 추출했으므로 while (!header.equals(EMPTY))로 폼 데이터가 나타나기 직전에 BufferedReader가 가리키고 있으므로 즉시 form data를 가져오면 된다.

  • FrontController에서 POST 메소드 분기 추가

    해당 uri에 맞는 컨트롤러를 찾아서 GET, POST 방식으로 실행할 수 있게 FrontController가 지원해준다.

  • UserController에서 POST 회원가입

    GET과 마찬가지로 httpRequest.getParameter로 값을 가져오는데 url과 form data인데 같은 메소드를 사용한다.

    • HttpRequest의 getParameter 메소드

      다음과 같이 url 쿼리스트링 값이 없다면 form 데이터를 조회하도록 메소드를 작성하여 하나의 메소드로 재사용할 수 있었다.

      위와 같이 사용하기 위해서는 requestLine.getQueryString 메소드 또한 약간의 조건을 추가해야 한다.

      • RequestLine의 queryStrings map은 url에 값이 있을때만 인스턴스가 생성되므로 null 체크를 해주어야 한다.

✔︎ 요구사항 4 - 302 status code 적용

회원가입을 완료하고 /index.html 페이지로 이동해야 한다. 회원가입의 url은 /user/create로 유지되므로 응답으로 전달할 정적인 html파일이 존재하지 않는다. 따라서 회원가입을 완료하면 main 페이지인 index.html로 이동시켜보자

  • 키워드

    • Redirection (HTTP Status: 302)
    • Http Location Header
  • UserController에서 sendRedirect 호출

    실제 서블릿을 생각했을때 리다이렉트는 컨트롤러에서 명령을 주게 된다. 따라서 httpResponse 객체에 리다이렉트 명령과 path를 넘겨준다.

  • HttpResponse.sendReadirect

    getFileBytes는 webapp에 존재하는 path에 대한 파일을 바이트로 받아온다. 이후 response302Header를 통해 response header를 완성하는데 다음과 같다.

    전달하는 http status는 302이며, 추가적인 header로 Location에 redirect할 url을 작성하면 웹브라우저는 해당 url로 redirect하게 된다.

✔︎ 요구사항 5 - 로그인하기 (이슈❗️)

로그인을 성공하게 되면 Set-Cookie 헤더를 통해 쿠키값 logined=true을 심어야 한다.

  • LoginController

    따라서 다음과 같이 로그인에 대한 데이터베이스와 연동된 UserService의 검증을 마치게 되면 쿠키를 추가하게 된다.
    userService.login은 boolean값을 반환하며 로그인을 실패하게 되면 false를 반환한다.

  • HttpResponse 메시지 생성시 쿠키 처리

    HttpResponse에서는 Map 자료형으로 쿠키를 담을 수 있게 필드를 두었는데 다음과 같다.
    private final Map<String, Object> cookies = new HashMap<>();
    따라서 addCookie를 통해 위 자료형에 쿠키를 key:value 타입으로 담을 수 있다.

    쿠키를 담고 FrontController에서 Response를 생성할때 getAllCookieMessage가 쿠키를 담을 response header를 작성하는데, 만약 쿠키가 존재하지 않는다면 “”값을 반환하게 한다.

  • 이슈

    302 Redirect시 Set-Cookie가 적용되지 않는 문제가 있다.

    현재 url의 흐름은 다음과 같다.

    • 클라이언트: /user/login → POST

    • 서버: POST→ 로그인 완료후 /index.html로 redirect

    • 클라이언트: /user/login 302 → /index.html로 redirect 완료

      여기서 초기 redirect된 index.html을 제외하고 나머지 url들은 Cookie가 요청헤더에 존재하지만
      index.html을 호출했을때는 Cookie 헤더가 존재하지 않는 이슈가 있다.

      스택오버플로우에 비슷한 경우가 존재해 해결하려 시도해봤지만, 해결되지 않음

      Sending browser cookies during a 302 redirect

  • 해결

    Set-Cookie header에 path를 추가한다. default로 추가되는 pasth는 /user가 붙으므로 /user가 붙지 않는 index.html에 대한 호출에는 Cookie값이 전달되지 않는다. 따라서 path=/ 를 붙여주면 해결됨

✔︎ 요구사항 6 - 사용자 목록 출력

  • 사용자 목록을 출력하기 위해 일단 HttpResponse에서 변경이 있었다.

    html을 동적으로 만드는 방법에 어려움이 있어서 일단 단순히 responseBody에 현재 유저 목록들을 출력하고자 했다. 따라서 responseBodyData가 null이 아니라면 text/plain타입으로 텍스트를 그대로 출력한다.

    이는 임시 방편이며 좋지 못한 방법이다.. ㅠㅠ

  • UserListController에서 사용자 목록 만들기

    userService를 통해 DB를 조회해 모든 사용자를 불러온다. 이후 StringBuilder를 통해 사용자 목록 리스트를 만들어 HttpResponse 객체에 responseBodyData로 넘겨주면 된다.

    💡 만약 로그인이 되어있지 않은 상태라면 login.html로 리다이렉트하는데 Boolean.valueOf()에서 만약 Cookie에 logined라는 키값 자체가 존재하지 않으면 NPE 발생위험이 있지 않나? 생각하겠지만,
    Boolean.valueOf()는 값이 null이라면 false로 자동변환하여 반환해주므로 그럴 위험이 없다.

✔︎ 요구사항 7 - CSS 지원하기

CSS는 GET으로 요청되기 때문에 GET을 지원하는 메소드에서 CSS인지 검사하여 분기점을 만든다.

  • FrontController에서 분기점 만들기

    requestUri에 css가 포함되어있다면 successStaticCss 메소드로 분기한다.

  • HttpResponse의 successStaticCss 메소드

  • HttpResponse의 response200HeaderWithCss 메소드

    Content-Type을 text/css로 두면 CSS가 적용된 화면을 볼 수 있다.


✅ 3.5 추가학습

로깅 레벨 설명

로그 레벨은 TRACE < DEBUG < INFO < WARN < ERROR 순서로 되어있다.

개발 단계에서 로그 레벨을 잘 나누어 구현하게 되면 로그 레벨의 조절만으로도 원하는 로깅을 확인할 수 있다.

개발 단계에선 주로 DEBUG로 두고, 실서비스에선 INFO 레벨로 둔다.
(DEBUG 레벨로 실서비스를 운영하면 굳이 노출되지 않아도 될 디버깅 내용들이 출력되므로 다른 로그를 확인하는데 불편함이 존재)

logback 설정으로, DEBUG 레벨로 설정되어 있다.

패키지별 로깅 라이브러리 설정

에러가 발생됐을때 데이터의 실질적인 확인이 필요한 경우, logging을 통해 확인할 수 있다. 실제 데이터를 출력하는 부분은 개발단계에서 확인하므로 INFO레벨보단 DEBUG레벨로 설정하는것이 좋겠다.

하지만 라이브러리에서 출력되는 로그들이 많아서 내가 원하는 로그의 가독성이 떨어진다.

가장 좋은 방법은 해당 라이브러리의 문서를 가서 설정을 찾아본다.

위의 설정은 전체 root 레벨은 INFO지만 net.slipp이라는 패키지는 DEBUG로 두어 원하는 로그만 출력하도록 한다. 이는 logback 공식문서에서 제공하는 방법이다.

💡 이러한 설정에 대한 결과 확인은 실제 임의로 회원가입을 해보거나 사이트를 조작하여 테스트할 수 있지만, 매번 서버를 키고 끄는것이 번거롭다. 따라서 JUnit을 통한 단위 테스트를 적용해보도록 하자

Logging Formatting

Logback 라이브러리는 의미없는 문자열 연산을 막기 위해 포메팅을 제공한다.

log.debug(”User=” + user); <> log.debug(”User={}”, user);

좌측의 연산은 로깅 레벨이 INFO여도 문자열 더하기 부분이 실행되지만, 우측은 그렇지 않다. 실제 문자열 연산은 생각보다 비용이 크므로 우측처럼 사용하자


✅ 3장에서 부족했던 점

  • 단위 테스트의 부재 → 기능 구현에 급급하고, 단위 테스트를 멀리함 → 좋지 못하다.
  • 로깅 레벨 설정의 오류 → 거의 모든 로그를 log.info로 표시했는데 살펴보면 대부분의 로그들은 debug레벨로 출력해야 함을 알았다.

✅ 4.2.1.1 요구사항 1 - index.html 응답하기

요청 HTTP, 응답 HTTP

## 요청
POST /user/create HTTP/1.1 # 요청라인
HOST: localhost:8080 # 요청헤더들
Connection-Length: 59
Content-Type: application/x-www-form-urlencoded
Accept: */*
											# 공백 라인("")
userId=javajigi&password=password # request body(요청 본문)

## 응답
HTTP/1.1 200 OK # 상태 라인
Content-Type: text/html;charset=utf-8  # 응답헤더들
Content-Length: 20
											# 공백 라인("")
<h1>Hello World</h1> # response body(응답 본문)

요청, 응답 HTTP의 형식은 첫째 라인만 다르고 동일한 형식을 보인다.

한 번의 localhost:8080/index.html 호출이지만, 여러번의 요청과 응답

index.html을 요청하게 되면 index.html뿐만 아니라 css, js와 같이 여러개의 요청이 발생한다.

이는 응답을 받은 브라우저의 HTML 내용을 분석해 CSS, JS, 이미지 등 자원의 포함여부가 확인되면 서버에 해당 자원을 요청하기 때문이다.


✅ 4.2.1.2 요구사항 2 - GET 방식으로 회원가입하기

URI 분석

/user/create?userId=javajigi&password=password&name=hoseok&email=javakigi%40slipp.net

위에서 /user/create는 자원의 위치를 나타내는 path이고

그 외에 강조처리된 부분을 쿼리 스트링(query string)이라 부른다.


✅ 4.2.1.3 요구사항 3 - POST 방식으로 회원가입하기

GET, POST 사용기준

  • GET: 서버에 존재하는 데이터(또는 자원)을 단순히 요청하여 가져올때
  • POST: 서버에 요청을 보내 데이터를 추가, 수정, 삭제와 같은 작업을 실행하도록함

→ POST를 이용한 수정, 삭제의 경우 HTTP 메소드 중 PUT, DELETE를 사용하면 좋지만 HTML은 GET, POST만 지원하기에 사용하는데 어려움이 있다. 이는 비동기 통신인 Ajax를 사용하면 해결할 수 있다.


✅ 4.2.1.4 요구사항 4 - 302 status code 적용

POST 방식으로 회원가입 이후 완료 이후 응답 url을 /index.html로 설정하면 정상적으로 홈 화면이 보여진다.
하지만 여기서 새로고침을 하게되면 방금 회원가입한 form data가 다시 서버로 전송된다.

브라우저는 새로고침하게 되면 직전에 했던 요청을 다시 요청하게 되므로 동일한 회원이 여러번 회원가입되는 상황이 발생할 수 있다.

이를 막기 위해서는 일명 PRG 패턴을 이용하자

PRG 방식

  • POST → Redirect → GET: POST로 데이터의 추가, 수정, 삭제를 요청하면 Redirect를 통해 웹브라우저가 새로운 화면으로 GET 호출하도록 한다. 따라서 새로고침을해도 직전에 변경한 데이터의 중복이 발생하지 않는다.

Redirect 원리

Redirect는 302 status code를 가진다. 웹 브라우저는 302 상태 코드를 보고 Response Header의 Location 헤더로 전송된 redirect할 uri를 찾고, 해당 uri로 재요청을 한다.

  • 예시
    앞선 상황처럼 POST로 회원가입을 하고 index.html로 redirect 해보자
    1. HTTP Response

        ```
        HTTP/1.1 302 Found
        ...
        Location: /index.html
        ```
        
    2. Web Browser → GET /index.html HTTP/1.1
        
        302 코드를 보고 Location의 /index.html로 GET 요청을 보낸다.
        
    3. Server → GET 방식으로 /index.html을 보여줌
  • 결과 화면

    POST 요청으로 회원가입

    Web Browser의 redirect 및 /index.html GET 요청


✅ 4.2.1.5 요구사항 5 - 로그인하기

로그인을 하기 위해서는 회원가입한 정보를 Database에 저장하는 과정이 필요하다.
이후 로그인하는 아이디와 비밀번호가 form data로 서버로 전달되면 DB를 조회해 해당 유저를 조회하고, 비밀번호까지 일치하면 로그인에 성공하게 된다.

여기서도 위에서 언급한 /index.html로의 PRG의 방식이 사용된다.

HTTP는 한번의 요청-응답 과정이 완료되면 연결을 끊는다. 따라서 새로운 요청에서 이전 요청의 값을 알 수 없다. 이는 HTTP가 무상태 프로토콜(Stateless Protocol)이기에 가능하다.

HTTP 1.1 부터는 한번 맺은 연결을 재사용하지만 서로 다른 요청간의 상태 데이터를 공유할 수 없는 무상태 프로토콜의 특성을 가진다.

그렇다면 로그인을 완료하고 다음 요청에서 로그인이 됐는지 확인하려면 어떻게 해야할까?
로그인 유지의 방식으로 Cookie가 있다.

서버에서 Set-Cookie 헤더에 로그인에 여부에 대한 상태를 담고 응답을 하게되면 웹브라우저는
Set-Cookie의 값을 보고 웹브라우저의 쿠키 저장소에 해당 데이터를 저장한다.
이후 다음 요청부터 요청 헤더에 Cookie 헤더를 추가해 받아온 쿠키값을 서버로 보내준다.

따라서 Cookie를 이용해 로그인을 유지시킬 수 있다.

하지만, 쿠키는 누구나 볼 수 있기에 만약 쿠키에 민감한 정보를 담게되면 탈취당할 위험이 존재한다.
이를 보완하기 위해 세션 + 쿠키의 조합을 사용한다.


✅ 4.2.1.6 요구사항 6 - 사용자 목록 출력

앞서 Cookie에 저장했던 logined의 값을 이용해 로그인 되어있는 상태라면 전체 사용자 목록을 출력하고 로그인이 되어있지 않다면 login.html로 이동시켜보자

사용자 목록 출력 순서

  1. 쿠키를 파싱해 logined값이 true인지 체크 → 로그인 안되어있다면 /login.html로 응답
  2. DB에서 전체 사용자 목록 조회
  3. StringBuilder를 통해 HTML 생성
  4. bytes로 변환해 응답

구현하는 내용은 크게 어려운점이 없다.


✅ 4.2.1.7 요구사항 7 - CSS 지원하기

웹 브라우저가 HTML을 분석해 JS, CSS를 요청하게 되면 우리가 구현한 코드에서는 다음과 같이 Content-Type의 값이 text/html로 설정되어 있다.

CSS는 text/css로 응답돼야 HTML에 적용되므로 Request Line을 분석해 만약 파일이 css파일 이라면 text/css로 반환하게 하면 적용할 수 있다.

Metadata

데이터에 대한 데이터로 여기서는 요청과 응답 헤더의 각 요청과 응답이 포함하고 있는 본문 컨텐츠에 대한 정보 즉, 데이터에 대한 정보를 담고있는 헤더들을 메타데이터라 부른다.

메타데이터는 애플리케이션 개발의 많은 곳에서 사용되는 용어이므로 반드시 알아두면 좋다.

🤔 고민되는 부분들

HttpResponse에서 응답처리

응답이 진행될때 제일 마지막에 거치는 클래스는 HttpResponse 클래스다.
하지만, FrontController에서 모든 요청에 대한 처리를 진행하는걸 목표로 코드를 작성했는데 마지막 응답에 대한 부분을 HttpResponse에서 진행하는것이 맞는지 의문이 들었습니다.

시작을 RequestHandler → FrontController로 받았다면 마무리도 FrontController → RequestHandler로 진행되어야 하지 않을지 흐름에 대한 고민이 있습니다.

profile
꾸준함이 주는 변화를 믿습니다.

0개의 댓글