[@MVC 구현하기 - 3단계] 리건(이한길) 미션 제출합니다.#810
Conversation
There was a problem hiding this comment.
안녕하세요 리건!
시간이 정말 빠르네요..
몇가지 리뷰 남겼으니, 자유롭게 고민 후 리뷰 요청 주세요!
전체적으로 테스트 코드가 보완되면 좋겠다는 생각을 했습니다.
시간 괜찮으시면 테스트 코드를 추가해보는 건 어떨까요 😁
추가로, 사소한 부분이지만 깃 커밋 기록이 1단계부터 누적되어 잡히는데요!
병렬로 진행하고 싶은 리건의 마음은 너무 이해되지만,
rebase 후 진행하시면 리뷰어의 입장에서 조금 더 깔끔한 커밋 기록을 확인할 수 있어 리뷰하기 더욱 편할 것 같습니다!
좋은 한 주 되세요! 이번주도 파이팅입니다~~~~~~ :) 🍀
|
|
||
| @Override | ||
| public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { | ||
| @RequestMapping(value = "/login/view", method = RequestMethod.GET) |
There was a problem hiding this comment.
uri를 통일하고 method로 요청을 분리하는 방법도 있었을텐데, 위와 같이 /view를 추가하신 이유가 있으신가요? 😲
There was a problem hiding this comment.
JSP 코드에 맞춰서 작성했습니다. JSP쪽을 수정하는게 더 낫겠네요!
|
|
||
| @RequestMapping(value = "/", method = RequestMethod.GET) | ||
| public ModelAndView index(final HttpServletRequest request, final HttpServletResponse response) { | ||
| return new ModelAndView(new JspView("/index.jsp")); |
There was a problem hiding this comment.
들어오는 모든 요청에 대해, 같은 파일임에도 JspView 인스턴스를 새롭게 생성하면 중복으로 불필요하게 메모리를 차지할 것 같은데요. 이를 방지하는 방법이 있을까요? 제가 이것에 대해 고민했어서 리건에게도 여쭤보고 싶었습니다!
There was a problem hiding this comment.
이 상황에서는 캐싱이나 View 클래스에 정적 필드를 두어 활용하는 것도 좋을 것 같습니다. 제 생각에도 매 요청마다 View 인스턴스를 새로 만드는 건 좋지 않은 것 같네요!
There was a problem hiding this comment.
저는 캐싱을 통해 개선했는데, View 클래스에 정적 필드를 두는 리건의 방법도 좋네요! 배워갑니다 👍
| public void render(final Map<String, ?> model, final HttpServletRequest request, HttpServletResponse response) | ||
| throws Exception { | ||
| ObjectMapper objectMapper = new ObjectMapper(); | ||
| String body = objectMapper.writeValueAsString(model); | ||
|
|
||
| response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||
| response.getWriter().write(body); |
| public void render(final Map<String, ?> model, final HttpServletRequest request, HttpServletResponse response) | ||
| throws Exception { | ||
| ObjectMapper objectMapper = new ObjectMapper(); | ||
| String body = objectMapper.writeValueAsString(model); | ||
|
|
||
| response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); | ||
| response.getWriter().write(body); |
There was a problem hiding this comment.
사용이 끝난 writer는 종료시켜주는 것이 어떨까요?!
There was a problem hiding this comment.
지적 감사합니다! 미처 생각하지 못했네요..
| @Test | ||
| @DisplayName("매핑은 HTTP 요청을 처리할 핸들러 목록을 초기화한다.") | ||
| void getHandler() throws NoSuchFieldException, IllegalAccessException { | ||
| Field filed = handlerMapping.getClass().getDeclaredField("handlerExecutions"); |
|
|
||
| final var handlerExecutions = (Map<HandlerKey, HandlerExecution>) filed.get(handlerMapping); | ||
|
|
||
| assertThat(handlerExecutions.keySet()).hasSize(12); |
There was a problem hiding this comment.
keySet() 메서드가 없어도 Map의 사이즈를 검증할 수 있기 때문에, 위 코드에서의 keySet 호출은 불필요해보입니다!
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class HandlerMappingTest { |
There was a problem hiding this comment.
아래 테스트는 HandlerMapping보다는 AnnotationHandlerMapping 클래스를 테스트하는 코드라고 생각되는데, 리건은 어떻게 생각하시나요?
There was a problem hiding this comment.
제 생각에도 HandlerMapping보다는 AnnotationHandlerMapping에 대한 테스트에 가까워보이긴 합니다. 인터페이스에 대한 테스트 보다는 구현 클래스에 대한 테스트로 전환해놓겠습니다!
| import org.junit.jupiter.api.Test; | ||
| import samples.TestSimpleController; | ||
|
|
||
| class HandlerAdapterTest { |
There was a problem hiding this comment.
Legacy 어댑터가 사라졌기 때문에, 현재는 List로 어댑터를 관리할 필요가 없어보여요! 현재는 AnnotationHandlerAdapterTest로 작성하는 것이 좋아보이는데, 리건의 생각이 궁금하네요!
There was a problem hiding this comment.
동의합니다. 위의 코멘트와 마찬가지로 구현체 테스트로 이동하겠습니다!
| @@ -1,4 +1,4 @@ | |||
| package com.techcourse; | |||
| package com.interface21.webmvc.servlet.mvc; | |||
There was a problem hiding this comment.
mvc 모듈 전체적으로 어떤 기준으로 클래스의 위치를 선정하셨는지 궁금합니다!
There was a problem hiding this comment.
기본적으로 Spring의 클래스 구조를 따랐습니다!
추가적으로 인터페이스와 같은 추상화 계층은 상위 패키지에, 이를 구현한 구현체들은 하위 패키지로 세분화 하였습니다.
helenason
left a comment
There was a problem hiding this comment.
리건 안녕하세요!
간헐적으로 깨지는 테스트가 하나 있어 RC 드립니다.
추가된 학습테스트에 대한 리뷰도 남겼으니 확인해주세요
아마 시간상 마지막 RC가 될 것 같네요 :)
마지막까지 화이팅입니다!
| @Test | ||
| @DisplayName("데이터가 2개 이상인 JSON 응답을 생성한다.") | ||
| void renderOverTwoData() throws Exception { | ||
| // given | ||
| HttpServletRequest request = mock(HttpServletRequest.class); | ||
| HttpServletResponse response = mock(HttpServletResponse.class); | ||
|
|
||
| StringWriter stringWriter = new StringWriter(); | ||
| PrintWriter writer = new PrintWriter(stringWriter); | ||
| when(response.getWriter()).thenReturn(writer); | ||
|
|
||
| // when | ||
| Map<String, ?> model = Map.of("test", "value", "hello", "world"); | ||
| jsonView.render(model, request, response); | ||
|
|
||
| // then | ||
| assertThat(stringWriter).hasToString("{\"test\":\"value\",\"hello\":\"world\"}"); | ||
| } |
| @Test | ||
| @DisplayName("매핑은 HTTP 요청을 처리할 핸들러 목록을 초기화한다.") | ||
| void getHandler() throws NoSuchFieldException, IllegalAccessException { | ||
| Field filed = handlerMapping.getClass().getDeclaredField("handlerExecutions"); | ||
| filed.setAccessible(true); | ||
|
|
||
| final var handlerExecutions = (Map<HandlerKey, HandlerExecution>) filed.get(handlerMapping); | ||
|
|
||
| assertThat(handlerExecutions.keySet()).hasSize(12); | ||
| } | ||
|
|
There was a problem hiding this comment.
| private Object instantiateClass(final Class<?> clazz) { | ||
| try { | ||
| Constructor<?> constructor = clazz.getDeclaredConstructor(); | ||
| constructor.setAccessible(true); |
There was a problem hiding this comment.
생성자의 접근제어자를 임시로 열어둔다면, 프로그램이 종료되기 전까지 열린 접근 제어자로 프로그램이 실행될 것 같아요! 인스턴스를 생성한 후 다시 닫아주는 것이 어떨까요?
+) field도 마찬가지로요!
| } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | | ||
| InvocationTargetException e) { |
There was a problem hiding this comment.
저는 개인적으로, 코드의 가독성을 위해 위와 같이 예외의 수가 많아지는 경우 상위 클래스로 묶어 예외를 잡는 편인데, 리건은 위 방식을 선호하시는 이유가 있으신가요?
There was a problem hiding this comment.
어떤 예외가 발생하는지 상세하게 알려주는 점에서 괜찮다고 생각하는 편입니다! 그래도 확실히 가독성은 떨어지는 것 같아서 각 예외 클래스의 상위 클래스인 ReflectiveOperationException으로 대체하겠습니다.
| private void initFields(final Object bean) { | ||
| final Field[] fields = bean.getClass().getDeclaredFields(); | ||
| try { | ||
| for (Field field : fields) { | ||
| initField(bean, field); | ||
| } | ||
| } catch (IllegalAccessException e) { | ||
| throw new IllegalArgumentException("초기화할 수 없는 필드입니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
객체지향 생활 체조 원칙 - 한 메서드에 오직 한 단계의 들여쓰기만 한다
depth를 1로 줄여보는 것이 어떨까요? 아래 initField 메서드도 마찬가지로요!
There was a problem hiding this comment.
di 패키지에 있는 클래스들을 활용해서 고쳤습니다!
| field.setAccessible(true); | ||
| final Class<?> fieldType = field.getType(); | ||
| for (Object o : beans) { | ||
| if (fieldType.isAssignableFrom(o.getClass())) { |
There was a problem hiding this comment.
저도 제 리뷰어에게 받은 리뷰인데요. 이번 미션을 기회로 Class.isInstance vs Class.isAssignableFrom vs instanceof 차이점에 대해 학습해보아요!
| public static Set<Class<?>> getAllClassesInPackage(final String packageName) { | ||
| return null; | ||
| Reflections reflections = new Reflections(packageName); | ||
| Set<Class<?>> classes = new HashSet<>(); | ||
|
|
||
| classes.addAll(reflections.getTypesAnnotatedWith(Repository.class)); | ||
| classes.addAll(reflections.getTypesAnnotatedWith(Service.class)); | ||
|
|
||
| return classes; | ||
| } |
There was a problem hiding this comment.
헉 저와 완전히 똑같이 구현하셨네요!
하지만 제가 리뷰 반영하면서 고민해보게 된 포인트인데요. Repository와 Service 클래스 추출 로직은 외부로 빼는 것이 맞다고 생각해요! 메서드명이 getAllClassesInPackage이기 때문에 파라미터로 받은 패키지 내부의 모든 클래스를 반환하는 역할을 갖는 것이 맞다고 생각합니다.
There was a problem hiding this comment.
Reflections의 Scanners.SubTypes.filterResultsBy(); 메소드를 활용해서 수정했습니다!
There was a problem hiding this comment.
stage4의 DIContainer 클래스에 대해서도 전체적으로 stage3의 DIContainer 클래스에 남겨드린 리뷰 반영해보시면 좋을 것 같아요 :)
helenason
left a comment
There was a problem hiding this comment.
수고하셨습니다 리건!
이만 머지하도록 할게요 ~~~~
다음 단계도 파이팅입니다 👊


안녕하세요, 에버! 👋
폭풍 같았던 데모데이가 지나가고 조금 여유로워졌네요..
에버에게 본의 아니게 리뷰를 재촉한 것 같아 죄송합니다..🥲
할일이 너무 많아서 조급해진 감이 없지 않아 있었던 것 같습니다.
평소라면 놓치지 않았을 코드 실수도 잦았던 것 같아요..
에버 덕분에 자잘한 실수와 생각해볼 거리가 많았던 미션이었습니다!
마지막 단계 리뷰 잘 부탁드립니다!
p.s. 최종 리뷰 이후의 변경 사항 링크입니다! 다음부터는 rebase 잊지 않겠습니다..🥲
p.s.2 최종 리뷰 이후 변경 사항 링크 👍