5. 스프링 MVC 구조 이해 (2) - 스프링 MVC로 변환

2025. 4. 29. 17:11Spring/MVC

[목차]

직접 만든 MVC 프레임워크 → 스프링 MVC로 변환 

1. 기존 컨트롤러: 인터페이스 방식의 컨트롤러 

2. 스프링 MVC의 컨트롤러: 애노테이션 방식의 컨트롤러
1) 애노테이션 기반 컨트롤러의 등장 배경
2) @RequestMapping의 역할과 동작 방식
3) 인터페이스 방식과의 비교 정리 (복습) 

3. 스프링 MVC의 애노테이션 방식의 컨트롤러 사용하기 
VERSION 1. 기본적인 사용법
VERSION 2. 컨트롤러 통합하기 
VERSION 3. 실용적인 방식으로 개선 


 

1. 기존 컨트롤러: 인터페이스 방식의 컨트롤러 

public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form"); //물리적인 주소는 프론트 컨트롤러에서 일괄적으로 처리한다
    }
}

public class MemberSaveControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));
        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);
        return mv;
    }
}


public class MemberListControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);
        return mv;
    }
}

 

2. 스프링 MVC의 컨트롤러: 애노테이션 방식의 컨트롤러

1) 애노테이션 기반 컨트롤러의 등장 배경

  • 기존에는 컨트롤러 인터페이스를 구현한 클래스를 직접 등록하고, 프론트 컨트롤러에 수동으로 URL-컨트롤러 매핑을 등록했었다.
  • 그러나 이제 스프링 MVC에서는 @Controller, @RequestMapping과 같은 애노테이션을 사용해 훨씬 간결하게 URL을 매핑하여 컨트롤러를 만들 수 있다.
  • 과거에는 스프링 프레임워크가 MVC 부분이 약해서 스프링을 사용하더라도 MVC 웹 기술은 스트럿츠 같은 다른 프레임워크를 사용했었다. 그런데 @RequestMapping 기반의 유연하고 실용적인 애노테이션 컨트롤러가 등장하면서, MVC 부분도 스프링의 완승으로 끝이 났다.

2) @RequestMapping의 역할과 동작 방식

  • @RequestMapping 기반의 컨트롤러가 동작 하기 위해서는, 스프링 MVC가 제공하는 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter가 DispatcherServlet에 등록되어 있어야 한다. 스프링 부트는 이 두 컴포넌트를 자동으로 등록해주기 때문에, 개발자는 신경 쓸 필요 없이 컨트롤러만 작성하면 된다.
  • @RequestMapping 기반의 컨트롤러에 사용되는 객체 정리 (스프링 부트가 자동 등록)  
    • HandlerMapping = RequestMappingHandlerMapping
    • HandlerAdapter = RequestMappingHandlerAdapter
    • 이들은 여러 핸들러 매핑과 핸들러 어댑터 중 가장 우선순위가 높다.
  • 현재 스프링 웹 애플리케이션에서는 거의 모든 컨트롤러가 @RequestMapping을 기반으로 작성된다. 

3) 인터페이스 방식과의 비교 정리 (복습) 

구분 인터페이스 기반 컨트롤러 어노테이션 기반 컨트롤러 
구조 명시적으로 인터페이스를 implements 해서 컨트롤러 작성 아무 인터페이스 없이 자유롭게 메서드 작성
URL 매핑 방식 프론트 컨트롤러에 직접 URL-컨트롤러 객체 매핑 @RequestMapping 어노테이션으로
컨트롤러의 메서드에 직접 맵핑 정보 작성
리턴 타입 고정 여부 인터페이스에 정의된 리턴 타입 따라야 함
(ex. ModelView, String 등)
자유롭게 리턴 가능
(String, ModelAndView, 객체 등)
매개변수 고정 여부 인터페이스에 정의된 파라미터를 따라야 함 자유롭게 파라미터 선언 가능
(@RequestParam, @ModelAttribute 등)
처리 방식 개발자가 모든 규칙에 맞춰 작성해야 함 스프링이 어노테이션 + 리플렉션으로 자동 처리
유연성 낮음 (인터페이스에 종속) 높음 (개발자 자유 ↑)

 

3. 스프링 MVC에서 @RequestMapping 컨트롤러 사용하기 

VERSION 1. 기본적인 사용법

@Controller
public class SpringMemberFormControllerV1 {
    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}

@Controller
public class SpringMemberSaveControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @RequestMapping("/springmvc/v1/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
        //mv.getModel().put("member", member);
        mv.addObject("member", member);
        return mv;
    }
}

@Controller
public class SpringMemberListControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @RequestMapping("/springmvc/v1/members")
    public ModelAndView process() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }
}

 

1) @Controller (컴포넌트 스캔 대상)

  • 스프링이 컴포넌트 스캔시 자동으로 스프링 빈으로 등록한다.
    내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨
  • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.

2) @RequestMapping (메타 데이터) 

  • 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다.
  • 메서드 레벨의 애노테이션으로 HandlerMapping 객체 초기화시 메서드와 URL의 매핑정보가 저장된다. 
  • 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.
  • ModelAndView : 모델과 뷰 정보를 담아서 반환하면 된다.

3) mv.addObject("member", member)

  • 스프링이 제공하는 ModelAndView 를 통해 Model 데이터를 추가할 때는 addObject() 메서드를 사용하면 된다. 
  • 이 데이터는 이후 뷰를 렌더링 할 때 사용된다.

VERSION 2. 컨트롤러 통합하기 

@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm() {
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
        //mv.getModel().put("member", member);
        mv.addObject("member", member);
        return mv;
    }

    @RequestMapping
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }
}

1)  컨트롤러 클래스 통합

  • @RequestMapping은 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다.
  • 따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.

2) 클래스 레벨 애노테이션 + 메서드 레벨 애노테이션의 조합

1. 메서드 레벨 애노테이션만 사용 
@RequestMapping("/springmvc/v2/members/new-form")
@RequestMapping("/springmvc/v2/members")
@RequestMapping("/springmvc/v2/members/save")

2. 클래스 레벨 애노테이션 + 메서드 레벨 애노테이션 조합 
클래스 레벨 
@RequestMapping("/springmvc/v2/members")
	+
메서드 레벨 
@RequestMapping("/new-form")
@RequestMapping("/save")
@RequestMapping

VERSION 3. 실용적인 방식으로 개선 

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    //@RequestMapping("/new-form")
    @GetMapping("/new-form")
    public String newForm() {
        //return new ModelAndView("new-form");
        return "new-form";
    }

    //@RequestMapping("/save")
    @PostMapping("/save")
    public String save(@RequestParam("username") String username,
                       @RequestParam("age") int age,
                       Model model) {
        //String username = request.getParameter("username");
        //int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        //ModelAndView mv = new ModelAndView("save-result");
        //mv.getModel().put("member", member);
        //mv.addObject("member", member);
        //return mv;
        model.addAttribute("member", member);
        return "save-result";
    }

    @GetMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        //ModelAndView mv = new ModelAndView("members");
        //mv.addObject("members", members);
        //return "members";
        model.addAttribute("members", members);
        return "members";
    }
}

 

1) @RequestMapping → @GetMapping, @PostMapping

  • @RequestMapping은 단순 URL 매핑뿐만 아니라 HTTP 메서드(GET, POST 등)까지 구분해서 매핑할 수 있다! (중요) 
    @RequestMapping(value = "/new-form", method = RequestMethod.GET) 
  • 위와 같은 @RequestMapping은 @GetMapping , @PostMapping 등등 으로 더 편리하게 사용할 수 있다
    @GetMapping("/new-form")
  • @GetMapping 코드를 열어보면 @RequestMapping 애노테이션을 내부에 가지고 있는 것 확인 가능함
URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑 
→ @RequestMapping(value = "/new-form", method = RequestMethod.GET) 
→ 또는 @GetMapping("/new-form")

 

2) 파라미터와 리턴값 편리하게 사용 

  • save() , members() 메서드를 보면 Model을 파라미터로 받는 것을 확인할 수 있다.
    Model을 메서드 내에서 직접 생성할 필요 없이 스프링에서 빈 Model 객체를 만들어서  파라미터로 넣어 준다. 
  • 또한 스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.
    • @RequestParam("username") 은 request.getParameter("username") 와 거의 같은 코드라 생각하면 된다.
    • 물론 GET 쿼리 파라미터, POST Form 방식을 모두 지원한다.
  • 리턴값으로는 뷰의 논리 이름(ViewName)을 String으로 직접 반환할 수 있다. 
  • 이렇게 컨트롤러의 파라미터와 리턴값을 자유롭게 사용할 수 있는 이유는 RequestMappingHandlerAdapter 덕분이다.

3) @RequestMapping 방식의 유연성: RequestMappingHandlerAdapter 

  • 위에서 본 것처럼 @RequestMapping 기반 컨트롤러는 메서드 이름, 파라미터, 리턴값이 자유롭다.
  • 이는 RequestMapping 컨트롤러(핸들러)의 핸들러 어댑터인 RequestMappingHandlerAdapter가 강력한 컴포넌트를 가지고 유연함을 제공하기 때문이다. 
    • HandlerMethodArgumentResolver: 컨트롤러 메서드의 파라미터를 알아서 분석하고 값을 주입
    • HandlerMethodReturnValueHandler: 컨트롤러 메서드의 리턴값을 해석해서 ModelAndView로 바꿔줌
  • RequestMappingHandlerAdapter의 역할
      HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler
  • @RequestParam("username"), @RequestParam("age")
    HandlerMethodArgumentResolver 중 RequestParamMethodArgumentResolver가 요청값 분석해서 파라미터로 값 넣어줌
    기존에 전달받은 HttpServletRequest 객체에서 직접 파라미터를 찾아서 형변환해주던 과정을 어댑터에서 다 해주는 것

    즉, int age = Integer.parseInt(request.getParameter("age")); @RequestParam("age")
  • 리턴값이 String이면
    HandlerMethodReturnValueHandler가 "hello"는 뷰 이름이구나 라고 해석하고 ModelAndView 만들어서 리턴해줌