DispatcherServlet

  • HttpServlet을 상속 받아서 서블릿으로 동작 (프론트 컨트롤러 역할)
  • 서블릿 호출 시, HttpServlet 이 제공하는 service()를 시작으로 DispacherServlet.doDispatch()가 실행
  • 스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다 (인터페이스 제공)
 

 

DispacherServlet.doDispatch()

  • 핸들러 조회
  • 핸들러 어댑터 조회 -> 핸들러를 처리할 수 있는 어댑터
  • 핸들러 어댑터 → 핸들러 실행
  • ModelAndView 반환
  • View Resolver 실행 (View 조회)
  • View 렌더링

 

핸들러 매핑과 핸들러 어댑터

 

스프링 MVC는 이미 내부적으로 구현되어 있다.

  • 가장 우선순위가 높은 RequestMappingHandlerMapping,RequestMappingHandlerAdapter
  • RequestMappingHandler은 @RequestMapping 또는 @Controller가 붙어있는 스프링 빈을 보고 매핑 정보로 인식

 

View Resolver

  • 스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록
  • application.properties에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정 정보를 사용해서 등록
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

그 외 간단 정리

  • @Controller : 컨트롤러는 반환 값이 String이면 뷰 이름으로 인식, 그래서 뷰를 찾고 뷰가 랜더링
  • @RequestMapping : URL 요청 정보를 매핑 (메서드단에 적용해서 컨트롤러 클래스를 유연하게 통합 가능)
  • @RequestParam : HTTP 요청 파라미터를 애노테이션으로 받을 수 있음 (생략 가능)
  • @PathVariable : URL 경로를 템플릿화하여 조회 가능 (members/{memberId})
  • @ModelAttribute : 요청 파라미터를 전달 받고, 객체를 생성해서 Model에 저장하는 과정을 자동화 (생략 가능)

 

 

HTTP 메시지 컨버터

  • view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력
  • RequestMapping 핸들러 어댑터에서 핸들러(컨트롤러)로 넘어가기 전 ArgumentResolver 에서 HTTP 메시지 컨버터 호출
  • ArgumentResolver컨트롤러의 파라미터, 애노테이션을 기반으로 필요한 데이터를 생성하고 컨트롤러에 전달
  • ReturnValueHandler컨트롤러 응답 값을 전달 받아 변환하고 처리
  • @RequestBody : HTTP 메시지 컨버터가 HTTP 메시지 바디를 원하는 문자나 객체 등으로 변환 (json도 객체로 변환 가능)
  • @ResponseStatus(HttpStatus.OK) 애노테이션으로 응답 코드도 설정 가능하지만 동적으로 응답 코드를 생성하는 경우 ResponseEntity를 사용
  • 컨트롤러에서 @ReqeustBody 사용 시, Message Converter가 해당 메시지를 읽을 수 있는지 확인하기 위해 canRead() 또는 canWrite()를 호출하고 가능하면 read() 또는 write() 호출

 

 

[참고]인프런 김영한님 강의를 공부한 내용입니다.

서블릿 (Servlet)

  • 동적 웹 페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
  • 웹 요청과 응답의 흐름을 간단한 메서드 호출만으로 체계적으로 다룰 수 있게 지원
  • HTTP 요청을 통해 매핑된 URL이 호출되면 service() 메서드를 실행
  • HTTP 요청, 응답 메시지를 편리하게 사용할 수 있도록 도와주는 HttpServletRequest, HttpServletResponse 객체

 

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
	@Override
    protected void service(HttpServletRequest request, HttpServletResponse
  response) throws ServletException, IOException {
  	...
   
  }
}

 

HTTP 통신 과정 변화

  • 서버 TCP/IP 연결 대기, 소켓 연결
  • HTTP 요청 메시지 파싱 (헤더부, 바디부 ..)Request 객체
  • 비즈니스 로직
  • HTTP 응답 메시지 생성Response 객체
  • TCP/IP 응답 전달, 소켓 종료

 

MVC 패턴

  • 비즈니스 로직과 뷰 렌더링을 한번에 처리 -> 너무 많은 역할
  • 비즈니스 로직을 수정하는 일과 UI를 수정하는 일은 각각 다르게 발생할 경우가 많고 대부분 서로에게 영향을 주지 않음
  • MVC 패턴Controller와 View 영역으로 서로 역할을 나눈 것
  • Controller : HTTP 요청을 받아서 파라미터 검증, 비즈니스 로직(Service 계층), View에 전달할 데이터를 Model에 저장
  • Model : View에 전달할 데이터를 저장
  • View : Model에 담겨있는 데이터를 사용해서 화면을 렌더링하는데 집중

 

Servlet + 기본 MVC 패턴의 문제점

  • View로 이동하는 dispatcher.forward(request, response) 코드 중복 호출
  • 모든 컨트롤러에 Servlet 선언 (종속성)
  • 공통 처리가 어려움 -> 컨트롤러 호출 전에 공통 기능을 처리해줄 무언가 필요 (프론트 컨트롤러)

 

프론트 컨트롤러

  • 스프링 MVC의 DispatcherServlet 역할
  • 하나의 서블릿으로 사용 (공통 처리)
  • URL 요청을 전부 이곳에서 받아 요청에 맞는 컨트롤러를 호출 -> 서블릿 종속성 제거
  • 각각의 컨트롤러에서 View 네임을 반환하면 프론트 컨트롤러에서 경로(viewPath)를 생성 -> 코드 중복 제거
  • 프론트 컨트롤러에서만 서블릿 선언을 해서 나머지 컨트롤러에서 서블릿 종속성 제거
  • 모든 컨트롤러에서 Model을 반환하지 않도록 프론트 컨트롤러에서 따로 Model 객체를 생성해서 전달
  • 여러 컨트롤러를 처리할 수 있도록 핸들러 어댑터 생성

 

@WebServlet(name = "FrontControllerSerlvetV5", urlPatterns = "/front-controller/v/*")
public class FrontControllerSerlvetV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    // 핸들러 매핑 등록, 핸들러 어댑터 등록
    public FrontControllerSerlvetV5() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }


    // 핸들러(컨트롤러) 초기화
    private void initHandlerMappingMap() {
    	//v3 버전 컨트롤러
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

		//v4 버전 컨트롤러
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    // 어댑터 초기화
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 핸들러 조회
        Object handler = getHandler(request);

        // 요청 URL에 맞는 핸들러 객체가 없으면 종료
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 3. 핸들러에 맞는 핸들러 어댑터 조회
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 5. 핸들러 어댑터에 전달
        // 어댑터는 request 파라미터를 Map 객체에 담고 model 객체를 따로 생성해서 전달
        // 핸들러는 Servlet 종속성이 제거되고 ViewName만 반환하면 됨
        ModelView mv = adapter.handle(request, response, handler);

        // 6. ViewName(논리 이름)으로 View Resolver(풀 경로 생성) 실행
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        // 7. View 렌더링
        view.render(mv.getModel(), request, response);
    }

    // 2. 요청 URL에 맞는 핸들러 객체를 handlerMappingMap에서 조회, 핸들러 객체 반환
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    // 4. 해당 핸들러를 지원하는 핸들러 어댑터가 있는지 확인
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("Error");
    }

    //ViewPass생성
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

 

[참고]인프런 김영한님 강의를 공부한 내용입니다.

빈 생명주기 콜백

  • 스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음 초기화 작업을 할 수 있는 준비가 된다.
  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 기능을 제공한다.
  • 또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 주어 안전하게 종료 작업을 할 수 있게 지원한다.

 

스프링이 지원하는 빈 생명주기 콜백 방법

 

1. 인터페이스

  • InitializingBean, DisposableBean
  • 스프링 초창기에 주로 사용하던 방법
  • 스프링 인터페이스에 의존적, 외부 라이브러리 적용 불가 등의 단점

 

2. 설정 정보

  • @Bean(initMethod = "init", destroyMethod = "close")
  • 메서드 이름 자유롭게 사용, 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 적용 가능
  • destroyMehtod의 경우 따로 적지 않아도 close, shutdown 이라는 메서드 이름을 추론해서 호출

 

3. 애노테이션

  • @PostConstruct, @PreDestroy
  • 자바 표준 기술로 가장 권장하는 방법
  • 유일한 단점으로는 외부 라이브러리에 적용 못하기 때문에 이 경우는 @Bean 설정 정보 사용

 

 

빈 스코프

  • 생성된 빈이 존재할 수 있는 범위
  • 일반적으로 스프링 빈은 싱글톤 스코프로 생성
  • @Scope

 

스프링이 지원하는 스코프

  • 싱글톤 : 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 기본 스코프
  • 프로토 타입 : 스프링 컨테이너가 의존관계 주입, 초기화까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 스코프 : request, session, application 등 특정 웹 요청까지 유지되는 스코프

 

프로토타입 빈

 

1. 특징

  • 스프링 컨테이너 생성 시점에 초기화가 되는 싱글톤 빈과 달리, 프로토타입 빈은 스프링 컨테이너에서 빈은 조회할 때 생성
  • 조회를 할 때마다 새로운 스프링 빈이 생성, 초기화
  • 프로토타입 빈을 조회한 클라이언트가 해당 빈을 관리

 

2. 문제점

  • 스프링 컨테이너 의존관계 주입 시점에 싱글톤 빈에 프로토타입 빈이 생성되어 주입
  • 이후 클라이언트가 프로토타입 빈의 메서드 요청 시 새로운 프로토타입 빈이 생성되는 것이 아니라 기존 빈 호출
  • 프로토타입 빈이 싱글톤 빈과 함께 계속 유지되는 문제 발생

 

3. 해결 방안

  • 의존관계 주입(D.I)이 아닌 의존관계 조회(Depengency Lookup)을 통해 필요시마다 프로토타입 빈을 새로 조회, 생성해야 한다.
  • 스프링이 제공하는 ObjectProvider (기존 ObjectFactory + 편의 기능 추가)
  • 지정한 빈을 컨테이너에서 대신 찾아주는 D.L 기능 제공
  • getObject() 메서드를 통해 항상 새로운 프로토타입 빈이 생성
  • 스프링에 의존적이지만 기능이 단순해서 단위테스트, mock 테스트 만들기 편리
  • 싱글톤 스코프 빈을 거의 사용하기 때문에 직접적으로 프로토타입 빈을 사용할 경우는 적음 

 

 

스코프와 프록시

 

  • @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
  • 스프링 컨테이너가 CGLIB (바이트코드를 조작하는 라이브러리)를 사용해서 가짜 프록시 객체를 생성
  • 실제 객체를 상속받은 프록시 객체를 스프링 빈으로 등록, 싱글톤 빈처럼 사용
  • 싱글톤을 사용하는 것 같지만 실제로는 다르게 동작하므로 주의해서 사용
  • 프록시 객체는 요청이 올 경우 그 때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • ObjectProvider를 사용하든, Proxy를 사용하든 핵심은 실제 객체 조회를 필요한 시점까지 지연 처리 한다는 점

 

 

 

참고(https://www.inflearn.com/users/@yh)

 

스프링의 핵심 원리는 다형성과 SOLID 원칙을 따르는 설계

 

SOLID 5 원칙

  • 로버트 마틴이 정의한 객체 지향 프로그래밍 설계의 기본 원칙
  • 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부

 

더보기

1. SRP (Single Responsibility) - 단일 책임 원칙

  • 하나의 클래스는 하나의 책임만 가져야 한다.
  • 변경이 있을 때 파급 효과가 적게 (ex. 객체의 생성과 사용을 분리)

 

2. OCP (Open Closed) - 개방 폐쇄 원칙

  • 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 인터페이스를 구현한 새로운 클래스 생성, 교체 (기존 클래스 변경 X)

 

3. LSP (Liskov Substitution) - 리스코프 치환 법칙

  • 프로그램의 정확성을 깨지 않으면서 하위 타입의 인스턴스로 교체
  • 인터페이스 규약을 지킬 것

 

4. ISP (Interface Segregation) - 인터페이스 분리 원칙

  • 역할에 따른 세분화된 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

 

5. DIP (Depengency Inversion) - 의존관계 역전 법칙

  • 구현 클래스에 의존하지 말고, 인터페이스(역할)에 의존 → 구현체를 유연하게 변경

 

구체 클래스에서 의존 관계를 설정하면 SOLID 원칙을 지킬 수 없다. -> 의존 관계를 외부에서 생성, 주입해주어야 한다.

 

public class MemberServiceImpl implements MemberService {
	// MemberRepository 구현체를 변경해야 된다면?
	private final MemberRepository repository = new NormalMemberRepository();
}

 


 

의존관계 주입 (D.I)

  • 애플리케이션 실행 시점에 외부에서 구현 객체를 생성하고 전달해서 의존 관계가 연결되는 것
  • 런타임시에 관계를 동적으로 주입, 결합도를 낮추고 유연성을 확보
  • 구체 클래스에 의존하는 코드 제거 가능

 

public class AppConfig {
	public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
	}
}

 

스프링을 이용한 의존관계 주입

  • 클래스에 @Configuration, 메소드에 @Bean을 붙이면 스프링은 해당 빈들을 스프링 컨테이너에 스프링 빈으로 등록

 

@Configuration
public class AppConfig {
	@Bean
	public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
	}
}
public static void main(String[] args) {
    // AppConfig appConfig = new AppConfig();
  	// MemberService memberService = appConfig.memberService();
	
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
}

 


 

스프링 컨테이너 (ApplicationContext)

  • 스프링 컨테이너는 XML 또는 애노테이션 기반의 자바 설정 클래스(AppConfig.class)로 만들 수 있다.
  • 스프링 빈을 스프링 빈 저장소에 등록

 

ApplicationContext

  • 스프링 컨테이너의 최상위 인터페이스 BeanFactory를 상속
  • 빈 관리 기능 + 편리한 부가 기능을 제공
  • 국제화 기능을 위한 MessageSource
  • 편리하게 리소스를 조회하는 ResourceLoader

 

BeanDefinition

  • @Bean 을 읽어서 메타 정보를 생성
  • 스프링 컨테이너는 메타 정보를 기반으로 스프링 빈 생성
  • 실제로 빈을 사용할때까지 생성을 지연할 수 있음

 

스프링을 왜 써야 하는가?

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
  • 순수한 자바 코드의 D.I 컨테이너는 고객의 요청이 올 때마다 객체를 새로 생성 -> 메모리 문제
  • 객체를 하나만 생성하고 공유하도록 설계를 해야 함
  • 싱글톤 패턴의 경우 SOLID 원칙을 지키기가 어려움
  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서객체 인스턴스를 싱글톤(1개만 생성)으로 관리

 

싱글톤으로 관리가 되는 이유?

  • 스프링은 CGLIB라는 클래스의 바이트코드를 조작하는 라이브러리를 사용
  • 실제 클래스를 상속 받은 임의의 클래스를 생성해서(프록시) 스프링 빈으로 등록
  • 설정 파일의 경우 @Configuration 애노테이션이 있으면 내부적으로 CGLIB 동작

 

스프링 빈을 일일이 등록하는 것은 너무 힘들다.

  • @ComponentScan@Component가 붙은 클래스를 확인하여 스프링 빈으로 등록 (선언된 패키지 위치부터)
  • @Repository, @Service, @Controller, @Configuration도 컴포넌트 스캔의 대상이 된다.
  • @SpringBootApplication 내부에도 메타 애노테이션(스프링 지원 기능)으로 @ComponentScan이 있음

 

의존 관계 주입은 어떻게?

  • 생성자 주입, 필드 주입, 수정자 주입, 메서드 주입이 있는데 주로 생성자 주입을 사용
  • 필드 주입의 경우 편리하지만 외부에서 변경이 불가능해서 테스트하기 힘듬, D.I에 의존적
  • 생성자 주입의 경우 한번만 호출되므로 불변하게 설계 가능, final 선언으로 컴파일 시점에서 오류 방지
  • @Autowired 를 사용하면 스프링이 의존관계 자동 주입
 

 

 참고(https://www.inflearn.com/course/스프링-핵심-원리-기본편)

+ Recent posts