인프런/스프링 MVC 1편

7)스프링 MVC 시작하기(버전1 ~ 버전3) ,Http api와 Rest api의 차이

backend dev 2023. 1. 18.

직접 MVC프레임워크를 만들면서 컨트롤러를 만들었는데 그 컨트롤러들을 @RequestMapping 기반의 Spring MVC 컨트롤러로 바꿔보자. 

 

 

어노테이션기반 스프링 컨트롤러도 버전업을 하면서

개선되어가는 모습을 살펴본다.

 

SpringMemberFormControllerV1 (버전1 , 멤버등록 컨트롤러)

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form"); //프론트컨트롤러인 DispatchServlet이 이 값을 반환받아 뷰리졸버를 찾아 뷰를 받고 렌더링해줄것이다.
    }
}

@RequestMapping 어노테이션을 이용하여 메소드와 url패턴을 매핑한다. 해당 url이 호출되면 이 메소드가 실행된다.

 

@Controller 내부에는 @Component어노테이션이 있어서 자동빈 등록이 된다. (컴포넌트 스캔의 대상이 되므로)

또 @Controller를 붙이면 스프링MVC에서 어노테이션 기반 컨트롤러로 인식한다. 

 

스프링 MVC는 어플리케이션 로딩 시점에 @Controller가 붙어있는 모든 클래스를 찾아서 그 안에 @RequestMapping의 메소드를  HandlerMethod로 변환해서 RequestMappingHandlerMapping에 보관하고 있는다.

(변환도RequestMappingHandlerMapping가 한다)

http 요청으로 url호출이 들어오면 RequestMappingHandlerMapping에서 해당 url패턴과 매핑되어있는 메소드정보를 가지고있는 HandlerMethod를 반환해준다. (정확히는 그 메소드의 정보가 담긴 HandlerMethod가 반환되어 HandlerMethod안에있는 컨트롤러의 정보를 가지고 어댑터를 찾는것이다.)

RequestMappingHandlerMapping은 "스프링 빈 중에서" @RequestMapping 또는 @Controller가 "클래스 레벨"에 붙어있는경우 매핑정보로 인식한다. 그래서RequestMappingHandlerMapping는  그안에 @RequestMapping으로 매핑된 메소드들을 HandlerMethod로 만들어 RequestMappingHandlerMapping에 저장한다. (스프링 3.0 미만에서)

@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록
@RequestMapping //클래스레벨에 @RequestMapping
public class SpringMemberFormControllerV1 {
    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}

@Controller가 없지만 @Component으로  스프링빈 등록하고, 클래스레벨에 @RequestMapping이 있으므로 

RequestMappingHandlerMapping가 이 컨트롤러의 @RequestMapping이 붙은 메소드들을 HandlerMethod로 변환하여 RequestMappingHandlerMapping에 매핑정보로 들어가게된다.

 

물론 수동 빈등록으로 빈등록하고 해도 된다.

@RequestMapping // 클래스레벨에 @RequestMapping 존재
public class SpringMemberFormControllerV1 {
   @RequestMapping("/springmvc/v1/members/new-form")
   public ModelAndView process() {
      return new ModelAndView("new-form");
   }
}
@ServletComponentScan //서블릿 자동등록 어노테이션, 자동빈등록에 쓰는 @ComponentScan과 같이 , 현재 패키지이하를 모두 뒤져서 서블릿을 찾아, 등록시켜준다.
@SpringBootApplication
public class ServletApplication {

   public static void main(String[] args) {
      SpringApplication.run(ServletApplication.class, args); //여기서 ServletApplication.class를 넘겨주는것이, 스프링컨테이너를 만들때
      //AnnotationConfigApplicationContext(구성설정.class)처럼 넘겨줘서 그 클래스안의 @Bean으로 수동빈등록한것들을 처리해주는것처럼 동작한다.
   }

   @Bean // Application에서 수동빈 등록이 가능한 이유는 SpringApplication.run(ServletApplication.class, args); 덕분이다.
   SpringMemberFormControllerV1 springMemberFormControllerV1() {
      return new SpringMemberFormControllerV1();
   }

}

 

수동 빈 등록을 Application.java에서 진행해도 등록이 된다.

 

다양한 방법이 되지만

@Controller를 붙이는게 가장 깔끔하므로 그 방법을 사용한다.

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form"); //프론트컨트롤러인 DispatchServlet이 이 값을 반환받아 뷰리졸버를 찾아 뷰를 받고 렌더링해줄것이다.
    }
}

이 방법.

주의사항

스프링 3.0이상에는 클래스레벨에 @RequestMapping이 있어도 스프링 컨트롤러로 인식하지않는다.

RequestMappingHandlerMapping.isHandler()에 RequestMapping.class 부분이 빠졌다.

@Override
protected boolean isHandler(Class<?> beanType) {
   return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}

커밋된 사항

SpringMemberSaveControllerV1 ( 버전1, 회원 저장 컨트롤러)

@Controller
public class SpringMemberSaveControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response) { //RequestMappingHandlerAdapter가 핸들러의 해당 메소드를 실행하면서 파라미터값을 전달해준다(전달해주는 값은 다양함)
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        //컨트롤러(핸들러)는 DispatchServlet(프론트컨트롤러)에 ModelAndView를 반환해줘야한다.
        ModelAndView modelAndView = new ModelAndView("save-result"); //논리적인 뷰 이름을 넣고 생성
//        modelAndView.getModel().put("member", member);  이렇게 모델에 데이터를 넣을수도있지만
        modelAndView.addObject("member", member); //이렇게 모델에 데이터를 넣을 수 있다. (좀더 깔끔)
        return modelAndView;
    }
}

@RuestMapping으로 해당 url과 메소드가 매핑되었고 그 메소드의 정보를 가지고 HandlerMethod 객체를 RequestMappingHandlerMapping가 생성하고 , RequestMappingHandlerMapping에 저장한다.

 

http 요청이 들어오면 해당 url요청에 맞는HandlerMethod를 RequestMappingHandlerMapping가 찾고, 그 HandlerMethod를 반환한다. 그러면 DispatchServlet이 HandlerMethod를 보고 해당 컨트롤러를 알아서 어댑터를 찾게되는것이다.  컨트롤러(핸들러)에 맞는 RequestMappingHandlerAdapter를 찾고, RequestMappingHandlerAdapter의 내부에서 컨트롤러(핸들러)의 메소드 (위에서는 process메소드 )를 실행할텐데 그때 전달되는 파라미터는, 즉 컨트롤러 메소드에 주입되는 값은 다양하다. 이 중 사용자가 받고 싶은 객체를 기술하면, 스프링 프레임워크가 이 객체를 맞춰서 주입한다고 한다.

 

객체의 종류는 다음 블로그를 참조한다.

 

Spring Controller 메소드 파라미터의 종류

Spring Controller 알고쓰자

velog.io

 

SpringMemberListControllerV1 ( 버전1, 회원목록 컨트롤러)

@Controller
public class SpringMemberListControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members")
    public ModelAndView process() { //여기서는 스프링프레임워크에게 주입 받을 파라미터가 필요없으니까 적지않는다.
        List<Member> members = memberRepository.findAll();

        ModelAndView modelAndView = new ModelAndView("members");
        modelAndView.addObject("members", members);
        return modelAndView;
    }

}

잘 동작한다!


스프링 MVC - 컨트롤러 통합 (버전2)

@Controller를 붙여주는이유

1. 스프링빈으로 등록

2. 스프링빈으로 등록되어있어야, RequestMappingHandlerMapping이 스프링빈중 @Controller가 붙은 클래스안에를 살펴서 @RequestMapping 어노테이션이 붙은 메소드들을 이용해서 HandlerMethod를 만들고  HandlerMethod를 매핑정보로 가져가기 떄문.

 

SpringMemberControllerV2 (통합 컨트롤러)

@Controller
public class SpringMemberControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v2/members/new-form")
    public ModelAndView newForm() {
        return new ModelAndView("new-form"); //프론트컨트롤러인 DispatchServlet이 이 값을 반환받아 뷰리졸버를 찾아 뷰를 받고 렌더링해줄것이다.
    }
    @RequestMapping("/springmvc/v2/members")
    public ModelAndView save() { //여기서는 스프링프레임워크에게 주입 받을 파라미터가 필요없으니까 적지않는다.
        List<Member> members = memberRepository.findAll();

        ModelAndView modelAndView = new ModelAndView("members");
        modelAndView.addObject("members", members);
        return modelAndView;
    }
    @RequestMapping("/springmvc/v2/members/save")
    public ModelAndView members(HttpServletRequest request, HttpServletResponse response) { //RequestMappingHandlerAdapter가 핸들러의 해당 메소드를 실행하면서 파라미터값을 전달해준다(전달해주는 값은 다양함)
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        //컨트롤러(핸들러)는 DispatchServlet(프론트컨트롤러)에 ModelAndView를 반환해줘야한다.
        ModelAndView modelAndView = new ModelAndView("save-result"); //논리적인 뷰 이름을 넣고 생성
//        modelAndView.getModel().put("member", member);  이렇게 모델에 데이터를 넣을수도있지만
        modelAndView.addObject("member", member); //이렇게 모델에 데이터를 넣을 수 있다. (좀더 깔끔)
        return modelAndView;
    }


}

각각 회원등록,회원저장,회원목록 컨트롤러를 만들어서 구현했었는데.

어차피 @ReuqestMapping을 쓸거라면 하나의 컨트롤러안에서 다 적어주면된다.

@RequestMapping은 메소드단위로 매핑해주기 때문이다.

잘 동작한다.

조합을 이용해서 중복을 제거하자!

클래스레벨에 @RequestMapping 어노테이션을 이용해서 앞에 공통적으로 붙는 url를 적고

메소드레벨의 @RequestMapping에는 마지막에 적힐 url만 적어주면 깔끔하게 구성가능하다.

@Controller는 필수! 

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

    @RequestMapping("/new-form") // 슬래쉬(/)가 없어도 잘동작한다. 클래스레벨에 붙인 RequestMapping의 url패턴뒤에 알아서 슬래쉬를 붙이고 여기 메소드레벨에 적은 url패턴을 더해주는것같다.
    public ModelAndView newForm() {
        return new ModelAndView("new-form"); //프론트컨트롤러인 DispatchServlet이 이 값을 반환받아 뷰리졸버를 찾아 뷰를 받고 렌더링해줄것이다.
    }
    @RequestMapping // 이 메소드는 /springmvc/v2/members가 호출되면 실행된다. ( @RequestMapping에 추가정보를 적지않았으니까)
    public ModelAndView members() { //여기서는 스프링프레임워크에게 주입 받을 파라미터가 필요없으니까 적지않는다.
        List<Member> members = memberRepository.findAll();

        ModelAndView modelAndView = new ModelAndView("members");
        modelAndView.addObject("members", members);
        return modelAndView;
    }
    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) { //RequestMappingHandlerAdapter가 핸들러의 해당 메소드를 실행하면서 파라미터값을 전달해준다(전달해주는 값은 다양함)
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        //컨트롤러(핸들러)는 DispatchServlet(프론트컨트롤러)에 ModelAndView를 반환해줘야한다.
        ModelAndView modelAndView = new ModelAndView("save-result"); //논리적인 뷰 이름을 넣고 생성
//        modelAndView.getModel().put("member", member);  이렇게 모델에 데이터를 넣을수도있지만
        modelAndView.addObject("member", member); //이렇게 모델에 데이터를 넣을 수 있다. (좀더 깔끔)
        return modelAndView;
    }


}

잘 동작.

이렇게 사용하면 만약 v2를 v3으로 바꿔야한다했을때 클래스레벨의 @RequestMapping의 url패턴부분만 수정해주면 되기 때문에 편하다( 원래대로였으면 메소드레벨에 있는 모든 @RequestMapping을 바꿨을것이다.)


스프링 MVC 실용적인 방식 (마지막 버전) 실무에서 가장많이 사용된다.

이전에 MVC 프레임워크만들때 v3의 컨트롤러 내부로직은 ModelView를 만들어서 직접반환했고, 

그걸 수정해서 v4는 그냥 논리적인 뷰 이름만 반환하게 바꾸었었다.

  

SpringMemberController V3 (마지막 버전)

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

    @RequestMapping("/new-form")
    public String newForm() {
        return "new-form";
    }
    @RequestMapping("/save")
    public String save(
        @RequestParam("username")String username,
        @RequestParam("age")int age, //쿼리스트링은 말그대로 String으로 값이 들어오는데, @RequestParam은 알아서 타입변환도 진행해준다.
        Model model //모델도 받을 수 있다. (스프링 프레임워크가 컨트롤러에 주입해주는 값은 다양하다)
        ) { //HttpServletRequest,Response 객체도 받을 수 있지만,@RequestParam을 이용해서 파라미터를 직접 받을 수 있다 (받을 수 있는 값 종류는 다양하다.)
        //@RequestParam으로 받을 수 있는 이유는 , Http form에서 데이터가 넘어오는것이다. Http form은 Body에 값이 담겨오지만,데이터가 쿼리스트링형식이기 때문에 파라미터값을 받는 어노테이션,메소드등을 사용할 수 있다.

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

        //ModelAndView를 반환해줬는데 , 필요없다 ModelAndView에는 논리적인 뷰이름과 모델이 들어있었는데, 논리적인 뷰이름은 직접 반환해줄것이고
        //모델은 해당 메소드를 호출하는,RequestMappingHandlerAdapter가 생성해서 넣어주는것 같다. 
        (스프링 프레임워크가 넣어준다고는 하지만,이 핸들러어댑터가 handle()이라는 메소드를 이용해 핸들러,필요한 정보들을 넘겨주고 handle()안에서 핸들러의 메소드가 실행되니까 그렇게 판단했다.)
        model.addAttribute("member", member); //전달된 모델에 데이터를 넣어준다.
        return "save-result";
    }

    @RequestMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members); //모델은 String(데이터이름), Object(데이터)를 받는다. Object이므로 어떤값이든 들어갈 수 있다.

        return "members";
    }

}

 

스프링이 어노테이션기반의 컨트롤러는 ModelAndView를 반환해도 되고, 문자를 반환해도 된다. 문자를 반환하게 되면 스프링이 알아서 뷰이름으로 알고 프로세스가 진행된다.

(인터페이스로 고정되어있지않고 매우 유연하게 설계되어있기때문이다, 물론 String을 뷰 이름이 아니고 다르게 인식하게끔 할 수 있는 방법도 있다.)

new-form만 적어놨는데, 밑줄이 생기면서, new-form.jsp로 인식하고있다는걸 보여준다.

 

 

@RequestMapping 문제점과 문제점 해결!

@RequestMapping은 매핑된 url에 대해 메소드를 실행해주고, 어떤 http 방식인지(get인지 post인지)는

신경안쓰고 실행시켜줬다.

 

그래서 http form방식은 post로 들어와야하는데 get방식으로 진행해도 성공을 했고

회원 등록하는 url를 호출할때 post방식으로 해도 정상적으로 호출되었다.

하지만 이렇게 아무방식으로도 접근되는것보다는

 

회원등록화면 접근에는 Get방식만, (데이터를 변경하고,추가하는게 아니니까)

회원저장 url에는 Post방식만

사용할 수 있게끔 해놓는게 좋다. (오류가 발생할 수 있음)

(매핑된 메소드를 접근할 수 있는 http method를 지정한다)

 

수정방법 1

@RequestMapping(value = "/new-form",method = RequestMethod.GET)
public String newForm() {
    return "new-form";
}

@RequestMapping에 어떤 http method로 접근할 수 있는지 지정한다.

post 방식으로 테스트 해보니 405오류를 발생하면서 url호출에 실패했다.
인텔리제이에도 오류에 대한 로그가 찍혔다.

그리고 Get방식으로 테스트해보니 url호출이 성공하였다.

 

수정방식2  ( 요즘사용되는 방식)!

@RequestMapping을 사용하지않고,

Http Method에 맞는 어노테이션을 사용한다.

@GetMapping, @PostMapping 등

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

    @GetMapping("/new-form")
    public String newForm() {
        return "new-form";
    }
    @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";
    }

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

        return "members";
    }

}

@GetMapping 어노테이션 안에 @RequestMapping(method = RequestMethod.GET)이 존재하기때문에 

@RequestMapping(method = RequestMethod.GET)와 같은 동작을 하는것이다.

 

스프링은 이런식으로 어노테이션들을 조합한 어노테이션을 제공하면서 개발자들이 편하게사용하게끔 한다.

컨트롤러 최종 수정 코드 (요즘 많이 사용하는 형식)

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

    @GetMapping("/new-form")
    public String newForm() {
        return "new-form";
    }
    @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";
    }

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

        return "members";
    }

}

 

Http api, Rest api 의 차이?

안녕하세요. rkskqksk님

HTTP API와 REST API는 사실 거의 같은 의미로 사용됩니다.

그런데 디테일하게 들어가면 차이가 있습니다.

HTTP API는 HTTP를 사용해서 서로 정해둔 스펙으로 데이터를 주고 받으며 통신하는 것으로 이해하시면 됩니다.

그래서 상당히 넓은 의미로 사용됩니다.

반면에 REST API는 HTTP API에 여러가지 제약 조건이 추가됩니다.

REST는 다음 4가지 제약조건을 만족해야 합니다.

(https://ko.wikipedia.org/wiki/REST)

- 자원의 식별

- 메시지를 통한 리소스 조작

- 자기서술적 메서지

- 애플리케이션의 상태에 대한 엔진으로서 하이퍼미디어

여러가지가 있지만 대표적으로 구현하기 어려운 부분이 마지막에 있는 부분인데요. 
이것은 HTML처럼 하이퍼링크가 추가되어서 다음에 어떤 API를 호출해야 하는지를 해당 링크를 통해서 받을 수 있어야 합니다.

그리고 이런 부분을 완벽하게 지키면서 개발하는 것을 RESTful API라고 하는데요. 실무에서 이런 방법으로 개발하는 것은 현실적으로 어렵고, 
또 추가 개발 비용대비 효과가 있는 것도 아닙니다.

그런데 이미 많은 사람들이 해당 조건을 지키지 않아도 REST API라고 하기 때문에, HTTP API나 REST API를 거의 같은 의미로 사용하고 있습니다.
하지만 앞서 말씀드린 것 처럼 엄격하게 위의 내용들을 모두 지켜야 REST API라고 할 수 있습니다.
(하지만 다들 HTTP API를 REST API라고 이미 하고 있기 때문에, 누군가 REST API라고 하면 그냥 아~ HTTP API를 이야기 하는구나 라고 생각하고 들으시면 됩니다. 
물론 엄격하게는 다릅니다.)

도움이 되셨길 바래요.

 

 

HTTP API vs REST API - 인프런 | 질문 & 답변

HTTP API를 기반으로 한 것이 REST API인가요? 무슨 차이가 있는지 궁금합니다. - 질문 & 답변 | 인프런...

www.inflearn.com

 

댓글