HTTP 서버 활용
#Java/adv2/HTTP
/HTTP 서버7 - 애노테이션 서블릿1 - 시작 : 애노테이션을 이용하여 ReflectionServlet의 문제점을 개선해보자.
애노테이션
@Retention( RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Mapping {
String value();
}
AnnotationServlet
public class AnnotationServletV1 implements HttpServlet {
private final List<Object> controllers;
public AnnotationServletV1(List<Object> controllers) {
this.controllers = controllers;
}
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();
for (Object controller : controllers) {
Method[] methods = controller.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Mapping.class)) {
Mapping mapping = method.getAnnotation(Mapping.class);
String value = mapping.value();
if (value.equals(path)) {
invoke(controller, method, request, response);
return;
}
}
}
}
throw new PageNotFoundException("request=" + path);
}
private void invoke(Object controller, Method method, HttpRequest request, HttpResponse response) {
try {
method.invoke(controller, request, response);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
- 리플렉션과 비슷하지만 차이점: 메서드의 이름과 path를 비교하는 대신, 메서드에서
@Mapping
애노테이션이 존재하는 메서드의 value와 path를 비교한다.
애노테이션을 적용한 컨트롤러
public class SiteControllerV7 {
@Mapping("/")
public void home(HttpRequest request, 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>");
}
@Mapping("/site1")
public void site1(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
@Mapping("/site2")
public void site2(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
}
- 이제
/
과 같이 처리하기 어려운 경로도 애노테이션을 통해 메서드와 매핑할 수 있다. - 애노테이션을 적용했기 때문에, 메서드 이름도 원하는 이름으로 변경해도 된다.
public class ServerMainV7 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
List<Object> controllers = List.of(
new SiteControllerV7(),
new SearchControllerV7()
);
HttpServlet servlet = new AnnotationServletV1(controllers);
ServletManager servletManager = new ServletManager();
servletManager.setDefaultServlet(servlet);
servletManager.add("/favicon.ico", new DiscardServlet());
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}
- 애노테이션 서블릿을 만들고 servletManager에 디폴트 서블릿으로 등록하자.
/HTTP 서버8 - 애노테이션 서블릿2 - 동적 바인딩
- 서버7에서 컨트롤러의 메서드를 보면,
HttpRequest request
가 항상 필요한게 아니다. 메서드가 필요한 파라미터만 유연하게 받을 수 있도록AnnotationServletV1
의 기능을 개선해보자.- 서버7에서 만약 메서드에 필요한 파라미터만 선언한다면, 메서드를 호출할 때 오류가 발생한다.
invoke()
하는 부분을 개선해보자.
- 서버7에서 만약 메서드에 필요한 파라미터만 선언한다면, 메서드를 호출할 때 오류가 발생한다.
컨트롤러에서는 필요한 매개변수만 선언해준다.
public class SiteControllerV8 {
@Mapping("/")
public 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>");
}
@Mapping("/site1")
public void site1(HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
@Mapping("/site2")
public void site2(HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
}
invoke()
부분을 수정해보자.
public class AnnotationServletV2 implements HttpServlet {
// 기존과 동일
// ...
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);
}
}
- 리플렉션을 통해 메서드에 존재하는 파라미터가 무엇인지 확인한다.
- 메서드의 파라미터 타입을 확인한 후에 각 타입에 맞는 값을
args[]
에 담아서 메서드를 호출한다.
이제 컨트롤러의 메서드는 자신에게 필요한 값만 선언하고, 전달 받을 수 있다. 이런 기능을 확장하면 HttpRequest
, HttpResponse
뿐만 아니라 다양한 객체들도 전달할 수 있다. (스프링 MVC도 이런 방식으로 매개변수 값을 전달한다.)
/HTTP 서버9 - 애노테이션 서블릿3 - 성능 최적화 : 서버8의 아쉬운 점(성능 최적화, 중복 매핑 문제)를 개선해보자.
- 문제1: 성능 최적화
- 모든 컨트롤러의 메서드를 하나하나 순서대로 찾는다. 모든 컨트롤러의 메서드 개수가 n개라면, 이것은 결과적으로 O(n)의 성능을 보인다.
- 더 큰 문제는 고객의 요청 때 마다 이 로직이 호출된다는 점이다. 동시에 100명의 고객이 요청하면 100 * n번 해당 로직이 호출된다.
- 문제2: 중복 매핑 문제
@Mapping("/site2") public void site2(HttpResponse response) {
@Mapping("/site2") public void page2(HttpResponse response) {
- 현재 로직에서는 먼저 찾은 메서드가 호출되는데, 이런 모호한 문제는 반드시 제거해야 한다. 개발자가 실수하지 않도록 컴파일 시점이나 실행 시점에 오류가 발생하도록 해야 한다.
개선한 코드
public class AnnotationServletV3 implements HttpServlet {
private final Map<String, ControllerMethod> pathMap;
public AnnotationServletV3(List<Object> controllers) {
this.pathMap = new HashMap<>();
initializePathMap(controllers);
}
private void initializePathMap(List<Object> controllers) {
for (Object controller : controllers) {
for (Method method : controller.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(Mapping.class)) {
String path = method.getAnnotation(Mapping.class).value();
// 중복 경로 체크
if (pathMap.containsKey(path)) {
ControllerMethod controllerMethod = pathMap.get(path);
throw new IllegalArgumentException(
"경로 중복 등록, path=" + path +
", method=" + method +
", 이미 등록된 메서드=" + controllerMethod.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);
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;
}
public void invoke(HttpRequest request, HttpResponse response) {
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);
}
}
}
}
초기화
- 프로그램이 실행되는 시점에
key(경로): value(ControllerMethod)
로 미리 초기화를 해둔다. (HashMap)- 컨트롤러와 메서드를
ControllerMethod
라는 클래스로 캡슐화하고, 이 클래스에invoke(request, response)
메서드를 선언한다. - 이렇게 하면
ControllerMethod
객체를 사용해서 편리하게 실제 메서드를 호출할 수 있다. if (pathMap.containsKey(path))
를 통하여 중복된 경로가 존재하는지 체크한다.- 이미 등록된 경로가 있으면 예외를 던진다.
- 컨트롤러와 메서드를
- 이후 오는 요청에 대해서는 O(1)로 경로에 매핑된 컨트롤러의 메서드를 찾아 호출할 수 있다.
'Java > Reflection, Annotation' 카테고리의 다른 글
[Java] 55. 애노테이션 (0) | 2025.07.01 |
---|---|
[Java] 54. 리플렉션(Reflection) (0) | 2025.07.01 |