개요

  • Spring 없이 웹 서버를 구축하는 과정이다. 이전에는 Socket 을 연결하고 HTTP Request 를 추출해 정적 파일 접근 시 해당 파일을 전달해주었다.
  • 하지만 이때, Content-Type 이 text/html 로 고정되어있기 때문에 css 혹은 이미지 파일이 정상적으로 로드되지 않는다.
  • 이번에는 다양한 타입의 파일을 전달해주도록 하자!

타입 추출

가장 먼저 해야할 일은 요청에 따라 타입을 추출하는 것이다.

  • “/index.html”
  • “/favicon.ico”

등의 요청이 주어질 때마다 적절한 Content-Type 을 Request 헤더에 추가해 보내주어야한다.

Accept

GET /index.html HTTP/1.1


Host: localhost:8080
Connection: keep-alive
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
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/126.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.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
  • 실제 /index.html 로 요청을 보냈을 때 요청 헤더이다.
  • 여러 헤더들 중 Accept 를 보자.
  • Accept 요청 HTTP 헤더는 MIME 타입으로 표현되는, 클라이언트가 이해 가능한 컨텐츠 타입이 무엇인지를 알려준다. 서버는 제안 중 하나를 선택하고 사용하며 Content-Type 응답 헤더로 클라이언트에게 선택된 타입을 알려준다. 브라우저는 요청이 이루어진 컨텍스트에 따라 해당 헤더에 대해 적당한 값들을 설정한다
  • 즉, Accept 헤더에는 클라이언트가 서버에게 해당 컨텐츠 타입 중 하나의 방식으로 전달해달라고 요청하는 것이다.
  • 따라서 서버의 입장에서는 해당 Accept 를 확인하고 거기에 존재하는 Content-type 으로 헤더를 설정해 응답해주면 된다.

Content-type 선택

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.7

위에서의 요청 헤더를 확인해보면 매우 많은 종류가 들어있는 것을 확인할 수 있다. 선택 시에 추가적으로 우선순위를 확인할 수 있다. Content-type 뒤에 ;q=? 이 부분이 우선순위를 나타낸다.

default 값은 1 이므로 없는 타입의 우선순위가 가장 높다고 할 수 있다. 위 헤더를 우선순위대로 정렬해보자면

  • 1 : text/html, application/xhtml+xml, image/avif, image/webp, image/apng
  • 0.9 : application/xml
  • 0.8 : /
  • 0.7 : application/signed-exchange

참고로 v=b3는 Accept 헤더에 있는 application/signed-exchange 형식의 버전을 나타내는 부분이다. 이 경우에서도 가장 높은 우선순위에 너무 많은 타입이 존재한다. 이미지 같은 경우에도 헤더에는 여러 타입이 존재하지만 우선순위가 1인 타입을 사용하더라도 클라이언트가 이를 제대로 해석하지 못해 정상적으로 로드되지 않는 문제가 발생한다..

결국에는 StartLine 에서 실제 확장자를 통해 타입을 지정해주는 방법을 사용하였다.

확장자.extentsion

    public String parseRequestContentType(String path) {
        String[] pathParts = path.split("/");
        if (pathParts.length < 1) {
            return ContentType.html.getContentType();
        }
        String lastPathPart = pathParts[pathParts.length - 1];

        // 파일 이름에서 확장자 추출
        int lastDotIndex = lastPathPart.lastIndexOf('.');
        if (lastDotIndex != -1) {
            return ContentType.valueOf(lastPathPart.substring(lastDotIndex + 1)).getContentType();
        } else {
            return ContentType.html.getContentType();
        }
    }

확장자를 추출하기 위한 parser 이다. StartLine 을 통해 “/index.html” 이 파라미터로 들어오게 된다.

  1. 가장 먼저 “/” 경로로 들어올 경우를 대비해 해당 부분을 처리해준다.
  2. 이후 “.” 이 존재하는 지 확인하고 존재하면 “.” 이후의 문자열을 반환해준다.

위 과정을 통해 “/index.html” -> “html” 이 추출된다.

ENUM

public enum ContentType {
    html("text/html"),
    css("text/css"),
    js("application/javascript"),
    ico("image/x-icon"),
    png("image/png"),
    jpg("image/jpeg"),
    svg("image/svg+xml");

    private final String contentType;

    private ContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getContentType() {
        return contentType;
    }
}

여러 확장자들에 대해서 ENUM 을 생성해줬다. 가장 대표적인 Content-type 을 value 로 넣어놨으며 이를 헤더에 집어넣으면 대부분의 파일이 잘 읽히는 것을 확인할 수 있었다.

FileReader Error!!

public byte[] readStaticResource(String uri) throws IOException {
        String path = "src/main/resources/static";
        File file = new File(path + uri);

        // Not Found
        if (!file.exists() || !file.isFile()) {
            return null;
        }

        StringBuilder contentBuilder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                contentBuilder.append(line).append("\n");
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + path);
            e.printStackTrace();
            return null;
        }

        return contentBuilder.toString().getBytes();
    }

실제 파일을 읽을 때 BufferedReader 를 사용해서 한 줄씩 읽었다. HTML 과 CSS 같은 경우에는 괜찮았지만 favicon.ico 같이 이미지 파일인 경우에는 바이트파일을 String 으로 읽기 때문에 파일을 읽는 과정에서 꺠지게 된다.

사실 이 부분에 대해서는 깊게 파고들었기 때문에 다른 포스팅에서 설명하겠다.

public byte[] readStaticResource(String uri) throws IOException {
        String path = "src/main/resources/static";
        File file = new File(path + uri);

        // Not Found
        if (!file.exists() || !file.isFile()) {
            return null;
        }

        FileInputStream fis = null;
        byte[] byteArray = null;

        try {
            fis = new FileInputStream(file);
            byteArray = new byte[(int) file.length()];

            int bytesRead = 0;
            int offset = 0;
            while (offset < byteArray.length
                    && (bytesRead = fis.read(byteArray, offset, byteArray.length - offset)) >= 0) {
                offset += bytesRead;
            }

            if (offset < byteArray.length) {
                throw new IOException("Could not completely read the file " + file.getName());
            }
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return byteArray;

일반적으로는 readallbytes() 를 사용해서 바로 읽을 수도 있다. 하지만 위에서는 java.io 를 사용했기에 이미지 파일을 byte[] 에 담아서 일정 크기씩 읽어 byte[] 자체를 반환한다. 해당 과정을 통해 모든 파일이 정상적으로 반환되는 것을 확인할 수 있었다.

업데이트:

댓글남기기