❯ http -v ":8080/hello?name=Spring"GET /hello?name=Spring HTTP/1.1Accept:*/* --> 클라이언트가 선호하는 미디어 타입Accept-Encoding:gzip, deflate --> 클라이언트가 선호하는 압축 인코딩Connection:keep-aliveHost:localhost:8080User-Agent:HTTPie/3.2.1HTTP/1.1200Connection:keep-aliveContent-Length:12Content-Type:text/plain;charset=UTF-8 --> 표현 데이터의 형식Date:Thu, 01 Dec 2022 01:45:15 GMTKeep-Alive:timeout=60Hello Spring
여러 요청을 처리하는데 반복적으로 등장하는 공통 작업을 하나의 오브젝트에서 일괄적으로 처리하게 만드는 방식
모든 요청, 혹은 일정 패턴을 가진 요청을 하나의 서블릿이 담당하도록 매핑
프론트 컨트롤러의 두 가지 중요한 기능은 매핑과 바인딩
매핑:프론트 컨트롤러가 HTTP 요청을 처리할 핸들러를 결정하고 연동하는 작업
바인딩: 핸들러에게 웹 요청 정보를 추출하고 의미있는 오브젝트에 담아서 전달하는 작업
프론트 컨트롤러로 전환
ServletWebServerFactory serverFactory =newTomcatServletWebServerFactory();WebServer webServer =serverFactory.getWebServer(servletContext -> {HelloController helloController =newHelloController();servletContext.addServlet("frontController",newHttpServlet() { @Overrideprotectedvoidservice(HttpServletRequest req,HttpServletResponse resp) throwsServletException,IOException {// 인증, 보안, 다국어, 공통 기능 ..if (req.getRequestURI().equals("/servlet/hello") &&req.getMethod().equals(HttpMethod.GET.name())) {String name =req.getParameter("name");String ret =helloController.hello(name);resp.setStatus(HttpStatus.OK.value());resp.setHeader(HttpHeaders.CONTENT_TYPE,MediaType.TEXT_PLAIN_VALUE);resp.getWriter().println(ret); }elseif (req.getRequestURI().equals("/user")) {// ... }else {resp.setStatus(HttpStatus.NOT_FOUND.value()); } } }).addMapping("/*"); });webServer.start();
Standalone Spring Application
스프링 컨테이너는 애플리케이션 로직이 담긴 평범한 자바 오브젝트(POJO)와 구성 정보(Configuration Metadata)를 런타임에 조합해서 동작하는 최종 애플리케이션을 생성
// 스프링 컨테이너 생성GenericApplicationContext applicationContext =newGenericApplicationContext(); // 빈 오브젝트 클래스 정보 등록applicationContext.registerBean(HelloController.class); // 구성 정보로 컨테이너 초기화(빈 오브젝트를 직접 생성)applicationContext.refresh(); ...// 컨테이너가 관리하는 빈 오브젝트 획득HelloController helloController =applicationContext.getBean(HelloController.class);
스프링 컨테이너는 싱글톤 레지스트리라고도 불린다.
싱글톤 패턴과 유사하게 애플리케이션이 동작하는 동안 단 하나의 오브젝트만을 만들고 사용되도록 지원
.
Dependency Injection
DI를 위해 N개의 오브젝트(인터페이스 구현체)가 동적으로 의존관계를 가지도록 도와주는 어셈블러가 필요
스프링 컨테이너(어셈블러)는 DI를 가능하도록 도와주는 어셈블러로 동작
의존관계가 없는 클래스들의 오브젝트로 서로 관계를 연결시켜주고 사용할 수 있도록 설정
스프링 컨데이너는 메타 정보를 가지고 클래스에 싱글톤 오브젝트를 생성
생성된 오브젝트가 사용할 다른 의존 오브젝트가 있다면 의존성 주입
의존성 주입 방법으로는 생성자 주입, 팩터리 메서드 등 존재
.
DispatcherServlet
스프링은 프론트 컨트롤러와 같은 역할을 담당하는 DispatcherServlet 을 가지고 있다.
DispatcherServlet 은 서블릿으로 등록되어서 동작하면서, 스프링 컨테이너를 이용해서 요청을 전달할 핸들러인 컨트롤러 오브젝트를 가져와 사용
DispatcherServlet 이 사용하는 스프링 컨테이너는 GenericWebApplicationContext 를 이용해서 작성
.
애노테이션 매핑 정보
DispatcherServlet 은 스프링 컨테이너에 등록된 빈 클래스에 있는 매핑 애노테이션 정보를 참고해서 웹 요청을 전달할 오브젝트와 메소드를 선정
클래스 레벨의 @RequestMapping 과 메소드 레벨의 @GetMapping 두 가지의 정보를 조합해서 매핑에 사용할 최종 정보 생성
컨트롤러 메소드의 리턴값을 웹 요청의 바디에 적용하도록 @ResponseBody 선언
그렇지 않으면 String 타입의 응답은 뷰 이름으로 해석하고 Thymeleaf 같은 뷰 템플릿을 탐색(이 경우 404 에러 발생)
@RestController 는 @ResponseBody 를 포함하고 있으므로 메소드 레벨의 @ResponseBody 를 넣지 않아도 적용된 것처럼 동작
/** * @Bean 메소드에서 독립적으로 생성 * * DispatcherServlet 이 필요로 하는 WebApplicationContext 타입 컨테이너 오브젝트는 -> dispatcherServlet.setApplicationContext(this); * 스프링 컨테이너의 빈 생애주기 메소드를 이용해서 주입 빋게 된다. */@BeanpublicDispatcherServletdispatcherServlet() {returnnewDispatcherServlet();}
DispatcherServlet 은 ApplicationContextAware 라는 스프링 컨테이너를 setter 메소드로 주입해주는 메소드를 가진 인터페이스를 구현
이러한 생애주기 빈 메소드를 가진 빈이 등록되면 스프링은 자신을 직접 주입
빈 생애주기 메소드를 통해 주입되는 오브젝트는 스프링 컨테이너가 스스로 빈으로 등록해서 빈으로 가져와 사용할 수도 있도록 지원
그밖에 스프링이 제공하는 생애주기 메소드
BeanNameAware's setBeanName,
BeanClassLoaderAware's setBeanClassLoader,
BeanFactoryAware's setBeanFactory,
EnvironmentAware's setEnvironment,
EmbeddedValueResolverAware's setEmbeddedValueResolver,
ResourceLoaderAware's setResourceLoader (only applicable when running in an application context),
ApplicationEventPublisherAware's setApplicationEventPublisher (only applicable when running in an application context),
MessageSourceAware's setMessageSource (only applicable when running in an application context),
ApplicationContextAware's setApplicationContext (only applicable when running in an application context),
ServletContextAware's setServletContext (only applicable when running in a web application context),
postProcessBeforeInitialization methods of BeanPostProcessors,
InitializingBean's afterPropertiesSet,
a custom init-method definition,
postProcessAfterInitialization methods of BeanPostProcessors
TEST
TestRestTemplate
웹 서버에 HTTP 요청을 보내고 응답을 받아서 검증하는 테스트에서는 TestRestTemplate 를 사용해 보자.
@Testvoidhello() {TestRestTemplate restTemplate =newTestRestTemplate();ResponseEntity<String> res =restTemplate.getForEntity("http://localhost:8080/hello?name={name}",String.class,"Spring");assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);assertThat(res.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.TEXT_PLAIN_VALUE)).isTrue();assertThat(res.getBody().trim()).isEqualTo("Hello Spring");}
.
단위 테스트
의존 오브젝트가 있는 경우, 테스트가 실행되는 동안에 수행될 최소한의 기능을 가진 의존 오브젝트 코드를 테스트용으로 만들어서 사용
@TestvoidhelloController() {HelloController helloController =newHelloController(name -> name);String ret =helloController.hello("Test");Assertions.assertThat(ret).isEqualTo("Test");}
.
Decorator Pattern and Proxy Pattern
Decorator Pattern
기존 코드에 동적으로 책임을 추가할 때 사용하는 패턴
오브젝트 합성 구조로 확장이 가능하도록 설계
DI를 적용해서 의존관계를 런타임에 주입할 수 있다면 의존 오브젝트와 동일한 인터페이스를 구현한 확장기능(데코레이터)을 동적으로 추가 가능
재귀적인 구조로 여러 개의 책임을 부가하는 것도 가능
Proxy Pattern
프록시는 다른 오브젝트의 대리자 혹은 플레이스 홀더 역할
프록시는 리모트 오브젝트에 대한 로컬 접근이 가능하게 하거나, 필요가 있을 때만 대상 오브젝트를 생성
보안이나 접속 제어 등에 사용
Auto Configuration
Meta-annotation
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Component// Meta Annotationpublic @interfaceService {}
애노테이션에 적용한 애노테이션
스프링은 메타 애노테이션의 효력을 적용
Composed-annotation
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller// Meta Annotation@ResponseBody// Meta Annotationpublic @interfaceRestController {...}
하나 이상의 메타 애노테이션이 적용된 애노테이션
모든 메타 애노테이션이 적용된 것과 동일한 효과
Application Logic Bean
애플리케이션의 비즈니스 로직을 담고 있는 빈
컴포넌트 스캐너에 의해서 빈 구성 정보가 생성되고 빈 오브젝트로 등록
ex. HelloController, HelloDecorator ..
Application Infrastructure Bean
스프링 부트에서 자동 구성 정보에 의해 컨테이너에 등록되는 빈
애플리케이션이 동작하는데 꼭 필요한 기술 기반을 제공하는 빈
ex. ServletWebServerFactory, DispatcherServlet..
Container Infrastructure(Infra) Bean
스프링 컨테이너의 기능을 확장해서 빈 등록과 생성, 관계설정, 초기화 등의 작업에 참여하는 빈
true 일 경우, @Configuration 클래스는 CGLib 를 이용해서 프록시 클래스로 확장 후 @Bean 이 붙은 메소드의 동작 방식을 변경
@Bean 메소드를 직접 호출해서 다른 빈의 의존 관계를 설정할 때 여러번 호출되더라도 싱글톤 빈처럼 참조할 수 있도록 매번 같은 오브젝트를 리턴
/** * Spring 은 하나의 빈을 두 개 이상의 다른 빈에서 의존하고 있을 경우, * Factory Method 호출 시마다 새로운 빈이 생성되는 문제를 해결하기 위해 * @Configuration class 는 기본적으로 proxy 를 만들어서 기능 확장 * (proxyBeanMethods = true) */staticclassMyConfigProxyextendsMyConfig {privateCommon common; @OverrideCommoncommon() {if (this.common==null) {this.common= super.common(); }returnthis.common; }}...@ConfigurationstaticclassMyConfig { @BeanCommoncommon() {returnnewCommon(); } @BeanBean1bean1() {returnnewBean1(common()); } @BeanBean2bean2() {returnnewBean2(common()); }}
단, @Bean 메소드 직접 호출로 빈 의존관계 주입을 하지 않는다면 굳이 복잡한 프록시를 생성할 할 필요가 없음
/** * 스프링 4.0에 추가된 애노테이션으로 모든 조건을 만족하는 경우에만 컨테이너에 빈으로 등록 */@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interfaceConditional {Class<? extends Condition>[] value();}.../** * @Conditional 에 지정되어서 구체적인 매칭 조건을 가진 클래스가 구현해야할 인터페이스 */@FunctionalInterfacepublicinterfaceCondition {booleanmatches(ConditionContext context,AnnotatedTypeMetadata metadata);}.../** * Condition interface 구현체 */staticclassBooleanConditionimplementsCondition { /** * @Conditional 애노테이션의 엘리먼트 정보를 가져올 수 있는 AnnotatedTypeMetadata 전달 */ @Overridepublicbooleanmatches(ConditionContext context,AnnotatedTypeMetadata metadata) {Map<String,Object> annotationAttributes =metadata.getAnnotationAttributes(BooleanConditional.class.getName());Boolean value = (Boolean) annotationAttributes.get("value");return value; }}
@Conditional은 @Configuration 클래스와 @Bean 메소드에 적용 가능
/** * 자동 구성으로 등록되는 빈과 동일한 타입의 빈을 직접 정의(@Configuration/@Bean)하는 경우, * 직접 정의 빈 구성이 자동 구성을 대체 */@Configuration(proxyBeanMethods =false)publicclassWebServerConfiguration { @BeanServletWebServerFactorycustomerWebServerFactory() {TomcatServletWebServerFactory serverFactory =newTomcatServletWebServerFactory();serverFactory.setPort(9090);return serverFactory; }}.../** * 자동 구성 클래스의 @Bean 메소드에 @ConditionalOnMissingBean 이 정의된 경우, * 유저 구성에 지정한 타입의 빈이 정의되어있으면 자동 구성 빈의 조건이 충족되지 않아 등록되지 않음. */@Bean("tomcatWebServerFactory")@ConditionalOnMissingBeanpublicServletWebServerFactoryservletWebServerFactory() {returnnewTomcatServletWebServerFactory();}
• 프로젝트 내 지정한 클래스의 존재를 확인해서 포함 여부 결정
• 주로 @Configuration 을 클래스 레벨에서 사용하지만, @Bean 메소드에도 적용 가능
• (단, 클래스 레벨의 검증 없이 @Bean 메소드에만 적용하면 불필요한 @Configuration 클래스가 빈으로 등록되기 때문에 클래스 레벨 사용을 우선)
Bean Conditions
@ConditionalOnBean
@ConditionalOnMissingBean
• 빈의 존재 여부를 기준으로 포함 여부 결정
• 빈의 타입 또는 이름을 지정할 수 있고, 지정된 빈 정보가 없으면 메소드의 리턴 타입을 기준으로 빈 존재 여부 체크
• 컨테이너에 등록된 빈 정보를 기준으로 체크하기 때문에 자동 구성 사이에 적용하려면 @Configuration 클래스의 적용 순서가 중요
• 개발자가 직접 정의한 커스텀 빈 구성 정보가 자동 구성 정보 처리보다 우선하기 때문에 이 관계에 적용하는 것은 안전하지만, 반대로 커스톰 빈 구성 정보에 적용하는 건 피하자.
@Configuration 클래스 레벨의 @ConditionalOnClass
@Bean 메소드 레벨의 @ConditionalOnMissingBean
조합은 가장 대표적으로 사용되는 방식
클래스 존재로 해당 기술 사용 여부 확인 → 커스텀 빈 구성 존재를 확인해서 자동 구성의 빈 오브젝트를 이용할지 최종 결정
Property Conditions
@ConditionalOnProperty
• 스프링의 환경 프로퍼티 정보를 이용
• 지정된 프로퍼티가 존재하고 값이 false 가 아니면 포함 대상
• 특정 값을 가진 경우를 확인하거나 프로퍼티가 존재하지 않을 때 조건을 만족하도록 설정 가능
• 프로퍼티의 존재를 확인해서 빈 오브젝트를 추가하고, 해당 빈 오브젝트에서 프로퍼티 값을 이용해서 세밀하게 빈 구성 가능
Resource Conditions
@ConditionalOnResource
• 지정된 리소스(파일) 존재 확인
Web Application Conditions
@ConditionalOnWebApplication
@ConditionalOnNotWebApplication
• 웹 애플리케이션 여부 확인
• ex. 웹 기술을 사용하지 않는 배치
SpEL Expression Conditions
@ConditionalOnExpression
• 스프링 SpEL(스프링 표현식) 처리 결과 기준으로 판단
• 매우 상세한 조건 설정 가능
embedded db 로 dataSource 교체 ➔ 프로퍼티로 설정한 DB 접속 정보 사용 X
@SpringBootTest
스프링 컨테이너를 띄우고 자동 구성까지 적용해서 테스트
서블릿 컨테이너 환경 여부 설정
웹 환경 세팅 제외: webEnvironment = SpringBootTest.WebEnvironment.NONE
웹 환경 세팅: webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT
.
스프링 부트 자세히 살펴보기
스프링 부트의 동작 방식 이해
사용 기술과 관련된 자동 구성 빈과 구성, 속성, 프로퍼티 설정 등을 분석
자동 구성 빈과 조건에 따라 달라지는 빈 선택 기준
이를 어떻게 활용할 수 있는지 파악
.
자동 구성 분석 방법
자동 구성 후보 목록과 조건 판단 결과 조회하기
-Ddebug or --debug 인자를 이용해서 스프링 부트 애플리케이션 실행
ConditionEvaluationReport 타입의 빈을 주입 받고, 필요한 정보만 선택해서 자동 구성 결과 확인
ListableBeanFactory 타입의 빈을 주입 받고, 빈 이름을 가져와서(필요 시 빈 오브젝트도) 등록된 빈 목록 확인
자동 구성 선정 결과를 기준으로 스프링 부트 레퍼런스 문서, 자동 구성 클래스 소스 코드, 프로퍼티 클래스, Customizer 등을 살펴보며 어떻게 어떤 조건으로 동작할지 분석
VM options: -Ddebug
Positive matches:----------------- AopAutoConfiguration matched: - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition) AopAutoConfiguration.ClassProxyingConfiguration matched: - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition) - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition) ApplicationAvailabilityAutoConfiguration#applicationAvailability matched: - @ConditionalOnMissingBean (types: org.springframework.boot.availability.ApplicationAvailability; SearchStrategy: all) did not find any beans (OnBeanCondition)...Negative matches:----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition) AopAutoConfiguration.AspectJAutoProxyingConfiguration: Did not match: - @ConditionalOnClass did not find required class 'org.aspectj.weaver.Advice' (OnClassCondition) ArtemisAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)...
ConditionEvaluationReport
조건이 매칭된 자동 구성 클래스와 메소드를 출력
@BeanApplicationRunnerrun(ConditionEvaluationReport report) {return args -> {long result =report.getConditionAndOutcomesBySource().entrySet().stream().filter(co ->co.getValue().isFullMatch()) // 컨디션 조건을 모두 통과한 빈 목록.filter(co ->co.getKey().indexOf("Jmx") <0) // Jmx 관련 구성 정보 제외.map(co -> {System.out.println(co.getKey());co.getValue().forEach(c -> {System.out.println("\t"+c.getOutcome()); // 컨디셔널 통과 조건 });System.out.println();return co; }).count();System.out.println(result); };}