Framework/Spring

[IBAS] Spring MVC의 Front Controller Pattern 구현하기

MINGYUM 2022. 3. 14. 15:05

막연하게 FrontController의 형태로 구현해 문제를 해결해보자고 생각했다. 

 

https://yeonyeon.tistory.com/103

 

[MVC] 프론트 컨트롤러 패턴

기존의 패턴을 설명하자면 아래 그림과 같다 각 클라이언트들은 Controller A, B, C에 대해 각각 호출한다. 공통 코드들은 별도로 처리되어 있지 않고 각 Controller에 포함되어 있다. 하지만 프론트 컨

yeonyeon.tistory.com

대충, 서브 컨트롤러들을 프론트 컨트롤러에 정의해놓은 Map에 URL과 함께 매핑한다. 

그리고 Request의 URI를 데려와 Key값을 이용해 Controller을 데려와

process 함수를 호출시킨다. 

 

여기서 문제들이 보인다. 

 

1. 호출해야하는 함수는 process뿐만이 아니다. CRUD 모든 URL에 대해서 적절한 함수에 매핑해야된다. 

2. Map 에 put할 때 Parameter의 value를 가져올 수 없다는 점. 

 

핵심은 menuId인데, 

 Map에 다음과 같이 저장할 수도 없다. 

menuId를 무조건 request에서 받아와서 MenuType을 분석한 다음에 그에 걸맞는 Controller를 호출해야한다. 

말도 안되는 코드라는 것을 보여주고 싶었다..

 

어짜피 Controller에서 처리할건데, 받은 menuId를 가져와서 처리하려면

Controller에서 MenuRepository를 가져와야하는 불상사가 생긴다.

 

MenuService를 만들어서, menuId에 대해 적절한 menuType을 switch 구문을 통해 구분하고

Bean Name을 가져오는 로직을 구현할 것이다.

 

이 과정에서 Application Context의 이해가 필요하고, 

return 구문을 어떻게 할지 고민해보기로 했다. 

 

https://kim-jong-hyun.tistory.com/21

 

[Spring] - IoC Container에 관리되는 Bean 목록 확인하기

Bean은 Spring IoC Container에 의해 관리되는 객체로 객체생성 및 의존주입의 제어권을 개발자가 아닌 Container가 가지고 있다 Spring Container의 대표적인 구현체는 BeanFactory와 ApplicationContext 2가지가..

kim-jong-hyun.tistory.com

 

@Autowired
private ApplicationContext applicationContext;

String [] beans = applicationContext.getBeanDefinitionNames();

 

이렇게 Application Context로 Bean 객체에 대한 String 형식의 List를 가져올 수 있다. 

Bean Name으로 Controller의 인스턴스를 호출할 수 있는 방안에 대해 고민하였으나, 

일단 저 if문에 뭘 구현해야할 지 모르겠고 new Controller를 수행함으로서 의존성 문제가 꼬이게 되었다.

 

다시 생각을 해보았다. 이렇게 Bean Name이 나오는 메서드를 구현한 것까진 좋다. 

그 이후에 Controller를 매핑해야하는 것이 문제이므로,

beanName을 Entity로 가지는 Controller를 호출하는 Controller 어노테이션을 구현해보자. 

 


동일한 URL에 대해, parameter의 value에 따라 전혀 다른 Controller를 매핑해야하는 것을 구현해야 한다.

 

기능 요구 사항

@ControllerScanner

BoardController에서 Create, Read, Update 메소드에 ControllerComponent를 스캔하는 메소드 붙이기.

 

내부로직 : @ExtendController 가 붙은 어노테이션을 스캔해, 

( 리플렉션에서 isAnnotationPresent() 메서드를 사용해 특정 어노테이션이 있는지 확인할 것)
URI 파싱해서 request.getParameter(menuId) 로 menuId 값 가져오고 MenuService에서 Bean Name 가져오기.

가져온 Bean name에 해당하는 ExtendController로 매핑한다. 

 

구현할 것

: menuId에 해당하는 Controller 조회 후 Bean Name 있으면 매핑, 없으면 Front Controller에서 바로 return 문 수행

 

To do 

1) 요청에서 menuId 가져와서 getParameter로 MenuType조사하기

2) MenuType을 기반으로 Contest 메뉴이면 Contest라는 이름이 포함된 Bean Name이 있는지 찾기

3) 있으면  그 Bean 이름에 해당하는 컨트롤러를 호출하기

4) 없으면 for 문을 빠져나와 원래 Service를 반환하기

 

 

1) 요청에서 menuId 가져와서 getParameter로 MenuType조사하기

 

menuService에 구현된 것 그대로 사용하면 됨

 

2) MenuType을 기반으로 Contest 메뉴이면 Contest라는 이름이 포함된 Bean Name이 있는지 찾기

 

ContestBoardController contestBoardController = ac.getBean("ContestBoardController", ContestBoardController.class);

 

나중에는 수정해서, MenuService 단에서 받아온 MenuId를 토대로 Controller를 탐색한 다음, 

실제 Controller 인스턴스를 반환해 내부적으로 호출하는 방안이다. 

MenuServiceImpl

테스트 결과 menuId를 조회해, CONTEST와 같이 컨트롤러가 분리된 형태의 게시판으로 인식이 되었을 때

NormalBoardController가 실행되고 내부에서 찾아진 ContestBoardController의 메서드가 실행이 되면서 

공모전 게시판의 정보가 잘 넘어가고 오는 것을 확인하였다. 

 


추가적으로 해결해야 될 문제가 있었다. 

 

바로 Save, Update 메서드 실행 시 클라이언트에서 받아오는 DTO를 획일화해야한다는 점인데, 

Bean Validation의 역할을 수행하고 있던 DTO를 NormalBoard와 ContestBoard의 DTO는 엄연히 다르게 존재하고 있었으며 생각해 낸 방법론은 다음과 같다.

 

1)  java - Interfaces and @RequestBody - Stack Overflow

 인터페이스를 설계하여 제너릭으로 다양한 종류의 DTO의 공통 부모 클래스를 가져와

@RequestBody의 DTO에 넣는 방법이다. 

 

이 방법은 인터페이스를 설계해야 구현할 수 있는데, 프론트 컨트롤러 형태에서는 인터페이스를 구현할 필요가 없을 뿐 더러, 제너릭으로 처리한다 해도 유연한 형태의 파라미터를 받기 힘들다는 점이 문제였다. 

 

 

 

2) MultiValueMap이나 Map과 같은 자료구조를 이용해 Map<String, String> 처럼 받아온 JSON 객체에 대해 Key와 Value로 처리한다. 

 

이 방법을 채택해서 구현하였다. 

이런 형태로 말이다. 

 

이렇게 해서 구현은 완료하였다 .


 

결론

https://github.com/InhaBas/Inhabas.com-api/pull/67

Map으로 받아온 필드들에 대해 Validation을 수행할 수 없다는 한계가 있었다. 

@Validated를 이용해 구현할 수 는 있으나 NormalBoardController, ContestBoardController인지에 따라 

파라미터에 맞게 변형하여 설계해야 한다. 

결국에는 menuId로 컨트롤러에 들어온 요청을 처리해서 별도의 작업을 수행해야한다는 것이다 .

 

결론적으로 처음의 방법대로 진행하게 되었다. 

 

이번 작업을 하면서 Interceptor, HandlerMapping, 제너릭 등 스프링의 기본 구조를 오버라이딩하여 

원하는 목적을 달성하도록 노력하는 경험을 할 수 있었다.