개발하는 삶

[Spring] 스프링 MVC 본문

Spring

[Spring] 스프링 MVC

삶_ 2022. 7. 20. 16:21

 

MVC 패턴

  • 프로젝트를 구성할 때 controller 가 실행되면 controller가 model 을 통해 데이터를 가져와서 view 에 데이터를 표현함

 

DispatcherServlet

  • HttpServlet 을 상속받고 서블릿으로 동작
    • DispatcherServlet < FrameworkServlet < HttpServletBean < HttpServlet
    • 서블릿이 호출되면 FrameworkServlet(HttpServlet 의 하위) 의 service()를 오버라이딩 받고,
    • DispacherServlet.doDispatch() 호출함
      • 핸들러 조회, 핸들러 어댑터 조회/실행, 뷰 렌더링 호출
  • 모든 경로(urlPatterns="/")에 대해서 매핑함
    • 더 자세한 경로가 우선순위가 높아 그것에 덮어질수있음


SpringMVC 구조

출처 - 김영한 스프링 강의


동작 순서

  1. 핸들러 조회
  2. 핸들러 어댑터 조회
  3. 핸들러 어댑터 실행
  4. 핸들러 실행
  5. 핸들러 어댑터는 핸들러에게 받은 ModelAndView 반환
  6. viewResolver 호출 해 View의 논리주소 -> 물리주소
  7. View 반환
  8. 뷰 렌더링




@RequestMapping

  • URL이 호출될 시, Controller의 요청정보(ex. 메서드)가 작동하도록 매핑할 때 사용하는 애노테이션
  • @Controller과 같이 쓰임
  • @RequestMapping("url") : 해당 URL이 호출시, 아래의 클래스/메서드가 호출됨
    • HTTP method : URL 설정, 요청방식(GET, POST, DELETE, PATCH) 설정
      • GET : 리소스 조회 (xxx.png 입력 -> 그림파일 요청. 데이터 받기용)
      • POST : 요청 데이터 처리, 데이터 등록 (회원가입 후 회원정보 등록)
      • PUT : 서버에게 리소스의 업데이트/리소스 생성 요청 (회원정보 수정 - 모든 데이터)
      • PATCH : 서버에게 리소스 업데이트 요청. 리소스 일부 변경. (회원정보 수정 - 부분 데이터)
      • DELETE : 서버에게 리소스 삭제 요청
      • 예시
        • @RequestMapping(value= "경로", method=RequestMethod.GET)
        • method 값 없으면 HTTP 메서드 모두 허용 (GET, HEAD, POST ...)
        • @GetMapping("경로") 으로 바꿀 수 있음
    • value : 경로
    • 클래스/메서드 서로 조합 가능
      • ex) 클래스에 붙일 땐 서로 중복/공통된 url을 넣고, 메서드에 붙일 땐 다른 url을 넣어줌
  • 스프링에서 주로 사용하는 핸들러 매핑과 핸들러 어댑터
    • RequestMappingHandlerMapping : 핸들러 매핑
      • @RequestMapping, @Controller 가 클래스에 붙어있는 경우에 매핑정보로 인식
    • RequestMappingHandlerAdapter : 핸들러 어댑터





InternalResourceViewResolver

  • 뷰 리졸버 이용
  • jsp를 처리할 수 있는 뷰를 반환
    • spring.mvc.view.prefix (예시 = /WEB-INF/jsp/ ), spring.mvc.view.suffix (예시 = .jsp) 설정
    • InternalResourceView 반환 (뷰 반환)

 

ModelAndView

  • 모델과 뷰 정보를 담아 반환
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
 	
    private MemberRepository memberRepository = MemberRepository.getInstance();
 	
    //Get 요청 방식의 API 생성
    @GetMapping("/new-form")
 	public String newForm() {
        return "new-form";
 	}
 	
    //Post 요청 방식의 API 생성
    @PostMapping("/save")
 	public String save(
    
    	//@RequestParam(파라미터값) : HTTP 요청 파라미터를 입력받음(request.getParameter("username"))
 		@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";
 	}
 	
    //Get 요청 방식의 API 생성
    @GetMapping
 	public String members(Model model) {
    
 		List<Member> members = memberRepository.findAll();
 		model.addAttribute("members", members);
 		return "members";
 	}
}



로깅

  • 필요한 정보를 출력할 때 사용
  • 로깅 라이브러리 사용 : SLF4J 등
//@Slf4j : 롬복 있으면 사용가능. log.error 이렇게 log만 적어도 됨. 로그 선언 하는법 (1)
//@RestController : 반환 값을 HTTP response body에 데이터를 담아 요청을 완료함
//(@Controller와 @ResponseBody가 합쳐진 어노테이션 느낌)
@RestController
public class LogTestController {
 	
    //출력하기 위해 로그 선언. 로그 선언 하는법 (2)
    private final Logger log = LoggerFactory.getLogger(getClass());
 	
    @RequestMapping("/log-test")
 	public String logTest() {
 	String name = "Spring";
    
 	//로그 출력 테스트 - 시간,로그레벨,프로세스ID,쓰레드명,클래스명,로그메시지
    //로그레벨 : TRACE > DEBUG > INFO > WARN > ERROR
    //개발 서버 : debug 출력
    //운영 서버 : 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);
    
 	//로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행됨, 이런 방식으로 사용하면 X
 	log.debug("String concat log=" + name);
 	return "ok";
 	}
}



//로그 레벨 설정 application.properties
//기본 info
logging.level.root=info
//hello.springmvc 패키지와 그 하위로그 레벨 설정
logging.level.hello.springmvc=debug




요청 매핑 - 경로 변수 사용


XXXMapping("경로와 경로변수") + @PathVariable("경로변수")

  • @PathVariable 의 경로변수 = mapping의 경로변수
  • 요즘엔 이 방식을 선호함
    • 게시판 번호 /7 이런식으로 나타내거나, url에 있는 값 그대로 출력하고 싶을때 사용
@GetMappingg("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
	log.info("mappingPath userId={}", data);
    return "ok";
}



XXXMapping("경로", produces = "text/html(예시)")

  • HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑함
  • 잘 안씀
@GetMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
 	log.info("mappingProduces");
 	return "ok";
}


//아래는 예시
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"



HTTP 요청 - 헤더 정보 조회

  • 웹사이트 기본값 설정하기
  • @RequestHeader
    • 특정 HTTP 헤더 조회하기
    • MultiValueMap
      • 키의 중복 허용. 값만 다르게 하면 됨. (키를 가져올 때 키-값 리스트 형태로 저장/반환)
      • 키와 값으로 이루어짐
    • @CookieValue
      • @CookieValue(value="쿠키이름", required=false) value 속성을 전달받을 쿠키의 이름을 지정
      • required = true : value 속성의 이름을 가진 쿠키가 존재한다는 뜻
@Slf4j
@RestController
public class RequestHeaderController {
 	@RequestMapping("/headers")
    
 		public String headers(HttpServletRequest request,
 			HttpServletResponse response,
 			HttpMethod httpMethod,  //HTTP 메서드 조회
 			Locale locale,  //Locale = 웹사이트 언어 정보 (다국어 지원용으로 쓰임) (기본값: 한국 ko_kr)
 			//모든 HTTP 헤더를 MultiValueMap 형식으로 조회
            		@RequestHeader MultiValueMap<String, String> headerMap,
            		//특정 HTTP 헤더를 조회
 			@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";
     }
}






HTTP 요청 파라미터 - @RequestParam

  • 파라미터 이름으로 바인딩
// 방법 (1)
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
 		@RequestParam("username") String membername,
 		@RequestParam("age") int memberage) {
 	log.info("username={}, age={}", username, age);
 	return "ok";
}


// 방법 (2)
// HTTP 파라미터 이름이 참조변수 이름과 같으면 ("파라미터") 생략 가능
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
 		@RequestParam String username,
 		@RequestParam int age) {
 	log.info("username={}, age={}", username, age);
 	return "ok";
}

 

  • 파라미터 필수여부, 기본값
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
		
        // 파라미터 필수 여부 (기본값은 true)
        // true 면 무조건 값이 할당되어야 한다는 의미
 		@RequestParam(required = true) String username,
        // defaultValue = 파라미터에 값이 없는 경우 기본값 설정
 		@RequestParam(required = false, defaultValue = "guest") Integer age) {
 	log.info("username={}, age={}", username, age);
 	return "ok";
}

 

  • 파라미터 Map으로 조회
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
	//받은 값을 paramap에 저장해서 출력
 	log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
 	return "ok";
}





HTTP 요청 파라미터 - @ModelAttribute

  • @RequestParam : String, 래퍼클래스, int(기본형) 등 기본형 타입에 사용 적합
  • @ModelAttribute : 객체 등의 나머지 타입들 사용 (argument resolver 로 지정해둔 타입 외)
  • 두개 다 생략이 가능하지만 정해놓은 타입에 따라 자동으로 적용이 되기 때문에, 생략하지 않는 걸 추천
//HelloData.class
//요청 파라미터를 받을 객체

//@Data는 롬복 애노테이션. getter,setter 등을 불러와줌
@Data
public class HelloData {
 	private String username;
 	private int age;
}


//modelAttributeV1 메서드
//매개 변수에 @ModelAttribute를 넣어 HelloData를 불러옴
//@ModelAttribute : 사용자가 요청해 전달한 값을 오브젝트 형태로 매핑해주는 어노테이션
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {

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





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

  • HTTP message body에 데이터를 직접 담아 요청
    • 요청 파라미터를 조회하는 기능 : @RequestParam, @ModelAttribute
    • HTTP message body를 직접 조회하는 기능 : @RequestBody
//Stream을 이용하기
/**
 * 스프링 MVC는 다음 파라미터를 지원한다
 * InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
 * OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
 */
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)
throws IOException {
	// 스프링에 내장된 유틸클래스.스트림->문자열로반환(HTTP 요청 메시지 바디 스트림 ,utf-8 형식으로)
 	String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
 	log.info("messageBody={}", messageBody);
    	// HTTP 응답 메시지 출력
 	responseWriter.write("ok");
}




//HttpEntity 이용하기
/**
 * HttpEntity: HTTP header, body 정보를 편리하게 조회
 * - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
 * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 *
 * 응답에서도 HttpEntity 사용 가능
 * - 메시지 바디 정보 직접 반환(view 조회X)
 * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 */
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {

	//요청받은 메시지 바디를 문자열로 받음
 	String messageBody = httpEntity.getBody();
 	log.info("messageBody={}", messageBody);
    
 	return new HttpEntity<>("ok"); //응답
}

 

@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) {
	
    //요청 메시지 바디 출력
 	log.info("messageBody={}", messageBody);
    
    //응답 메시지 바디 출력
 	return "ok";
}





HTTP 요청 메시지 - JSON

  • JSON 데이터 형식 조회
  • @RequestBody 요청 : JSON 요청 -> HTTP 메시지 컨버터 -> 객체
  • @ResponseBody 응답 : 객체 -> HTTP 메시지 컨버터 -> JSON 응답
//기존 JSON 코드
/**
 * {"username":"hello", "age":20}
 * content-type: application/json
 */
@Slf4j
@Controller
public class RequestBodyJsonController {
	 //ObjectMapper을 통해 JSON 라이브러리 사용
     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 data = objectMapper.readValue(messageBody, HelloData.class);
         //객체 속 저장된 username,age 출력
         log.info("username={}, age={}", data.getUsername(), data.getAge());
		//요청이 완료되면 응답메시지 출력
         response.getWriter().write("ok");
 }
}
//@RequestBody, @ResponseBody 를 이용해 간추린 것
/**
 * @RequestBody
 * HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 *
 * @ResponseBody
 * - 모든 메서드에 @ResponseBody 적용
 * - 메시지 바디 정보 직접 반환(view 조회X)
 * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 */
 ....
 private ObjectMapper objectMapper = new ObjectMapper();
 
@ResponseBody //getWriter() 과정 생략해줌
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {

	//@RequestBody : stream으로 받은 요청사항 과정을 다 알아서 해줌
 	HelloData data = objectMapper.readValue(messageBody, HelloData.class);
 	log.info("username={}, age={}", data.getUsername(), data.getAge());
 	return "ok"; //응답
}
//@RequestBody에 직접만든 객체 지정 - 최종 간추리기
/**
 * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
 * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (contenttype: application/json)
 *
 */
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {

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





출처

 

'Spring' 카테고리의 다른 글

[Spring] 로그인, 스프링 시큐리티  (0) 2022.08.18
[Spring] HTTP 요청/응답  (0) 2022.07.23
[Spring] HTTP Method  (0) 2022.07.11
[Spring] 웹서버  (0) 2022.07.09
[Spring] 스코프, 프록시  (0) 2022.07.06