2024.09.10.화.TIL Spring MVC

김기남·2024년 9월 10일
post-thumbnail

안녕하세요, 오늘은 Spring MVC 에 대해 정리해 보았습니다.

어댑터 패턴

💡 다양한 컨트롤러(Handler)를 유연하게 만들기위해 어댑터 패턴을 도입한다. 컨트롤러들은 **동일한 인터페이스를 구현(Target)**하도록 하고 해당 인터페이스와 공통 로직 사이에 어댑터를 두어 유연하게 만든다. → 서로 다른 인터페이스를 갖는 두 클래스를 연결해주는 패턴
  • 어댑터 패턴 예시


1. 컨트롤러(Handler)는 비지니스 로직을 처리하고 알맞은 결과를 반환한다.
2. 어댑터는 공통 로직과 컨트롤러(Handler)가 자연스럽게 연결되도록 한다.
3. 프론트 컨트롤러는 공통으로 처리되는 로직을 수행한다.

  • 프론트 컨트롤러, 어댑터, 핸들러 모두 각자의 역할만 수행한다.

  • 새로운 컨트롤러(Handler)가 추가되어도 컨트롤러와 어댑터만 추가한다면
    공통 로직의 변경이 발생하지 않는다.

  • 클래스 다이어그램

  1. Target : Client가 사용하는 Interface
  2. Adaptee : Client가 가지고 있는 인터페이스 (Adapter에서 사용하고자 하는 인터페이스)
  3. Adapter : Target Interface를 구현하는 클래스, 이 때 Adaptee의 함수 사용
  4. Client : Target Interface를 사용하는 주체
  • 코드 예시
💡 온도를 섭씨에서 화씨로 변환하는 예시 **`CelsiusTemperature`**이라는 외부 라이브러리의 인터페이스가 있고, 이를 활용하는 내부 시스템의 인터페이스 **`FahrenheitTemperature`**이 필요한 상황**`CelsiusToFahrenheitAdapter`** 클래스가 두 인터페이스를 연결하여 섭씨를 화씨로 변환한다. 클라이언트 코드에서는 **`FahrenheitTemperature`**을 통해 화씨로 변환된 온도를 얻을 수 있습니다.
// 섭씨를 화씨로 변환하는 외부 라이브러리의 인터페이스
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의 구조
  • Spring MVC

  1. 요청이 오면 Controller에서 파라미터 정보 확인하여 비지니스 로직을 실행한다.
  2. 비지니스 로직의 결과 Data 를 Model에 담아서 View에 전달해준다.
  3. View는 모델의 Data를 참조하여 화면을 그려준다.
  • Spring MVC 구조

빨간색 Dispatcher Servlet - Spring에 프론트 컨트롤러 패턴이 적용된것

노란색 View - 인터페이스로 구성, 확장성

  • 실행순서
  1. Client로 부터 HTTP Request를 받는다.
  2. Handler 조회
    Handler Mapping을 통해 요청 URL에 Mapping된 Handler(Controller)를 조회
  3. Handler를 처리할 Adapter 조회
    Handler를 처리할 수 있는 Handler Adapter를 조회
  4. Handler Adapter 실행(handle)
    알맞은 **어댑터가 존재한다면 **Handler Adapter에게 요청을 위임한다.
  5. Handler 실행(호출)
    Handler Adapter가 실제 Handler(Controller)를 호출하여 실행 및 결과 반환
  6. Model And View 반환(return)
    Handler Adapter는 Handler가 반환 하는 정보를 ModelAndView 객체로 변환하여 반환
  7. viewResolver 호출(알맞은 View 요청)
    View Resolver를 찾고 실행
  8. View 반환
    View Resolver는 View의 논리 이름을 물리 이름으로 전환, Rendering 역할을 담당하는 View 객체를 반환
  9. View Rendering
    View를 통해서 View를 Rendering

정리

프론트 컨트롤러

  • 클라이언트 HTTP Request를 알맞게 파싱하고 클라이언트에게 알맞은 응답을 반환
  • 핸들러 목록 정보를 알고있다.
  • 핸들러 어댑터 목록 정보를 알고있다.

핸들러 어댑터

  • 자신이 처리할 수 있는 핸들러인지 확인할 수 있는 기능(Method) 필요
  • 요청을 위임받았을 때 핸들러에게 요청 실행을 지시하는 기능(Method) 필요
    • 반환 시 핸들러로부터 전달받은 결과를 알맞은 응답으로 변환한다.

핸들러###

  • 요청에 대한 로직을 수행하는 기능(Method) 필요

Dispatcher Servlet

💡 Spring MVC는 프론트 컨트롤러 패턴으로 구현되어 있다. Spring MVC의 프론트 컨트롤러는 Dispatcher Servlet(Servlet의 한 종류)이다. → 스프링 MVC 핵심기능, 양이 어마무시하니 핵심만 짚어보자.
  • IntelliJ Class Diagram

  • Dispatcher Servlet은 HttpServlet을 상속 받아서 사용하고 Servlet의 한 종류이다.
  • Spring Boot는 Dispatcher Servlet을 서블릿으로 자동으로 등록하고
    → 내장 Tomcat WAS를 실행하면서 DispatcherServlet을 등록한다.
    모든 URL 경로에 대해서 Mapping 한다. (urlPatterns=”/”)
  • 더 자세한 URL 경로ex) /member 가 우선순위가 높다.
    → 우리가 만든 Servlet이 동작하는 이유
    → 항상 더 자세한게 선언된것이 우선 순위가 높다!
    ex) application.yml < application-local or dev or prod ⇒ profile 설정
  1. Servlet이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
  2. Spring MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 Override 해두었다.

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);
}

Spring MVC의 주요 Interface

💡 Spring MVC는 DispatcherServlet 코드의 변경 없이 기능변경 및 확장이 가능하다. 기능들이 대부분 **인터페이스**로 만들어져 있기 때문이다. 해당 인터페이스를 구현하여 DispatcherServlet에 등록만하면 나만의 Controller가 완성된다.
  • org.springframework.web.servlet
  1. HandlerMapping
  2. HandlerAdapter
  3. ViewResolver
  4. View

정리

사실 우리는 복잡하고 양이 엄청나게 많은 내부 구조를 모두 알 필요는 없다.
개발자가 직접 내부 기능을 직접 확장하거나 컨트롤러를 만드는 일 또한 없다.
→ 이미 개발자들의 요구사항에 의해 다 만들어져 있다.

하지만 전체적인 동작 방식을 알아야 어떤 부분에서 문제가 발생했는지 파악할 수 있다.

  • 내가 구현하고자 하는 기능이 어떤 인터페이스에서 확장해야 하는지 파악할 수 있다.
    간단하게 전체적으로 이런 구조를 가지고 있다만 보면된다.

Handler Mapping, Handler Adapter

Controller Interface

  • 과거의 Spring Controller(어노테이션 기반이 아닐 때)

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 {
		// 로직
	}
}
  1. 해당 URL(http://localhost:8080/example-controller)로 HTTP 요청을 하게되면 응답결과가 반환된다
    1. ExampleController가 어떻게 호출되는것인가?
  • ExampleController가 실행되려면? 2개 필요

  1. Handler Mapping

    1. 핸들러 매핑에서 ExampleController를 찾을 수 있어야 한다.

    → Spring Bean의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요

  2. Handler Adapter

    1. Handler Mapping을 통해 찾은 핸들러를 실행할 수 있는 Handler Adapter가 필요

    → Controller Interface를 실행할 수 있는 Handler Adapter를 찾고 실행

Spring에는 이미 HandlerMapping과 HandlerAdapter를 대부분 구현되어있다.
개발자가 직접 HandlerMapping과 HandlerAdapter를 구현하는 일은 거의 없음

Spring Boot

💡 스프링 부트를 사용하면 자동으로 등록하는 HandlerMapping과 HandlerAdapter들이 있다
  • HandlerMapping

    • 우선순위 순서
    1. RequestMappingHandlerMapping(99.99%)
      • 우선순위가 가장 높다
      • Annotation 기반의 Controller인 @RequestMapping에 사용
    2. BeanNameUrlHandlerMapping(위 예시코드에 사용)
      • Spring Bean Name으로 HandlerMapping
  • HandlerAdapter

    • 우선순위 순서
    1. RequestMappingHandlerAdapter(99.99%)
      • Annotation 기반의 Controller인 @RequestMapping에서 사용
    2. HttpRequestHandlerAdapter
      • HttpRequestHandler 처리
    3. SimpleControllerHandlerAdapter(위 예시코드에 사용)
      • Controller Interface 처리

참고

HandlerMapping, HandlerAdapter 모두 우선순위대로 조회한다

HttpRequestHandler

💡 이전에 배웠던 **Servlet**과 가장 유사한 Handler
  • 예시코드
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 {
		// 구현 로직
	}

}

실행순서

  1. HandlerMapping 으로 핸들러 조회

    1. BeanName으로 Handler 조회(BeanNameUrlHandlerMapping 실행)
    2. ExampleRequestHandler 반환
  2. HandlerAdapter 조회

    1. HandleAdapter의 supports()를 우선순위 순서대로 호출
    2. HttpRequestHandlerAdapter가 HttpRequestHandler Interface를 지원한다
    • HttpRequestHandlerAdapter.supports()

  1. HandlerAdapter 실행

    1. DispatcherServlet이 조회한 HttpRequestHandlerAdapter를 실행하며 Handler 정보도 넘긴다
    2. HttpRequestHandlerAdapter 는 ExampleRequestHandler를 내부에서 실행 후 결과를 반환
    • HttpRequestHandlerAdapter.handle()
      → 단순히 handleRequest를 호출한다 = 오버라이딩된 handleRequest() 호출

DispatcherServlet에서 호출 → ha.handle()


@RequestMapping

💡 가장 높은 우선순위의 HandlerMapping인 `RequestMappingHandlerMapping` 가장 높은 우선순위의 HandlerAdapter인 `RequestMAppingHandlerAdapter` 이 두가지가 실제로 사용(99.99%)하는 Annotation 기반의 @Controller 를 지원한다.

View Resolver

💡 반환된 ModelAndView 객체를 알맞은 View로 전달하기 위해 DispatcherServlet에서 ViewResolver를 호출하여 View 정보를 설정한다 → 논리적인 View를 물리적인 View로 변환
  • ExampleController.handleRequest()
@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를 못찾는다

  • SpringBoot ViewResolver를 만들어야 한다
    application.properties 설정
spirng.mvc.view.prefix=/WEB-INF/
spirng.mvc.view.suffix=.jsp

SpringBoot

💡 스프링 부트를 사용하면 자동으로 등록하는 ViewResolver 들이 있다 → 아래 두가지 이외에 많음
  • 우선순위 순서
  1. BeanNameViewResolver
    1. Bean Name으로 View를 찾아 반환
  2. InternalResourceViewResolver(위 예시코드)
    1. application.properties 설정파일에 등록한 prefix, suffix 설정 정보를 사용하여 ViewResolver 등록
// 아래 코드를 자동으로 해주는것과 마찬가지이다.
@Bean
InternalResourceViewResolver internalResourceViewResolver() {
	return new InternalResourceViewResolver("/WEB-INF/", ".jsp");
}

ViewResolver 동작 방식

실행순서

  1. HandlerAdapter 호출
    HandlerAdapter를 통해 “form” 논리 View Name 얻음
  2. ViewResolver 호출
    ”form” 이라는 View Name으로 viewResolver를 우선순위 대로 호출
    BeanNameViewResolver는 View를 못찾음
    InternalResourceViewResolver 호출
  3. InternalResourceViewResolver
    InternalResourceViewResolver.buildView(String viewName)
    InternalResourceView 반환

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() 과정없이 자동화 되어있다

정리


  • 프론트 컨트롤러 패턴
  • 프론트 컨트롤러 패턴이 적용된 Spring MVC
  • HandlerMapping, HandlerAdapter, ViewResolver

오늘의 코드카타

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));
    }
}

배열의 같은 값 카운트

profile
새로운 시작~!

0개의 댓글