강의 : 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
11. HTTP 서버 프로그램 - 애노테이션
리플렉션을 활용한 서버 프로그램은 요청 URL과 메서드 이름이 같을 때만 동작한다.
따라서 / 요청을 처리하기 위한 작업은 컨트롤러에 둘 수 없고 별도의 서블릿으로 구현해야 했다.
그리고 /site1이 와도 page1()과 같은 메서드를 호출 하듯이 요청 URL과 메서드 이름을 다르게 할 수 없었다.
이 문제를 해결하려면 애노테이션을 사용해야 한다.
그리고 site1(), site2() 같은 함수는 HttpRequest 파라메터를 필요로 하지 않는다.
리플렉션으로 메서드를 호출 할 때 동적으로 바인딩 하도록 리플렉션으로 함수를 호출하는 부분을 개선한다.
마지막으로 요청을 처리할 메서드를 찾기 위해 컨트롤러에 선언한 모든 메서드를 반복문으로 찾아야 하고
같은 요청을 처리하는 메서드가 중복되는 경우가 발생할 수 있다.
이 문제를 해결하기 위해 컨트롤러 메서드를 List로 관리하던 부분은 Map으로 관리하도록 수정한다.
11-1. Conroller
URL 정보로 호출할 메소드를 찾기 위해 Mapping 이라는 애노테이션을 정의한다. 컨트롤러의 메서드들에 Mapping 애노테이션을 추가한다. / URL은 Mapping(value = “/”) 애노테이션을 통해 home() 메서드를 호출할 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Mapping {
String value();
}
public class SiteController {
@Mapping(value = "/")
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(value="/site1")
public void site1(HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
@Mapping(value="/site2")
public void site2(HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
}
public class SearchController {
@Mapping(value = "/search")
public void search(HttpRequest request, HttpResponse response) {
String query = request.getQueryParameter("q");
response.writeBody("<h1>Search</h1>");
response.writeBody("<ul>");
response.writeBody("<li>query: " + query + "</li>");
response.writeBody("</ul>");
}
}
11-2. AnnotationServlet
AnnotationServlet가 초기화 할 때 컨트롤러들을 받고 pathMap에 Mapping 애노테이션의 value를 키 값으로 해서 메서드를 저장한다.
메서드를 호출 할 때 동적 바인딩으로 호출 할 수 있도록 ControllerMethod 클래스를 만들고 리플렉션으로 메서드를 호출할 때 메서드의 파라메터 정보를 확인해 호출하도록 만든다.
service() 메서드에서는 HttpRequest에서 URL path를 구하고 pathMap에서 호출할 ControllerMethod를 찾아 요청을 처리할 메서드를 호출한다.
public class AnnotationServlet implements HttpServer {
private final Map<String, ControllerMethod> pathMap;
public AnnotationServlet(List<Object> controllers) {
this.pathMap = new HashMap<>();
initializePathMap(controllers);
}
private void initializePathMap(List<Object> controllers) {
for (Object controller : controllers) {
Method[] methods = controller.getDeclaredMethods();
for (Method method : methods) {
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 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);
}
}
}
}
11-3. Server
Servlet과 관련된 부분만 수정하고 HttpServer에 대한 부분은 수정하지 않았다.
public class ServerMainV8 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
List<Object> controllers = List.of(new SiteController(), new SearchController());
ServletManager servletManager = new ServletManager();
servletManager.setDefaultServlet(new AnnotationServlet(controllers));
servletManager.add("/favicon.ico", new DiscardServlet());
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}