당니의 개발자 스토리

request 스코프 예제 만들기 본문

스프링/스프링 핵심 원리 - 기본편

request 스코프 예제 만들기

clainy 2024. 1. 28. 01:50

request 스코프 예제 만들기

이번 시간에 request 스코프 예제를 만들어 볼 건데, 예제 코드로 바로 동작하는 거 보여드릴게요.

일단 웹 스코프웹 환경에서만 동작하기 때문에 웹이 동작하도록 라이브러를 추가하겠습니다.

이걸 복사해서

붙여넣고 코끼리를 눌러줍니다. 받는데 시간이 좀 걸립니다.

그 다음에 External Library를 들어가면,

web과 관련된 기술들이 들어온 걸 확인할 수 있습니다.

이렇게 추가해놓고 뭘 실행해야 되냐면,

스프링 부트CoreApplication이 있을 거예요.

여기서 main 메서드를 실행하면 특별한 문제가 없으면 잘 실행될 거예요.

예전에는 이게 없었는데, 이게 뜨면 웹 기술이 들어가면서 스프링 부트에 서버가 띄워진 겁니다.

자 그래서 http://localhost:8080/에 들어가면,

오류 페이지가 나와야 성공입니다.

다시 꺼주고,

이렇게 라이브러리를 추가하면, 스프링 부트가 내장 톰캣 서버를 활용해서 웹 서버와 스프링을 함께 실행시킵니다. 스프링 입문 강의에서 봤었죠.

그리고 참고로 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 띄우는데, 웹 라이브러리를 딱 넣잖아요? 그럼 스프링 부트가 AnnotationConfigServletWebServerApplicationContext을 기반으로 스프링 컨텐츠를 운영합니다.

왜냐면 웹 관련된 기능이 더 추가로 필요하기 때문에 이걸로 동작을 합니다. 필요한 분들은 @Autowired 해서 ApplicationContext를 받아가지고 찍어보면 AnnotationConfigServletWebServerApplicationContext이 나올 겁니다.

그리고 만약에 충돌이 포트가 8080 때문에 나면, 저걸 넣어주면 됩니다.


자 이제 한번 request 스코프 예제를 개발해볼게요.


만약에 동시에 여러 HTTP 요청이 와요. 서비스를 만들었는데 장사가 너무 잘 되는 거예요. 동시에 여러 개의 요청이 오면 로그를 여러 개 남길 거잖아요. 그런데 막 섞인단 말이에요.

동시에 고객 요청이 들어오면서 여러 스레드에서 로그를 남기기 때문에 뭐가 뭔지 잘 구분하기 어렵거든요. 이럴 때 딱 사용하기 좋은 게 바로 request 스코프예요.

이렇게 구분자와 함께 로그가 남도록 request 스코프를 활용해서 기능을 추가해서 개발해봅시다.

이렇게 앞에 붙어요. 그래서 뭔가 고객의 요청이 처음 들어오면, 무조건 이 UUID 라는 걸 남기는 거에요.

UUID 라는 유니크 아이디로, 전세계에 딱 하나만 생성되는 아이디를 만들 수 있거든요. 그걸 만들어서 여기 앞에다가 찍는 거에요. 그래서 같은 request인 경우에는 [d06b992f…]를 남기는 거죠.

그래서 앞에 것만 보면 ‘아 이거는 같은 request네’ 알 수 있고, request가 들어오고 나갈 때까지는 [d06b992f…]가 찍히는 겁니다. 그래서 UUID가 같으면 '아 이건 같은 고객한테서 왔구나~' 하고 알 수 있겠죠.

그래서 UUID를 사용해서 HTTP 요청을 구분할 거고요. requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 로그를 남겼는지도 확인해보겠습니다.

지금 보면, controllerservice 로직이에요. [http://localhost:8080/log-demo] 이거를 보고 '아 이 요청에서 왔구나' 라는 걸 알 수 있어요. 고객이 log-demo 라는 URL을 호출했는데, 그럼 log-demo라는 요청에서 온 controller랑 service가 호출되겠죠.


한번 코드로 만들어 보겠습니다.

common 이라는 패키지를 만들고, MyLogger 라는 클래스를 만들겠습니다. 로그를 찍는 request 스코프로 올릴 빈이에요.

그리고 @Component를 추가하고, Scoperequest로 설정합니다.

물론, value 빼도 됩니다. 그런데 나중에 할 게 있어서 일단 넣을게요.

그리고 필드를 2개 만들어 놓고,

requestURL은 나중에 별도로 세팅할 수 있도록 setter로 값을 중간에 받도록 할게요.

그 다음에 로그를 남길 겁니다. MyLogger.log 라고 남기면 되겠죠.

기대하는 공통 포맷 그대로 만들어 보겠습니다.

이렇게 해놓고 그 다음에 중요한 게 하나 더 있습니다. @PostConstruct를 활용해볼게요.

init()을 만들고, java.utilUUID가 있어요. 필드의 UUID를 초기화해주는 거죠.

여기서 randomUUID()를 toString으로 받으면,

글로벌하게 유니크한 아이디가 하나 생성됩니다. 이거는 절대로 겹치지 않아요.

그 다음에 @PreDestroy 할 때 소멸됩니다.

request 스코프 @PreDestroy가 호출되어서 소멸이 가능합니다. 스프링 컨테이너가 계속 관리하기 때문입니다.

request 스코프는 고객 요청이 들어올 때,

최초로 스프링한테 호출해달라고 해서 init()이 호출되고요. 그리고 스프링이 관리하다가 고객 요청이 우리 서버에서 빠져나가면,

그 때 이 close()를 호출하면서 빈이 소멸됩니다.

그리고 나서,

이렇게 출력을 찍어주면 됩니다.


그래서 설명을 해보면,

MyLogger 클래스는 로그를 출력하기 위한 클래스예요.

@Scope(value = "request") 를 사용해서 request 스코프로 지정했죠. 이제 이 빈은 HTTP 요청하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸됩니다.

정확한 생성 시점은 스프링 컨테이너한테 빈을 요청하는 시점에 생성됩니다.

이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해둡니다.

이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있겠죠.

그리고 이 빈이 소멸되는 시점에 @PreDestroy 를 사용해서 종료 메시지를 남깁니다.

그리고 requestURL 은 이 빈이 생성되는 시점에는 알 수 없어요. 그래서 외부에서 setter로 입력 받도록 했습니다. url 정보를 어디선가 넣어줘야 되겠죠.


이번에는 controller를 만들어볼게요.

web이라는 패키지를 만들고, 여기 안에다가 LogDemoController를 만들겠습니다.

그리고 @Controller를 적어주고 이전에 배웠던 @RequiredArgsConstructor를 적어주겠습니다.

이렇게 하면, final이 붙은 애들을 가지고 생성자를 만들어서, 생성자에 @Autowired가 자동으로 들어가서 의존관계 자동 주입이 됩니다.

일단 얘부터 만들겠습니다.

일단 이렇게만 해놓고 다시 돌아와서,

그 다음에 web이니까,

@RequestMapping 해서 log-demo 라는 요청이 오면 호출되어야 합니다.

지금 화면이 없는데, view 화면 없이, 그냥 바로 문자를 반환하고 싶어요. 그럴 때 @ResponseBody를 쓰면 돼요.

원래는 고객 controller 요청이 오면, view 템플릿을 거쳐서 렌더링 돼서 나가야 되는데, @ResponseBody를 쓰면 문자를 그대로 응답으로 보낼 수 있습니다.

이렇게 HttpServletRequest로 받으면, 자바에서 제공하는 표준 서블릿 규약이 있는데, 그 규약에 의한 HTTP request 정보를 받을 수 있습니다. 고객 요청 정보를 받을 수 있는 거예요.

이러면, 고객이 어떤 URL로 요청했는지 알 수 있습니다.

그 다음에 이 값을 myLogger에다가 세팅해주는 거예요. 그러면 myLogger는 request 스코프죠. 왜 이렇게 하냐면,

로그를 남길 때, controller나 service에서 이거까지 같이 남게 하려고 requestURL을 넘긴 거예요.

그 다음에 여기가 컨트롤러 테스트인 걸 알려주는 로그 메시지를 남긴거죠.

MyLogger의 log는 이랬습니다.

이번에는 서비스에서도 이 log를 호출을 해봐야겠죠.

MylogDemoService의 어떤 로직이 호출이 됐다고 치고, testID 라는 걸 파라미터로 넘겨볼게요.

일단 이렇게 만들고 logic()을 만들러 가보겠습니다.

여기서도 의존관계 주입이 필요합니다. 따라서 @RequiredArgsConstructor를 넣고,

이렇게 하면, 'service에서 넘어온 건 id야' 하고 넘기면 완성입니다. 그러면 이제 우리가 기대하는 대로 되는지 실행해보겠습니다.

다시 돌려보면,

지금 서버가 뜨지도 않습니다. 오류가 나는 게 정상입니다.

왜냐? 스프링이 뜰 때 무슨 일이 벌어집니까?

메시지를 보면 myLogger와 관련된 Scope 'request'가 active 되지 않았대요.

여러분 보세요. 스프링 컨테이너가 지금 뜨겠죠. 그런데 잘 생각해봐요.

@RequiredArgsConstructor가 지금 저 역할을 해주는 거예요.

그러면 스프링 컨테이너가 뜰 때, 이 LogDemoController 컨트롤러를 스프링 빈으로 등록을 해야 된단 말이에요. 그럼 그때 의존관계 주입이 일어나죠. 의존관계 주입이 일어나면 어떻게 됩니까?

스프링 컨테이너한테 'MyLogger 내놔!' 라고 하는데 문제가 있어요. 지금 scope이 request죠. 얘를 내놓으려고 하는데 지금 request가 없는 거예요.

MyLogger생존 범위고객 요청(HTTP request)이 들어와서 나갈 때까지예요. 그런데 지금 스프링을 띄우는 단계에서는 HTTP request가 들어왔나요? 안 들어왔죠. localhost:8080/log-demo를 하지도 않았는데 지금 request scope를 의존관계 주입 받겠다고 거예요.

그래서 다시 정리해서 말씀드리면, HTTP request가 들어와서 나갈 때까지 MyLogger를 쓸 수 있는데, 스프링 컨테이너가 뜨는 시점에 HTTP request가 없는 거예요.

그러니까 생존 범위가 아닌데, 스프링한테 '의존관계 주입할거니까 내놔!' 라고 하니까 '없는데 어쩌라는 거야' 하고 오류를 내는 겁니다.

그래서 이게 스코프 request가 활성화 되지 않았다는 거예요. 그럼 어떻게 해결해야 될까요? 바로 우리가 이전에 배웠던 Provider를 쓰면 되겠죠.

그거에 대한 방법은 다음 시간에 알아보고, 일단 정리를 다시 하겠습니다.

일단 이렇게 출력되길 기대했는데, 지금 스코프가 안 맞는 거예요.

request 고객 요청이 들어오지도 않았는데 스프링이 뜰 때 달라고 해버리니까 오류가 난 겁니다.

우리가 방금 한 게 로거가 잘 작동하는지 확인하는 테스트용 컨트롤러죠.

여기서 HttpServletRequest를 통해서 요청 URL을 받았습니다. 그럼 고객이 뭘 요청했는지 알 수 있거든요.

원래 서버가 정상적으로 뜨면, localhost:8080/log-demo 해서

이렇게 요청하려고 했지만, 당연히 지금은 안되겠죠.

그래서 이렇게 받은 requestURL 값을 myLogger에 저장해두면, 나중에 서비스든, 리포지토리든 어디든지 로거를 찍으면 이 URL이 같이 찍히겠죠.

myLoggerHTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 됩니다.

그리고 컨트롤러에서 controller test라는 로그를 남기고 서비스를 호출했습니다.

참고로,

requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나, 서블릿 필터 같은 곳을 활용하는 것이 좋거든요.

여기서는 예제를 단순화하고, 아직 스프링 인터셉터를 학습하지 않은 분들을 위해서 컨트롤러를 사용했는데요, 스프링 웹에 익숙하다면 인터셉터를 사용해서 구현해보면, 위의 저 코드를 뺄 수 있습니다. 동일한 기능을 하도록 공통처리를 해볼 수 있을 겁니다.

인터셉터는 간단하게 말씀드리면, HTTP request 요청이 컨트롤러 호출 직전 공통화해서 처리를 할 수 있는 거예요.

그래서 이 로직을 공통 처리할 수 있어요. 이렇게 이해하시면 됩니다.


그리고 나서, LogDemoService를 추가했죠.

비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해볼겁니다.

여기서 중요한점이 있는데, MyLoggerrequest scope를 사용하지 않고, 파라미터로 이 모든 정보를 서비스 계층에 넘겨서 출력할 수도 있어요.

여기 파라미터에 uuid도 넘기고, requestURL도 넘기고 해서 myLogger를 안 쓰고 만들수는 있겠죠.

그런데 이렇게 하면 파라미터가 너무 많아서 지저분해져요. 더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 될 수도 있어요.

예를 들어서, LogDemoService 입장에서는 requestURL 자체가 필요 없는 거예요.

그런데 파라미터로 requestURL을 넣어버리면, 웹과 관련없는 서비스 계층까지 넘어가게 되는거죠.

그래서 웹과 관련된 부분은 컨트롤러까지만 사용하고, 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋습니다.

request scopeMyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다는 장점도 있습니다.

그래서 실행을 해보면,

이런 출력이 나오기를 기대했죠.

그런데 실제로는 오류가 발생했습니다. 왜냐?

스프링 컨테이너한테 'request 스코프 주세요' 라고 해서 주입하려고 했는데, 스프링 컨테이너가 뜨는 시점에는 아직 request 요청 자체가 안 왔잖아요. 내가 localhost:8080/log-demo 했을 때 HTTP 요청이 만들어지는데 스프링 컨테이너가 뜨는 시점에는 localhost:8080/log-demo를 못하니까 HTTP 요청이 안 만들어져서 오류가 난 겁니다.

따라서 이 빈은 실제 고객의 요청이 와야 생성할 수 있어요.

결국 스프링 컨테이너한테 "나 이 스프링 빈을 주세요" 라고 하는 단계를 의존관계 주입 단계가 아니라, 실제 고객 요청이 왔을 때로 지연을 시켜야돼요. 뒤로 미뤄야 되는 거죠.

그러면 우리가 앞에서 배웠던 Provider를 쓰면 해결이 되겠죠.

그래서 다음 시간에는 Provider를 써서 이 문제를 해결해보겠습니다.