이번 강의에서는 간단한 클라이언트-서버 네트워크 프로그램을 만들어보자.
⬛ 객체화된 요청과 응답 처리
- HttpRequest:
- 요청 메서드(GET, POST), 경로(Path), 쿼리 파라미터(Query Parameters) 등을 구조화.
- HttpResponse:
- 응답 상태 코드(200, 404 등), 헤더(Content-Type 등), 바디 작성 지원.
🔷 HttpRequest
/**
* HTTP 요청을 파싱하는 클래스
*/
public class HttpRequest {
private String method; // HTTP 메서드 (GET, POST 등)
private String path; // 요청 경로
private final Map<String, String> queryParameters = new HashMap<>(); // 쿼리 파라미터
private final Map<String, String> headers = new HashMap<>(); // 헤더 정보
/**
* HttpRequest 생성자
* @param reader 클라이언트로부터 입력을 읽는 BufferedReader
* @throws IOException 요청 파싱 실패 시 예외 발생
*/
public HttpRequest(BufferedReader reader) throws IOException {
parseRequestLine(reader); // 요청 라인 파싱
parseHeaders(reader); // 헤더 파싱
// 메시지 바디는 이후 필요에 따라 추가 처리
}
/**
* 요청 라인 파싱
* 요청 형식: METHOD PATH HTTP_VERSION
* 예: GET /search?q=hello HTTP/1.1
*/
private void parseRequestLine(BufferedReader reader) throws IOException {
String requestLine = reader.readLine();
if (requestLine == null) { // 연결만 하고 데이터 전송없이 연결 종료시 null 반환
throw new IOException("EOF: No request line received");
}
String[] parts = requestLine.split(" ");
if (parts.length != 3) {
throw new IOException("Invalid request line: " + requestLine);
}
method = parts[0]; // HTTP 메서드
String[] pathParts = parts[1].split("\\?");
path = pathParts[0]; // 경로 추출
if (pathParts.length > 1) {
parseQueryParameters(pathParts[1]); // 쿼리 문자열 파싱
}
}
/**
* 쿼리 파라미터 파싱
* @param queryString 쿼리 문자열 (예: q=hello&lang=en)
*/
private void parseQueryParameters(String queryString) {
for (String param : queryString.split("&")) {
String[] keyValue = param.split("=");
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8); // 키 디코딩
String value = keyValue.length > 1 ?
URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : ""; // 값 디코딩
queryParameters.put(key, value);
}
}
/**
* 헤더 파싱
* @param reader 클라이언트 입력
* @throws IOException 헤더 파싱 실패 시 예외 발생
*/
private void parseHeaders(BufferedReader reader) throws IOException {
String line;
while (!(line = reader.readLine()).isEmpty()) {
String[] headerParts = line.split(":");
headers.put(headerParts[0].trim(), headerParts[1].trim()); // 헤더 키-값 저장
}
}
// GETTER 메서드
public String getMethod() {
return method;
}
public String getPath() {
return path;
}
public String getParameter(String name) {
return queryParameters.get(name);
}
public String getHeader(String name) {
return headers.get(name);
}
@Override
public String toString() {
return "HttpRequest{" +
"method='" + method + '\'' +
", path='" + path + '\'' +
", queryParameters=" + queryParameters +
", headers=" + headers +
'}';
}
}
🔷 HttpResponse
/**
* HTTP 응답을 작성하는 클래스
*/
public class HttpResponse {
private final PrintWriter writer; // 클라이언트에 데이터를 전송하는 PrintWriter
private int statusCode = 200; // HTTP 상태 코드 (기본값: 200 OK)
private final StringBuilder bodyBuilder = new StringBuilder(); // 응답 바디를 저장하는 StringBuilder
private String contentType = "text/html; charset=UTF-8"; // 기본 콘텐츠 타입
/**
* HttpResponse 생성자
* @param writer 클라이언트 출력 스트림
*/
public HttpResponse(PrintWriter writer) {
this.writer = writer;
}
/**
* 상태 코드 설정
* @param statusCode HTTP 상태 코드 (예: 200, 404)
*/
public void setStatus(int statusCode) {
this.statusCode = statusCode;
}
/**
* 콘텐츠 타입 설정
* @param contentType MIME 타입 (예: text/html, application/json)
*/
public void setContentType(String contentType) {
this.contentType = contentType;
}
/**
* 응답 바디 작성
* @param body 클라이언트에 보낼 응답 내용
*/
public void writeBody(String body) {
bodyBuilder.append(body);
}
/**
* HTTP 응답 전송
*/
public void flush() {
String body = bodyBuilder.toString(); // 응답 바디를 문자열로 변환
int contentLength = body.getBytes(UTF_8).length; // UTF-8로 인코딩된 바디 길이 계산
// 응답 헤더 작성
writer.println("HTTP/1.1 " + statusCode + " " + getReasonPhrase(statusCode));
writer.println("Content-Type: " + contentType);
writer.println("Content-Length: " + contentLength);
writer.println(); // 빈 줄로 헤더와 바디 구분
// 응답 바디 전송
writer.print(body);
writer.flush(); // 모든 내용 플러시
}
/**
* 상태 코드에 따른 메시지 반환
* @param statusCode HTTP 상태 코드
* @return 상태 메시지 (예: 200 -> "OK", 404 -> "Not Found")
*/
private String getReasonPhrase(int statusCode) {
return switch (statusCode) {
case 200 -> "OK";
case 400 -> "Bad Request";
case 404 -> "Not Found";
case 500 -> "Internal Server Error";
default -> "Unknown Status";
};
}
public void setStatusCode(int i) {
this.statusCode = i;
}
}
🟢 주요 구조와 흐름
- HttpServerV4:
- 서버의 진입점으로 클라이언트 요청을 수신.
- 소켓(Socket)을 통해 연결을 관리하며, 각 연결을 스레드풀(ExecutorService)에서 처리.
- 요청을 HttpRequestHandlerV4에 전달.
- HttpRequestHandlerV4:
- 클라이언트의 요청을 읽고 파싱하여 HttpRequest 객체로 변환.
- 응답을 작성할 HttpResponse 객체 생성.
- 요청 URL에 따라 적합한 서비스 메서드 호출 (home, site1, search, 등).
- 최종적으로 HttpResponse.flush()를 호출하여 응답을 전송.
🔷 HttpServerV4
public class HttpServerV4 {
private final ExecutorService es = Executors.newFixedThreadPool(10);
private final int port;
public HttpServerV4(int port) {
this.port = port;
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
log("서버 시작 port: " + port);
while (true) {
Socket socket = serverSocket.accept(); // 클라이언트 연결 요청 수락
es.submit(new HttpRequestHandlerV4(socket)); // 요청을 읽어 객체로 변환
}
}
}
🔷 HttpRequestHandlerV4
// 객체화된 요청 및 응답 처리
import was.httpserver.HttpRequest;
import was.httpserver.HttpResponse;
/**
* HTTP 요청을 처리하는 Runnable 구현체
* 클라이언트 소켓에서 받은 요청을 파싱하고, 경로에 따라 적절한 응답을 생성 및 전송하는 역할
* (클라이언트의 요청이 오면 요청 정보를 기반으로 `HttpRequest` 객체를 만들어둔다. 이때 `HttpResponse` 도 함께 만든다.)
*/
public class HttpRequestHandlerV4 implements Runnable {
private final Socket socket;
/**
* HttpRequestHandlerV4 생성자
* @param socket 클라이언트 소켓
*/
public HttpRequestHandlerV4(Socket socket) {
this.socket = socket;
}
/**
* Runnable 인터페이스의 run 메서드 구현
* 소켓을 처리하는 메서드를 호출
*/
@Override
public void run() {
try {
process(socket);
} catch (Exception e) {
log(e); // 예외 로그 출력
}
}
/**
* 소켓을 통해 HTTP 요청을 처리
* @param socket 클라이언트 소켓
* @throws IOException 입출력 예외 처리
*/
private void process(Socket socket) throws IOException {
try (
socket; // 소켓 자동 닫기
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), UTF_8)
);
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), UTF_8), false
)
) {
// 요청 및 응답 객체 생성
HttpRequest request = new HttpRequest(reader);
HttpResponse response = new HttpResponse(writer);
// favicon 요청은 무시
if (request.getPath().equals("/favicon.ico")) {
log("favicon 요청");
return;
}
// 요청 정보 로그 출력
log("HTTP 요청 정보 출력");
System.out.println(request);
// 요청 경로에 따라 응답 처리
switch (request.getPath()) {
case "/site1" -> site1(response);
case "/site2" -> site2(response);
case "/search" -> search(request, response);
case "/" -> home(response);
default -> notFound(response);
}
// 응답 전송
response.flush();
log("HTTP 응답 전달 완료");
}
}
/**
* 홈 경로 처리
* @param response 응답 객체
*/
private static void home(HttpResponse response) {
response.writeBody("<h1>home</h1>");
response.writeBody("<ul>");
response.writeBody("<li><a href='/site1'>site1</a></li>");
response.writeBody("<li><a href='/site2'>site2</a></li>");
response.writeBody("<li><a href='/search?q=hello'>검색</a></li>");
response.writeBody("</ul>");
}
/**
* /site1 경로 처리
* @param response 응답 객체
*/
private static void site1(HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
/**
* /site2 경로 처리
* @param response 응답 객체
*/
private static void site2(HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
/**
* 검색 요청 처리 (추가 구현 필요)
* @param request 요청 객체
* @param response 응답 객체
*/
private static void search(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>검색 결과</h1>");
response.writeBody("<p>추가 구현 필요</p>");
}
/**
* 404 Not Found 처리
* @param response 응답 객체
*/
private static void notFound(HttpResponse response) {
response.setStatus(404);
response.writeBody("<h1>404 Not Found</h1>");
}
}
⬛ 커맨드 패턴
🟢 커맨드 패턴 도입 목적:
- HTTP 서버와 서비스 로직의 명확한 분리.
- URL 경로에 따라 실행되는 기능(서비스 로직)을 관리 및 확장 가능하도록 구조화.
🟢 주요 구성 요소
- HttpServlet (인터페이스):
- 모든 서비스 로직의 기본 인터페이스.
- service(HttpRequest request, HttpResponse response) 메서드를 구현하여 요청 처리.
- 서비스 서블릿:
- HomeServlet, Site1Servlet, SearchServlet 등 URL 경로별 로직 구현체.
- 각각의 클래스가 HttpServlet 인터페이스를 구현하여 특정 URL 요청 처리.
- 공용 서블릿:
- NotFoundServlet, InternalErrorServlet, DiscardServlet 등 공통 처리 로직 구현.
- 예: NotFoundServlet은 404 에러 응답을 반환.
- ServletManager (서블릿 관리 클래스):
- URL 경로와 서블릿 매핑(servletMap 사용).
- URL 요청에 따라 해당 서블릿을 실행.
- 기본 서블릿(defaultServlet)과 에러 서블릿(notFoundErrorServlet, internalErrorServlet) 관리.
- HttpRequestHandler:
- 요청 처리 흐름의 간소화:
- 요청 데이터는 HttpRequest로 구조화.
- 응답 데이터는 HttpResponse를 통해 생성.
- ServletManager.execute()로 서블릿 호출.
- 요청 처리 흐름의 간소화:
🟢 실행 흐름
- 클라이언트 요청 → HttpRequestHandler가 ServletManager에 전달.
- ServletManager → URL 경로에 맞는 서블릿 실행.
- 서블릿(HttpServlet) → 요청 처리 및 응답 작성.
- 응답 데이터 → 클라이언트로 전송.
- 예외 발생 시 공용 서블릿(NotFoundServlet, InternalErrorServlet) 실행.
🔷 HttpServlet
import java.io.IOException;
public interface HttpServlet {
/**
* HttpRequest 를 통해서 HTTP 요청 정보를 꺼내고, HttpResponse 를 통해서 필요한 응답
* */
void service(HttpRequest request, HttpResponse response) throws IOException;
}
🔷 ServerMain
public class ServerMainV5 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
ServletManager servletManager = new ServletManager();
servletManager.add("/", new HomeServlet());
servletManager.add("/site1", new Site1Servlet());
servletManager.add("/site2", new Site2Servlet());
servletManager.add("/search", new SearchServlet());
servletManager.add("/favicon.ico", new DiscardServlet());
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}
🔷 ServletManager
import was.httpserver.servlet.InternalErrorServlet;
import was.httpserver.servlet.NotFoundServlet;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* ServletManager 는 HTTP 요청에 대해 적절한 HttpServlet 을 매핑하고 실행
* - 사용자 정의 서블릿, 기본 서블릿, 에러 서블릿을 관리합니다.
*/
public class ServletManager {
private final Map<String, HttpServlet> servletMap = new HashMap<>(); // URL 경로와 HttpServlet 매핑
private HttpServlet defaultServlet; // 기본 서블릿
private HttpServlet notFoundErrorServlet = new NotFoundServlet(); // 404 에러 서블릿
private HttpServlet internalErrorServlet = new InternalErrorServlet(); // 500 에러 서블릿
/**
* ServletManager 생성자
* - 기본 생성자로 서블릿 맵 초기화
*/
public ServletManager() {
}
/**
* URL 경로에 서블릿을 등록
* @param path URL 경로 (예: "/home")
* @param servlet 등록할 HttpServlet
*/
public void add(String path, HttpServlet servlet) {
servletMap.put(path, servlet);
}
/**
* 기본 서블릿 설정
* @param defaultServlet 기본으로 사용할 HttpServlet
*/
public void setDefaultServlet(HttpServlet defaultServlet) {
this.defaultServlet = defaultServlet;
}
/**
* 404 Not Found 에러 서블릿 설정
* @param notFoundErrorServlet 사용할 404 에러 서블릿
*/
public void setNotFoundErrorServlet(HttpServlet notFoundErrorServlet) {
this.notFoundErrorServlet = notFoundErrorServlet;
}
/**
* 500 Internal Server Error 서블릿 설정
* @param internalErrorServlet 사용할 500 에러 서블릿
*/
public void setInternalErrorServlet(HttpServlet internalErrorServlet) {
this.internalErrorServlet = internalErrorServlet;
}
/**
* 요청에 따라 적절한 서블릿을 실행
* @param request HttpRequest 객체 (요청 정보)
* @param response HttpResponse 객체 (응답 작성)
* @throws IOException 입출력 오류 발생 시 예외
*/
public void execute(HttpRequest request, HttpResponse response) throws IOException {
try {
// 요청 경로에 등록된 서블릿 가져오기, 없으면 기본 서블릿 사용
HttpServlet servlet = servletMap.getOrDefault(request.getPath(), defaultServlet);
// 서블릿이 없으면 404 PageNotFoundException 발생
if (servlet == null) {
throw new PageNotFoundException("Request URL not found: " + request.getPath());
}
// 적절한 서블릿 실행
servlet.service(request, response);
} catch (PageNotFoundException e) {
// 404 에러 처리
e.printStackTrace();
notFoundErrorServlet.service(request, response);
} catch (Exception e) {
// 500 에러 처리
e.printStackTrace();
internalErrorServlet.service(request, response);
}
}
}
🟢 정리
- HTTP 서버의 기본 동작 원리: 요청 수신 → 요청 파싱 → 응답 생성 → 클라이언트로 전송.
- 구조화 설계: HTTP 관련 코어 로직(HttpServer, HttpRequest)과 서비스 로직(HttpServlet) 분리.
- 확장성 확보: 커맨드 패턴과 서블릿 매니저를 활용한 유연한 설계.
- 객체화로 유지보수성 향상: 요청과 응답을 구조화하여 코드 중복 제거 및 재사용성 강화.
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 2편 섹션14 애노테이션 (4) | 2024.11.28 |
---|---|
[Java] 김영한의 실전 자바 - 고급 2편 섹션13 리플렉션 (0) | 2024.11.25 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션6 네트워크,자원 정리 (0) | 2024.11.04 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션5 네트워크 기본이론 / HTTP (2) | 2024.10.31 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션5 File,Files (0) | 2024.10.28 |