제가 구현중인 MVC 프레임 워크 프로젝트는 DI 컨테이너를 맵의 형태로 직접 구현하게 되었습니다.
public class ControllerManager {
private static Map<String, Method> controllerMap = new HashMap<>();
static {
Reflections reflections = new Reflections(DbConfig.BASE_PACKAGE_PATH);
Set<Class<?>> controllers = reflections.getTypesAnnotatedWith(Controller.class);
for (Class<?> controller : controllers) {
for (Method method : controller.getDeclaredMethods()) {
registerMethod(method);
}
}
}
}
즉, BASE_PACKAGE_PATH의 경로 내에 있는 @Controller 어노테이션이 붙은 클래스를 찾아 리플렉션을 활용해서 컨트롤러 맵에 넣게 됩니다.
그러나 이는 url의 문자열을 key 값으로 컨트롤러 맵에 넣게 되었고, 이는 순서의 차이에 따라 둘 중 한 메서드를 인식하지 못하는 이슈를 발생시켰습니다.
AS-IS
제 ArticleController에서 문제가 발생했던 메서드는 다음과 같습니다.
@Controller
public class ArticleController {
@Autowired
private ArticleService articleService;
//...다른 로직
@GetMapping("/usr/article/write")
public void showWrite(Rq rq) {
rq.view("usr/article/write");
}
@PostMapping("/usr/article/write")
public void write(Rq rq) {
String title = rq.getParam("title", "");
String body = rq.getParam("body", "");
if (title.length() == 0) {
rq.historyBack("제목을 입력해주세요.");
return;
}
if (body.length() == 0) {
rq.historyBack("내용을 입력해주세요.");
return;
}
long id = articleService.write(title, body);
rq.replace("/usr/article/%d".formatted(id), "%d번 게시물이 생성 되었습니다.".formatted(id));
}
//...다른 로직
}
showWrite 메서드와 write 메서드는 어노테이션 인자로 받는 url이 같기 때문에,
맵에 넣을 때 <url 문자열 : Method 타입> 으로 각각 아래와 같이 들어가게 됩니다.
- showWrite() : < "/usr/article/write" : showWrite >
- write() : < "/usr/article/write" : write >
즉, 두 값이 같은 키로 들어가기 때문에 맵에는 write 메서드에 대한 값만 저장되어 있는 것입니다.
HTTP 메서드를 고려하지 않고 맵에 넣었던 것입니다.
처음에는 모든 메서드별로 url 패턴을 바꿔서 매핑하도록 하는 방법을 생각했습니다.
그러나 이왕 스프링 유사 MVC 프레임 워크를 구현하는 것이기 때문에 url 패턴이 동일하더라도 메서드를 구분해서 동작하도록 해보자는 생각을 하게 되었습니다.
TO-BE
그래서 저는 애초에 키 값을 넣을때 HTTP 메서드를 포함해서 key값으로 넣는 방법을 선택했습니다.
만약, "/usr/article/write"로 @GetMapping으로 요청이 들어올 경우, "GET:/usr/article/write"로 문자열을 추가하는 방법입니다.
그래서 아래와 같이 개선했습니다.
private static void registerMethod(Method method) {
String methodType = null;
String mappingValue = null;
if (method.isAnnotationPresent(GetMapping.class)) {
methodType = "GET";
GetMapping getMapping = method.getAnnotation(GetMapping.class);
mappingValue = getMapping.value();
} else if (method.isAnnotationPresent(PostMapping.class)) {
methodType = "POST";
PostMapping postMapping = method.getAnnotation(PostMapping.class);
mappingValue = postMapping.value();
} else if (method.isAnnotationPresent(PutMapping.class)) {
methodType = "PUT";
PutMapping putMapping = method.getAnnotation(PutMapping.class);
mappingValue = putMapping.value();
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
methodType = "DELETE";
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
mappingValue = deleteMapping.value();
}
if (methodType != null && mappingValue != null) {
System.out.println("methodType : " + methodType);
System.out.println("mappingValue : " + mappingValue);
String generalizedMappingValue = generalizeMappingValue(mappingValue);
controllerMap.put(methodType + ":" + generalizedMappingValue, method);
}
}
어노테이션 별로 methodType 문자열에 값을 달리해서 마지막에 맵에 넣을때
methodType + ":" + url로 조합해서 key로 넣었습니다.
그리고 조회할 때에는 methodType을 제외한 url만을 얻어와야하기 때문에, 다음과 같이 조회하는 로직을 수정했습니다.
public static void runAction(HttpServletRequest req, HttpServletResponse resp) {
String requestURI = req.getRequestURI();
String requestMethod = req.getMethod();
String generalizedRequestURI = generalizeMappingValue(requestURI);
String key = requestMethod + ":" + generalizedRequestURI;
Method method = controllerMap.get(key);
if (method != null) {
try {
Object controller = Container.getObj(method.getDeclaringClass());
Rq rq = new Rq(req, resp);
method.invoke(controller, rq);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
(generalizedRequestURI는 무시하셔도 됩니다.)
즉, controllerMap으로부터 get할때, req.getMethod()를 사용해서 HTTP 메서드를 가져와서
다시 request + ":" + url로 조합한 키 값으로 맵으로부터 조회를 하게 됩니다.
그 결과, url이 같더라도, HTTP 메서드별로 다른 함수를 실행시킬 수 있게 되었습니다.
'📙Language > ☕Java' 카테고리의 다른 글
[Java] url 변수 동적으로 받도록 개선하기 (0) | 2023.05.18 |
---|---|
[JDBC] 동적으로 파라미터 바인딩 하기 (0) | 2023.04.26 |
[JDBC] Connection과 PreparedStatement, 그리고 ResultSet (0) | 2023.04.24 |
[Java] 스트림(Stream) 잘 사용하기 (0) | 2023.03.15 |
[Java] Jackson Databind 이용하기 (0) | 2023.03.02 |