728x90
반응형
SMALL

참고자료:

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com

스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작한다. 그래서 매우 유연하고 실용적이다.

 

@RequestMapping

이 애노테이션이 바로 스프링이 사용하는 애노테이션 기반 컨트롤러이다. 이 애노테이션을 기반으로 핸들러 매핑과 핸들러 어댑터가 존재한다. 핸들러 매핑과 핸들러 어댑터가 뭔지 모른다면 이전 포스팅을 꼭 읽고 오길 바란다. 핸들러 매핑을 통해 URL과 매핑된 컨트롤러를 찾고 그 컨트롤러를 처리할 수 있는 핸들러 어댑터를 찾아내는게 핸들러 어댑터이다. 그게 스프링은 굉장히 여러 형태의 구현체로 존재하는데 이 애노테이션 기반은 다음 두 개이다.

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

가장 우선순위가 높은 핸들러 매핑핸들러 어댑터이다. 애노테이션의 이름을 따서 만든 이름이다.

지금까지 쭉 만들어왔던 스프링 MVC 구조를 이해하기 위해서 작업했던 것들을 스프링 MVC로 바꿔보자.

 

참고로, 이 글은 이전 포스팅을 의존하기 때문에 이전 포스팅을 읽지 않았다면 먼저 읽고 오는 것을 권장한다.

2024.07.07 - [Spring MVC] - Spring MVC 구조 이해하기 2 - 스프링 MVC 전체구조

 

Spring MVC 구조 이해하기 2 - 스프링 MVC 전체구조

참고자료: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의

cwchoiit.tistory.com

2024.05.07 - [Spring MVC] - Spring MVC 구조 이해하기 1 - Front Controller

 

Spring MVC 구조 이해하기 1 - Front Controller

참고자료: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 | 김영한 - 인프런김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심

cwchoiit.tistory.com

 

 

SpringMemberFormControllerV1

package org.example.servlet.web.springmvc.v1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringMemberFormControllerV1 {

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


}

클래스 레벨에 @Controller 애노테이션이 붙었다. 이건 뭘까? 스프링이 자동으로 스프링 빈으로 등록하게 해주는 애노테이션이다. 저 애노테이션 내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 된다. 또한, 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다. 그래서 @Controller 하나만 있어도 스프링 빈으로 자동 등록해주고 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식하게 된다. @RequestMapping은 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 

 

RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다. 그래서 다음과 같은 코드도 동일하게 동작한다.

package org.example.servlet.web.springmvc.v1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Component
@RequestMapping
public class SpringMemberFormControllerV1 {

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


}

 

또는 컴포넌트 자동 스캔을 사용하지 않고 직접 빈으로 등록한다면 이런 코드도 가능하다.

package org.example.servlet.web.springmvc.v1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@RequestMapping
public class SpringMemberFormControllerV1 {

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


}

근데 굳이 그럴 필요없이 @Controller를 사용한다면 모든게 충족된다.

주의! 스프링 부트 3.0이상부터는 클래스 레벨@RequestMapping이 있어도 스프링 컨트롤러로 인식하지 않는다. 오직 @Controller가 있어야 스프링 컨트롤러로 인식한다. 참고로 @RestController는 해당 애노테이션 내부에 @Controller가 있으므로 인식이 된다. 따라서 위에 설명한 두개의 코드는 이제 스프링 컨트롤러로 인식되지 않는다. RequestMappingHandlerMapping이 이제 @Controller만 인식을 한다.

 

 

SpringMemberListControllerV1

package org.example.servlet.web.springmvc.v1;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class SpringMemberListControllerV1 {

    private final 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;
    }
}

 

모두 반환타입이 ModelAndView 타입이다. 이는 이 컨트롤러는 어떤 특정 뷰를 보여줄것을 의미한다. 그리고 반드시 그 뷰의 이름을 넣어주게 되어 있고 필요하다면 해당 뷰에서 사용할 데이터를 ModelAndViewModel에 담는다. 담을땐 mv.addObject("key", "value")로 넣으면 된다. 그리고 이 ModelAndView를 반환하면 끝이다.

 

근데, 지금 코드는 불편한 부분이 있다. 이 세개의 컨트롤러가 모두 나뉘어져 있는것이 상당히 불편하고 번잡하다. 하나로 합칠 수 있다. 그것을 해보자. 그리고 사실 이전 포스팅에서 배웠지만 단순 문자열만 반환해도 뷰를 보여줄 수 있었다. 그것 또한 차근차근 알아보자.

 

SpringMemberControllerV2

package org.example.servlet.web.springmvc.v2;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class SpringMemberControllerV2 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

    @RequestMapping("/springmvc/v2/members")
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);

        return mv;
    }

    @RequestMapping("/springmvc/v2/members/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) throws Exception {
        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.addObject("member", member);
        return mv;
    }
}

@RequestMapping 애노테이션이 메서드 레벨에 붙기 때문에 연관성 있는 메서드들끼리 묶어서 한 컨트롤러에서 모두 처리가 가능하다.

파일3개가 파일1개가 돼버리니 훨씬 기분이 좋아진다. 그리고 지금 @RequestMapping의 URL 정보 중 `/springmvc/v2/members/` 까지는 모두 동일하다. 이것 또한 하나로 줄일 수 있다.

 

SpringMemberControllerV2

package org.example.servlet.web.springmvc.v2;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

    @RequestMapping("")
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);

        return mv;
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) throws Exception {
        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.addObject("member", member);
        return mv;
    }
}

위 코드처럼 아예 클래스 레벨에 동일하게 들어가는 Path를 고정시키고 변경되는 부분만 메서드의 @RequestMapping으로 지정해주면 된다. 

 

근데 여전히 불편한 부분이 있다. 위에서 잠깐 얘기했지만 모든것들이 다 ModelAndView를 반환해야 하고, 또 요청 URL에서 파라미터를 받아오는 부분이 굉장히 거슬린다. 이 부분을 실무에서 많이 사용하는 방식으로 변경해보자.

 

 

SpringMemberControllerV3

package org.example.servlet.web.springmvc.v3;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

    @RequestMapping("")
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        return "members";
    }

    @RequestMapping("/save")
    public String save(@RequestParam("username") String username,
                       @RequestParam("age") int age,
                       Model model) {
        Member member = new Member(username, age);
        memberRepository.save(member);
        model.addAttribute("member", member);
        return "save-result";
    }
}

 

우선 반환타입은 이제 단순 문자열이고 그 문자열이 곧 뷰 이름이 된다. 그리고 필요한 경우 뷰에 전달할 데이터를 모델에 담기 위해 ModelAndView를 사용했는데 파라미터에 그냥 Model을 받을 수가 있다. 그래서 이 녀석의 메서드 중 addAttribute()를 사용하면 알아서 반환할 뷰에게 데이터가 전달된다. 그리고 또한, URL로부터 가져와야 할 파라미터를 @RequestParam을 사용해서 아주 간편하게 가져올 수 있을뿐 아니라 보면 알겠지만 int 타입으로 알아서 형변환까지 해준다. 원래 URL은 무조건 모든게 다 문자열이다. 그래서 문자열이 아닌 경우를 원할땐 다 형변환을 해줘야했다(V2를 생각해보면 된다). 근데 그럴 필요 없이 알아서 타입을 맞춰준다. 

 

참고로, @RequestParamGET 요청의 URL 파라미터도 가져오지만 POST 요청의 바디 FormData도 가져올 수 있다.

 

그리고 마지막으로, 지금의 경우 HTTP Method가 구분이 전혀 안 된 상태이다. 그니까 모든 멤버들을 조회하는 members()를 호출할때 GET, POST, PUT, PATCH, DELETE 다 사용이 가능한 상태이다. 아주 좋지 않다. 이 점도 깔끔하게 수정이 가능하다.

아래처럼 @RequestMapping()에는 Method라는 값을 전달할 수가 있다. 그래서 딱 원하는 Method로 결정할 수 있다.

@RequestMapping(value = "", method = RequestMethod.GET)
public String members(Model model) {
    List<Member> members = memberRepository.findAll();
    model.addAttribute("members", members);
    return "members";
}

 

근데! 이것마저 귀찮다고 이런걸 만들었다.

SpringMemberControllerV3

package org.example.servlet.web.springmvc.v3;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

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

    @PostMapping("/save")
    public String save(@RequestParam("username") String username,
                       @RequestParam("age") int age,
                       Model model) {
        Member member = new Member(username, age);
        memberRepository.save(member);
        model.addAttribute("member", member);
        return "save-result";
    }
}

 

이게 가장 최신의 깔끔한 방식이다. V1과 비교해보면 정말 군더더기 없이 깔끔하다. 그러나 V1에서 이 V3로의 역사를 알면 더 깊은 이해를 할 수가 있다. 

728x90
반응형
LIST

+ Recent posts