728x90
반응형

[Spring Security를 사용하기 위해 알아야하는 흐름]

 

스프링 Security는 Spring의 어떤 Layer에서 동작할까요?

해당 질문에 답하기 위해서는 필터와 인터셉터를 구분하고, 필터가 어떤 layer의 어떤 순서로 동작하는지 알고 있어야 합니다.

먼저, 스프링부트가 스프링에 비해 가지는 강점은 Tomcat(WAS 서버)가 내장되어 있기 때문에 WAS 서버를 별도로 설치할 필요가 없다는 것입니다.
(빌드의 결과물인 ~.jar 는 WAS 서버를 포함한 것이고, war 를 사용한다면 Tomcat서버를 따로 설치하여 실행해야 합니다.)

스프링부트가 WAS 서버까지 포함하고 있기 때문에 클라이언트의 Request가 왔을 때 어떤 순서로 서버가 해당 요청을 처리해주는지 흐름을 이해해야 합니다.

간략히 말하면, 클라이언트의 Request가 오면 서블릿 컨테이너를 거치고 스프링 컨테이너를 거치게 됩니다.

여기에 필터와 인터셉터를 덧붙여보면 클라이언트의 Request가 오면 서블릿 컨테이너를 거치고, 서블릿 필터를 거치고 스프링 컨테이너를 거치고 인터셉터를 거치게 됩니다.

여기에 Spring Security까지 더해보겠습니다.

이제는 클라이언트 Request가 오면 서블릿 컨테이너를 거치고 서블릿 필터를 거치고 Spring Security를 거치고 스프링 컨테이너를 거치고 그 다음 인터셉터가 동작하게 됩니다.

그럼 Spring Security는 서블릿 필터를 거친 뒤, DispatcherServlet에 가기전에 동작한다라고 볼 수 있습니다.

그런데 Spring Security는 아예 별개의 layer라기 보다는 Servlet Filter의 맨 끝에 붙어서 동작합니다.

즉, Servlet Filter가 실행된 뒤 Spring Security도 Servlet Filter처럼 동작합니다.

그럼 여기서 의문이 생기는 점은 Spring Security는 어떻게 서블릿 필터를 동작하게 할 수 있을까? 하는 점입니다.

왜냐하면 스프링 컨테이너는 스프링 빈만을 관리하고, 서블릿 필터는 서블릿 컨테이너가 관리합니다.
즉, 서블릿 필터는 스프링에서 정의된 Bean을 주입해서 사용할 수 없습니다.

그래서 스프링에서는 DelegatingFilterProxy 라는 클래스가 존재하고, 이 클래스가 서블릿 필터와 Spring Security 동작의 핵심입니다.

DelegatingFilterProxy

DelegatingFilterProxy는 스프링은 자체적으로 서블릿의 Bean을 관리할 수 없기 때문에 서블릿 필터를 호출할 수 있는 DelegatingFilterProxy 를 호출한다고 이해하면 됩니다.

서블릿 컨테이너는 서블릿과 필터를 관리하며, 서블릿과 필터의 라이프사이클을 관리합니다. 따라서 스프링이 서블릿 필터를 호출하려면 서블릿 컨테이너를 통해 이를 수행해야 한다. DelegatingFilterProxy는 이를 가능하게 해주는 역할을 합니다.

이름그대로 해석하면 Filter Proxy를 위임한다 즉 가짜 서블릿 필터를 스프링에게 위임한다(제가 이해한..) 는 뜻인 것 같습니다.

헷갈릴 수 있는 부분

그런데 만약 스프링에서 Servlet Filter의 기능을 동작하고 싶으면 Filter를 생성하여서 WebConfig에 FilterRegistrationBean 와 @WebFilter 를 사용하는 방법도 있는데, 이것과는 다른 것입니다.
이 방법은 스프링과 직접적인 관련이 없고, 서블릿 API를 사용하는 방식입니다.
(그리고 보통 스프링을 사용하면 서블릿 필터보다는 스프링 인터셉터를 사용하여 처리하는 것이 더 많은 옵션을 제공하여 장점을 가집니다.)

FilterRegistrationBean 을 사용하는 방법은 서블릿 필터 API를 사용하여 필터를 등록하는 방법이고 DelegatingFilterProxy 는 스프링 컨테이너의 관리를 받고, Spring Security와 같이 사용됩니다.

FilterChainProxy

FilterChainProxy는 서블릿 필터로 등록되어 있고, DelegatingFilterProxy의 핵심 클래스입니다.
DelegatingFilterProxy 로 부터 요청 위임을 받고 실제 보안 요소들을 처리하는 역할입니다.

FilterChainProxy는 스프링에 빈으로 등록되어 있지만, Spring Security에서 서블릿 필터 마지막에 붙어서 서블릿 필터처럼 동작할 수 있습니다.

그럼 FilterChainProxydoFilter() 를 실행하게 됩니다.

FilterChainProxy는 해당 요청을 처리할 SecurityFilterChain 을 찾습니다.

일치하는 SecurityFilterChain 의 Security Filter를 순서대로 거쳐서 인증 및 인가처리를 하게 됩니다.

여기서 Security Filter에 해당하는 다양한 Filter들이 있습니다.
필요한 Filter만 적용할 수 있으며, Filter들의 순서를 조정할 수 있습니다. 여기서 로그인, 회원가입, JWT 토큰 생성 등을 할 때 보았던 UsernamePasswordAuthenticationFilter 가 있는 것입니다.

Filter에는 다음과 같은 종류가 있습니다.

SecurityContextPersistenceFilter

SecurityContextRepository에서 SecurityContext를 가져와 유저의 Authentication에 접근할 수 있도록 하는 Filter

LogoutFilter

로그아웃 요청을 처리하는 Filter

UsernamePasswordAuthenticationFilter

ID와 PW를 사용하는 Form기반 유저 인증을 처리하는 Filter

DefaultLoginPageGeneratingFilter

커스텀 로그인 페이지를 지정하지 않았을 경우, 기본 로그인 페이지를 반환하는 Filter

AnonymousAuthenticationFilter

이 Filter가 호출되는 시점까지 사용자가 인증되지 않았다면, 익명 사용자 토큰을 반환하는 Filter

ExceptionTranslationFilter

Filter Chain 내에서 발생된 모든 예외를 처리하는 Filter

FilterSecurityInterceptor

권한 부여와 관련된 결정을 AccessDecisionManager에게 위임해 권한 부여 및 접근 제어를 처리하는 Filter

RequestCacheAwareFilter

로그인 성공 후, 이전 요청 정보를 재구성 하기위해 사용하는 Filter

SessionManagementFilter

로그인 이후 인증된 사용자인지 확인하거나, 설정된 세션의 메커니즘에 따라 작업을 수행하는 Filter

BasicAuthenticationFilter

HTTP 요청의 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장하는 Filter

RememberMeAuthenticationFilter

세션이 사라지거나 만료 되더라도 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리하는 Filter

용어 정리

  • Authentication : 해당 사용자가 본인인지 확인하는 절차 => 인증
  • Authorization : 특정 페이지나 자원에 접근 가능한지 결정하는 요소 => 인가
  • Principal : 인증 대상 => 아이디
  • Credential : 인증하기 위해 필요한 정보 => 비밀번호
  • details : 인증 부가 정보

정리

그러니까 원래 서블릿 컨테이너 -> 서블릿 필터 -> Spring Security -> 스프링 컨테이너 -> 인터셉터 .. 이런식으로 동작하게 되는데, Spring Bean 중에 FilterChainProxy는 Spring Security의 필터들을 서블릿 필터 끝단에 붙게 해서 서블릿 필터처럼 동작하게 한다는 것입니다.

FilterChainProxy도 서블릿 필터로 등록되어 있으며, 해당 필터의 doFilter() 메서드가 호출되면 스프링 시큐리티 필터 체인이 실행되는 것입니다.

따라서, 스프링 시큐리티는 서블릿 필터가 동작한 다음에 적용되며, 이를 가능하게 한 것이 FilterChainProxy라고 생각할 수 있습니다.

클라이언트가 Request를 보내게 되면 서블릿의 필터를 거친 뒤, DelegatingFilterProxy를 호출하게 됩니다.

DelegatingFilterProxy는 핵심 클래스인 FilterChainProxy를 호출하게 된다. FilterChainProxy는 스프링에 빈으로 등록되어 있지만, Spring Security에서 서블릿 필터 마지막에 붙어서 서블릿 필터처럼 동작할 수 있습니다.

그럼 FilterChainProxy는 doFilter() 를 실행하게 됩니다.

FilterChainProxy는 해당 요청을 처리할 SecurityFilterChain 을 찾습니다.

일치하는 SecurityFilterChain 의 Security Filter를 순서대로 거쳐서 인증 및 인가처리를 하게 됩니다.

728x90
반응형
728x90
반응형

현재 상황

  1. spring boot의 각 서비스들을 jar 빌드, Docker 빌드, 서버 배포 하는데 시간이 너무 오래걸림
  2. 마이크로서비스를 쿠버네티스 클러스터 서버에 배포해야하는데 각각의 서비스 수정하고 배포하는데 너무 오래 걸림
  3. git action으로 자동화를 하려고 했지만 디버깅하기가 어려워서 로컬에 동일한 환경을 만들어서 테스트하려고 함.

해결 방법

진행하려는 프로젝트에서 springboot 서비스들 중, 로컬로 실행할 수 있는 것들을 로컬에서 테스트하려고 함

→ 그러기 위해서 springboot의 설정 파일들을 분리해야 함.

예) application.yml , application-local.yml , bootstrap.yml , bootstrap-local.yml

  1. 로컬에서 실행할 파일 설정은 application-local.yml , bootstrap-local.yml 을 적용
  2. 서버에 배포할 파일 설정은 application.yml , bootstrap.yml 로 둠

Intellij에서 설정하기

Intellij에서 spring boot를 run하면 console 제일 상단에 찍히는 로그가 설정 파일 등을 담은 명령어이다.

기본으로 설정하면 default를 잡기 때문에 application.yml 설정 파일을 가지고 실행한다.

해당 사항을 바꾸려면 2가지 방법이 있다.(물론 결국에 같다)

  1. edit configuration에 들어가서 active profiles에 적용할 yml 입력하기
    1. 예를 들어, application-local.yml을 적용하고 싶으면 local만 입력하면 된다.
  2. edit configuration에 들어가서 modify option > Add VM options에서

-Dspring.profiles.active=local 이린식으로 적어주기

시행착오

설정을 local 설정으로 바꿔서 실행했는데, 계속 application.yml에 있는 내용이 적용되는 경우

이 부분에 대해서 오늘 꼬박 고생을 했는데.. 계속해서 application.yml에 있는 내용이 적용되어서 config server의 문제인지.. api-gateway의 문제인지 고민했는데 결국은 스프링 기본을 몰랐던 것이였다.

스프링은 application.yml과 application-local.yml이 실행되고, profile을 application-local.yml로 설정해주더라도, 공통된 내용은 application-local.yml 이 적용되지만 application-local.yml에 없는 내용은 application.yml 내용이 적용된다.

예를 들어서, application.yml과 application-local.yml이 아래와 같을 때

# application.yml
greeting:
  hi
  
  
---
# application-local.yml
greeting:
  hi2
@RestController
@RequiredArgsConstructor
public class controller {

    private final Environment env;

    @GetMapping("/greeting")
    public String get() {
        return env.getProperty("greeting");
    }
}

다음 코드를 active-profile을 local로 지정하면 hi2가 나온다.

즉, 둘 다 같은 키값이 있을 때는 local이 적용된다.

만약 두 파일이 다음과 같으면 어떻게 될까?

# application.yml
greeting:
  hi
  
  
---
# application-local.yml
greeting2:
  hi2

위의 코드를 active-profile을 local로 지정하면 hi가 나온다.

즉, application-local에 없는 greeting 이라는 키의 경우 application.yml 파일의 내용이 적용된 것을 확인할 수 있다.

따라서 **run 모드**가 **local**로 설정되면 Spring Boot는 application.yml 및 **application-local.yml**을 모두 읽어들여서, 중복되는 설정이 있다면 **application-local.yml**의 설정이 우선시된다.

일반적으로 **application.yml**은 모든 환경에서 사용되는 설정을 정의하고, **application-local.yml**은 로컬 환경에서만 필요한 설정을 정의한다. 이러한 설정은 **application-local.yml**에만 포함될 것입니다.

(이 내용을 몰라서 application.yml에 적용한게 실행될 때 같이 적용되어서 문제가 무엇인지 한참 고민했다.)

bootstrap.yml을 적용하고 싶을 때

다음과 같이 사용하면 bootstrap.yml을 application.yml보다 먼저 적용할 수 있다.

spring:
  config:
    import: classpath:/bootstrap.yml

이렇게 하면 bootstrap.yml을 먼저 적용한다. 그럼 위와 같이 만약 profiles를 local로 실행한다면 bootstrap.yml에 있는 내용과 application.yml이 겹치면 bootstrap.yml에 있는 내용이 적용되고, 없는 내용은 application.yml에 있는 키 값의 value를 찾을 수 있다.

728x90
반응형
728x90
반응형

Spring Boot를 Initializer를 통해 설정할 때, 아직 url이 존재하지 않아서 애플리케이션을 실행하면 오류가 생기는 경우가 있다.

 

이럴 경우에 물론 데이터베이스 url을 설정하면 바로 해결할 수 있지만 그렇게 하지 않고 실행이 되도록 할 수 있는 방법이 있다.

 

자동으로 데이터베이스 Configuration Auto로 설정된 것을 exclude하게 되면 데이터베이스를 설정하고자 하는 트라이를 하지 않게 된다.

 

 

728x90
반응형
728x90
반응형

HTTP 메시지 컨버터는 언제 사용되는 것인가

클라이언트에서 서버로 요청 데이터를 전달할 때는 3가지 방법이 있다.

 

첫번째는 GET 메소드를 통한 쿼리 파라미터로 전달하는 방법이다.
이 때 url은 보통 /board?page=1&year=2024 이런식으로 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달한다.

 

두번째는 POST 메서드를 통해 HTML form 데이터를 보낼 수 있다. 메시지 바디에 쿼리 파라미터 형식으로 page=1&year=2024 형식으로 전달한다.

첫번째와 두번째 방법의 공통점은 둘 다 요청 데이터를 request.getParameter()로 데이터를 읽어서 처리할 수 있다는 것이다.


세번째 방법은 HTTP message body에 데이터를 직접 담아서 요청하는 방법이다.

이 방법을 사용하게 될 때 HTTP 메시지 컨버터가 사용되게 된다.

 

HTTP message body를 통해 데이터가 직접 넘어오는 경우에는 @RequestParam 이나 @ModelAttribute와는 상관이 없다.
이 2가지 어노테이션은 요청 파라미터를 처리하는 것이고, 세번째 방법과 관련된 어노테이션은 @RequestBody와 @ResponseBody이다.

 

서버에서 클라이언트로 응답 데이터를 만드는 방법도 3가지이다.

 

첫번째는 정적 리소스 자체를 제공해주는 것, 두번째는 뷰 리졸버를 사용하는 뷰 템플릿을 사용하는 방법이다.

 

세번째는 HTML을 제공하는 것이 아니라 JSON 형태의 데이터를 전달하는 HTTP 메시지를 사용하는 방법이 있다.

이 세번째 방법에 HTTP 메시지 컨버터가 사용되게 된다.

 

언제 사용되게 되는지 보았을때 '데이터'를 사용할 때 HTTP 메시지 컨버터가 사용되는 것으로 보인다.
즉, HTTP 메시지 컨버터는 메시지 바디의 내용을 우리가 원하는 문자(String)이나 객체 등으로 변환해준다.

HTTP 메시지 컨버터는 왜 사용하는 것인가

만약에 JSON 데이터를 메시지 바디에 넣어서 요청을 하거나 응답을 할 때 사용한다고 생각해보자.

 

먼저 요청의 경우, 클라이언트가 보낸 JSON 데이터를 읽어서 서버가 처리를 하려면 스트림 처리를 해야된다.
즉, requestStream을 읽어서 직접 JSON으로 변환을 했어야 한다.

 

또한 응답의 경우도 JSON 데이터를 HTTP 메시지 바디에 넣어서 반환을 하려면 response.getWriter()로 직접 넣는 작업이 필요하다.


이런 부분이 불편하기 때문에 스프링은 HTTP 메시지 컨버터라는 것을 제공해준다.

HTTP 메시지 컨버터는 알아서 변환하여 HTTP 메시지 바디를 읽거나 쓰는 것을 도와준다.

HTTP 메시지 컨버터 사용

스프링은 다음 어노테이션의 경우 HTTP 메시지 컨버터를 적용한다.

  1. @RequestBody, HttpEntity(RequestEntity)
  2. @ResponseBody, HttpEntity(ResponseEntity)

HTTP 메시지 컨버터는 요청과 응답 둘 다 사용된다. 컨버터는 양방향이다.

  • canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크.
  • read() , write() 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

@ResponseBody의 사용 원리

만약 @ResponseBody 를 사용한다면

  • HTTP의 body에 문자 내용을 직접 반환한다.
  • viewResolver 대신에 HttpMessageConverter 가 동작하게 된다.
  • 기본 문자처리는 StringHttpMessageConverter 가 처리한다.
  • 기본 객체처리는 MappingJackson2HttpMessageConverter 가 처리한다.
    • 이건 Json으로 바꿔주는 ObjectMapper를 통해서 객체가 json으로 바뀌어서 응답 메시지에 넣어져서 나간다.
  • 이외에 byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.

클라이언트의 HTTP Accept 헤더 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HttpMessageConverter 가 선택된다.

HTTP 메시지 컨버터의 구성

HTTP 메시지 컨버터는 인터페이스로 되어있다.

이유는 Json으로 처리해주는 컨버터, String으로 처리해주는 컨버터 등등 여러가지가 있기 때문이다.


그래서 위에서 말한대로 응답의 경우 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보를 확인해서 선택하게 된다.


String은 StringHttpMessageConverter, Json은 MappingJackson2HttpMessageConverter가 각각 처리해준다.

 

스프링 부트는 다양한 메시지 컨버터를 제공하는데, 대상 클래스 타입 미디어 타입 둘을 체크해서 HTTP 메시지 컨버터의 사용여부를 결정한다.

→ 여기서 미디어 타입이라는 것은 Http 요청 메시지에서 메시지 바디에 있는 content-type이 무슨 타입이라고 알려주는 것이다.

 

예를 들어서 content type이 application/json이다. 그러면 '메시지 바디에 있는게 json 이구나!' 하면서 해당 메시지 컨버터가 선택이 되는 것이다. 그것만 따지는 것이 아니고 대상 클래스의 type 그리고 media-type 둘 다 체크해서 사용 여부를 결정한다.

만약 여기서 만족하지 않으면 canRead, canWrite 를 실행해서 다음 컨버터로 넘어가게 되는 것이다.

(우선순위는 높은 순서대로 0은 Byte, 1은 String, 2는 Json, ..)

 

그래서 항상 HTTP 메시지에 body 데이터가 있다면 그 컨텐츠 타입을 지정해줘야 한다

HTTP 메시지 컨버터가 데이터를 처리하는 과정

HTTP 요청이 오면 컨트롤러에서 @RequestBody나 HttpEntity 파라미터를 사용한다면 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해서 canRead() 를 호출한다.

그럼 먼저 대상 클래스 타입을 지원하는가를 확인하고, HTTP 요청의 Content-Type 미디어 타입을 지원하는가를 확인한다.

 

조건을 만족하면 Read를 호출해서 객체를 생성하고 그 컨트롤러의 파라미터로 넘겨준다.

 

응답 데이터 같은 경우에는 컨트롤러에서 @ResponseBody 이거나 HttpEntity 로 반환이 되면 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해서 canWrite() 를 호출한다.

호출해서 대상 클래스 타입을 지원하는가를 확인하고, HTTP 요청의 Accept 미디어 타입을 지원하는가를 같이 보게 된다.

 

즉, 요청의 경우에는 Content-Type에 Media-Type을 확인하고 응답이 나갈때는 HTTP 요청 메시지에 있는 Accept 미디어 타입을 지원하는지.

(클라이언트가 읽을 수 있는 메시지를 서버가 줘야하기 때문에 HTTP 요청에 Accept Media Type을 지원하는가도 추가적으로 체크를 하는 것이다. )

그럼 HTTP 메시지 컨버터는 스프링 MVC에서 어디쯤에서 사용되는 것일까?

이것은 @RequestMapping 을 처리하는 핸들러 어뎁터인 @RequestMappingHandlerAdapter와 관련이 있다.

 

HTTP 요청을 Spring이 처리하는 과정을 생각해보면,

먼저 실제로 HTTP 요청이 오게 되면 DispatcherServlet은 핸들러 매핑에게 해당 요청을 처리할 수 있는 핸들러를 조회하도록 위임한다.

그럼 등록되어 있는 핸들러 어댑터 중에 해당 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.

 

다음 이 핸들러 어댑터를 통해서 실제 컨트롤러가 호출이 된다. (핸들러를 @Controller에 정의한 요청을 처리하는 처리기라고 생각하면 된다)

 

여기서 나누어 지게 되는데, 핸들러(컨트롤러)의 리턴값을 보고 어떻게 처리할지 판단한다.

 

만약에 View의 경우 뷰 이름에 해당하는 뷰를 찾아서 모델 데이터를 렌더링하는 것이고, @ResponseBody가 있다면 Converter를 사용해서 응답을 처리하게 되는 것이다.

 

어노테이션 기반의 컨트롤러의 여러 파라미터를 만들어서 호출할 수 있는 것이 바로 이 핸들러 어댑터에서 HTTP 메시지 컨버터와 관련이 있는 것이다.

 

그럼 요청 매핑 핸들러 어댑터(@RequestMappingHandlerAdapter)가 어떤 식으로 동작할까?

 

우리가 Spring을 통해서 Controller를 구성할 때, @ModelAttribute 이든, InputStream 이든 HttpEntity 같은 것들을 함수의 파라미터로 사용한다. 그러면 이걸 누군가 이런 함수의 파라미터들을 데이터로 전달해주어야 한다.

 

여기서 ArgumentResolver라는 개념이 나오게 된다.

ArgumentResolver와 HTTP 메시지 컨버터

RequestMappingHandlerAdapter는 ArgumentResolver라는 매개변수를 처리해주는 것이 있다.
(argument가 파라미터, 매개변수를 의미하고 스프링에서 Resolver라는 것이 나오면 뭔가를 처리해주는 것을 의미한다.)

 

생각해보면 어노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있다. 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.

 

핸들러 어댑터 혼자 처리하기 어려워서 ArgumentResolver를 사용해서 필요한 객체들을 생성하게 된다. 그리고 ArgumentResolver가 MessageConverter를 사용하게 되는 것이다.

 

어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor 는 바로 이 ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)를 생성한다. 그리고 이렇게 파라미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다. 스프링은 30개가 넘는 ArgumentResolver를 기본으로 제공한다.

 

여기서 supportsParameter가 해당 파라미터를 지원하는지 확인한다. 반환타입이 Object이다.

즉, ArgumentResolver의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크하고, 지원하면 resolveArgument()를 호출해서 실제 객체를 생성한다. 그리고 이렇게 생성된 객체가 컨트롤러 호출 시 넘어가게 되는 것이다.

 

반환할 때도 언제는 View, 언제는 ModelAndView 이런식으로 되게 많은데 이걸 처리해주는 것도 인터페이스가 되어있다. ReturnValueHandler 이다.

ReturnValueHandler도 응답 값을 반환하고 처리한다. 그래서 컨트롤러에서 String으로 뷰 이름만 반환해도 동작하는 이유가 바로 ReturnValueHandler 덕분이다.

 

여기까지 알고 이제 HTTP 메시지 컨버터는 어디서 동작할까? 이건 ArgumentResolver랑 ReturnValueHandler가 사용하는 것이다.

ArgumentResolver는 Argument를 찾는 거고 그 ArgumentResolver들 중에서 HTTP 메시지 바디에 있는 걸 바로 뭔가 처리해야 된다고 하면 메시지 컨버터를 호출한다.

 

스프링은 ArgumentResolver, ReturnValueResolver, MessageConverter 세가지를 모두 인터페이스로 제공한다. OCP 원칙을 지키면서 확장할 수 있다.

 

즉, ArgumentResolver는 Controller에 선언된 Parameter를 생성하는 역할, ArgumentResolver가 MsgConverter를 사용하는 것이다.

728x90
반응형
728x90
반응형

JWT(Json Web Token)

JWT란 Json 객체로 안전하게 전송하기 위한 방식이다. 이 정보는 디지털 서명이 되어 있으므로 신뢰할 수 있다.

보통 HMAC 알고리즘이나 RSA 알고리즘을 사용하여 암호화를 한다.

핵심은 서명된 토큰이라는 점이고, 내가 쓴게 맞다! 라는 의미로 사용한다.

서명된 토큰은 그 안에 포함된 클레임(정보)의 무결성을 확인할 수 있게 해준다.

 

JSON은 Base64URL로 인코딩되어 있다. Base64URL은 암호화를 하고, 복호화를 할 수 있게 해주는 방법이다. (해시랑 다른 것이다. 예를 들어 비밀번호를 데이터베이스에 저장할 때는 해시 함수를 이용하여 암호화를 해주기 때문에 암호화된 데이터로는 원래의 비밀번호가 어떤건지 알 수 없다. -> 그래서 비밀번호 찾기에서 비밀번호를 알 수는 없는 것이다. )

그러니까 사실 이상하게 적혀있긴 하지만 Header와 payload에 대한 정보는 사실 그냥 디코딩만 하면 알 수 있다. (Base64 URL을 통해 디코딩)

JWT의 핵심은 signature이다. 

 

JWT의 구조는 xxxx.yyyy.zzzz -> 헤더, payload, signature로 구성되어 있다.

signature은 헤더 + payload + 개인키 를 HMAC(HS256)으로 암호화한 것이다. 

 

헤더에 들어가는 정보

  • 사용중인 알고리즘
  • 토큰 유형(JWT)

payload에 들어가는 정보

  • 클래임
    • 등록된 클레임 : 필수 조건은 아니지만 권장되는 미리 정의된 클레임(발행자, 주체, 청중..)
    • 개인 클레임 : 유저아이디 같은 정보를 넣을 수 있다. 유저를 특정할 수 있는 것을 개인 클레임에 보통 넣는다.

secret키는 서버만 알고 있는 키이다. 클라이언트가 username으로 tae77777을 보내고 비밀번호로 1234를 보낸다고 생각해보자. 그리고 secret키가 'secret'이라고 했을 시 아래와 같다. 여기서 signature만 HS256으로 암호화를 한 뒤에 Base64로 인코딩을 하게 되는 것이다. HS256 방식은 HMAC 방식과 SHA256방식을 합해서 말한 것으로 시크릿 키를 포함한 암호화 방식이다. 

중요한 점은 복호화 할 수 없다 는 점이다. 

 

JSON web token을 client는 보통 local Storage에 넣어둔다. 이후에 클라이언트가 서버에 요청할 때 JWT를 실어서 요청하면 서버가 JWT가 신뢰할 수 있는 token인지 확인한다.

그럼 서버에서는 JWT 토큰에 대해서 어떻게 검증할까? -> Signature에 HS256으로 암호화된 정보가 유효한지 알아야 한다.

일단, JWT를 받으면 Header와 Payload를 알 수 있다. 그럼 Base64URL로 인코딩된 header와 Base64URL로 인코딩된 payload와 secret key를 한번에 HS256으로 암호화를 한다. 그럼 클라이언트에게서 받은 JWT의 signature부분을 Base64URL로 복호화해서 같은지 비교해서 인증한다.
-> 인증이 되면 payload에 있는 user 정보를 통해서 DB에서 select해서 돌려주면 된다.

이 방법은 비밀번호를 저장할 때도 같은 방식이다!


세션과 JWT 토큰

Http는 stateless인데 stateful처럼 쓰기 위해서 세션을 만들고 쿠키를 만든다. 쿠키는 동일 도메인에서만 요청이 올 때 발동한다. (CSRF)

 

쿠키와 세션은 http의 장점인 stateless를 벗어나는 stateful하게 되는 것이다. 즉 서버에 상태를 저장한다. 서버가 한 대일때는 세션과 쿠키에 별다른 단점이 없다. 세션의 문제는 서버가 여러 개가 생길 때 문제가 생긴다. 

 

만약 서버를 scale out하여 개수를 늘린다면 서버마다 세션에 대한 정보를 공유하여야 사용자를 확인할 수 있다. 그럼 결국 중앙 세션 저장소를 반드시 만들어야 한다. 

 

Security Config 작성

Security Filter Chain은 다른 Filter보다 무조건 먼저 작동한다. 그리고 Jwtfilter는 Security 동작전에 실행되어야 한다.

 

http Basic 방식 : 매번 요청할 때마다 ID와 Password를 다 알고 요청하게 된다. -> 쿠키, 세션을 만들 필요가 없다.
-> ID, pw가 중간에 노출될 수 있기 때문에 https서버를 사용해야 한다.

 

우리가 쓰려는 방식은 Bearer 방식이다.
: Authorization에 token을 넣는 방식. -> 노출되어도 얘 자체가 ID, PW가 아니기 때문에 위험부담이 적다.
이런 방식이 http Bearer방식이다.

728x90
반응형
728x90
반응형

1. 기존에 서버를 배포하던 방식

기존에는 수동으로 배포를 진행하였다. 보통 배포하는 서버는 EC2를 사용하고, Docker를 이용해서 배포하였다.

  1. Gradle을 사용해서 Spring 프로젝트를 빌드하여 jar 파일을 만든다.
  2. Dockerfile을 사용하여서 도커 이미지를 만든다.
    1. 사용한 Dockerfile은 다음과 같다.
    2. FROM openjdk:11 ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]
  3. 생성된 Docker 이미지를 Docker hub에 push한다.
  4. EC2를 ssh를 통해 접속한다
  5. EC2에서 docker hub의 이미지를 pull해서 가져온다.
  6. sudo docker pull (도커HOST)/(이미지 이름) // sudo docker pull jakeheon/MZTI
  7. docker run을 통해 실행한다.
  8. // 그냥 실행 sudo docker run jakeheon/MZTI // 백그라운드 실행 sudo docker run -it -d jakeheon/MZTI // 포트 80으로 실행 sudo docker run -it -d -p 80:8080 jakeheon/MZTI

문제점

해당 방식은 수동으로 진행하는 것이라서 시간도 걸리고 불편하다. 그리고 여러 사람이 같이 협업을 할 경우 충돌 문제가 생길 수 있다. 특정 branch에 올릴 때 해당 branch를 배포 자동화하려고 하였다.

2. GithubAction 사용해서 자동으로 CI/CD 진행하기

1. GithubAction이란?

자동으로 CI, CD를 도와주는 Github 내장 프로그램

2. GithubAction VS Jenkins

Github Actions or Jenkins? Making the Right Choice for You

3. GithubAction으로 간단한 Spring Project 배포하기

  1. 먼저 Spring Project를 하나 만들고 Github Repository와 연결해준다.
    1. 사용 기술 : java 11, springboot 2.7.15
  2. github에 직접 들어가서 Action탭에서 yml파일을 작성해준다.
    1. 해당 yml 파일에 적힌대로 실제 코드가 git에 push되면 알아서 실행을 해준다.
    2. 만약 실패할 경우… 메일로 실패했다고 전달해온다. (며칠간 100개는 받은것 같다..)
    3. 먼저 아래의 코드를 실행할 때 secret키들은 따로 저장해두어야 한다.
      1. Github Repository에 setting에 action setting에 들어가서 설정할 수 있다.
      2. 이 부분은 다른 블로그에도 많이 있으므로 따라하면 된다.
    4. 정말 간단한데.. 하나씩 test해보면서 로그를 보고 어디가 문제인지 찾으면 된다. (너무 고생한것들을 트러블 슈팅에 기록하였다.)
    5. 여기서 ‘- name: ‘ 이게 하나의 단위이다. 이 상태대로 stage가 나누어지기 때문에 log를 볼때 해당 사항을 잘 살펴보면 된다.
    6. name: Java CI with Gradle on: push: branches: [ "main" ] pull_request: branches: [ "main" ] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Grant execute permission for gradlew and build run: | chmod +x gradlew ./gradlew build - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' - name: init with Gradle uses: gradle/gradle-build-action@v2 - name: Build with Gradle run: | chmod +x ./gradlew ./gradlew clean build - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 with: version: v0.7.0 - name: Docker build & push to docker repo run: | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} docker build -t jakeheon/githubaction -f Dockerfile . docker push jakeheon/githubaction - name: Build and deploy uses: appleboy/ssh-action@master id: deploy with: host: ${{ secrets.HOST }} username: ubuntu key: ${{ secrets.KEY }} envs: GITHUB_SHA script: | sudo docker stop $(sudo docker ps -aq) sudo docker rm -f $(docker ps -qa) sudo docker pull jakeheon/githubaction sudo docker run -it -d -p 80:8080 jakeheon/githubaction
  3. yml파일 내용 간단 설명gradle로 빌드할 수 있도록 권한을 주는 코드이다. 만약 이 부분에서 잘못된다면 아마 Spring project의 위치 문제일 것이다. run 제일 첫번째 줄에 directory 실행 위치를 찍어보면 알 수 있다. 아래 트러블 슈팅에서 더 자세히 설명하겠다.차례대로 JDK 11을 사용하겠다는 의미와 Gradle을 통해 build하는 과정이다. 아마 이 부분은 위와 같이 directory 문제가 아니라면 오류가 생기지 않을 것이다.Docker를 통해 로그인을 하고, build한 뒤 push 하는 코드이다. Dockerfile의 위치를 잘 확인해야 한다. 지금 여기에는 jakeheon/githubaction으로 내가 만들 이미지로 되어있는데 해당 내용도 secret에 두는 것이 좋은것 같다. 본인이 원하는 호스트와 이미지를 Dockerfile을 이용해서 만드는 코드이다.
    • EC2에 접속해서 배포하는 코드이다. EC2에 접속할 때 필요한 HOST와 KEY를 secret에 저장해둔다. pem.key의 경우 EC2에서 다운받은걸 모두 복사해서 secret에 넣어두면 된다. HOST는 접속하는 EC2의 주소를 의미하므로 ip주소나 DNS주소를 입력하면 된다.
    • 나머지는 원래 배포하던 코드랑 같다. 먼저 실행하고 있는 docker container를 stop 시켜준 뒤, 컨테이너를 삭제하고, 이미지를 Pull 받아서 실행한다.
  4. - name: Build and deploy uses: appleboy/ssh-action@master id: deploy with: host: ${{ secrets.HOST }} username: ubuntu key: ${{ secrets.KEY }} envs: GITHUB_SHA script: | sudo docker stop $(sudo docker ps -aq) sudo docker rm -f $(docker ps -qa) sudo docker pull jakeheon/githubaction sudo docker run -it -d -p 80:8080 jakeheon/githubaction
  5. - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 with: version: v0.7.0 - name: Docker build & push to docker repo run: | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} docker build -t jakeheon/githubaction -f Dockerfile . docker push jakeheon/githubaction
  6. - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' - name: init with Gradle uses: gradle/gradle-build-action@v2 - name: Build with Gradle run: | chmod +x ./gradlew ./gradlew clean build
  7. - name: Grant execute permission for gradlew and build run: | chmod +x gradlew ./gradlew build

3. 트러블 슈팅

1. Spring 프로젝트와 Dockerfile 위치 문제

문제 원인

매번 내가 github repository를 만들 때 잘못하고 있었던 것이다.. (이번으로 다른 프로젝트도 수정해야 할 것같다)

.git의 위치를 항상 Spring project 바깥에 두어서 github action이 해당 프로젝트에 들어올려면 폴더 하나를 더 거쳐야하는 문제가 생겼었다.

예를 들자면, TestRepositorySpring이라는 Spring boot 프로젝트를 생성했을 때 repository이름도 TestRepository라고 한다면 TestRepository 에 .git 파일과 TestRepositorySpring이라는 스프링 프로젝트 파일이 존재하는 것이다. 그럼 github action을 실행하기 위해서는 cd 명령어로 TestRepositorySpring으로 들어가야하는 문제가 생긴다…

동일하게 Dockerfile을 통해 빌드할 때도 Dockerfile을 TestRepositorySpring 안에 있으므로 안되는 문제가 생긴다.

보통 다른 블로그나 글들을 찾아봤을때 나와 같은 문제는 없었던 것 같다. (나만 계속 이상하게 하고 있는 것이므로..) 호오오옥시 나랑 동일한 문제가 있는 것 같다면 아래 방법으로 해결하면 된다.

해결 방법

  1. Spring Project 안에 들어가서 실행해야할 Dockerfile빌드라던가 jar 빌드 등을 할 때, yml에 cd를 통해 해당 폴더로 이동 뒤에 실행한다. (실수하거나 깜빡할 수 있으므로 2번을 그냥.. 하는게 맞는것 같다)
  2. .git을 Spring 프로젝트 안에 둔다.

2. jar 파일 빌드 문제(Error: Invalid or Corrupt jar file /app.jar)

문제 원인

이건 신기하게 Action은 다 성공해서 체크 모양이 떴는데 실제 반영이 안된 문제였다. 그래서 뭐가 문제인지 한참을 고민하다가 EC2에서 docker pull을 받아온 다음에 docker를 실행시켰더니 다음과 같은 메시지가 떴었다.

로그를 봤을 때 빌드는 성공적으로 된 것을 확인했는데 저런 오류 메시지가 발생하였다면 .jar파일이 겹쳐서 생긴문제이다. 즉, gradle로 빌드할 때 (프로젝트 이름)-plain.jar도 생기고, (프로젝트 이름).jar 2개가 생기므로 겹쳐서 오류가 발생한 것이다. 이걸 하나로 만들어줘야 해결된다.

해결 방법

build.gradle에 다음과 같이 작성하면 plain.jar는 생기지 않는다.

jar {
    enabled = false
}

3. 기존 실행하던 컨테이너 중단, 종료 문제

문제 원인

이제 진짜 다 되었다! 했을 때 생긴문제.. Action도 다 잘되고 성공한 것을 보고 처음 배포했을 때 서버ip주소로 잘 접속되는 것까지 확인했는데 생긴 문제다. 이제 코드를 바꿔서 push해서 테스트 해봐야지~ 했는데 실패했다. (바뀌지 않았다.)

실제 EC2에 접속해서 docker 로그를 확인해보니 기존에 실행하던 이미지와 겹쳐서 실행이 안되던 것이였다. 즉, 기존에 잘 돌아가고 있는 컨테이너를 중지하고 새로 배포한 이미지를 컨테이너에 올려야되는데 중지를 안해서 생긴 문제였다. (물론 위에 yml 파일은 해당 오류를 해결한 코드이다.)

해결 방법

yml에 Build and deploy script 맨 위에 다음과 같은 코드를 추가한다. docker container를 stop하고 remove하는 코드이다.

sudo docker stop $(sudo docker ps -aq)
sudo docker rm -f $(docker ps -qa)

4. EC2에 접속할 때 필요한 pem.key를 어떻게 두어야할지 몰랐을 때

문제 원인과 해결 방법

이것도 처음에 너무 고민이였다.. 사실 pem key를 제대로 이해를 안하고 있었던 것 같다. pem key도 단순히 해시 알고리즘을 통해서 바뀐 String일텐데.. 그 생각을 못했던 것 같다. pem.key를 그대로 열어서 secret에 모두 복사해서 붙여넣으면 된다!

4. 앞으로 해결할 기능들

1. Docker-Compose를 이용하여서 데이터베이스와 함께 배포해보기

2. jar로 빌드된 것을 cache를 이용하여서 속도를 빠르게 하기

3. Spring 프로젝트 이외에 React나 언어의 프로젝트도 배포해보기

4. 온프레미스 환경에 배포해보기

728x90
반응형

'백엔드' 카테고리의 다른 글

[JWT] JWT 인증을 사용하는 이유  (2) 2023.10.04
세션  (0) 2023.08.25
[AWS] Elastic BeanStalk 배포와 Trouble Shooting  (0) 2023.05.23
[Spring] Spring으로 웹 개발을 진행할 때 알아야할 정보  (0) 2023.05.23
Gradle  (0) 2022.12.27
728x90
반응형

세션

사용자가 웹 브라우저를 킨다고 가정해보자. 예를 들어서 http://www.naver.com을 웹 브라우저에 입력하였다. 최초로 웹브라우저에 들어가면 일단 서버는 해당 주소에 맞는 controller 메서드를 찾아서 html을 사용자에게 리턴해줄 것이다.
그런데 다음에 접속할 때는 상황이 다르다. 서버는 이제 사용자가 누구인지 알아서 거기에 따른 맞춤 광고나 정보들을 제공해준다. 다음에 요청할 때는 세션 ID라는 것이 같이 전송되기 때문이다. 여기서 세션 이라는 것을 처음 접할 수 있다.

웹 브라우저는 세션 ID를 받아서 특정한 설정을 안해주는데도, 자동으로 웹 브라우저에 쿠키라는 저장 영역에 세션ID가 담기게 된다. 이 쿠키라는 영역에 담기는 세션 ID는 언제 생기냐 하면, 최초로 사용자가 서버에 요청했을 때 생기게 된다.

만약에 최초 요청이 아니라, 두번째, 세번째 요청일 시 세션 ID를 header 영역에 달고 요청하게 되는 것이다. (header안에 쿠키라는 영역이 있다)

세션ID는 그럼 뭘 확인해주는가? 세션 ID는 처음 접속했는지, 아니면 2번째, 3번째 접속했는지를 알 수 있다.

여기서 그럼 이런 생각을 할 수도 있다. 세션 ID를 위조해서 header의 쿠키에 넣어서 전달하면, 내가 아닌 다른 사람으로 서버가 인식한다는 것이니까, 위조를 할 수 있는 것 아닐까?

그럼 세션 ID를 위조할 수는 없을까?

이를 방지하기 위해서 세션 ID를 만들어줄때마다 서버는 세션ID의 목록을 가지고 있어야 된다.

즉, 세션ID는 최초 요청 시에 만들어지고, 웹 브라우저는 계속 요청할때마다 세션ID를 들고간다.

그럼 세션ID는 언제 사라지게 되는 것일까? 3가지 경우가 있다.

  1. 서버 쪽에서 session 목록을 날려버릴 때
  2. 사용자가 브라우저를 닫을 때 (종료할 때) -> 서버에 값이 살아있지만 특정 시간(보통 30분)이 지나면 이것도 사라짐
  3. 특정 시간이 지났을 시 -> "우리가 세션이 종료되었습니다. 다시 로그인해주세요" 이런 메시지가 오는 경우 생각

세션을 사용한 로그인

세션을 로그인할 때 사용한다면 로직은 다음과 같다.

  1. 유저가 처음에 서버에 요청을 한다.
  2. 서버는 세션에 세션ID를 하나 만들어주고(ex> 1234) 서버 세션공간에 기록해둔다.
  3. 서버는 유저에게 응답(response)를 돌려준다.
  4. 유저가 다음 요청을 하게 될 때는 세션 1234를 쿠키에 넣어서 이번엔 로그인을 요청한다.
  5. 서버는 DB에 로그인 정보가 있는지 확인한다.
  6. 해당하는 user가 있다면, 서버의 세션에서 세션ID가 1234인 곳에 user 정보를 저장하게 된다.
  7. 그럼 이제 다음번에 사용자가 예를 들어 사용자 프로필 화면에 접속했다고 하면, 세션 있는지 먼저 확인한다
  8. 세션이 있다면 DB에서 사용자 정보를 찾아서 서버에 응답해준다.
  9. 해당 정보를 돌려준다!

즉, 세션을 통해서 그 사용자 인증도 할 수 있고, 민감한 정보를 요청할 때 세션을 통해서 세션값이 있는지 확인하고 정보가 존재하는지 확인한다.

세션의 단점

세션의 단점에 대해서 생각하려면 사용자가 많을 때를 가정해봐야 한다.
만약 서버가 100명을 처리할 수 있는 서버인데, 1000명이 동시에 접속했다고 가정해보자. 그럼 900명은 대기해야한다. 그래서 회사는 서버를 늘리고 싶어한다. 늘리면 당연히 1000명을 받을 수 있을텐데 문제는 세션이다.

세션은 서버에 같이 존재하는데, 만약 이전 서버에 로그인해서 세션에 기록되었다면 새로 늘린 서버에 접속하면 해당 서버의 세션에는 로그인 정보가 없기 때문에 로그인이 잘 되었는지 이용자에게 전달해줄 수 없는 문제가 생긴다.

이걸 해결하려면 몇 가지를 생각해볼 수 있다.

  1. 먼저 Sticky Server라는 것을 만든다. 이 서버는 이제 최초에 들어온 사람은 로드 밸런싱 이런거 없이 무조건 처음 접속한 서버로만 들어가게 강제하는 것이다.
  2. 아니면 세션 전체를 복제시켜서 서버마다 세션을 저장해줄 수 있다.
  3. 또, 세션에 저장하는 게 아니라 다른 한 군데 데이터베이스에 값을 넣고 이걸 세션이라고 생각해서 공유해서 사용하는 것이다.

문제는 해결책들이 기존의 세션의 장점을 사라지게 한다.

세션은 일단 서버에 인메모리 즉, 속도가 빠르다는 장점이 있는데, 3번째 방법은 데이터베이스를 따로 둔다는 것은 하드디스크에서 읽어와야 하기 때문에 속도가 느려진다. (하드디스크에 접속한다는 것은 I/O가 일어난다는 것이기 때문에..)

그래서 현재 세션을 사용하는 서버들은 DB가 아니라 메모리 공유 서버를 사용한다. 그래서 모든 서버들이 메모리 서버에 접근하는데 여기서 나오는 대표적인 인메모리 데이터베이스가 Redis이다.

세션을 사용하지 않고, JWT 토큰을 사용하는 방법이 더 많이 사용된다. JWT 토큰에 대해서도 정리해보겠다.

728x90
반응형
728x90
반응형

웹 브라우저가 HTTP 요청을 하게 되면 HTTP Header, Body 등 웹 브라우저에서 보낸 다양한 정보가 서버로 넘어가게 된다.


웹 브라우저가 HTTP 요청을 하면 해당 메시지를 서버가 받아서 메시지를 파싱도 하고, URL이 뭔지 판단도 하고, content-type도 넘어오니까, 원하는 정보를 몇 글자인지 보고 파싱해서 사실하면 로직을 처리할 수 있다. 거기에 response를 client에게 전달해 주면 된다. 그러나, 이런 작업은 반복적인 작업도 많고, 단순한 비즈니스 로직 하나를 처리하기 위해서도 귀찮은 일이 생긴다. 이걸 더 편리하게 해 주기 위해서 나온 게 서블릿이다.

 

서블릿은 이런 HTTP 작업을 단순화해 준다. HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest 객체와 HTTP 응답 정보를 편리하게 사용할 수 있는 HttpServletresponse 객체를 제공해 줘서, 개발자는 HTTP 스펙을 쉽게 사용할 수 있게 되는 것이다.

서블릿(Servlet)

  • 자바 웹 애플리케이션 개발을 위한 기술로, 동적인 웹 콘텐츠를 생성하는 데 사용
  • HTTP 요청과 응답을 처리하기 위한 자바 클래스
  • 서블릿 컨테이너(예: Apache Tomcat)에서 실행되며, 요청에 대한 처리와 스레드 관리를 담당

HTTP 요청과 응답을 서블릿이 어떻게 처리해주는지 알아보자.

  1. HTTP 요청이 들어오면 WAS는 Request 객체랑 Response 객체를 새로 만들어서 서블릿 객체를 호출한다.
  2. 그럼 Request 객체에서 HTTP로 어떤 요청이 들어왔는지 확인하고
  3. 해당 로직을 처리한 다음(해당 로직을 처리 주는 것이 바로 서블릿 객체이다)
  4. Response 객체에 Client에게 전달해야 할 응답 정보를 담아서 전달해 준다. 이때 WAS가 HTTP 응답 정보를 생성해 준다.
  5. Client가 해당 정보로 렌더링 해서 이제 보여준다.

서블릿 컨테이너

WAS 안에는 서블릿 컨테이너라는 게 있는데 서블릿 컨테이너가 서블릿 객체를 자동으로 생성해 준다. 호출도 해주고, 관리까지(예를 들면 WAS 종료될 때 서블릿도 같이 종료시키는) 해준다. 톰캣은 서블릿 컨테이너의 종류 중 하나이다.

즉, WAS는 웹 서버와 서블릿 컨테이너로 구성된다고 생각하면 된다. 사실 경계가 모호하기 때문에 종류로 생각하는 게 편하다. 

웹 서버는 nginx와 apache가 해당하는 것이고, 서블릿 컨테이너는 apache tomcat(아파치 톰캣)이 해당한다.

 

서블릿 컨테이너의 장점 중 하나는 싱글톤으로 관리된다라는 사실이다. 싱글톤은 객체를 하나만 생성해놓고 서로 공유해서 사용하는 디자인 패턴을 의미한다. 만약 사용자가 10000명 이상으로 온다면 비즈니스 로직으로 개게를 엄청나게 만들어야 되는데, 싱글톤을 사용하면 하나를 공유해서 사용하면 되므로 딱 봐도 이점이 느껴진다.

 

그렇다고, request랑 response를 다 공유하는 건 아니다. 웹 브라우저에서 사용자마다 전달해오는 데이터는 당연히 다를 것이다.

(내 아이디 비번과 다른 사람의 아이디 비번이 다른 것과 마찬가지이다)

 

request, response 객체는 따로 생성하지만 비즈니스 로직인 서블릿은 공유해서 사용하는 것이다. 항상 재사용한다. 그래서 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근하게 된다. (그래서 공유 변수 사용에 주의해야 한다)

 

또 다른 중요한 장점은 서블릿 컨테이너는 멀티 스레드 처리도 지원을 한다는 사실이다. 

 

위에 그림처럼 request가 생기면 서블릿을 호출하게 되는데, 그럼 서블릿은 누가 호출하는 건가??

쓰레드가 호출하게 된다. 스레드는 애플리케이션의 흐름이라고 알고 있는데, 말 그대로이다. 스레드가 아무 작업을 안 하고 쉬고 있다가, 만약에 요청이 들어오면 스레드를 할당해 줘서 서블릿을 호출해 주는 시스템인 것이다. 요청하고 응답까지 다 하면, 스레드는 다시 휴식하게 되는 것이다. 

 

다중 요청이 들어올 때도, 쓰레드가 여러 개면 해결이 될 것 같다. 요청마다 스레드를 생성해 주면 되는 것이다. 이렇게 생각했는데 이 방법은 당연히 단점이 있다. 

장점으로는 동시 요청을 처리하면서, 하나의 쓰레드가 지연되어도, 나머지 스레드는 정상 동작하기 때문에 상관이 없다.

그렇지만 쓰레드는 생성 비용이 매우 비싸고, 고객의 요청이 올 때마다 새로운 스레드를 생성하면 응답 속도가 늦어지고, 고객 요청이 너무 많이 오면 결국 임계점을 넘기면 서버가 죽을 수도 있는 것이다. 

 

근데 WAS는 멀티 스레드를 다 처리해주기 때문에 멀티 스레드 관련 코드를 신경 안 써도 된다.

 

웹 서블릿의 동작 원리

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("HelloServlet.service");
        System.out.println("request = " + request);
        System.out.println("response = " + response);

        String username = request.getParameter("username");
        System.out.println("username = " + username);

        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("hello " + username);


    }
}

위의 코드를 한번 살펴보자. @WebServlet 어노테이션을 선언해주면 이제 서블릿 컨테이너가 관리를 해주게 되는 것이다. 

 

서블릿은 클라이언트의 요청을 HTTP 형태로 받는다. @WebServlet 어노테이션의 Parameter로는 name과 urlPatterns를 받는다.

단어 그대로, name은 서블릿의 이름이고, urlPattern은 클라이언트에서 요청하는 URL이다. 이 둘을 매핑해 주는 것이다.

만약 HTTP를 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행한다.

protected void service(HttpServletRequest request, HttpServletResponse response);

위의 함수 파라미터 그대로 해석하면 request에는 클라이언트 요청, response는 서블릿이 이제 대답을 준다. 해당 함수를 어떻게 커스터마이징하고 조작하는 역할을 이제 서블릿 컨테이너 안의 서블릿이 해주는 것이다. 그 조작을 service함수 내에서 하면 되는 것이다.

그래서 서블릿의 이름과 urlPattern은 중복되게 하면 안된다. 

 

그럼 위의 코드에서 request 객체와 response 객체에는 어떤 것들이 오는지 로그를 찍어보자.

apache.catalina는 tomcat쪽 라이브러리이다.(서블릿 컨테이너 쪽 라이브러리)

HttpServletRequest나 HttpServletResponse는 interface이다.

이제 Tomcat이나 Jetty 등등 이런 WAS 서버들이 서블릿 표준 스펙을 구현하는 것이다. 여기 찍히는 것은 Tomcat의 구현체라고 볼 수 있다.

쿼리 파라미터

서블릿의 장점 중 하나는 쿼리 파라미터를 아주 쉽게 읽을 수 있게 해 준다는 사실이다.

request로 query parameter를 전송하면 request 객체에서 getParameter를 사용하면 해당 query 파라미터를 읽어낼 수 있다.

응답 메시지를 보여줄려면 response에 넣어줘야 한다.

setContentType를 text/plain으로 준다면 단순 문자열을 전송해 주는 것이다.

setCharacterEncoding은 utf-8로 둔다.

그럼 여기까지 정보는 Header에 들어가게 된다.

나머지는 response.getWriter().write() 를 통해서 보내준다.

 

모든 HTTP 요청 응답 정보들을 보고 싶으면 아래와 같이 설정하면 된다. (application.properties)

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

지금까지의 동작 과정

  1. 스프링부트로 프로젝트를 생성 후 실행 → 스프링부트 안에 있는 내장 톰캣 서버를 띄워준다.
  2. 톰캣 서버는 내부에 서블릿 컨테이너를 가지고 있다.
  3. 서블릿 컨테이너를 통해서 내부에 서블릿을 다 생성해준다. → HelloServlet 생성
  4. GET /hello?username=kim HTTP/1.1 Host: localhost:8080 웹브라우저가 이런 형식으로 HTTP 메시지를 만들어서 서버에 요청을 한다.
  5. 서버는 Request, Response 객체를 만들어서 HelloServlet의 Service 메서드를 호출하면서 response 객체를 만들어서 웹 브라우저에게 전달해 준다.
  6. HTTP 응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해 준다. (WAS가 해줌)

HttpServletRequest의 역할

위에서 말했듯이 HTTP 요청 메시지를 개발자가 직접 파싱 하기 어려우니까, 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱 한다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.

추가적인 역할

  1. 임시 저장소 기능 : HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
    1. 저장 : request.setAttribute(name, value)
    2. 조회 : request.getAttribute(name)
  2. 세션 관리 기능 : request.getSession(create: true)

클라이언트에서 HTTP를 통해 서버로 전달하는 방법

  1. GET - 쿼리 파라미터
    1. 검색, 필터, 페이징 등에서 많이 사용하는 방식
    2. 서버는 HttpServletRequest를 가지고 정보를 쉽게 가져올 수 있다
  2. POST - HTML Form
    1. content-type: application/x-www-form-urlencoded
    2. 메시지 바디에 쿼리 파라미터 형식으로 전달 → 똑같이 request.getParameter로 꺼낼 수 있다!
    3. 클라이언트 입장에서는 차이가 있는데, 서버 입장에서는 둘의 형식이 동일한 것이다
    4. 회원가입, 상품 주문에 사용
  3. HTTP message body에 데이터를 직접 담아서 요청
    1. HTTP API에서 주로 사용, JSON형식으로 사용
    2. POST, PUT, PATCH
    3. String으로 주고 받는 방식(잘 쓰이지 않음)
@WebServlet(name = "requestBodyStringServlet",urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        System.out.println("messageBody = " + messageBody);
        response.getWriter().write("OK");

    }
}
  • getInputStream을 하면 메시지 body의 내용을 byte코드로 바로 얻을 수 있다
  • byte 코드를 String으로 바꿔야 하는데 Spring에 StreamUtils라는 유틸리티 클래스를 사용하면 된다.
    • 인코딩 정보를 알려줘야 한다
  • 지금 이건 String 형식으로 주고 받는 것이다.
  • JSON으로 주고 받는 방식이 보통 사용되는데 코드는 아래와 같다.
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // request
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println("messageBody = " + messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        System.out.println("helloData.username = " + helloData.getUsername());
        System.out.println("helloData.age = " + helloData.getAge());

    }
}
// HelloData
@Getter @Setter
public class HelloData {

    private String username;
    private int age;

}
  • 스프링은 JSON 라이브러리를 기본적으로 jackson이라는 것을 사용한다.
  • ObjectMapper를 선언하고 readValue를 통해서 class타입으로 변환할 수 있다. 여기서는 HelloData로 변환을 하였다.
  • 나중에는 MVC가 제공하는 기능을 통해서 더 쉽게 JSON을 파싱할 수 있다

서블릿과 자바로 동적으로 변경해야 하는 부분을 보여주고 싶을 때 템플릿 엔진을 사용할 수 있다. 템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있다.

 

템플릿 엔진에는 JSP, Thymleaf, Freemarker, Velocity 등이 있다. 이전에는 JSP가 많이 사용되었었지만, 스프링은 현재 Thymeleaf를 사용하는 걸 추천한다.

 

단순히 서블릿만을 사용하기에는 HTML을 다루기가 너무 힘들기 때문에 JSP나 타임리프 같은 것들을 사용하게 된다.

 

JSP(JavaServer Pages)

  • 서블릿을 기반으로 하는 웹 페이지 개발을 위한 기술
  • HTML 코드 내에 자바 코드를 삽입하여 동적인 웹 콘텐츠를 생성할 수 있다
  • JSP 파일은 서블릿으로 변환되어 서블릿 컨테이너에서 실행

서블릿과 JSP의 한계

분리되어 있지 않다. 이게 제일 핵심 단점이다. 서블릿과 JSP를 사용하다 보면 비즈니스 로직이랑 화면(View)랑 분리되지 않아서 지저분하고 나중에 코드를 볼 때도 복잡하여 유지보수가 어렵다. 


그래서 등장하게 된 것이 MVC 패턴이다.

MVC 패턴

MVC 패턴은 컨트롤러와 뷰라는 영역으로 서로 역할을 나눈다. MVC 용어에서 알 수 있듯이 여기서 V는 view이고, C는 Controller이다.
M은 모델인데, 모델은 뷰에 출력할 데이터를 담아두고 뷰가 필요한 데이터를 모두 모델에 담아서 전달한다. 덕분에 뷰는 비즈니스 로직이나 데이터 접근 같은 걸 몰라도 되고, 화면만 렌더링 하면 되는 것이다.

 

그리고, Service로도 이후에 나눈다. Controller에 비즈니스 로직을 둘 수도 있지만 이렇게 되면 또 컨트롤러가 부담을 많이 지는 문제가 발생하기 때문에 Service 계층을 따로 나누어서 처리한다. Controller는 서비스 계층을 호출하는 역할만 하면 된다.

 

MVC 패턴과 관련된 프레임워크도 많았었는데, 어노테이션 기반의 스프링 MVC가 나오면서 거의 정리되었다. 

 

MVC 패턴의 한계

MVC 패턴의 한계라기보다는 MVC 패턴을 개선하는 방안이다. MVC 패턴은 요청하는 부분과 응답하는 부분, 즉 request response 코드가 controller마다 따로 구성되어 있다. 중복된 코드는 항상 개선점에 해당한다. 그래서 FrontController라는 것이 고안되었다.

FrontController 패턴

  • Front Controller 패턴은 소프트웨어 디자인 패턴 중 하나로, 웹 애플리케이션에서 중앙 집중화된 컨트롤러 역할을 수행하는 컴포넌트
  • 이 패턴은 클라이언트의 모든 요청을 하나의 진입점으로 보내고, 해당 요청을 처리하기 위해 적절한 핸들러(컨트롤러)로 라우팅 하는 구조를 가지고 있다
  • Front Controller를 제외한 나머지 Controller는 서블릿을 사용하지 않아도 된다.

스프링 웹 MVC에서 보면 FrontController를 사용한다. 우리가 DispatcherServlet으로 알고 있는 것이 FrontController 패턴으로 구현되어 있다. 프런트 컨트롤러 서블릿이 클라이언트 요청을 다 받고 프런트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출하는 즉, 입구를 하나로 만드는 전략이다.

개선점

위의 서블릿 코드에서 override 된 service코드를 보면 HttpServletRequest랑 HttpServletResponse를 계속하여 쓰게 되는데, 해당 코드가 컨트롤러 입장에서는 필요 없다. Controller는 View로 객체를 전달하기만 하면 되는데, 해당 객체를 Model 객체라고 한다. 그럼 이제 우리가 아는 구조가 나오게 된다.

전형적인 Front Controller 패턴의 구조는 다음과 같다:

  1. 클라이언트는 요청을 Front Controller에게 전송
  2. Front Controller는 요청을 받아 해당 요청을 처리할 적절한 핸들러(컨트롤러)로 라우팅
  3. 핸들러는 요청을 처리하고 필요한 작업을 수행한 후 응답을 생성
  4. Front Controller는 핸들러로부터 받은 응답을 클라이언트에게 전송

뷰 리졸버 (View Resolver)

뷰 리졸버(View Resolver)는 컨트롤러가 처리한 결과를 어떤 뷰로 보여줄지를 결정하는 역할을 한다.
뷰 리졸버는 뷰의 논리적인 이름을 실제 뷰 객체로 변환하여 반환하는 기능을 담당한다.

웹 애플리케이션은 MVC (Model-View-Controller) 아키텍처를 따르는데, 이 구조에서 컨트롤러는 요청을 처리하고 필요한 작업을 수행한 후 결과를 뷰에 전달한다. 뷰 리졸버는 컨트롤러가 반환한 뷰의 논리적인 이름을 실제 뷰 객체로 매핑하여 클라이언트에게 보여줄 뷰를 결정하게 되는 것이다.

Spring MVC

  • Spring Framework에서 제공하는 웹 애플리케이션 개발을 위한 모듈
  • Model-View-Controller(MVC) 아키텍처 패턴을 기반으로 한다
  • 요청을 처리하는 컨트롤러(Controller), 데이터를 처리하는 모델(Model), 화면을 표현하는 뷰(View)로 구성
  • 애노테이션 기반의 설정과 느슨한 결합을 통해 유연하고 효율적인 웹 애플리케이션 개발을 지원
  • 요약하면, 서블릿은 자바 기반의 웹 애플리케이션 개발을 위한 기술이고, JSP는 서블릿을 기반으로 동적인 웹 페이지를 개발하기 위한 기술
  • Spring MVC는 Spring Framework에서 제공하는 모듈로, 웹 애플리케이션 개발을 위한 MVC 패턴을 구현하는 방식
  • Spring MVC는 서블릿과 JSP를 기반으로 하며, 개발자에게 편의성과 유연성을 제공하여 웹 애플리케이션 개발을 보다 쉽게 할 수 있도록 도와준다

 

 

참고 : 인프런 김영한 님 스프링 MVC 편

728x90
반응형

+ Recent posts