⬛ 애노테이션 기반 서블릿 설계
🟢 애노테이션 서블릿
- 애노테이션 활용: @Mapping 애노테이션으로 URL 경로와 메서드를 연결하여 간결하고 효율적인 매핑을 구현.
- 리플렉션 사용: 메서드의 애노테이션 값을 기반으로 호출할 메서드를 동적으로 탐색.
- 유연성과 효율성: 필요한 파라미터만 메서드에 전달하도록 설계(AnnotationServletV2), 해시맵.
처음엔 잘 구조가 커서 파악이 잘 안됐는데, 계층적 탐색이라고 생각하니 이해가 쉽게 됐다.
큰 폴더에서 세부 폴더로 점점 구체적인 파일을 찾아가는 과정과 비슷하다!
- HttpServer:
- HTTP 요청을 받아 ServletManager를 통해 적절한 서블릿으로 전달.
- ServletManager:
- URL 경로에 따라 요청을 적합한 서블릿(AnnotationServletV3)에 매핑.
- 기본 서블릿(defaultServlet)과 특정 경로(Error) 매핑 관리.
- AnnotationServletV3:
- pathMap을 통해 URL 경로와 컨트롤러 메서드(ControllerMethod)를 매핑.
- 요청 시 pathMap에서 URL에 해당하는 메서드를 찾아 실행.
- ControllerMethod:
- 특정 컨트롤러 객체와 메서드를 캡슐화하여 요청에 따라 호출 가능.
- SiteControllerV8/SearchControllerV8:
- 사용자 요청 처리 로직을 정의.
- @Mapping으로 URL과 메서드 간 매핑 설정.
이를 쉽게 비유하자면 아래와같다.
- HttpServer: 접수 데스크 (전체 흐름 관리).
- ServletManager: 메뉴 관리자 (적합한 부서 연결).
- AnnotationServletV3: 주문서 처리 시스템 (요청-메서드 어노테이션 기반 매핑).
- ControllerMethod: 요리사 (요리 실행).
- Controller(SiteControllerV8 / SearchControllerV8): 요리 부서 (특정 요청 처리).
🔷 Mapping
package was.httpserver.servlet.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Mapping {
String value();
}
🔷 HttpServer
package was.v8;
import java.io.IOException;
import java.util.List;
import was.httpserver.HttpServer;
import was.httpserver.HttpServlet;
import was.httpserver.ServletManager;
import was.httpserver.servlet.DiscardServlet;
import was.httpserver.servlet.annotation.AnnotationServletV3;
import was.v7.SearchControllerV7;
import was.v7.SiteControllerV7;
public class ServerMainV8 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
// 1. 컨트롤러 객체 리스트 생성
List<Object> controllers = List.of(new SiteControllerV7(),new SearchControllerV7());
// 2. Annotation 기반 서블릿 생성
HttpServlet servlet = new AnnotationServletV3(controllers);
// 3. 서블릿 매니저 초기화
// HTTP 요청 경로와 서블릿 객체를 매핑하고 관리하는 역할을 담당
ServletManager servletManager = new ServletManager();
// 4. 기본 서블릿 설정
servletManager.setDefaultServlet(servlet);
// 5. 특정 경로에 대한 서블릿 매핑
// "/favicon.ico" 경로로 들어오는 요청을 DiscardServlet으로 처리하도록 설정
servletManager.add("/favicon.ico", new DiscardServlet());
// HTTP 서버 초기화 및 시작
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}
🔷 ServletManager
- URL 요청에 따라 어떤 서블릿이 처리해야 할지 결정하는 로직
package was.httpserver;
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);
}
}
}
🔷 AnnotationServletV3 (ControllerMethod)
- 애노테이션(@Mapping) 기반으로 URL과 메서드를 매핑
package was.httpserver.servlet.annotation;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import was.httpserver.HttpRequest;
import was.httpserver.HttpResponse;
import was.httpserver.HttpServlet;
import was.httpserver.PageNotFoundException;
public class AnnotationServletV3 implements HttpServlet {
private final Map<String, ControllerMethod> pathMap; // {요청 경로 : [컨트롤러 메서드] }
public AnnotationServletV3(List<Object> controllers) {
this.pathMap = new HashMap<>();
initializePathMap(controllers); // 경로 맵 초기화
}
// 경로 맵 초기화: 컨트롤러의 메서드들을 순회하며 @Mapping 어노테이션이 붙은 메서드를 경로에 매핑
private void initializePathMap(List<Object> controllers) {
for (Object controller : controllers) { // 각 컨트롤러를 순회
for (Method method : controller.getClass().getDeclaredMethods()) { // 컨트롤러의 모든 메서드 가져오기
if (method.isAnnotationPresent(Mapping.class)) { // @Mapping 어노테이션이 붙은 메서드만 처리
String path = method.getAnnotation(Mapping.class).value(); // 어노테이션의 경로 값 가져오기
// 중복 경로 체크: 동일 경로가 이미 등록되어 있으면 예외 발생
if (pathMap.containsKey(path)) {
ControllerMethod existingMethod = pathMap.get(path);
throw new IllegalArgumentException("경로 중복 등록, path=" + path +
", method=" + method + ", 이미 등록된 메서드=" + existingMethod.method);
}
// 경로-컨트롤러 메서드 매핑 저장
pathMap.put(path, new ControllerMethod(controller, method));
}
}
}
}
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath(); // 클라이언트 요청 경로 가져오기
ControllerMethod controllerMethod = pathMap.get(path); // 경로에 해당하는 메서드 찾기
// 경로에 매핑된 메서드가 없으면 404 예외 발생
if (controllerMethod == null) {
throw new PageNotFoundException("request=" + path);
}
// 매핑된 메서드 실행
controllerMethod.invoke(request, response);
}
// 컨트롤러,메서드 관리
private static class ControllerMethod {
private final Object controller;
private final Method method;
public ControllerMethod(Object controller, Method method) {
this.controller = controller;
this.method = method;
}
// 메서드 실행: HttpRequest와 HttpResponse를 파라미터로 전달
public void invoke(HttpRequest request, HttpResponse response) {
Class<?>[] parameterTypes = method.getParameterTypes(); // 메서드의 파라미터 타입 가져오기
Object[] args = new Object[parameterTypes.length]; // 메서드 호출에 필요한 인자 배열 생성
// 메서드 파라미터 매핑: HttpRequest와 HttpResponse 타입에 맞게 인자를 설정
for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i] == HttpRequest.class) {
args[i] = request; // HttpRequest 객체 매핑
} else if (parameterTypes[i] == HttpResponse.class) {
args[i] = response; // HttpResponse 객체 매핑
} else {
// 지원하지 않는 파라미터 타입이면 예외 발생
throw new IllegalArgumentException("Unsupported parameter type: " + parameterTypes[i]);
}
}
// 리플렉션을 사용하여 메서드 실행
try {
method.invoke(controller, args); // 컨트롤러와 인자를 사용하여 메서드 호출
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}
⬛ 동적 바인딩 (Dynamic Binding)
site1()은 HttpResponse만 받고, HttpRequest는 받지 않는다.
search()는 HttpRequest와 HttpResponse 둘 다 사용한다.
이러한 비효율성은, 동적바인딩을 통해 해결 가능하다.
🟢 동적 바인딩 (Dynamic Binding)
- 컨트롤러 메서드가 필요한 파라미터만 선택적으로 받을 수 있도록 설계.
- 모든 메서드가 HttpRequest와 HttpResponse를 강제로 받지 않게 개선.
- 메서드의 파라미터 타입을 분석하여 적절한 객체를 동적으로 제공.
구현 방식:
- 리플렉션을 사용해 메서드의 파라미터 타입 확인.
- HttpRequest, HttpResponse 타입만 파라미터로 전달.
- 불필요한 파라미터를 제외하고 필요한 값만 동적으로 생성해 전달.
private void invoke(HttpRequest request, HttpResponse response, Object controller, Method method) {
// 메서드의 파라미터 타입 분석
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i] == HttpRequest.class) {
args[i] = request; // 필요할 때만 전달
} else if (parameterTypes[i] == HttpResponse.class) {
args[i] = response; // 필요할 때만 전달
} else {
throw new IllegalArgumentException("Unsupported parameter type: " + parameterTypes[i]);
}
}
try {
method.invoke(controller, args); // 동적으로 메서드 호출
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
⬛ 성능 최적화
문제점
- 기존 방식(AnnotationServletV2)에서는 요청마다 모든 컨트롤러와 메서드를 순차적으로 탐색.
- O(n)의 성능으로, 메서드 수와 요청 수가 늘어나면 성능 저하.
개선 코드: 서버 시작 시 모든 URL과 메서드를 HashMap에 미리 매핑하여 O(1) 성능을 구현.
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();
ControllerMethod controllerMethod = pathMap.get(path);
if (controllerMethod == null) {
throw new PageNotFoundException("No mapping found for: " + path);
}
controllerMethod.invoke(request, response); // 매핑된 메서드 호출
}
⬛ 정리
- 구성 역할과 사용 역할의 분리
- MemberServerMain: 구성 역할을 담당. 각종 컴포넌트를 조립하고 설정.
- 예: 어떤 MemberRepository, 컨트롤러, HttpServlet을 사용할지 결정.
- 나머지 클래스: 사용 역할을 담당. 실제 기능(회원 관리 로직 등)을 제공.
- MemberServerMain: 구성 역할을 담당. 각종 컴포넌트를 조립하고 설정.
- 비유
- 레고 블록을 조립하거나, 컴퓨터의 CPU, 메모리, GPU를 선택해 조립하는 과정과 유사.
- 구성 역할 분리의 장점
- 유연성 향상: 필요 시 구성 요소를 쉽게 교체.
- 테스트 용이성: 각 컴포넌트를 독립적으로 테스트 가능.
- 코드 재사용성 증가: 독립적 컴포넌트는 다른 프로젝트에서 재사용 가능.
- 관심사의 분리: 구성과 비즈니스 로직을 명확히 분리해 가독성과 관리 용이.
- 유지보수 용이성: 특정 부분 수정 시 다른 부분에 영향을 최소화.
- 확장성 개선: 새로운 기능 추가 시 기존 코드 수정 없이 구성만 변경.
- 소프트웨어 설계 원칙 적용
- 다형성, 개방-폐쇄 원칙(OCP), 의존관계 주입(DI) 등 소프트웨어 설계 원칙 준수 필요.
- 이런 설계를 간소화하는 도구가 바로 스프링 프레임워크.
- 스프링 프레임워크의 역할
- 구성과 사용 역할 분리를 쉽게 구현하도록 지원.
- 실무 백엔드 개발에서 핵심 기술로 활용.
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 3편 섹션3 함수형 인터페이스 (0) | 2025.04.03 |
---|---|
[Java] 김영한의 실전 자바 - 고급 3편 섹션1,2 람다 (2) | 2025.03.31 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션14 애노테이션 (4) | 2024.11.28 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션13 리플렉션 (0) | 2024.11.25 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션12 HTTP 서버 만들기, 서블릿 (3) | 2024.11.21 |