지금까지는 http웹 서버에 접속하면 어떤 url로 접속하더라도 "Hello World"문자열만 출력하는데 http://localhost:8080/index.html로 접속했을 때 webapp디렉토리의 index.html 파일을 읽어 클라이언트에 응답하게 해야한다.
요청 읽기
일단 먼저 요청라인에서 path를 읽어야한다.
BufferedReader br =
new BufferedReader(
new InputStreamReader(in, StandardCharsets.UTF_8)
);
이 한 줄은 “바이트로 들어오는 데이터를 UTF-8 문자로 해석해서, 줄 단위로 읽을 수 있게 준비한다”다.
여기서 in은
InputStream in = connection.getInputStream();
- connection = 클라이언트 1명과 연결된 Socket
- in = 그 연결로 “클라이언트가 보내는 바이트”가 들어오는 통로
StandardCharsets.UTF_8
- “UTF-8 문자 인코딩”을 의미하는 상수다.
- 바이트를 문자로 바꿀 때(디코딩할 때) 어떤 규칙으로 바꿀지 지정한다.
- 요청 라인은 대부분 ASCII라 큰 차이가 안 날 수 있지만, 안정성을 위해 명시
new InputStreamReader(in, UTF_8) 의 역할은
바이트 스트림(InputStream)을 문자 스트림(Reader)로 변환한다.
- in은 바이트만 읽을 수 있다.
- 그런데 BufferedReader는 Reader(문자 기반)를 감싸서 동작한다.
- 그래서 중간 변환기인 InputStreamReader가 필요하다.
이때 중요한 건 “변환”이 그냥 캐스팅이 아니라는 것:
- 입력으로 들어온 바이트들을
- UTF-8 규칙으로 디코딩해서
- 자바의 char/문자열로 만들어준다.
요청의 시작이 이런 바이트라면:
47 45 54 20 2F 20 48 54 54 50 ...
(G E T / H T T P ...)
이걸 문자 "GET / HTTP..."로 바꿔준다.
new BufferedReader(Reader)
BufferedReader는 “문자 스트림을 더 편하게/효율적으로 읽게 해주는 래퍼”다.
(A) 버퍼링(Buffering)
- 네트워크에서 1글자씩 읽으면 너무 비효율적이다.
- BufferedReader는 내부적으로 한 번에 어느 정도를 버퍼에 모아두고,
- 너가 필요할 때 그 버퍼에서 꺼내준다.
(B) readLine() 기능 제공
가장 핵심.
- HTTP 요청은 처음부터 “줄 단위”로 온다.
- readLine()은 한 줄을 문자열로 읽어준다.
- “한 줄”의 기준은 줄바꿈(\n)이다. (\r\n이 와도 내부적으로 처리한다)
즉 BufferedReader를 쓰는 1순위 이유는 readLine() 때문이다.
요청 첫 줄 읽기
String requestLine = br.readLine();
전에 말했던 것처럼 readLine()을 사용해
br이 소켓으로부터 문자를 계속 읽다가 줄 끝(개행) 을 만나면 거기까지를 한 덩어리 문자열로 반환한다.
브라우저가 보내는 HTTP 요청은 보통 이런 식이다:
GET /index.html HTTP/1.1\r\n
Host: 15.165.xx.xx:8080\r\n
User-Agent: ...\r\n
Accept: ...\r\n
\r\n
(바디가 있으면 여기부터)
requestLine == "GET /index.html HTTP/1.1"
그리고 중요한 성질 2개:
(1) readLine()은 블로킹될 수 있다
- 아직 클라이언트가 데이터를 안 보냈으면 기다린다.
- 즉 접속은 됐는데 요청이 안 오면 여기서 멈춰 있을 수 있음.
(2) 반환값이 null일 수 있다
- 상대가 연결을 닫아버리면 null이 올 수 있다. (예: 요청을 보내기 전에 탭을 닫거나, 네트워크가 끊기거나, 프로그램이 죽어버리면)
- 그래서 보통 if (requestLine == null) 체크를 한다.
로그를 찍어보자
log.debug("Request Line: {}", requestLine);
"Request Line: {}"의 {}는
SLF4J 스타일의 플레이스홀더다.
- {} 자리에 뒤의 값(requestLine)이 들어간다.
- 문자열 덧셈("..." + requestLine)보다 깔끔하고 성능도 유리할 때가 많다.
debug 레벨이라 안 보일 수도 있다
로그 설정이 INFO 이상이면 debug가 출력 안 된다.
- 안 찍히면 log.info(...)로 바꾸면 무조건 보인다.
현재 콘솔에 로그가
Request Line: GET /index.html HTTP/1.1
이렇게 잘 찍히는 것을 확인했다.
요청 첫 줄에서 path(index.html) 뽑기
브라우저가 http://서버:8080/index.html로 오면 로그에:
- requestLine: GET /index.html HTTP/1.1
- path: /index.html
이렇게 찍히도록 해보겠다.
requestLine 아래부분 부터 시작해보면
if (requestLine == null || requestLine.isBlank()) {
return;
}
먼저 이렇게 해서
요청 라인이 없거나 비정상이면 아래 파싱 코드로 넘어가지 않게 하겠다.
그리고 요청 첫 줄은 규칙이 항상 이거라서:
METHOD PATH VERSION
GET /index.html HTTP/1.1
공백 기준으로 자르면 된다.
String[] parts = requestLine.split(" ");
String method = parts[0];
String path = parts[1];
- requestLine.split(" ")
요청 첫 줄(예: "GET /index.html HTTP/1.1")을 공백 기준으로 잘라서 배열로 만든다.
그래서 parts는 보통 이렇게 됨:- parts[0] = "GET"
- parts[1] = "/index.html"
- parts[2] = "HTTP/1.1"
- String method = parts[0];
잘린 것 중 첫 번째(0번째)를 꺼내서 HTTP 메서드로 저장한다. (GET, POST 같은 거)
String path = parts[1];
두 번째(1번째)를 꺼내서 요청 경로(URL path) 로 저장한다. (/, /index.html 같은 거)
즉, 한 줄에서 “메서드랑 경로를 뽑아내는” 파싱이다.
log.debug("method={}, path={}", method, path);
그리고 이렇게 로그를 추가해서 확인해보겠다.
14:44:41.205 [DEBUG] [Thread-0] [webserver.RequestHandler] - method=GET, path=/index.html
14:44:41.268 [DEBUG] [Thread-1] [webserver.RequestHandler] - method=GET, path=/favicon.ico
이렇게 잘 찍히는 것을 볼 수 있다.
여기서 왜 쓰레드 다른게 궁금했었다.
찾아보니
- 브라우저는 페이지를 보여주려고 여러 리소스를 동시에 받아와야 함
(HTML, CSS, JS, 이미지, favicon 등) - 그래서 동시에 여러 TCP 연결을 열어 병렬로 요청함
- 그 결과 스레드가 여러 개 생기고 로그가 여러 줄 찍힘
즉, “사용자 1명 = 요청 1개”가 아니라
“사용자 1명 = 여러 요청을 병렬로 보낼 수 있음” 이라고 한다.
그리고
- 요청을 보낸다 = 이미 열린 소켓에 HTTP 데이터를 보낸다.
- 그런데 연결이 없으면 먼저 소켓(TCP 연결)을 새로 만들고 요청을 보낸다.
이 서버는 요청 1개 처리 후 연결을 끊으니까,
브라우저는 요청마다 새 소켓을 만드는 것처럼 보이는 것이었다.
index.html파일 응답
이제 path가 /index.html일 때 실제로 webapp/index.html 파일을 읽어서 바이트로 만들고, 200 응답(헤더+바디)으로 내려주겠다.
먼저 이렇게 파일을 읽어서 byte[]로 만들겠다.
byte[] body = Files.readAllBytes(Path.of("webapp", "index.html"));
Path = “파일 경로”를 표현하는 객체
- 문자열로 "webapp/index.html"을 그냥 쓰는 대신,
- 자바가 OS에 맞게 경로를 다루도록 Path 객체로 만든다.
Path.of("webapp", "index.html")의 의미
- "webapp"과 "index.html"을 경로 조각으로 받아서 합친다.
- 결과적으로 경로는 이런 의미가 된다:
- mac/linux: webapp/index.html
- windows: webapp\index.html
즉, 운영체제 경로 구분자(/, \)를 신경 안 쓰게 해준다.
그리고 이 경로는 상대경로다.
“현재 프로그램을 실행한 디렉토리(working directory) 기준”으로 webapp/index.html을 찾는다.
Files = 파일 관련 기능 모음(유틸 클래스)
java.nio.file.Files는 파일을 읽고/쓰고/복사하는 기능들이 모여있다.
readAllBytes = “파일을 끝까지 전부 읽어라”
- 인자로 받은 Path가 가리키는 파일을 열고
- 파일의 내용을 처음부터 끝까지 읽어서
- 그 내용을 byte[]로 반환한다.
그리고 파일 내용을 byte[]로 들고 있으면 그대로 dos.write(body)로 보낼 수 있다.
그리고 응답 헤더/바디를 기존 함수로 내려주면 된다:
response200Header(dos, body.length);
responseBody(dos, body);
webapp 디렉토리 위치
프로그램을 어디에서 실행하느냐에 따라 webapp/index.html 경로가 달라질 수 있다.
가장 간단한 기준은 이거:
- 서버 실행하는 현재 작업 디렉토리(보통 프로젝트 루트)에 webapp/index.html이 있어야 함
구조 예:
프로젝트루트/
webapp/
index.html
src/
target/
확인

이렇게 잘 뜨는것을 확인할 수 있다.
출처 : 《자바 웹 프로그래밍 Next Step》, 박재성, 로드북
'Book > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
| 3장_ 메모: HTTP 요청 라인 파싱과 멀티 스레드 처리 (0) | 2026.01.31 |
|---|---|
| 3장_ 요구사항2 : GET 방식으로 회원가입하기 (0) | 2026.01.31 |
| 3장_ 코드 이해: RequestHandler 클래스 (0) | 2026.01.29 |
| 3장_ 코드 이해: WebServer 클래스 (0) | 2026.01.29 |
| 3장_ 소스코드 재배포 (0) | 2026.01.29 |
