웹서버를 띄우고 브라우저에서 http://localhost:8080 같은 주소로 접속하면 콘솔에 로그가 여러 줄 쏟아진다. 보통 처음엔 “내가 요청 하나 보냈는데 왜 두세 개가 오지?”가 제일 헷렸다.
결론부터 말하면:
- 브라우저는 페이지 하나를 보여주기 위해 요청을 여러 개 보낸다.
- 그리고 서버는 요청(정확히는 연결/소켓) 하나당 스레드를 하나 만들어 처리하는 구조로 짜두면, 로그에 Thread-0, Thread-1 같은 게 동시에 찍힌다.
“요청이 여러 개”라는 말은 무슨 뜻인가
브라우저 주소창에 /로 접속한다고 해서 서버가 /만 요청 받는 게 아니다.
브라우저는 보통 이런 걸 추가로 자동 요청한다.
- / (메인 HTML)
- /favicon.ico (탭 아이콘)
- /css/... (CSS 파일이 있으면)
- /js/... (JS 번들이 있으면)
- /images/... (이미지 리소스가 있으면)
즉 “사용자 1명 = 요청 1개”가 아니라
“사용자 1명 = 여러 리소스를 가져오기 위한 여러 요청”이 기본이다.
그래서 서버 로그에는 짧은 시간 안에 요청이 여러 번 찍히는 게 정상이다.
포트가 매번 다른 이유: 클라이언트는 “임시 포트(ephemeral port)”를 쓴다
로그에 이런 식으로 찍힌다.
- Connected IP: ..., Port: 56299
- Connected IP: ..., Port: 56300
여기서 중요한 포인트는:
- 8080은 서버 포트이다. (서버가 “기다리는” 포트)
- 56299, 56300은 클라이언트(브라우저) 포트이다. (브라우저가 “요청 보낼 때 임시로 쓰는 포트”)
브라우저는 서버로 연결을 만들 때마다(또는 TCP 커넥션이 새로 생길 때마다)
OS가 임의의 포트를 하나 잡아준다. 이게 클라이언트 임시 포트이다.
그래서 요청이 2개면 임시 포트도 2개가 될 수 있고,
결과적으로 서버 로그에 “서로 다른 포트로 연결됨”이 보인다.
왜 Thread-0, Thread-1이 동시에 찍히나: 요청당 스레드 처리 모델
내가 만든 서버가 대체로 이런 흐름이라면:
- ServerSocket.accept()로 클라이언트 연결을 받는다
- 연결이 들어올 때마다 RequestHandler를 만들고
- new RequestHandler(socket).start() 로 스레드를 시작한다
이 구조에서는 연결 하나당 스레드 하나가 생긴다.
그래서 브라우저가 거의 동시에 요청을 2개 보내면
- 첫 번째 요청은 Thread-0
- 두 번째 요청은 Thread-1
이렇게 서로 다른 스레드가 동시에 실행된다.
여기서 “동시에”의 의미는 진짜 물리적으로 완전 동시에일 수도 있고(멀티코어),
아니면 OS 스케줄러가 빠르게 번갈아 실행하면서 동시에처럼 보이게 할 수도 있다.
중요한 건 서버가 요청을 한 줄로 줄 세워서 순서대로만 처리하는 구조가 아니라
각 요청을 독립적인 실행 흐름(스레드)으로 처리한다는 점이다.
실제로 서버가 읽는 HTTP 요청은 “텍스트”이고, 첫 줄이 핵심
서버는 브라우저가 보내는 요청을 소켓 InputStream으로 읽는다.
이 요청은 그냥 문자열(텍스트) 덩어리이다.
HTTP 요청은 크게 이런 구조이다.
- 요청 시작줄(Request Line)
- 헤더들(Header Lines)
- 빈 줄(Blank Line) ← 헤더 끝 표시
- (POST라면) 바디(Body)
즉, “첫 줄이 뭐냐”가 가장 핵심이다.
요청 시작줄(Request Line)의 형태: 요청마다 다를 수 있다
요청의 첫 줄은 보통 이런 형태이다.
<METHOD> <PATH>?<QUERY> HTTP/1.1
예시:
- 메인 페이지 요청:
GET / HTTP/1.1
- 파비콘 요청:
GET /favicon.ico HTTP/1.1
- 회원가입 요청(쿼리스트링 포함):
GET /user/create?userId=javajigi&password=password&name=JaeSung&email=javajigi%40slipp.net HTTP/1.1
여기서 서버 구현이 보통 하는 일은:
- 첫 줄에서 URL만 뽑는다. (/, /favicon.ico, /user/create?...)
- URL에 ?가 있으면 경로와 쿼리를 분리한다.
- 쿼리를 파싱해서 Map으로 만들고, User 같은 객체를 만든다.
즉 요청마다 첫 줄이 다르다는 건 너무 정상이다.
브라우저가 요청하는 리소스가 다르니까.
“각 요청의 마지막은 빈 문자열”이 무슨 뜻인가
서버에서 BufferedReader.readLine()으로 요청을 읽을 때, 많은 구현이 이런 루프를 쓴다.
- 한 줄씩 읽는다
- 빈 줄이 나오면 헤더가 끝났다고 판단하고 종료한다
HTTP 규칙에서:
- 헤더의 끝은 CRLF로만 이루어진 한 줄, 즉 빈 줄이다.
- 그래서 헤더를 다 읽으면 마지막에 빈 줄이 반드시 하나 나온다.
즉 “요청 마지막이 빈 문자열”이라는 말은 정확히는:
- 헤더 블록의 끝이 빈 줄로 표시된다는 의미이다.
(그리고 이 뒤에 POST면 Body가 이어질 수 있다)
로그를 “서버 켰을 때 상황”으로 더 이해하기 쉽게 다시 구성해보기
실제로 서버를 켜고 브라우저로 접속하면 이런 흐름이 자주 나온다(예시 로그).
[INFO ] Web Application Server started 8080 port.
[DEBUG] Thread-0 New Client Connect! ... Port: 56299
[DEBUG] Thread-0 method=GET, path=/
[DEBUG] Thread-0 응답: webapp/index.html 내려줌
[DEBUG] Thread-1 New Client Connect! ... Port: 56300
[DEBUG] Thread-1 method=GET, path=/favicon.ico
[DEBUG] Thread-1 응답: webapp/favicon.ico 내려줌 (없으면 404가 맞음)
여기서 핵심만 뽑으면:
- 브라우저가 / 요청과 /favicon.ico 요청을 거의 동시에 보냈다.
- 요청이 2개라서 연결이 2개 잡혔다.
- 연결마다 스레드가 따로 생겨서 Thread-0, Thread-1이 동시에 처리했다.
- 각 요청의 첫 줄은 GET / ..., GET /favicon.ico ...처럼 서로 달랐다.
- 요청은 헤더 끝에 빈 줄이 하나 존재한다.
참고: 로그에 Is a directory가 뜨는 경우의 전형적인 의미
이건 실제로 많이 겪는 케이스이다.
- 요청 path가 /인데,
- 파일로 읽으려면 보통 webapp/ 아래에서 /에 해당하는 파일을 찾아야 한다.
- 그런데 그대로 webapp/ 디렉터리를 readAllBytes 하려고 하면 “Is a directory”가 터진다.
즉 / 요청을 받았으면 서버는 보통 이렇게 매핑한다.
- / → /index.html
이걸 구현해주면 에러가 사라지고 브라우저도 정상 렌더링 된다.
출처 : 《자바 웹 프로그래밍 Next Step》, 박재성, 로드북
'Book > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
| 3장_ 요구사항3: POST방식으로 회원가입 (0) | 2026.02.01 |
|---|---|
| 3장_ 메모: 요청 메시지의 형태 (0) | 2026.01.31 |
| 3장_ 요구사항2 : GET 방식으로 회원가입하기 (0) | 2026.01.31 |
| 3장_ 요구사항1: index.html 응답하기 (0) | 2026.01.30 |
| 3장_ 코드 이해: RequestHandler 클래스 (0) | 2026.01.29 |
