IT recording...
[스프링 MVC1] 6. MVC 기본 기능 본문
원문 링크
https://adorable-aspen-d23.notion.site/MVC1-6-MVC-9b59951ec4af4e779869255454e17f55
김영한님의 [스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 강의를 듣고 작성한 글입니다.
요약
------------------------------------------
1. GET - 쿼리 파라미터
2. POST - HTML Form
요청 - @RequestParam, @ModelAttribute
------------------------------------------
3. HTTP message body에 데이터 직접 넣어서 전달
요청 - @HttpEntity<T>, @RequestBody
------------------------------------------
------------------------------------------
1. 정적 리소스
2. 뷰 템플릿
3. HTTP 메시지
응답 - @ResponseBody , @HttpEntity<T>
------------------------------------------
** 로깅
* SLF4J
* Logback
**@Slf4j**
@RestController
public class LogTestController {
//private final Logger log = LoggerFactory.getLogger(getClass());
//@Slf4j를 넣으면 자동으로 이게 넣어진다.
@RequestMapping("/log-test")
public String logTest(){
String name = "Spring";
System.out.println("name = " + name);
//log.info("name = "+ name); //사용할 수 있지만 사용하지 말자! (+연산이 일어나서 쓸모없는 리소스가 사용되는 것임)
//log의 레벨을 정할 수 있다.
**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";
}
}
- 로그 레벨 설정
- LEVEL : TRACE > DEBUG > INFO > WARN > ERROR
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
- 로그 사용 장점
- 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
- 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
- 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다.
- 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
- 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등)
1. 요청 매핑
- URL주소 형태
- /hello-basic
- /hello-basic
@RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
// url로 매핑 , 배열도 가능
public String helloBasic(){
log.info("helloBasic");
return "ok";
}
- HTTP 메서드 매핑
/**
* method 특정 HTTP 메서드 요청만 허용
* **GET, HEAD, POST, PUT, PATCH, DELETE**
*/
@RequestMapping(value = "/mapping-get-v1", **method = RequestMethod.GET**)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
- HTTP 메서드 매핑 축약
/**
* 편리한 축약 애노테이션 (코드보기)
*** @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping**
*/
**@GetMapping**(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
- PathVariable(경로 변수)사용
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
*
* **@PathVariable("userId") String userId -> @PathVariable userId**
* /mapping/userA
* */
@GetMapping("/mapping/**{userId}**") //url에서 직접 파라미터 받아옴
public String mappingPath(**@PathVariable("userId") String data**){
log.info("mappingPath userId = {}", data);
return "ok";
}
- PathVariable 다중 사용
/**
* PathVariable 다중 사용
*/
@GetMapping("/mapping/users/**{userId}/orders/{orderId}**")
public String mappingPath(**@PathVariable("userId") String userId, @PathVariable("orderId") Long orderId**){
log.info("mappingPath userId = {}, orderId = {}",userId,orderId);
return "ok";
}
- 특정 파라미터 조건 매핑
/**
* 파라미터로 추가 매핑
*** params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}**
*/
@GetMapping(value = "/mapping-param", **params = "mode=debug"**) //파라미터에 debug라는게 있어야만 호출이 된다.
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
- 특정 헤더 조건 매핑
/**
* 특정 헤더로 추가 매핑
*** 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";
}
- 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume
- HTTP요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.
- 만약 맞지 않으면 HTTP 415상태코드(Unsupported Media Type)을 반환한다.
/**
* **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 요청 Accept, produce
- HTTP요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.
- 만약 맞지 않으면 HTTP 406상태코드(Not Acceptable)을 반환한다.
/**
* **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";
}
- 매핑 예시
@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;
}
}
2. HTTP요청
** HTTP요청 데이터 조회 종류
- 클라이언트 → 서버로 요청 데이터를 전달하는 방법
1. GET - 쿼리 파라미터
2. POST - HTML Form
요청 - @RequestParam, @ModelAttribute
응답 - @ResponseBody
------------------------------------------
3. HTTP message body에 데이터 직접 넣어서 전달
요청 - @HttpEntity<T>, @RequestBody
응답 - @ResponseBody
2-1) 기본,헤더 조회
@Slf4j
@RestController
public class RequestHeaderController {
@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); //HTTP메서드 조회
log.info("locale={}", locale); // Locale정보 조회
log.info("headerMap={}", headerMap); //모든 HTTP헤더를 MultiValueMapp형태로 조회
log.info("header host={}", host); //특정 HTTP헤더를조회
log.info("myCookie={}", cookie); //특정 쿠키 조회
return "ok";
}
}
- MutliValueMap
- MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
- HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
- keyA=value1&keyA=value2
2-2) HTTP요청 파라미터 - @RequestParam
요청 파라미터(request parameter) 조회
- GET 쿼리 파라미터, HTML Form 전송 방식 모두 사용
- V1 - 서블릿과 비슷한 모양
//서블릿과 비슷한 모양
@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");
}
- V2 - RequestParam사용
//RequestParam 사용하기
@ResponseBody //return값이 string일 때 view 조회를 하지 않고 바로 응답에 string이 박혀서 나간다. (RestController와 같은 역할)
@RequestMapping("/request-param-v2")
public String requestParamV2(
**@RequestParam("username") String memberName,
@RequestParam("age") int memberAge**
){
log.info("username={}, age={}",memberName,memberAge);
return "ok";
}
- V3 - RequestParam사용 시 매핑값과 이름 같을 때
- 생략가능
@ResponseBody //return값이 string일 때 view 조회를 하지 않고 바로 응답에 string이 박혀서 나간다. (RestController와 같은 역할)
@RequestMapping("/request-param-v3")
public String requestParamV3(
**@RequestParam String username, //매핑값과 이름이 같으면 이름 생략 가능
@RequestParam int age**
){
log.info("username={}, age={}",username,age);
return "ok";
}
- required 정보 명시
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(**required = true**) String username, //꼭 들어와야 함
@RequestParam(**required = false**) Integer age //안들어와도 됨
){
log.info("username={}, age={}",username,age);
return "ok";
}
- defaultValue 사용
//defaultValue는 "" 빈 문자의 경우도 빈 것으로 간주한다.
@ResponseBody
@RequestMapping("/request-param-default")
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";
}
- 파라미터 Map으로 한번에 받기
- 파라미터 값이 하나가 아닌게 확실한 경우 사용
- 근데 이런 경우 별로 없다.
- 파라미터 값이 하나가 아닌게 확실한 경우 사용
//파라미터를 한꺼번에 Map으로 받을 수 있다.
@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";
}
2-3) HTTP요청 파라미터 - @ModelAttribute
: 요청 파라미터에 객체를 사용한다면 일일이 매핑을 해줘야 하지만 ModelAttribute는 이런 수고를 덜어준다.
@Data
public class HelloData {
private String username;
private int age;
}
- V1 - 직접 매핑
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@RequestParam String username, @RequestParam int age){
HelloData helloData = new HelloData();
helloData.setUsername(username);
helloData.setAge(age);
log.info("username={}, age={}",helloData.getUsername(),helloData.getAge());
log.info("helloData = {}",helloData);
return "ok";
}
- V2 - ModelAttribute사용
- HelloData를 생성한 후, 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.
- 각 프로퍼티의 setter를 호출한 후 데이터를 바인딩한다.
//ModelAttribute를 사용하면 자동으로 HelloData 객체를 생성한다.
//요청 파라미터의 이름으로 HelloData의 프로퍼티를 찾고 알아서 set시킨다.(바인딩)
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(**@ModelAttribute HelloData helloData**){
log.info("username={}, age={}",helloData.getUsername(),helloData.getAge());
log.info("helloData = {}",helloData);
return "ok";
}
- V3 - ModelAttribute 생략 가능
/**
* @ModelAttribute 생략 가능
* **String, int 같은 단순 타입 = @RequestParam
* argument resolver 로 지정해둔 타입 외 = @ModelAttribute**
*/
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(**HelloData helloData**) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
2-4) HTTP요청 파라미터 - 단순 텍스트
: HTTP message body에 직접 데이터를 담아 요청하는 경우
- @RequestParam, @ModelAttribute를 사용할 수 없다.
- V1 - 직접 inputstream사용
@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");
}
- V2 - 불필요한 request,resonse제거
//불필요한 Request,Response 제거
@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");
}
- V3 - HttpEntity<T> 사용
- 문자가 들어왔을 때 HTTP Converter가 작동해서 자동으로 컨버트해준다.
- HttpEntity
- HTTP header, body정보를 편리하게 조회
- 메시지 바디 정보 직접 조회
- 요청 파라미터 조회는 하지 않음 (@RequestParam X, @ModelAttribute X)
- 응답에도 사용 가능하다. (메시지 정보 직접 반환)
//String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
//하는 것도 귀찮음. 스프링 너가 대신 해줘!
//-> HttpEntity<String>과 같이 해놓으면 문자가 들어왔을 때 HttpBody에 있는거를 convert해줄게
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(**HttpEntity<String> httpEntity**) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody = {}",messageBody);
return new HttpEntity<>("ok");
}
- V4 - @RequestBody , @ResponseBody
//다 귀찮다. 다 해줘!!! @RequestBody, @ResponseBody
/**
* **@RequestBody
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용**
*/
**@ResponseBody**
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(**@RequestBody String messageBody**) throws IOException {
log.info("messageBody = {}",messageBody);
return "ok";
}
2-5) HTTP요청 파라미터 - JSON
- V1 - mapping직접
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");
}
- V2 - @RequestBody 스트링, @ResponseBody 스트링 사용
//@RequestBody, @ResponseBody사용하자!
@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";
}
- V3 - @RequsetBody 객체
- @RequestBody에 직접 만든 객체를 지정하면 알아서 HttpMessageConverter가 이름 알아서 찾아서 바인딩해 준다.
- @RequestBody 직접만든객체 의 경우 애노테이션을 생략하면 @ModelAttribute(parameter 받아오는 것)이 동작하므로 오류가 난다. ⇒ 생략XX!!
//json 매핑하는것도 귀찮은데?
//RequestBody에 직접 만든 객체를 지정하면 알아서 HttpMessageConverter가 이름 알아서 찾아서 바인딩해 준다.
//@RequestBody 직접만든객체 의 경우 애노테이션을 생략하면 @ModelAttribute(parameter 받아오는 것)이 동작하므로 오류가 난다.
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(**@RequestBody HelloData helloData**) throws IOException {
log.info("username={}, age={}",helloData.getUsername(),helloData.getAge());
return "ok";
}
- V4 - response json으로 보내기 @ResponseBody 객체
//response를 json으로 보내기
**@ResponseBody**
@PostMapping("/request-body-json-v4")
public HelloData requestBodyJsonV4(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}",helloData.getUsername(),helloData.getAge());
**return helloData**;
}
3. HTTP응답
3-1) HTTP응답 - 정적 리소스, 뷰 템플릿
- 정적 리소스
/static, /public, /resources, /META-INF/resources
- src/main/resources/static/basic/hello-form.html
- 뷰 템플릿
: 뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어 전달한다.
src/main/resources/templates
- src/main/resources/templates/response/hello.html
@Controller
public class ResponseViewController {
//1. ModelAndView 직접 반환
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1(){
**ModelAndView mav = new ModelAndView("response/hello")**
.addObject("data","hello!");
return mav;
}
//2. Model은 파라미터로, 반환은 뷰 논리 이름
@RequestMapping("/response-view-v2")
public String responseViewV2(**Model model**){
model.addAttribute("data","hello!");
return "response/hello";
}
}
3-2) HTTP응답 - HTTP API, 메시지 바디에 직접 입력
: 응답으로 JSON과 같은 데이터를 보낼 때
@Slf4j
@Controller
//ResponseBody는 클래스 레벨에 붙여도 된다.
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() throws IOException{
return new **ResponseEntity<>("ok", HttpStatus.OK);**
}
**@ResponseBody**
@GetMapping("/response-body-string-v3")
public String responseBodyV3() throws IOException{
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>(helloData,HttpStatus.OK);**
}
//status값을 지정 가능
**@ResponseStatus(HttpStatus.OK)**
**@ResponseBody**
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
**return helloData;**
}
}
- @ResponseEntity를 사용하면 함수적으로 상태코드를 설정할 수 있다.
- @RequestBody를 사용하면 상태코드는 하드코딩해야 한다.
- → 상황에 따라 골라 쓰자!
- @RestController
- @ResponseBody를 사용하면 뷰 템플릿을 사용하지 않고 바로 HTTP 메시지 바디에 데이터를 입력한다.
- Rest API 를 만들 때 사용하는 컨트롤러이다.
**@Controller + @ResponseBody = @RestController!**
4. HTTP 메시지 컨버터
4-1) HTTP 메시지 컨버터란?
- ex) @ResponseBody를 사용
- viewResolver대신에 HttpMessageConverter가 동작한다.
- 기본 문자 처리 : StringHttpMessageConverter
- 기본 객체 처리 : MappingJackson2HttpMessageConverter
- viewResolver대신에 HttpMessageConverter가 동작한다.
- HTTP 메시지 컨버터 사용 경우
- HTTP 요청 : @RequestBody, @HttpEntity(RequestEntity)
- HTTP 응답 : @ResponseBody, @HttpEntity(ResponseEntity)
0 = ByteArrayHttpMessageConverter
클래스 타입 : byte[]
미디어 타입 : */*
1 = StringHttpMessageConverter
클래스 타입 : String
미디어 타입 : */*
2 = MappingJackson2HttpMessageConverter
클래스 타입 : 객체,HashMap
미디어 타입 : application/json
- 대상 클래스 타입 + 미디어 타입을 체크해서 사용 여부를 결정한다.
- 예시
- SpringHttpMessageConverter
content-type: application/json @RequestMapping void hello(@RequetsBody String data) {}
- Mapping2Jackson2HttpMessageConverter
content-type: application/json @RequestMapping void hello(@RequetsBody HelloData data) {}
- ??
content-type: text/html @RequestMapping void hello(@RequetsBody HelloData data) {}
- 예시
- Http 요청 데이터 읽기
- Http요청이 오고, 컨트롤러에서 @RequestBody, @HttpEntity 파라미터를 사용한다.
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출한다.
- 대상 클래스 타입 확인 (요청 데이터의 byte[], String, 객체)
- Content-Type 확인 (text/plain, application.json, /)
- canRead()를 만족하면 read()를 호출해서 객체를 생성하고, 반환한다.
- Http 응답 데이터 생성
- 컨트롤러에서 @ResponseBody, @HttpEntity로 값이 반환된다.
- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
- 대상 클래스 타입 확인
- Accept 미디어 타입 확인 (@RequestMapping의 produces)
- canWrite()를 만족하면 write()를 호출해서 HTTP응답 메시지 바디에 데이터를 생성한다.
4-2) 요청 매핑 핸들러 어댑터 구조
- 핸들러 어댑터에 요청이 들어왔을 때,
- 변경 가능한 요청의 형태인지를 확인하고 그렇게 변경해주는 Argument Resolver가 동작한다.
--요청--
HttpServletRequest, Model
@RequestParam, @ModelAttribute, @RequestBody, HttpEntity
- 등이 요청 형태로 controller에 존재할 수 있는데,
- Argument Resolver는 변환이 가능한지를 확인하고, 변환해서 핸들러에 넘겨준다.
--응답--
ModelAndView, @ResponseBody, HttpEntity
Argument Resolver는
변환이 가능한지를 확인하고, 변환해서 어댑터에 리턴한다.
- 등이 응답으로 들어오면,
- Argument Resolver는 변환이 가능한지를 확인하고, 변환해서 어댑터에 리턴한다.
--요청--
@RequestBody, HttpEntity
--응답--
@ResponseBody, HttpEntity
- 가 요청,응답으로 들어오면
- Argument Resolver는 HTTP 메시지 컨버터를 호출해서 HTTP 형태로 데이터를 변환한다.
'Spring' 카테고리의 다른 글
[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계 (0) | 2022.02.07 |
---|---|
[스프링 MVC1] 7. 웹 페이지 만들기 (0) | 2022.02.07 |
[스프링 MVC1] 5. MVC 패턴 (0) | 2022.01.14 |
[스프링 MVC1] 4. MVC 프론트 컨트롤러 패턴 (0) | 2022.01.14 |
[스프링 MVC1] 3. 서블릿,JSP,MVC 패턴 (0) | 2022.01.14 |
Comments