이 시리즈는 인프런 강의(김영한 님의 ‘스프링 핵심 원리 - 기본편’)로 공부하며 혼자 기록하고, 사람들과도 공유할 수 있도록 작성하는 글이다. 최대한 추가적인 정보는 공식 홈페이지, 문서를 보며 얻을 예정이다.
(개인적인 생각과 이해가 들어가 있기 때문에 저의 ‘무식함’이 있을 수 있습니다😜 혹시라도 이 글을 보게 되시는 분이 계시다면 잘못된 부분 댓글로 많이 알려주시면 너무 감사하겠습니다!!)
GitHub Repository : https://github.com/jcw1031/spring-core-study
웹 스코프 빈은 웹 환경에서만 동작한다. 웹 스코프 빈은 프로토타입 스코프 빈과는 달리 스프링 컨테이너가 빈 소멸 시점까지 관리한다. → 종료 메서드가 호출된다.
각자 생명주기는 모두 다르지만, 동작 방식은 비슷하기 때문에 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을 요청했는지도 확인하자.
@Scope
의 vlaue
를 request
로 설정하여 request 스코프로 지정한다. 그리고 UUID
와 requestURL
을 저장하는 필드가 필요하다. requestURL
은 따로 setter를 만들어 생성할 때가 아닌 중간에 설정해 주도록 하겠다.
로그를 출력하는 로직의 메서드 log()
도 추가한다.
그리고 @PostConstruct
와 @PreDestroy
애노테이션을 사용해 초기화 작업과 종료 작업을 하는 init()
, destroy()
메서드를 추가한다. 초기화 작업 메서드에서 UUID
를 생성해 필드에 저장한다.
이제 HTTP 요청을 받고 응답하는 계층인 LogDemoController
를 생성한다.
@Controller
애노테이션을 추가하고, 아직 만들지 않았지만 이후에 만들 LogDemoService
와 MyLogger
를 의존관계 주입을 받는다.
@RequestMapping
애노테이션에 URI 매핑을 설정한다.(localhost:8080/log-demo
로 요청이 오면 해당 메서드가 실행된다.) @ResponseBody
애노테이션은 메서드가 return 하는 문자열을 그대로 response body에 담아 응답하도록 한다. HttpServletRequest
의 getRequestURL()
은 요청 URL을 StringBuffer
형태로 반환한다. 따라서 toString()
으로 String
형태로 변환한다. 그리고 MyLogger
의 setter를 통해 requestURL
에 값을 저장한다.
그 후에 MyLogger
의 log()
메서드에 메시지를 매개변수로 전달하여 로그를 찍도록 한다. (MyLogger
빈이 생성되어 주입된 상태이므로 초기화 메서드가 실행되어 UUID
가 설정되었고, setRequestURL()
을 통해 requestURL
을 설정했기 때문에 모든 정보가 출력될 것이다.)
LogDemoService
에 만들 logic()
메서드도 호출한다.
LogDemoService
클래스를 생성하고 MyLogger
를 주입받는다.
그리고 logic()
메서드는 MyLogger
의 log()
메서드를 호출한다.
이제 한 번 실행해 보자. CoreApplication
을 실행하면.. 에러가 발생한다! 이유를 생각해 보자. request 스코프는 HTTP Request가 들어왔을 때 생성된다. request 스코프인 MyLogger
빈은 HTTP Request가 들어와야 생성되는 것이다. 하지만 request가 들어오기 전에 LogDemoController
와 LogDemoService
에서 MyLogger
를 주입받으려 했기 때문에 MyLogger
빈이 존재하지 않아 오류가 발생하는 것이다.
그럼 어떻게 해결해야 할지 생각해 보자.
우리가 이전에 싱글톤 스코프 빈과 프로토타입 스코프 빈을 함께 사용했을 때 발생하는 문제를 어떻게 해결했는지 떠올려 보자. 바로 ObjectProvider
를 사용해서 해결했다. 필요할 때 스프링 컨테이너로부터 빈을 조회해 사용하는 DL 방식을 사용했다. 이 문제도 이 ObjectProvider
를 사용해 해결할 수 있다!!
LogDemoController
와 LogDemoService
모두 ObjectProvider
를 사용하도록 변경한다.
다시 서버를 실행시키고, 웹 브라우저에서 localhost:8080/log-demo
로 접속하면 로그가 잘 출력되는 것을 확인할 수 있다. UUID
를 사용했기 때문에 아무리 요청이 많이 와도 각 HTTP 요청을 구분할 수 있다.
하지만 항상 사람들은 더 편하고 간단한 방법을 찾기 마련이다. 그래서 프록시 모드를 설정해 해결하는 방법이 있다. 프록시가 무엇인지는 코드를 보며 설명하겠다.
아래처럼 @Scope
에 proxyMode = ScopeProxyMode.TARGET_CLASS
를 지정한다.
적용할 대상이 class인 경우에는
TARGET_CLASS
, interface인 경우에는INTERFACES
를 입력한다.
그리고 LogDemoController
와 LogDemoService
는 처음과 동일하게 MyLogger
를 바로 주입받는 형식으로 작성하면 된다.
이렇게 하면
MyLogger
의 가짜 프록시 클래스가 만들어져, HTTP Request가 오기 전에 이 가짜 프록시 객체를 다른 빈에 주입이 가능하다.
실행해 보면 잘 동작한다.
프록시 가짜 객체는 어떻게 만들어지는 것일까. 한 번 MyLogger
객체를 출력해 보자. getClass()
메서드를 사용해 객체를 출력한다.
일반적인 MyLogger
가 아니다. 전에 @Configuration
에 대해 공부할 때 봤던 CGLIB 라이브러리가 보인다. 스프링 컨테이너가 바이트 코드를 조작하는 CGLIB 라이브러리를 이용해 내부에서 MyLogger
를 상속받는 가짜 프록시 객체를 생성한 것이다. 그래서 순수한 MyLogger
가 아닌 다른 객체인 것이다. 그리고 이 가짜 MyLogger
객체가 주입되는 것이다.
가짜 프록시 객체에는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 가짜 프록시 객체는 진짜 MyLogger
객체를 찾는 방법을 내부에 갖고 있는 것이다. 클라이언트가 log()
메서드를 호출하는 것은 가짜 프록시 객체의 메서드를 호출한 것이고, 이 가짜 프록시 객체가 진짜 MyLogger
객체의 log()
메서드를 호출하게 된다.
가짜 프록시 객체는
MyLogger
를 상속받아 만들어졌기 때문에, 이 객체를 사용하는 클라이언트는 마치 순수MyLogger
인지 가짜 프록시인지 모를 정도로 동일하게 사용이 가능하다. - 다형성
이런 특별한 스코프는 꼭 필요한 곳에서만 최소화하여 사용하자. 무분별하게 사용하면 유지 보수가 어려워진다.
이렇게 길고 긴 강의가 끝났다. 팀 프로젝트를 위해 무작정 개발부터 시작했던 스프링. 이렇게 핵심 원리를 배우니 새롭고, 그동안 내가 만든 코드가 어떤 방식으로 동작하는지 알게 되었다.
스프링을 처음 막 시작했을 때, 인터넷을 뒤져보며 감으로 코드를 작성하고 많은 오류가 발생해 방치해 두었던 프로젝트 파일을 오랜만에 열어보았다. 참 신기하게 오류 메시지를 읽으면 문제가 무엇인지 알 수 있었고, 해결이 가능했다. 예전에는 스프링 빈의 개념이 없었기에 무슨 오류인지도 몰랐지만, 스프링 빈으로 등록되지 않았는데 @Autowired
로 자동 주입을 받으려고 해서 발생한 오류, 같은 타입의 객체가 두 개나 빈으로 등록되어 의존관계 자동 주입 시에 발생하던 충돌, 이제 눈에 들어오는 게 정말 신기하고 뿌듯했다.
이제 다음으로 나아갈 차례이다. 내 목표는 스프링 MVC를 익히고, 세션과 쿠기를 다뤄보고, JPA를 좀 더 깊이 있게 공부하는 것이다. 이후에는 AWS 사용법을 익혀 볼 생각이다.
열심히 해보자.