[Java] 56. HTTP 서버에 Reflection & Annotation 활용

2025. 7. 1. 17:05·Java/Reflection, Annotation

 

 

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() 하는 부분을 개선해보자.

컨트롤러에서는 필요한 매개변수만 선언해준다.

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)로 경로에 매핑된 컨트롤러의 메서드를 찾아 호출할 수 있다.

Ref) 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션 강의 | 김영한 - 인프런

'Java > Reflection, Annotation' 카테고리의 다른 글

[Java] 55. 애노테이션  (0) 2025.07.01
[Java] 54. 리플렉션(Reflection)  (0) 2025.07.01
'Java/Reflection, Annotation' 카테고리의 다른 글
  • [Java] 55. 애노테이션
  • [Java] 54. 리플렉션(Reflection)
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (456)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (129)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (5)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 56. HTTP 서버에 Reflection & Annotation 활용
상단으로

티스토리툴바