춤추는 개발자

[inflearn] 스프링 MVC 2편 review 본문

Developer's_til/스프링 프레임워크

[inflearn] 스프링 MVC 2편 review

Heon_9u 2023. 6. 11. 18:04
728x90
반응형

✅ 강의 소개

 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있습니다.

 

✅ 강의별 후기

 

[ 메시지, 국제화 ]

다양한 메시지를 한 곳에서 관리하는 기능으로 메시지 관리용 파일(ex. messages.properties)에서 화면에 보이는 문구 등을 관리한다.
덧붙여서 메시지들을 나라별 언어로 관리하면 서비스를 국제화 할 수 있다.
(ex. messages-ko.properties, messages-en.properties)

- 스프링의 경우, 인터페이스인 MessageSource로 메시지 관리 기능을 제공하며, 구현체인 ResourceBundleMessgaeSource를 스프링 빈으로 등록하면 된다.

- 스프링 부트는 MessageSource를 자동으로 스프링 빈으로 등록한다.

 o 부트는 application.properties에서 spring.messages.basename=messages(default) 방식으로 메시지 소스를 설정할 수 있다.

- MessageSource.getMessage(code, args, locale): locale의 default는 ko_KR (ex. Locale.KOREA, Locale.ENGLISH 등)

- 웹에서 국제화 기능을 테스트할 경우, 브라우저의 언어 설정 값을 변경하면 된다. (크롬 브라우저 > 설정 > 언어)

- 스프링은 헤더의 Accept-Language값으로 Locale을 선택하며, LocaleResolver로 Locale 선택 방식을 변경할 수 있다.

- messages.properties 예시

label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량

 

[ 검증1 - Validation ]

타입 검증, 필드 검증, 특정 필드의 범위 검증 등
웹 서비스에서 폼 입력 시, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 알려준다.
클라이언트 검증과 서버 검증을 적절히 섞어서 사용하되 ,서버 검증은 필수
단계별(V1 ~ V4) 로직 리팩토링

- V1: 필드별로 빈값이 들어오거나 범위 검증 로직 도입

 o 검증 오류 발생 시, 입력 폼을 다시 보여주고 고객에게 안내, 고객이 입력한 데이터 유지

 o 뷰 템플릿에 중복 처리가 많고, 숫자 필드에 문자 입력 시, 컨트롤러 호출 전에 400에러가 발생

 

- V2: 검증 오류를 관리하는 객체 BindingResult

 o 검증하고자 하는 객체 뒤에 BindingResult를 붙여준다.

 o 인터페이스인 Validator를 상속받은 ItemValidator를 만들고, 검증하고자 하는 객체 앞에 @Validated 어노테이션을 붙인다.

 

- 에러 메시지를 관리하는 설정 파일 (errors.properties)

 o application.properties에 추가. spring.messages.basename=errors

 o 오류 코드의 핵심은 구체적인 것에서 덜 구체적인 것으로 만들어준다.

 o DefaultMessageCodeResolver의 기본 메시지 생성 규칙

 

- 객체 오류:

객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required

 

- 필드 오류:

필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
1. code + "." + object name + "." + field
2. code + "." + field
3. code + "." + field type
4. code
예) 오류 코드: typeMismatch, object name "user", field "age", field type: int
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"

 

[ 검증2 - Bean Validation ]

어노테이션만으로 특정 필드에 대한 타입, NULL, 범위 검증 등을 할 수 있다.
검증뿐만 아니라 에러 발생 시, 특정 Message를 설정할 수 있다.

- 간단한 빈 검증 어노테이션

 o NotBlank: 빈값 + 공백만을 허용하지 않는다.

 o NotNull: NULL을 허용하지 않는다.

 o Range(min = 1000, max = 10000): 범위 안의 값이어야 한다.

 o MAX(999): 최대 999까지만 허용한다.

 

- 검증 순서

 o @ModelAttribute로 각각의 필드에 대한 타입 변환 시도

 o Validator 적용

 

- 오브젝트 오류(글로벌 오류)의 경우, @ScriptAssert 대신, 자바 코드로 수정하는 것을 권고한다.

- Item 등록/수정에 대한 검증 분기 방법

 o Bean Validation의 groups 기능을 사용한다. (복잡도 증가로 인해 지양)

 o 각 기능에 대한 전송용 Form 객체를 따로 생성 및 검증 어노테이션도 각각 설정한다.

- HTTP 메시지 컨버터

 o @ModelAttribute는 HTTP 요청 파라미터(URL, 쿼리 스트링, POST form)를 다룰 때 사용하며, 각각의 필드 단위로 세밀하게 적용

 o @RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용하며, 전체 객체 단위로 적용, 메시지 컨버터 단계에서 JSON 데이터를 객체로 변환하지 못하면 이후 단계가 진행되지 않아 예외 발생


 

[ 로그인처리1 - 쿠키, 세션 ]

쿠키와 세션 기반으로 로그인 처리
쿠키의 보안 문제에 대한 방안으로 세션 활용

- 패키지 구조 설계

 o 도메인: 화면, UI, 기술 인프라 등의 영역을 제외한 시스템이 구현해야 하는 핵심 비즈니스 업무 영역, 웹과는 의존성X

 o web은 domain에 의존하지만, web은 domain에 의존하지 않아야 한다. 즉, domain은 web을 참조하면 안된다.

 

- 쿠키

 o 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지

 o 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지

 o HttpServletResponse.addCookie(new Cookie(key, value));

 o @CookieValue를 사용하여 편리하게 쿠키 조회가 가능

클라이언트 전달1
클라이언트 전달2

- 쿠키의 보안 문제

 o 쿠키 값은 임의로 변경할 수 있다.

 o 쿠키에 보관된 정보는 쉽게 훔칠 수 있다.

 o 대안: 쿠키에는 중요한 값을 노출하지 않고, 예측 불가능한 토큰값을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. (토큰은 서버에서 관리), 쿠키의 만료시간을 짦게 설정

 

- 세션

 o 쿠키의 보안적인 이슈를 해결하기 위해 서버에 중요한 정보를 보관하고 연결을 유지하는 방법

 o 세션id를 추적불가능한 UUID 기반으로 생성

 o 서블릿의 세션 기능인 HttpSession (HttpServletRequest.getSession()

 o @SessionAttribute(name = "", required = t/f): 세션 조회 어노테이션

 o 세션의 유효시간을 생성 시간이 아닌, 사용자가 서버에 최근에 요청한 시간을 기준으로 잡는다.

 

- HttpSession 데이터

 o sessionId: 세션 ID (JSESSION ID의 값)

 o maxInactiveInterval: 세션의 유효시간

 o creationTime: 세션 생성일시

 o lastAccessedTime: 세션과 연결된 사용자가 최근에 서버에 접속한 시간

 o isNew: 새로 생성된 세션인지



 

[ 로그인 처리2 - 필터, 인터셉터 ]

로깅이나 인증 처리처럼 웹과 관련된 공통 관심사(cross-cutting concern)는 필터나 인터셉터로 모듈화한다.
서블릿 필터와 스프링 인터셉터를 비교하여 적용한다.

 

- 서블릿 필터

HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 서블릿 -> 컨트롤러

o 필터가 호출된 다음에 서블릿이 호출되므로 모든 고객의 요청 로그를 남기는데 활용

o 특정 URL 패턴에 적용 가능 / 로그인 여부 체크에 적합

o Filter 인터페이스를 구현하고 등록하면 싱글톤 객체를 생성하고, 관리

  init(): 필터 초기화 메서드

  doFilter(): 고객의 요청이 올 때마다 해당 메서드가 호출, 필터의 로직을 구현

  destory(): 필터 종료 메서드

o 필터마다 UUID로 로그를 남기면 HTTP 요청별로 구분할 수 있다.

o FilterRegistrationBean을 사용해서 필터를 등록

  setFilter: 등록할 필터 지정

  setOrder: 필터 순서

  addUrlPattern: 필터를 적용할 URL 패턴 적용("/*")

o 실무에서 HTTP 요청 시, 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색해보자.

o 필터에는 다음 필터 또는 서블릿을 호출할 때, request, response를 다른 객체로 바꿀 수 있다.

 

- 스프링 인터셉터

HTTP 요청 -> WAS -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

o 서블릿 필터와 비슷하게 동작하지만, 적용되는 순서와 범위, 사용방법이 다르다.

o URL 패턴을 필터보다 정밀하게 설정할 수 있다.

o HandlerInterceptor 인터페이스를 구현하여 사용

preHandle: 컨트롤러(햅들러 어댑터) 호출 전에 호출된다.

postHandle: 컨트롤러 호출 후에 호출된다.

afterCompletion: 뷰가 렌더링 된 이후에 호출된다. 예외가 발생해도 호출되며 예외 정보를 로그로 출력할 수 있다.

o 요청 로그를 구분하기 위한 UUID를 활용하며, preHandle에 생성하고 postHandle / afterCompletion에서 사용하려면 어딘가에 담아두어야 한다. (HttpServletRequest.setAttribute(LOG_ID, uuid)

o preHandle의 파라미터인 handler는 HandlerMethod(@RequestMapping)와 ResourceHttpRequestHanlder(정적 리소스)로 구분한다.

  addInterceptor: 인터셉터 등록

  ordre: 순서 지정

  addPathPatterns: 인터셉터를 적용한 패턴 등록

  excludePathPattern: 인터셉터에서 제외할 패턴 등록

 

- ArgumetResolver

 o 직접 만든 @Login 어노테이션이 있으면 ArgumentResolver가 동작해서 자동으로 세션 기반의 로그인 검증을 할 수 있다.

 o 세션 검증 로직이 있는 LoginMemberArgumentResolver(HandlerMethodArgumentResolver)를 생성 및 WebConfig(WebMvcConfigurer)에 등록

 o 공통 작업이 필요할 때, 컨트롤러를 더욱 편리하게 사용


 

[ 예외 처리와 오류 페이지 ]

서블릿 예외 처리부터 스프링 부트가 제공하는 예외 처리

- 서블릿은 2가지 방식으로 예외 처리 지원

 o Exception(예외)

  WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)

 o response.sendError(HTTP 상태 코드, 오류 메시지)

  WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError())

 

- sendError를 활용하면, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달

- 예외 발생과 오류 페이지의 요청 흐름

1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/errorpage/500)
-> View

 

- 오류 발생 시, WAS는 오류 정보를 request에 추가해서 넘겨준다. (javax.servlet.error)

 o exception: 예외

 o exception_type: 예외 타입

 o message: 오류 메시지

 o request_uri: 클라이언트 요청 URI

 o serlvet_name: 오류가 발생한 서블릿 이름

 o status_code: HTTP 상태 코드

 

- 오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생한다. 이때, 필터, 서블릿, 인터셉터 등이 다시 호출되는 비효율적인 프로세스가 진행된다. 서블릿은 이런 문제를 해결하기 위해 DispatcherType을 제공한다.

서블릿 필터의 경우, setDispatcherTypes()로 필터를 설정할 수 있다.

 o Request: 클라이언트 요청

 o ERROR: 오류 요청

 o FORWARD: 서블릿에서 다른 서블릿이나 JSP를 호출

 o INCLUDE: 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때

 o ASYNC: 서블릿 비동기 호출

 

- 스프링 부트는 ErrorPage를 자동으로 등록. resources/templates/error의 경로에 오류와 관련된 뷰 템플릿(404.html)을 등록하면 기본으로 설정된다.

- BasicErrorController에서 오류 정보를 Model에 포함할지 여부를 application.properties에서 선택할 수 있다.

(never / always / on_param: 파라미터가 있을때 사용)

 o server.error.include-exception = false: exception 포함 여부

 o server.error.include-message = never: message 포함 여부

 o server.error.include-stacktrace = never: trace 포함 여부

 o server.error.include-binding-erros = never: errors 포함 여부

 o server.error.path = /error: 오류 페이지 경로

 

- ErrorController를 상속받아서 에러 공통 처리 컨트롤러의 기능을 변경할 수 있다.


 

[ API 예외 처리 ]

API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 반환한다.

- 서블릿 기반의 경우, ResponseEntity를 반환

- 스프링 부트는 BasicErrorController가 제공하는 정보들을 기반으로 오류 API를 생성

- HandlerExceptionResolver

 o API마다 오류 메시지, 형식 등을 다르게 처리

 o ExceptionResolver는 ModelAndView를 반환하여 Exception을 해결하고 정상 흐름처럼 변경


 

- 서블릿 기반으로 ExcpetionResolver를 구현하기에는 상당히 복잡, 스프링에서는 기본적으로 제공하는 HanlderExceptionResolverComposite을 활용한다.

 o ExceptionHandlerExceptionResolver: @ExceptionHandler를 처리, API 예외처리는 대부분 이 기능으로 해결

 o ResponseStatusExceptionResolver: HTTP 상태 코드를 지정해준다.

   (@ResponseStatus(value = HttpStatus.NOT_FOUND)

 o DefaultHandlerExceptionResolver: 스프링 내부 기본 예외를 처리

 

- @ExceptionHandler를 활용하여 컨트롤러마다 처리하고 싶은 예외 지정

- @ControllerAdvice 또는 @RestControllerAdvice를 사용하면 컨트롤러별로 ExceptionHandler 지정 가능

 o @RestControllerAdvice(assignableTypes = {CustomController.class, Custom2Controller.class})

 o @RestControllerAdvice(annotations = RestController.class)

 o @RestControllerAdvice("org'.example.controllers")


 

[ 스프링 타입 컨버터 ]

자동으로 타입을 변환화여 타입 변환 로직을 공통으로 관리

- 인터페이스 Converter 기반으로 새로운 타입으로 데이터 타입 변환이 가능, 용도에 따라 다양한 방식의 컨버터를 제공

 o Converter: 기본 타입 컨버터

 o ConverterFactory: 전체 클래스 계층 구조

 o GenericConverter: 정교한 구현, 대상 필드의 어노테이션 정보 사용 가능

 o ConditionalGenericConverter: 특정 조건이 참일 경우에만 실행

 

- ConversionService: addConverter 메서드로 타입 컨버터를 묶어서 활용하는 인터페이스

 o 이를 기반으로 컨버터의 등록과 사용을 분리(ISP)

 o @RequestParam은 이를 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환

 

- Formatter

 o 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능

 o 문자 + 현지화(Locale)에 특화 (ex. 1000 -> 1,000)

 o 포맷터 인터페이스로 구현

 o FormattingConversionService는 포맷터를 지원, 우선 순위는 컨버터가 우선

 

- 스프링이 제공하는 기본 포맷터

 o @NumberFormat(pattern = "###,###"): 숫자 관련 형식 지정 포맷터

 o @DateTimeFormat(pattern= "yyyy-MM-dd HH:mm:ss): 날짜 관련 형식 지정 포맷터

 

- 메시지 컨버터(HttpMessageConverter)는 컨버전 서비스가 적용되지 않는다. 메시지 컨버터 내부에는 Jackson같은 라이브러리를 사용한다. 따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리의 설정을 통해서 포맷을 지정해야 한다.

 o 컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용


 

[ 파일 업로드 ]

HTML Form을 통한 파일 업로드
파일을 업로드하려면 문자가 아니라 바이너리 데이터를 전송해야 한다.
application/x-www-form-urlencoded
multipart/form-data

- application/x-www-form-urlencoded

 o Form 태그에 별도의 enctype 옵션이 없으면 HTTP 메시지의 헤더에 Content-type: application/x-www-form-urlencoded을 추가한다.

 

 

- multipart/form-data

 o Form 태그에 enctype = "multipart/form-data"을 지정해야 한다.

 o 다른 종류의 파일들과 폼의 내용(문자)을 전송 가능

 o Content-Disposition으로 항목별 헤더가 구분

 o 스프링의 DispatcherServlet에서 MultipartResolver를 실행

 o HttpServletRequest를 MultipartHttpServletRequest로 변환해서 반환

 

- 멀티파트 사용 옵션(application.properties)

 o 업로드 사이즈 제한

   spring.servlet.mutipart.max-file-size(파일 하나)

   spring.servlet.mutipart.max-request-size(전체 파일)

 o 사용 제한

   spring.servlet.mutipart.enabled = false

 o 로그 제한

   logging.level.org.apache.coyote.http11 = debug

 

- 스프링과 파일 업로드 (MultipartFile)

 o @RequestParam MultipartFile file: 업로드하는 HTML Form name에 맞춰 적용 (@ModelAttribute도 가능)

 o getOriginalFilename(): 업로드 파일명,  transferTo(...): 파일 저장

 o 다운로드 시, UrlResource로 파일을 열어서 @ResponseBody로 이미지 바이너리를 반환


✅ review

 드디어 MVC 1, 2의 강의를 마무리했다. 이번 강의에서는 평소에 간단하게 진행하는 프로젝트보다 실무에서 다룰만한 주제들을 다뤘다. 유지보수에 용이한 내용들로 메시지, 국제화, 에러 처리, 검증 등 포스팅하면서 내가 맡고 있는 업무에 적용하면 좋을 법한 내용이 많았다.

 특히, 실무에서 개발하는 시스템이 워낙 크고 복잡하다 보니 에러 처리나 검증과 같은 주제가 가장 와닿았다. 업무 중에 VoC로 인해 오류를 확인할 때마다 어떤 Exception이 발생했는지 파악하고 소스 코드를 단계별로 확인하여 원인을 파악한다. 간혹, 레거시 코드들은 예외 처리도 원인 파악도 어려워서 로컬에서 디버그 모드로 하나씩 해당 기능을 검토해야 한다... 기회만 있다면 에러 코드를 정형화해서 RestControllerAdvice기반으로 정리했으면 하는 바램이 있다.

728x90
반응형