[Spring] 09-2. 웹 스코프

지찬우·2023년 1월 19일
1

Spring

목록 보기
27/27
post-thumbnail

이 시리즈는 인프런 강의(김영한 님의 ‘스프링 핵심 원리 - 기본편’)로 공부하며 혼자 기록하고, 사람들과도 공유할 수 있도록 작성하는 글이다. 최대한 추가적인 정보는 공식 홈페이지, 문서를 보며 얻을 예정이다.
(개인적인 생각과 이해가 들어가 있기 때문에 저의 ‘무식함’이 있을 수 있습니다😜 혹시라도 이 글을 보게 되시는 분이 계시다면 잘못된 부분 댓글로 많이 알려주시면 너무 감사하겠습니다!!)

GitHub Repository : https://github.com/jcw1031/spring-core-study


웹 스코프

웹 스코프 빈은 웹 환경에서만 동작한다. 웹 스코프 빈은 프로토타입 스코프 빈과는 달리 스프링 컨테이너가 빈 소멸 시점까지 관리한다. → 종료 메서드가 호출된다.

웹 스코프의 종류

  • request : HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프
  • session : HTTP Session과 동일한 생명주기를 갖는 스코프
  • application : 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 갖는 스코프
  • websocket : 웹 소켓과 동일한 생명주기를 갖는 스코프

각자 생명주기는 모두 다르지만, 동작 방식은 비슷하기 때문에 request 스코프로 설명한다.

request 스코프 📌

HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프이다. 각각의 HTTP 요청마다 별도의 스프링 빈 인스턴스가 생성되고 관리된다. 싱글톤과 다르게 각 요청마다 다른 객체가 생성되는 것을 인지하자.


코드로 작성해 보자.

웹 환경이 동작할 수 있도록 라이브러리를 추가해야 한다. build.gradle에 의존성 설정을 해준다. (꼭 추가하고 build.gradle을 reload 하자)

implementation 'org.springframework.boot:spring-boot-starter-web'

해당 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 사용해 웹 서버와 스프링을 함께 실행시킨다. 또한 웹 라이브러리가 추가되면 웹과 관련된 설정 및 환경이 필요하므로 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동한다.

동시에 여러 HTTP 요청이 들어왔을 때, 정확히 어떤 요청이 남긴 로그인지 구분하기 위해 request 스코프를 활용해 로그를 남기는 추가 기능을 개발해 보자.

우선 common 패키지에 MyLogger라는 클래스를 만든다.

[UUID][requestURL] {message}를 공통 출력 포맷으로 정하자. UUID는 임의로 랜덤 한 ID를 제공하는데, 중복될 확률이 매우 매우 매우 낮다고 한다. 이 UUID를 통해 HTTP 요청을 구분하자. requestURL도 출력하여 어떤 url을 요청했는지도 확인하자.

@Scopevlauerequest로 설정하여 request 스코프로 지정한다. 그리고 UUIDrequestURL을 저장하는 필드가 필요하다. requestURL은 따로 setter를 만들어 생성할 때가 아닌 중간에 설정해 주도록 하겠다.


로그를 출력하는 로직의 메서드 log()도 추가한다.


그리고 @PostConstruct@PreDestroy 애노테이션을 사용해 초기화 작업과 종료 작업을 하는 init(), destroy() 메서드를 추가한다. 초기화 작업 메서드에서 UUID를 생성해 필드에 저장한다.


이제 HTTP 요청을 받고 응답하는 계층인 LogDemoController를 생성한다.


@Controller 애노테이션을 추가하고, 아직 만들지 않았지만 이후에 만들 LogDemoServiceMyLogger를 의존관계 주입을 받는다.


@RequestMapping 애노테이션에 URI 매핑을 설정한다.(localhost:8080/log-demo로 요청이 오면 해당 메서드가 실행된다.) @ResponseBody 애노테이션은 메서드가 return 하는 문자열을 그대로 response body에 담아 응답하도록 한다. HttpServletRequestgetRequestURL()은 요청 URL을 StringBuffer 형태로 반환한다. 따라서 toString()으로 String 형태로 변환한다. 그리고 MyLogger의 setter를 통해 requestURL에 값을 저장한다.


그 후에 MyLoggerlog() 메서드에 메시지를 매개변수로 전달하여 로그를 찍도록 한다. (MyLogger 빈이 생성되어 주입된 상태이므로 초기화 메서드가 실행되어 UUID가 설정되었고, setRequestURL()을 통해 requestURL을 설정했기 때문에 모든 정보가 출력될 것이다.)
LogDemoService에 만들 logic() 메서드도 호출한다.


LogDemoService 클래스를 생성하고 MyLogger를 주입받는다.


그리고 logic() 메서드는 MyLoggerlog() 메서드를 호출한다.


오류 발생 ❗️

이제 한 번 실행해 보자. CoreApplication을 실행하면.. 에러가 발생한다! 이유를 생각해 보자. request 스코프는 HTTP Request가 들어왔을 때 생성된다. request 스코프인 MyLogger 빈은 HTTP Request가 들어와야 생성되는 것이다. 하지만 request가 들어오기 전에 LogDemoControllerLogDemoService에서 MyLogger를 주입받으려 했기 때문에 MyLogger 빈이 존재하지 않아 오류가 발생하는 것이다.

그럼 어떻게 해결해야 할지 생각해 보자.


해결 방안 💡

[ ObjectProvider ]

우리가 이전에 싱글톤 스코프 빈과 프로토타입 스코프 빈을 함께 사용했을 때 발생하는 문제를 어떻게 해결했는지 떠올려 보자. 바로 ObjectProvider를 사용해서 해결했다. 필요할 때 스프링 컨테이너로부터 빈을 조회해 사용하는 DL 방식을 사용했다. 이 문제도 이 ObjectProvider를 사용해 해결할 수 있다!!

LogDemoControllerLogDemoService 모두 ObjectProvider를 사용하도록 변경한다.


다시 서버를 실행시키고, 웹 브라우저에서 localhost:8080/log-demo로 접속하면 로그가 잘 출력되는 것을 확인할 수 있다. UUID를 사용했기 때문에 아무리 요청이 많이 와도 각 HTTP 요청을 구분할 수 있다.


[ Proxy ]

하지만 항상 사람들은 더 편하고 간단한 방법을 찾기 마련이다. 그래서 프록시 모드를 설정해 해결하는 방법이 있다. 프록시가 무엇인지는 코드를 보며 설명하겠다.

아래처럼 @ScopeproxyMode = ScopeProxyMode.TARGET_CLASS를 지정한다.

적용할 대상이 class인 경우에는 TARGET_CLASS, interface인 경우에는 INTERFACES를 입력한다.


그리고 LogDemoControllerLogDemoService는 처음과 동일하게 MyLogger를 바로 주입받는 형식으로 작성하면 된다.

이렇게 하면 MyLogger의 가짜 프록시 클래스가 만들어져, HTTP Request가 오기 전에 이 가짜 프록시 객체를 다른 빈에 주입이 가능하다.


실행해 보면 잘 동작한다.


프록시 가짜 객체는 어떻게 만들어지는 것일까. 한 번 MyLogger 객체를 출력해 보자. getClass() 메서드를 사용해 객체를 출력한다.


일반적인 MyLogger가 아니다. 전에 @Configuration에 대해 공부할 때 봤던 CGLIB 라이브러리가 보인다. 스프링 컨테이너가 바이트 코드를 조작하는 CGLIB 라이브러리를 이용해 내부에서 MyLogger를 상속받는 가짜 프록시 객체를 생성한 것이다. 그래서 순수한 MyLogger가 아닌 다른 객체인 것이다. 그리고 이 가짜 MyLogger 객체가 주입되는 것이다.


가짜 프록시 객체에는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 가짜 프록시 객체는 진짜 MyLogger 객체를 찾는 방법을 내부에 갖고 있는 것이다. 클라이언트가 log() 메서드를 호출하는 것은 가짜 프록시 객체의 메서드를 호출한 것이고, 이 가짜 프록시 객체가 진짜 MyLogger 객체의 log() 메서드를 호출하게 된다.

가짜 프록시 객체는 MyLogger를 상속받아 만들어졌기 때문에, 이 객체를 사용하는 클라이언트는 마치 순수 MyLogger인지 가짜 프록시인지 모를 정도로 동일하게 사용이 가능하다. - 다형성


정리 📝

  • 웹 스코프 빈은 싱글톤 스코프 빈과 다르다. 웹 스코프 빈은 각 HTTP 요청마다 모두 다른 객체가 생성되어 관리된다. 이 웹 스코프 빈은 HTTP Request가 들어와야 생성되기 때문에 처음부터 다른 빈이 주입을 받을 수 없다. 그래서 사용하는 것이 프록시 방식이다.
  • 프록시는 진짜 객체의 클래스를 상속받아 가짜 클래스가 만들어지고, 가짜 객체가 빈으로 등록되어 의존관계 주입이 일어난다. 가짜 클래스를 만들고 객체를 생성하는 것은 DI 컨테이너가 라이브러리를 통해 해준다.
  • 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 다형성과 DI 컨테이너가 가진 큰 장점이다.

이런 특별한 스코프는 꼭 필요한 곳에서만 최소화하여 사용하자. 무분별하게 사용하면 유지 보수가 어려워진다.


이렇게 길고 긴 강의가 끝났다. 팀 프로젝트를 위해 무작정 개발부터 시작했던 스프링. 이렇게 핵심 원리를 배우니 새롭고, 그동안 내가 만든 코드가 어떤 방식으로 동작하는지 알게 되었다.

스프링을 처음 막 시작했을 때, 인터넷을 뒤져보며 감으로 코드를 작성하고 많은 오류가 발생해 방치해 두었던 프로젝트 파일을 오랜만에 열어보았다. 참 신기하게 오류 메시지를 읽으면 문제가 무엇인지 알 수 있었고, 해결이 가능했다. 예전에는 스프링 빈의 개념이 없었기에 무슨 오류인지도 몰랐지만, 스프링 빈으로 등록되지 않았는데 @Autowired로 자동 주입을 받으려고 해서 발생한 오류, 같은 타입의 객체가 두 개나 빈으로 등록되어 의존관계 자동 주입 시에 발생하던 충돌, 이제 눈에 들어오는 게 정말 신기하고 뿌듯했다.

이제 다음으로 나아갈 차례이다. 내 목표는 스프링 MVC를 익히고, 세션과 쿠기를 다뤄보고, JPA를 좀 더 깊이 있게 공부하는 것이다. 이후에는 AWS 사용법을 익혀 볼 생각이다.

열심히 해보자.

profile
좋은 개발자가 되자.

0개의 댓글