제육's 휘발성 코딩
Published 2021. 8. 13. 09:44
스프링 MVC - 기본 기능 🔷 Spring/MVC
반응형

Welcome 페이지

  • 스프링 부트에 Jar 를 사용하면 /resources/static/index.hml 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.

로깅

  • 스프링 부트 라이브러리는 스프링 부트 로깅 라이브러리를 포함한다.
    • SLF4J (Logback, Log4J, Log4J2 통합) - 인터페이스
    • Logback - 구현체 (대부분 사용)
@Slf4j // 애노테이션 설정으로 편리하게 사용 
@RestController // 디폴트의 뷰 반환이 아닌 문자 반환설정 (http 바디에 담아서 전송)
public class LogTestController {

//    private final Logger log = LoggerFactory.getLogger(getClass()); //해당 클래스 log 설정

    @RequestMapping("/log-test")
    public String logTest() {
        String name = "Spring";


        // 로그 레벨별 : 밑으로 갈수록 심각, 운영서버는 주로 info 레벨 부터
        log.trace("trace log= {}", name);
        log.debug("debug log= {}", name);
        log.info("info log= {}", name);
        log.warn("warn log= {}", name);
        log.error("error log = {}", name);

        return "ok";
    }

}
  • 로그 레벨 설정 (TRACE > DEBUG > INFO > WARN > ERROR) - default : info레벨
    • 개발서버 : debug
    • 운영서버 : info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정 , logging.level.root 로 전체 경로 설정도 가능 
logging.level.hello.springmvc=info
  • 올바른 로그 사용법
    • log.debug("data="+data) - 사용 X
      • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다. 결과적으로 문자 더하기 연산이 발생한다.
    • log.debug("data={}", data)
      • 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다.
  • 로그 사용시 장점
    • 파일, 네트워크등 로그를 별도로 남길 수 있다.
    • 성능 자체가 System.out 보다 좋다. (내부 버퍼링, 멀티 쓰레드 등)
    • 로그 레벨별 설정을 통해 상황에 맞게 로그 출력 가능

요청 매핑

@PathVariable

     /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     * @PathVariable("userId") String userId -> @PathVariable userId
     * /mapping/userA
     */
@GetMapping("/mapping/{userId}") //http://localhost:8080/mapping/userA, 템플릿화 
public String mappingPath(@PathVariable("userId") String data) {
    log.info("mappingPath userId={}", data);
    return "ok";
}

//http://localhost:8080/mapping/users/userA/orders/100 
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
                          orderId) {
    log.info("mappingPath userId={}, orderId={}", userId, orderId);
    return "ok";
}
  • 자주 사용하는 방식
  • url 변수 값과 파라미터 변수 값이 다르면 @PathVariable("url 변수 값 지정"), 같으면 생략 가능하다.

파라미터 추가 매핑

     /**
     * 파라미터로 추가 매핑
     * params="mode",
     * params="!mode"
     * params="mode=debug"
     * params="mode!=debug" (! = )
     * params = {"mode=debug","data=good"}
     */
    //http://localhost:8080/mapping-param?mode=debug 추가 파라미터 매핑
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }
  • 추가된 params 정보가 있어야만 호출 된다. 사용할 일이 별로 없다.

특정 헤더 추가 매핑

    /**
     * 특정 헤더로 추가 매핑
     * headers="mode",
     * headers="!mode"
     * headers="mode=debug"
     * headers="mode!=debug" (! = )
     */
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }
  • 헤더에 추가 정보를 넣어서 요청해야 된다. (postman 사용)

미디어 타입 조건 매핑

     /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }
  • HTTP 요청 Content-Type를 consume을 통해 조건을 매핑할 수 있다.
  • consumes = {"text/plain", "application/*"}과 같이 지정 가능
    /**
     * Accept 헤더 기반 Media Type
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     */
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
  • HTTP 요청 Accept, produce - header 의 Accept(받아드릴 수 있는 타입)가 일치해야 된다.
예시)
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"

요청 매핑 - API

회원 관리 API

  • 회원 목록 조회: GET /users
  • 회원 등록: POST /users
  • 회원 조회: GET /users/{userId}
  • 회원 수정: PATCH /users/{userId}
  • 회원 삭제: DELETE /users/{userId}
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    @GetMapping
    public String user() {
        return "get users";
    }

    @PostMapping
    public String addUser() {
        return "post user";
    }

    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId=" + userId;
    }

    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "update userId=" + userId;
    }

    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId=" + userId;
    }
}
  • class 에 @RequestMapping으로 공통된 url을 관리할 수 있다.

HTTP 요청 - 기본, 헤더 조회

@RequestMapping("/headers")
    public String headers(HttpServletRequest request, HttpServletResponse response,
                          HttpMethod httpMethod, Locale locale, @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader("host") String host, @CookieValue(value = "myCookie", required = false) String cookie) {
        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "ok";
    }
  • locale : 우선순위가 가장 높은 언어
  • headerMap={host=[localhost:8080], connection=[keep-alive], sec-ch-ua=["Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"] ~
  • header host=localhost:8080
  • MultiValueMap : MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
    • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다
    • map.get("key")를 하면 List를 반환한다.

HTTP 요청 - 쿼리 파라미터, HTML Form

  • 클라이언트에서 서버로 요청 데이터 전달하는 주 3가지 방법
    • GET - 쿼리 파라미터
    • POST - HTML Form
      • 메시지 바디에 쿼리파라미터 형식으로 전달 (x-www-form-urlencoded)
    • Http messsage body에 데이터를 직접 담아서 요청
      • HTTP API에서 주로 JSON 사용
      • POST, PUT, PATCH
@Slf4j
@Controller
public class RequestParamController {

    //http://localhost:8080/request-param-v1?username=hello&age=20
    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        log.info("username = {}, age = {},", username, age);

        response.getWriter().write("ok");
    }

    @ResponseBody // 클래스 레벨이 @Controller 인데 반환값을 데이터로 하고 싶을 때
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge
    ) {
        log.info("username = {}, age = {},", memberName, memberAge);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-v3") //변수 이름이 같으면 name = 생략 가능
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age
    ) {
        log.info("username = {}, age = {},", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-v4")  //String, int , Integer 등 단순 타입이면 어노테이션도 생략 가능
    public String requestParamV4(
           String username,
           int age
    ) {
        log.info("username = {}, age = {},", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-required") //required 가 true이면 무조건 있어야한다. 디폴트 true
    public String requestParamRequired(
            @RequestParam(required = true) String username, //필수 값이여도 ""인경우는 빈문자로들어와서 ok, 아예없으면 배드리퀘스트 주의
            @RequestParam(required = false) Integer age //Integer는 객체이기때문에 Null 가능, int 는 널일 수 없어서 객체로 선언
    ) {
        log.info("username = {}, age = {},", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-default") // defaultValue를 사용하면 사실상 required가 필요없다. 비어있으면 채워주기 때문에 "" 경우도 기본값으로 변경처리
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username,
            @RequestParam(required = false, defaultValue = "-1") int age
    ) {
        log.info("username = {}, age = {},", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
        log.info("username = {}, age = {},", paramMap.get("username"), paramMap.get("age"));
        return "ok";
    }
}
  • 클래스가 @Controller 일 때 메서드에서 데이터를 반환하고 싶으면 메서드에 @ResponseBody를 사용할 수 있다.
  • null과 ""는 다르다. ""는 빈문자로 인식

HTTP 요청 파라미터 - @ModelAttribute

@Data
public class HelloData {
    private String username;
    private int age;
}
------------------------------
@Slf4j
@Controller
public class RequestParamController {
     ...
    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {

        log.info("username = {}, age = {},", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData) {

        log.info("username = {}, age = {},", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
}
  • 스프링은 생략시 다음과 같은 규칙을 적용한다.
    • String, int, Integer 같은 단순 타입 - @RequestParam
    • 나머지 @ModelAttribute (argument resolver (HttpServletRequest 와 같은) 타입 제외)

HTTP 요청 메시지 - 단순 텍스트

  • HTTP 메시지 바디를 통해 데이터가 직접 데이터가 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.
@Slf4j
@Controller
public class RequestBodyStringController {

    @PostMapping("/request-body-string-v1")
    public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}", messageBody);

        response.getWriter().write("ok");
    }

    @PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {

        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody={}", messageBody);
       responseWriter.write("ok");
    }

    @PostMapping("/request-body-string-v3") // 메시지 컨버터 기능
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {

        String messageBody = httpEntity.getBody(); //http 메시지에있는 바디를 꺼냄
        log.info("messageBody={}", messageBody);

        return new HttpEntity<>("ok");
    }

      @ResponseBody
    @PostMapping("/request-body-string-v4") // 메시지 컨버터 기능
    public HttpEntity<String> requestBodyStringV4(@RequestBody String messageBody) throws IOException {

        log.info("messageBody={}", messageBody);

        return new HttpEntity<>("ok");
    }
}
  • 파라미터에 @RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회 가능
    • 헤더 정보가 필요하면 HttpEntity 나 @RequestHeader를 사용
  • 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam , @ModelAttribute 와는 전혀 관계가 없다.

HTTP 요청 메시지 - JSON

@Slf4j
@Controller
public class RequestBodyJsonController {

    private ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/request-body-json-v1")
    public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        response.getWriter().write("ok");
    }

    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {

        log.info("messageBody={}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }


    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

    @ResponseBody
    @PostMapping("/request-body-json-v4") //HttpEntity 사용
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) throws IOException {
        HelloData data = httpEntity.getBody();
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

    @ResponseBody
    @PostMapping("/request-body-json-v5") // 반환형도 객체로 적용 가능 , 들어올 때 나갈 때 모두 적용됨 
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data;
    }
}
  • @RequestBody 객체 파라미터 - 직접 만든 객체를 지정
    • HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다 (문자, json 등)
    • 생략 불가능, 생략시에 규칙에 의해 @ModelAttribute가 적용이 되버린다.
    • JSON 요청 -> HTTP 메시지 컨버터 -> 객체
  • @ResponseBody
    • 응답의 경우에도 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다.
    • HttpEntity를 사용해도 된다.
    • 객체 -> HTTP 메시지 컨버터 -> JSON 응답

HTTP 응답 - 정적 리소스, 뷰 템플릿

  • 스프링(서버)에서 응답 방법 주로 3가지
  1. 정적리소스
    • 예) 웹 브라우저에 정적인 HTML, CSS, JS를 제공할 때
  2. 뷰 템플릿 사용
    • 예) 웹 브라우저에 동적인 HTML을 제공할 때
  3. HTTP 메시지 사용
    • 예) HTTP API를 제공하는 경우 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

뷰 템플릿

  • 뷰 템플릿 경로 : src/main/resources/templates
@Controller
public class ResponseViewController {

    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1() {
        ModelAndView mav = new ModelAndView("response/hello")
                .addObject("data", "hello!");

        return mav;
    }

    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model) {
        model.addAttribute("data", "hello!");
        return "response/hello";
    }

    @RequestMapping("/response/hello") //권장하지 않는 방법 (불명확) - Controller의 경로와 view의 논리적 이름이 같은 경우 생략
    public void responseViewV3(Model model) {
        model.addAttribute("data", "hello!");
    }
}
  • String을 반환하는경우 @ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고 렌더링,
  • 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello라는 문자가 입력된다.
# 타임리프 설정
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

HTTP 응답 - HTTP API, 메시지 바디 직접 입력

  • HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄
@Slf4j
@Controller
public class ResponseBodyController {

    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok");
    }

    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2()  {
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }

    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3()  {
        return "ok";
    }

    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);

        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }

    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        return helloData;
    }
}
  • @ResponseBody를 사용해서 Http 메시지 바디에 있는 데이터를 받아오게 되면 ResponseEntity와 같이 Http 상태 코드를 지정할 수 없다. 그렇기 때문에 @ResponseStatus를 통해 지정해야 한다.
    • @ResponseStatus 는 애노테이션을 지정하기때문에 동적으로 변경할 수없다.
    • 동적으로 바꾸려면 ResponseEntity를 사용하면 된다.
    • @ResponseBody를 클래스레벨에 지정하면 모든 메서드에 Http 메시지 바디에 있는 데이터를 받을 수 있다. 즉 @RestController@ResponseBody@Controller를 합친 기능이다.

HTTP 메시지 컨버터

  • HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

@ResponseBody 동작 과정

sec6  사진1

  • @ResponseBodyviewResolver 대신에 HttpMessageConverter를 거쳐 처리 한다.
    • 기본 문자 처리 : StringHttpMessageConverter
    • 기본 객체 처리 : MappingJackson2HttpMessageConverter
    • byte 처리(ByteArrrayHttpMessageConverter) 등 여러 종류들이 등록되어 있다.

스프링 MVC의 메시지 컨버터

  • HTTP 요청 : @RequestBody , HttpEntity(RequestEntity)
  • 메시지 컨버터가 읽을 수 있는지 확인하기 위해 canRead() 호출
    • 대상 클래스 타입을 지원하는가
    • 예) @RequestBody의 대상 클래스 (byte[], String, HelloData)
    • HTTP 요청의 Content-Type 미디어 타입을 지원하는가
    • 예 ) text/plain, application/json , */*
    • canRead() 조건을 만족하면 read()를 호출해서 객체를 생성하고 반환
  • HTTP 응답 : @ResponseBody , HttpEntity(ResponseEntity)
  • 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 호출
    • 대상 클래스 타입을 지원하는가
    • 예 ) return의 대상 클래스(byte[], String, HelloData)
    • Http 요청의 Accept 미디어 타입을 지원하는가? (@RequestMapping 의 produces)
    • 예 ) text/plain, application/json , */*
    • canWrite()조건을 만족하면 write()로 호출해서 HTTP 응답 메시지 바디에 데이터를 생성

주요 메시지 컨버터 동작

  • ByteArrayHttpMessageConverter : byte[] 데이터 처리
    • 클래스 타입 : byte[] , 미디어타입 */*
    • 요청 예 ) @RequestBody byte[] data
    • 응답 예 ) @ResponseBody return byte[] 쓰기 미디어타입 application/octet-stream
  • StringHttpMessageConverter : String 문자 처리
    • 클래스 타입 : String, 미디어타입 */*
    • 요청 예 ) @RequestBody String data
    • 응답 예 ) @ResponseBody return "ok" , text/plain
  • MappingJackson2HttpMessageConverter: application/json
    • 클래스 타입 : 객체 또는 HashMap, 미디어타입 application/json
    • 요청 예 ) : @RequestBody HelloData data
    • 응답 예 ): @ResponseBody return HelloData 쓰기 미디어타입 application/json
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
-------------------------------------------------    
content-type: application/json
@RequestMapping
void hello(@RequetsBody String data) {}
  • 0 관련 없으므로 패스, 1 : String , */*이므로 채택 -> StringHttpMessageConverter 사용
content-type: application/json
@RequestMapping
void hello(@RequetsBody HelloData data) {}
  • 0, 1 패스, 2: 객체, application/json 이므로 채택 -> MappingJackson2HttpMessageConverter 사용
content-type: text/html
@RequestMapping
void hello(@RequetsBody HelloData data) {}
  • 0,1 패스 , 2: 객체 지만 , html 타입 이므로 탈락 -> 메시지컨버터 동작x

요청 매핑 핸들러 어댑터 구조

  • Http 메시지 컨버터는 MVC 동작 과정에서 어디쯤 사용될까? 정답은 DispatcherServlet에서 RequestMapping 과정에서 Argument Resolver , ReturnValueHandler에서 동작한다.

sec6  사진2

  • InputStream,HttpServletRequest, @RequestParam, @ModelAttribute 등 다양한 파라미터를 Argument Resolver 덕분이다.
  • 애노테이션 기반 컨트롤러 처리하는 RequestMappingHandlerAdaptorArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터 값(객체)을 생성한다.
  • 요청의 경우 ArgumentResolver을 통해 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성
  • 응답의 경우 ReturnValueHandler를 통해 HTTP 메시지 컨버터를 호출해서 응답 결과 생성
  • 확장의 경우 WebMvcConfigurer를 상속 받아 스프링 빈으로 등록하면된다.(실제로 사용할일 거의 없다)
@Bean
public WebMvcConfigurer webMvcConfigurer() {
 return new WebMvcConfigurer() {
 @Override
 public void addArgumentResolvers(List<HandlerMethodArgumentResolver>
resolvers) {
 //...
 }
 @Override
 public void extendMessageConverters(List<HttpMessageConverter<?>>
converters) {
 //...
 }
 };
}

본 포스팅은 인프런 김영한님 강의(스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)를 토대로 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^