MappingRegistry

MappingRegistry는 아까 살펴본 AbstractHandlerMethodMapping의 내부 클래스다. MappingRegistryhandler method에 대한 모든 mapping을 유지 관리하고 lookup을 수행하는 method를 가지고 있고 동시성을 가진 접근을 가능하게 해주는 레지스트리다.

A registry that maintains all mappings to handler methods, exposing methods to perform lookups and providing concurrent access.
Package-private for testing purposes.

가장 중요한 부분이 handler method에 대한 모든 mapping을 유지 관리하고 lookup을 수행하는 method를 가지고 있다는 것이다.

class MappingRegistry {
  private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

  private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

  private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

  private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

  private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

  private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  ...
  ...

  // methods
}

MappingRegistry 안에서 Map 자료구조를 가진 멤버 변수들이 있다. 그 중에서LinkedMultiValueMap이라는 자료구조를 사용한다. 이건 한개의 key에 여러 value들을 저장하는 MultiValueMapLinkedHashMap으로 감싼 자료구조로 Spring이 만든 자료구조다.

urlLookupLinkedMultiValueMap의 자료구조인데, key는 url을 가지고, value는 RequestMappingInfo를 가진다. LinkedMultiValueMap을 쓰는 이유는 하나의 url에 여러 handlerMethod들에 대한 정보가 담기기 때문이다.

예를 들어 "/app/user"라는 url 아래 user에 대한 정보를 조회하는 GET,user를 추가하는 POST가 매핑될때, 아래처럼 RequestMappingInfo가 들어가는 것이다.

key : "/app/user/ 
value : [GET /app/user,POST /app/user]

위와 같은 구조를 통해 MappingRegistryurl에 해당하는 handlerMethod를 구별할 수 있게 된다. 코드로 보자.

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  List<Match> matches = new ArrayList<>();
  List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
  if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
  }
  ...
  ...
  if (!matches.isEmpty()) {
    ...
    ...
    matches.sort(comparator);
    Match bestMatch = matches.get(0);
    ...
    ...
    request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
    handleMatch(bestMatch.mapping, lookupPath, request);
    return bestMatch.handlerMethod;
  }

위는 아까 잠깐 언급한 lookupHandlerMethod이다. 적절한 handlerMethod를 가져온 후 return 한다 고 했는데 그 과정이 담겨있다.
길다고 겁먹지 말고 한줄씩 보자. (match되는 것이 없거나, 2개 이상인 경우는 제외함)

List<Match> matches = new ArrayList<>();

match를 담는 matches라는 리스트가 있다.

List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);

현재 url에 mapping되는 handler method들의 RequestMappingInfo들을 getMappingsByUrl로 가져온 후 directPathMatches에 저장한다. 예를 들어 url/app/user이면 directPathMatches에는 [GET /app/user, POST /app/user] 와 같은 정보가 들어오는 것이다.

if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
}

그 후 [GET /app/user, POST /app/user] 중에서 request 정보와 일치하는 것들을 addMatchingMappings을 통해서 matches에 추가한다.

matches.sort(comparator);
Match bestMatch = matches.get(0);

matches들을 우선순위에 맞게끔 정렬하고, request와 가장 일치하는 0번째 matchbestMatch에 저장한다.

request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;

bestMatch의 멤버인 handlerMethod를 return해서 최종적으로 적합한 handler method를 찾게 된다.

private class Match {

  private final T mapping;

  private final HandlerMethod handlerMethod;

}

Reflection

이제 마지막 궁금증만이 남았다.

출처

MappingReigstry javadoc
LinkedMultiValueMap javadoc

본 글은 Do I need an interface with Spring boot?을 번역한 글입니다.

잘 쓰여진 글을 정리 하는 겸 한글로 공유하고 싶어서 번역했습니다.

들어가면서

Spring boot를 사용하다보면, 종종 service (@Service annotation을 붙인 bean)을 사용하게 된다. 인터넷 상의 많은 예시에서, 사람들이 service들을 위해서 interface를 사용하는 걸 볼 수 있을것이다. 예를 들어서 , 우리가 todo 어플리케이션을 만든다고 할때, TodoService라는 interfaceTodoServiceImpl이라는 구현체를 만들때가 있다.

이 포스트에서, 우리는 왜 그런 것을 하는지와 필요한가에 대해서 알아볼 것이다.

짧은 결론은

짧은 결론은 꽤나 간단하다. interface를 만들 필요가 없다.
service를 만든다고 하면, class의 자체의 이름을 TodoService라고 하고 autowire를 통해서 bean들에 주입하면 된다.
예를 들어서 이런 코드가 있다고 해보자.

@Service
public class TodoService {
    public List<Todo> findAllTodos() {
        // TODO: Implement
        return new ArrayList<>();
    }
}

@Component
public class TodoFacade {
    private TodoService service;

    public TodoFacade(TodoService service) {
        this.service = service;
    }
} 

위에 있는 예시는 @Autowired를 이용한 field injection을 사용하던 생성자 주입을 사용하던간에 작동할 것이다.

그럼 왜 신경써야할까?

만약, 우리가 그게 필요하지 않다면... 왜 그런 방식(inteface를 이용한 방식)을 종종 쓰곤 할까?
음, 첫 번째 이유는 사실 좀 역사적인것이다. 하지만 그걸 살펴보기 전에 , Spring에서 annotation이 어떻게 작동하는지를 설명해야만 한다.

만약 @Cacheable같은 annotation을 사용한다고 하면, cache에서 결과를 얻을것이라고 예상할 수 있다. Spring에서 그것이 작동되는 방식은 bean들을 위한 proxy를 만들고 그 proxy들에 필요한 로직을 추가해주는것이다. 원래 스프링은 JDK dynamic proxies를 사용했다. 이 dynamic proxies는 오직 interface들만을 위해서 만들어졌고, 이것이 예전에는 interface를 작성해줘야 했던 이유다.

그러나, 10여 년 전부터 , Spring이 CGLIB proxying도 지원하기 시작했다. 이 proxy들은 별도의 interface를 필요로 하지 않는다. 심지어 Spring 3.2 버전부터는 CGLIB가 Spring에 내장되어 있어서 별도로 추가해줄 필요도 없다.

느슨한 결합

아마 두 번째 이유는 두 class 간의 느슨한 결합을 만들기 위해서 일 것이다. interface를 사용함으로써, service에 의존하는 class는 더 이상 service의 구현에 의존하지 않게 된다. 이것이 service를 독립적으로 사용할 수 있게 해준다. 예를 들어서 이런 코드가 있다.

public interface TodoService {
    List<Todo> findAllTodos();
}

@Service
public class TodoServiceImpl {
    public List<Todo> findAllTodos() {
        // TODO: Implement
        return new ArrayList<>();
    }
}

@Component
public class TodoFacade {
    private TodoService service;

    public TodoFacade(TodoService service) {
        this.service = service;
    }
}

그러나 위의 예시에서, 개인적인 의견으로 TodoFacadeTodoServiceImpl이 함께 한다고 생각한다. 여기서 interface를 추가하는건 추가적인 복잡도를 늘릴 수 있다. 개인적으로, 그만한 가치는 없어 보인다.

여러 방식의 구현

느슨한 결합이 유용한 부분은 여러 가지 구현체를 가질 때이다. 예를 들어서 TodoService가 두 가지 구현체를 가진다고 해보자. 하나는 todo 리스트를 메모리에서 가져오는 것이고, 하나는 DB와 같은 곳에서 가져오는 것이다.

public interface TodoService {
    List<Todo> findAllTodos();
}

@Service
public class InMemoryTodoServiceImpl implements TodoService {
    public List<Todo> findAllTodos() {
        // TODO: Implement
        return new ArrayList<>();
    }
}

@Service
public class DatabaseTodoServiceImpl implements TodoService {
    public List<Todo> findAllTodos() {
        // TODO: Implement
        return new ArrayList<>();
    }
}

@Component
public class TodoFacade {
    private TodoService service;

    public TodoFacade(TodoService service) {
        this.service = service;
    }
}

이런 경우에선 느슨한 결합이 매우 유용한데, TodoFacade가 todo가 메모리에 저장되어 있는지 DB에 저장되어 있는지 알 필요 없기 때문이다. 그건 Facade의 책임이 아니라 어플리케이션 설정의 책임이다.

원하는 것에 따라서 구현방식은 달라진다. 만약에 TodoFacade가 모든 구현체를 호출해야 한다면, collection을 주입해야 한다.

@Component
public class TodoFacade {
    private List<TodoService> services;

    public TodoFacade(TodoService services) {
        this.services = services;
    }
}

만약 구현체 중에 하나가 99%의 상황에서 사용되고 나머지들은 아주 특수한 경우에만 사용된다면, @Primary를 사용해라.

@Primary
@Service
public class DatabaseTodoServiceImpl implements TodoService {
    public List<Todo> findAllTodos() {
        // TODO: Implement
        return new ArrayList<>();
    }
}

@Primary를 사용함으로써, Spring container에게 TodoService에 의존성 주입을 해야할때, 이 구현체를 사용하라고 알려주는 것이다. 만약 다른 걸 사용해야 한다면, @Qualifier를 사용하거나 특정 구현체를 주입함으로써 명시적으로 설정해야 한다. 개인적으로 난 이런 방식을 분리된 @Configuration class에서 사용하는데, 그렇지 않으면 , TodoFacade를 또 다시 구현체에 관한 정보들로 오염시키기 때문이다.

예시 코드를 보자.

@Configuration
public class TodoConfiguration {
    @Bean
    // Using @Qualifier
    public TodoFacade todoFacade(@Qualifier("inMemoryTodoService") TodoService service) {
        return new TodoFacade(service);
    }

    @Bean
    // Or by using the specific implementation
    public TodoFacade todoFacade(InMemoryTodoService service) {
        return new TodoFacade(service);
    }
}

제어의 역전

느슨한 결합의 또 다른 방식은 IoC 혹은 제어의 역전이다. 개인적으로 서로에게 의존하는 여러 가지 module을 사용할 때 제어의 역전이 유용했다. 예를 들어서 OrderServiceCustomerService가 있다고 해보자. Customer는 자신의 profile을 삭제할 수 있고 그때 pending 상태의 order들은 취소되어야 한다. interface 없이 구현했다면, 이런 방식으로 할것이다.

@Service
public class OrderService {
    public void cancelOrdersForCustomer(ID customerId) {
        // TODO: implement
    }
}

@Service
public class CustomerService {
    private OrderService orderService;

    public CustomerService(OrderService orderService) {
        this.orderService = orderService;
    }

    public void deleteCustomer(ID customerId) {
        orderService.cancelOrdersForCustomer(customerId);
        // TODO: implement
    }
}

이렇게 한다면, 상황은 매우 나빠질 수 있다. 어플리케이션 내부의 domain들이 모두 결합되게 되고, 결과적으로 강하게 결합된 어플리케이션을 만들게 될것이다.

그러는 대신에, CustomerDeletionListener라는 interface를 만들 수 있다.

public interface CustomerDeletionListener {
    void onDeleteCustomer(ID customerId);
}

@Service
public class CustomerService {
    private List<CustomerDeletionListener> deletionListeners;

    public CustomerService(List<CustomerDeletionListener> deletionListeners) {
        this.deletionListeners = deletionListeners;
    }

    public void deleteCustomer(ID customerId) {
        deletionListeners.forEach(listener -> listener.onDeleteCustomer(customerId));
        // TODO: implement
    }
}

@Service
public class OrderService {
    public void cancelOrdersForCustomer(ID customerId) {
        // TODO: implement
    }
}

@Component
public class OrderCustomerDeletionListener implements CustomerDeletionListener {
    private OrderService orderService;

    public OrderCustomerDeletionListener(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void onDeleteCustomer(ID customerId) {
        orderService.cancelOrdersForCustomer(customerId);
    }
}

예시를 보면, 제어의 역전이 일어난 것을 볼 수 있다. 첫 번째 예시에서 우리가 OrderService 안에 있는 cancelOrderForCustomer()를 바꾸면, CustomerService 역시 바뀌어야 한다. 이 말은 OrderService가 제어되고 있다는 것을 말한다.

두 번째 예시에서는 OrderService가 제어되고 있지 않다. 우리가 cancelOrderForCustomer()를 변화시키면, 다른 module의 일부인 오직 OrderCustomerDeletionListener만 바뀌어야 한다. 이것은 CustomerService가 제어하고 있음을 말한다. 또, 두 service들은 느슨하게 결합되어 있기 때문에, 하나가 다른 하나에 직접적으로 의존하고 있지 않다.

비록 두 번째 방법이 복잡도를 더 늘리긴 하지만 (classinterface가 각각 한개씩 늘었으니) domain들이 서로 결합되지 않게 해준다. 리팩토링 하기가 쉬워지는 것이다. 이 listenerevent-driven한 구조로 리팩토링 될 수 있다. domain-driven modular design이나 MSA같은 구조로 리팩토링하기 쉽게 해주는 것이다.

Test

마지막으로 말하고 싶은 건 테스트다. 몇몇 사람들은 dummy 구현체를 가지기 위해서 (여러 구현체를 가질 수 있으니) interface가 필요하다고 주장하곤 한다. 하지만 Mockito같은 mocking 라이브러리가 이 문제를 해결해 준다.

단위 테스트를 작성할 때, MockitoExtension을 사용할 수 있다.

@ExtendWith(MockitoExtension.class)
public class TodoFacadeTest {
    private TodoFacade facade;
    @Mock
    private TodoService service;

    @BeforeEach
    void setUp() {
        this.facade = new TodoFacade(service);
    }

    // TODO: implement tests
}

이 방법은 service가 무엇을 하는지 몰라도 facade를 적절히 테스트할 수 있게 해준다. Mockito.when()을 사용함으로써 service mock이 무엇을 반환하게 하는지 제어할 수 있고, Mockito.verfiy()를 사용함으로써 특정 method가 호출되었는지 확인할 수 있다.
예시 코드다.

@Test
void findAll_shouldUseServicefindAllTodos() {
    Todo todo = new Todo();
    when(service.findAllTodos()).thenReturn(todo);
    assertThat(facade.findAll()).containsOnly(todo);
    verify(service).findAllTodos();
}

심지어 Spring container를 필요로 하는 통합 테스트를 작성할때도,@MockBean annotation을 이용해서 bean들을 mock할 수 있다. 실제 구현체가 있는 package를 탐색하지 않게 해라.

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = TodoFacade.class)
public class TodoFacadeTest {
    @Autowired
    private TodoFacade facade;
    @MockBean
    private TodoService service;
}

그러니까 대부분의 경우에서, 테스트 할때 interface는 필요하지 않다.

결론

만약 개인적으로 interfaceserivce에 사용해야 하냐는 질문을 받는다면, 내 대답은 아니오다. 유일한 예외는 제어의 역전을 사용하거나 여러개의 구현체를 신경써야 하는 경우다.

만약의 경우를 위해서 interface를 만드는 게 좋지 않겠냐고 생각할 수 있다. 개인적으로 여전히 아니오다.
첫 번째로, "You aren't going to need it"(YAGNI) 라는 원칙을 믿는다. 필요할지도 몰라 라는 이유로 복잡성을 높일 이유는 없는데 , 일반적으로 필요하지 않기 때문이다.
두 번째로 필요한 경우라도 전혀 문제 없다. 대부분의 IDE들은 기존의 class에서 method만 추출해서 interface를 만들수 있게 해주고, 모든 코드들을 그 interface를 사용하게끔 순식간에 만든다.

참고하면 좋은 자료

JDK Dynamic Proxy와 CGLIB의 차이점은 무엇일까?
퍼사드 패턴
느슨한 결합 vs 긴밀한 결합
How Mockito Works?
소프트웨어 개발 3대 원칙 : KISS,YAGNI,DRY

스프링 부트에 interface가 필요한가에 대해서는 당연히 YES지만 이 글에선 serviceinterface 가 필요한지, 정확히 말하면 service의 구현체가 필요한지에 대해서 논하고 있습니다.

프로젝트를 시작하면서 Spring boot에서 구현체와 인터페이스를 구분해야하는지 고민이 많았는데 꽤나 자세하고 명쾌해서 도움이 되었습니다.

소프트웨어공학 수업에서 배운 YAGNI를 실제로 보니까 반갑네요. 그냥 무지성으로 외웠는데..

글이 매우 복잡하고 깁니다. 양해 부탁드립니다.

틀린 정보나 이해가 가지 않는 부분은 댓글 남겨주시면 참고하겠습니다.

HandlerMapping의 역할

Spring MVC에 대해서 공부하던 중, HandlerMapping이 request를 처리하기에 적절한 handler를 찾아온다는 설명을 들었다.
좀 더 찾아보니 HandlerMappingrequest의 URL과 매칭되는 handler를 선택하는 역할을 수행한다 는 것을 보았다.

request의 URL만 보고 어떻게 찾아온다는 것일까? 그리고 찾아진 handler는 method인데 어떠한 방식으로 가져온다는 것일까?

한가지만 기억하고 가자.
HandlerMapping은 원하는 handler를 찾아오는 역할을 수행한다.

Spring MVC Request flow

HandlerMapping의 역할에 대해서 살펴보기 전에 Spring MVC에서 request가 어떠한 순서로 처리되는지 먼저 보아야 한다.

처리 순서

  1. 먼저 front-controller의 역할을 하는 DispatcherServlet이 request를 받는다.
  2. DispatcherServlet은 적절한 controller를 선택하는 일을 HandlerMapping에게 요청한다.
  3. HandlerMapping은 적합한 controller를 선택한다.
  4. DispatcherServlet은 선택된 controller의 비즈니스 로직 실행 작업을 HandlerAdapter에게 위임한다.
  5. HandlerAdpater가 controller의 비즈니스 로직을 호출하고 결과를 ModelAndView 객체에 담아서 DispatcherServlet이 에게 return한다.
  6. DispatcherServletViewResolver를 이용하여 결과를 보여줄 View를 가져온다.
  7. View 객체에게 DispatcherServlet이 응답 결과 생성을 요청한다.

이 긴 과정 속에서 이 글에서 살펴볼 과정은 2,3번이다.
Request flow 순서대로 HandlerMapping에 대해서 알아볼 것이다.

DispatcherServlet

먼저 DispatcherServlet에서 부터 출발해야한다. 상속구조부터 보면,

public class DispatcherServlet extends FrameworkServlet
            ↓
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware
            ↓
public abstract class HttpServletBean extends HttpServlet
            ↓
public abstract class HttpServlet extends GenericServlet

이렇게 상속구조를 통해 DispatcherServlet은 결국 HttpServlet을 상속함을 알 수 있다.
그렇기 때문에 DispatcherServletServlet의 생명주기와 비슷하게 흘러감을 알 수 있다. (init(),doGet(),doPost(),service() 등등)

실제로 디버깅을 해보면, doService가 호출된다.
그 후 DispatcherServletfront-controller 역할을 하기 때문에 doDispatch를 호출한다.

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
...
    try {
    doDispatch(request, response);
    }
...
...
}

doDispatch의 javadoc을 보면 Servlet의 HandlerMapping을 순서대로 처리하여 handler를 가져온다고 되어있다.

Process the actual dispatching to the handler. The handler will be obtained by applying the servlet's HandlerMappings in order. The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters to find the first that supports the handler class.

doDispatch의 실제 코드를 보면 아래처럼 request에 대해서 handler를 가져오는 getHandler 함수를 호출하고 있다.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
...
    try {
    ...
        // Determine handler for the current request.
    mappedHandler = getHandler(processedRequest);
    ...
...
...

getHandler 함수는 DispatcherServlet의 method로 아래와 같다.
이게 실제로 적절한 handler를 가져오는 방식인데 전혀 감이 안온다. 하나하나 풀이해보자.

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
  if (this.handlerMappings != null) {
    for (HandlerMapping mapping : this.handlerMappings) {
      HandlerExecutionChain handler = mapping.getHandler(request);
      if (handler != null) {
        return handler;
      }
    }
  }
  return null;
}

DispathcerServlet은 처음 init되는 과정에서 여러가지 handlerMapping들을 등록하고 List를 통해 handlerMappings라는 이름으로 관리하고 있다. handelrMappings안에는 여러가지 handlerMapping들이 등록되어 있는 것이다.

그러므로 아래 코드는 DispatcherServlet 안에 handlerMapping들이 등록되었다면 이라는 뜻이다.

if(this.handlerMappings!=null)

등록되어있는 HandlerMapping들을 loop 하면서

for(HandlerMapping mapping : this.handlerMappings){

HandlerMapping들에게 request에 맞는 handler를 가져오게하고, 가져왔다면 그 handler를 return하는것이다.

  HandlerExecutionChain handler=mapping.getHandler(request);
  if (handler!=null)
    return handler;

핵심 부분은 HandlerMapping에게 request에 맞는 handler를 가져오는 부분이다. 이게 궁금해서 이 먼 길을 돌아온 것이다.

DispatcherServlet부분의 내용을 정리하자면,

  1. doService이 호출된다.
  2. doService내에서 doDispatch가 호출된다.
  3. doDispatch내에서 getHandler가 호출된다.
  4. getHandler내에서 등록된 HandlerMapping 중에서 request에 걸맞는 handler를 가져온다.

이제 거의 다왔다.

HandlerMapping이 handler를 가져오는 과정

HandlerMappinginterface로 함수의 선언부만 가지고 있다.

public interface HandlerMapping {
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

실제로 handler를 가져오는 getHandler는 추상 클래스인 AbstractHandlerMapping에 정의되어 있다.

우리가 흔히 아는 RequestMappingHandlerMapping,SimpleUrlHandlerMapping 같은 것들의 부모(바로 윗단계는 아니지만)가 AbstratHandlerMapping이다.

아래는 AbstractHandlerMappinggetHandler 코드이다.
getHandlerInternal을 통해서 handler을 찾아오고, HandlerExecutionChain을 return하는데,
우리가 원하는건 handler를 찾아오는 방식이므로 getHandlerInternal을 봐야겠다.

HandlerExecutionChain은 간단하게 handler와 handler interceptor들을 모아놓은 것이다.
Handler execution chain, consisting of handler object and any handler interceptors.

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
  Object handler = getHandlerInternal(request);
  ...
  HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
  ...
  return executionChain;
}

getHandlerInternalAbstractHandlerMapping을 상속한 AbstractHandlerMethodMapping에 정의되어 있다.
AbstractHandlerMethodMapping은 복잡하지만 이런 구조를 가지고 있다.

아래는 getHandlerInternal의 코드다. 이번에도 차근차근 살펴보자.

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping {
...
...
// Look up a handler method for the given request.
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
  String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  this.mappingRegistry.acquireReadLock();
  try {
    HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
    return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
  finally {
    this.mappingRegistry.releaseReadLock();
  }
}
...
...
}

먼저 javadoc을 보면 주어진 request에 대한 handler method를 찾습니다. 라고 되어있다.
동작원리의 핵심적인 부분인것이다.

Look up a handler method for the given request.

lookupPath는 현재 servlet mapping 안에서의 검색경로인데, request 요청을 분석해서 얻을 수 있다.
그리고 mappingRegistry에 대한 ReadLock을 가져오고 있다.

String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();

lookupPath를 바탕으로 lookupHandlerMethod를 통해서 적절한 handlerMethod를 가져온 후 return 한다.

handlerMethod가 바로 우리가 직접 Controller 안에 정의한 함수인것이다.

try {
  HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
  return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}

정리해보자면, DispatcherServlet 함수 안에서 handlerMapping이 여러 과정을 거쳐서 적절한 handlerMethod 를 가져온다는것은 알 수 있다.

그러나 궁금증이 더 남아있다.

url에 해당하는 적절한 method를 구별하는 방법과, method를 가져오는 것이 여전히 궁금하다.
각각 MappingRegistryReflection이 답이다.

나머지 궁금증은 2편에서 마저 다루도록 한다.

출처

Interceptor 사용법 : Request flow에 대해서 잘 정리되어 있었다.
AbstractHandlerMethodMapping javadoc
MappingReigstry javadoc
LinkedMultiValueMap javadoc

+ Recent posts