
안녕하세요, 오늘은 Spring MVC 에 대해 정리해 보았습니다.
💡 다양한 컨트롤러(Handler)를 유연하게 만들기위해 어댑터 패턴을 도입한다. 컨트롤러들은 **동일한 인터페이스를 구현(Target)**하도록 하고 해당 인터페이스와 공통 로직 사이에 어댑터를 두어 유연하게 만든다. → 서로 다른 인터페이스를 갖는 두 클래스를 연결해주는 패턴어댑터 패턴

1. 컨트롤러(Handler)는 비지니스 로직을 처리하고 알맞은 결과를 반환한다.
2. 어댑터는 공통 로직과 컨트롤러(Handler)가 자연스럽게 연결되도록 한다.
3. 프론트 컨트롤러는 공통으로 처리되는 로직을 수행한다.
프론트 컨트롤러, 어댑터, 핸들러 모두 각자의 역할만 수행한다.
새로운 컨트롤러(Handler)가 추가되어도 컨트롤러와 어댑터만 추가한다면
공통 로직의 변경이 발생하지 않는다.
클래스 다이어그램

// 섭씨를 화씨로 변환하는 외부 라이브러리의 인터페이스
public interface CelsiusTemperature {
double getTemperature(); // 섭씨 온도를 얻기 위한 메서드
}
// 외부 라이브러리 클래스
public class CelsiusTemperatureImpl implements CelsiusTemperature {
private double temperature; // 섭씨 온도를 저장하는 변수
public CelsiusTemperatureImpl(double temperature) {
this.temperature = temperature;
}
@Override
public double getTemperature() {
return temperature; // 섭씨 온도 반환
}
}
// 내부 시스템의 인터페이스
public interface FahrenheitTemperature {
double getTemperatureInFahrenheit(); // 화씨 온도를 얻기 위한 메서드
}
// Adapter 클래스
public class CelsiusToFahrenheitAdapter implements FahrenheitTemperature {
private CelsiusTemperature celsiusTemperature; // 외부 라이브러리의 인터페이스를 갖는 변수
public CelsiusToFahrenheitAdapter(CelsiusTemperature celsiusTemperature) {
this.celsiusTemperature = celsiusTemperature;
}
@Override
public double getTemperatureInFahrenheit() {
// 섭씨를 화씨로 변환하는 공식을 적용
return (celsiusTemperature.getTemperature() * 9 / 5) + 32;
}
}
// 클라이언트 코드
public class Client {
public static void main(String[] args) {
// 외부 라이브러리 객체 생성 (섭씨 25도)
CelsiusTemperature celsiusTemperature = new CelsiusTemperatureImpl(25);
// Adapter를 사용하여 섭씨를 화씨로 변환
FahrenheitTemperature adapter = new CelsiusToFahrenheitAdapter(celsiusTemperature);
// 화씨로 변환된 온도 출력
System.out.println("Temperature in Fahrenheit: " + adapter.getTemperatureInFahrenheit());
}
}
💡 프론트 컨트롤러 패턴이 적용된 Spring MVC의 구조Spring MVC 구조


빨간색 Dispatcher Servlet - Spring에 프론트 컨트롤러 패턴이 적용된것
노란색 View - 인터페이스로 구성, 확장성
💡 Spring MVC는 프론트 컨트롤러 패턴으로 구현되어 있다. Spring MVC의 프론트 컨트롤러는 Dispatcher Servlet(Servlet의 한 종류)이다. → 스프링 MVC 핵심기능, 양이 어마무시하니 핵심만 짚어보자.Dispatcher Servlet

(urlPatterns=”/”)ex) /member 가 우선순위가 높다.
FrameworkServlet.service()를 시작으로 여러 메서드가 호출됨과 동시에
DispatcherServlet.doDispatch()가 호출된다. - 가장 중요

protected void doDispatch() {
...
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response); // NotFound 404
}
// 2. 핸들러 어댑터 조회 : 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행
// 4. 핸들러 어댑터를 통해 핸들러 실행
// 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 여기 안에서 render
processDispatchResult(processedRequest, response, mappedHandler, mv,dispatchException);
...
}
// processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
if (mv != null && !mv.wasCleared()) {
// View Render 호출
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
}
// render()
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. ViewResolver를 통해 View 조회
// 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. View Rendering
view.render(mv.getModelInternal(), request, response);
}
사실 우리는 복잡하고 양이 엄청나게 많은 내부 구조를 모두 알 필요는 없다.
개발자가 직접 내부 기능을 직접 확장하거나 컨트롤러를 만드는 일 또한 없다.
→ 이미 개발자들의 요구사항에 의해 다 만들어져 있다.
하지만 전체적인 동작 방식을 알아야 어떤 부분에서 문제가 발생했는지 파악할 수 있다.
Handler Mapping, Handler Adapter
org.springframework.web.servlet.mvc.Controller
public interface Controller {
ModelAndView handleRequest(
HttpServletRequest request,
HttpServletResponse response
) throws Exception;
}
Controller Interface는 우리가 사용하는 @Controller와는 아무 상관이 없다.
@Component("/example-controller") // Spring Bean 이름을 URL로 설정
public class ExampleController implements Controller {
@Override
public ModelAndView handleRequest(
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
// 로직
}
}

Handler Mapping
→ Spring Bean의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요
Handler Adapter
→ Controller Interface를 실행할 수 있는 Handler Adapter를 찾고 실행
Spring에는 이미 HandlerMapping과 HandlerAdapter를 대부분 구현되어있다.
개발자가 직접 HandlerMapping과 HandlerAdapter를 구현하는 일은 거의 없음
HandlerMapping
HandlerAdapter
참고
HandlerMapping, HandlerAdapter 모두 우선순위대로 조회한다
public interface HttpRequestHandler {
// return type void
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
구현 예시
@Component("/example-handler")
public class ExampleRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 구현 로직
}
}
HandlerMapping 으로 핸들러 조회
HandlerAdapter 조회
HttpRequestHandlerAdapter.supports()
HandlerAdapter 실행
HttpRequestHandlerAdapter.handle()
DispatcherServlet에서 호출 → ha.handle()


💡 반환된 ModelAndView 객체를 알맞은 View로 전달하기 위해 DispatcherServlet에서 ViewResolver를 호출하여 View 정보를 설정한다 → 논리적인 View를 물리적인 View로 변환View Resolver
@Component("/example-controller") // Spring Bean 이름을 URL로 설정
public class ExampleController implements Controller {
@Override
public ModelAndView handleRequest(
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
// 로직
return new ModelAndView("form"); // Parameter = String viewName
// 현재 "form" 은 논리적인 이름이다 -> viewResolver에서 물리적인 이름으로 바꾸어야함
}
}

webapp → WEB-INF → form.JSP

위 실행순서 중 (논리 이름을 물리 이름으로 전환)

현재상태 “form”으로 View를 못찾으면(없으면)?

컨트롤러는 호출되지만 View를 못찾는다
spirng.mvc.view.prefix=/WEB-INF/
spirng.mvc.view.suffix=.jsp
// 아래 코드를 자동으로 해주는것과 마찬가지이다.
@Bean
InternalResourceViewResolver internalResourceViewResolver() {
return new InternalResourceViewResolver("/WEB-INF/", ".jsp");
}
ViewResolver 동작 방식

실행순서
“form” 논리 View Name 얻음”form” 이라는 View Name으로 viewResolver를 우선순위 대로 호출InternalResourceViewResolver.buildView(String viewName)

InternalResourceView
이전 시간의 JSP와 같이 서버에서 이동하는 forward()를 호출하는 경우와 같을 때 사용한다.

renderMergedOutputModel() → Model을 Request로 바꾼다.

5. view.render()
외부에서 view.render()를 호출하고 ****RequestDispatcher를 가져와서 forward()한다
→ 어디서 호출하는지 찾지맙시다 힘들어유
우리가 사용하는 Thymeleaf는 View와 Resolver가 이미 존재한다.
라이브러리 의존성만 추가해주면 SpringBoot가 모두 자동으로 해준다.
return “stirng”; 만으로 View Rendering 된다.
다른 View들은 실제 View를 Rendering 하지만 JSP는 forward() 를 통해 JSP로 이동 후
Rendering이 된다 JSP를 제외한 나머지 View Template들은 forward() 과정없이 자동화 되어있다
오늘의 코드카타
class Solution {
public int solution(String[] s1, String[] s2) {
int answer = 0;
for (int i = 0; i < s1.length; i++) {
for (int j = 0; j < s2.length; j++) {
if (s1[i].equals(s2[j])) {
answer++;
}
}
}
return answer;
}
}
public class Main {
public static void main(String[] args) {
Solution solution = new Solution();
String[] a1 = {"a", "b", "c"};
String[] a2 = {"n", "omg"};
String[] b1 = {"com", "b", "d", "p", "c"};
String[] b2 = {"m", "dot"};
System.out.println(solution.solution(a1,b1));
System.out.println(solution.solution(a2,b2));
}
}
배열의 같은 값 카운트