Spring?

  • 자바 언어 기반의 통합 프레임워크
    • 객체 지향 앱을 개발
  • di를 통해 다형성과 ocp, dip를 지킬 수 있도록 지원
    • 클라이언트 코드의 변경 없이 기능을 확장

Container

  • javabeans 객체의 life-cycle을 관리하고 의존성을 주입해주는 component
    • ioc, di 컨테이너
    • 컨테이너는 물리적인 개념이라기 보다 객체 관리의 개념 또는 시스템에 가까움
    • 등록된 빈의 부모 타입으로 조회하면, 자식 타입도 함께 조회됨
  • 별도 설정을 하지 않을 경우 컨테이너는 빈 객체를 싱글톤 범위로 관리
    • 싱글톤을 보장하기 위해 CGLIB라이브러리를 사용해서 설정 클래스를 상속받는 프록시 객체를 생성한 후 등록

BeanFactory

public interface BeanFactory {
 
	String FACTORY_BEAN_PREFIX = "&";
	
	Object getBean(String name) throws BeansException;
	
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;
	
	Object getBean(String name, Object... args) throws BeansException;
	
	<T> T getBean(Class<T> requiredType) throws BeansException;
	
	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
	
	<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
	
	<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
	
	boolean containsBean(String name);
	
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
	
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
	
	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
	
	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
	
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;
	 
	@Nullable
	Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
	
	String[] getAliases(String name);
}
  • 컨테이너의 최상위 인터페이스
  • 빈 생성과 검색에 대한 기능을 정의
    • 생성된 객체를 검색하는 데 필요한 getBean()
    • 빈 객체를 lazy-loading 개념에 따라 필요한 시점에 생성
  • transaction 관리, aop, 이벤트 처리 등의 고급 기능은 제공하지 않음

ApplicationContext

  • BeanFactory를 확장해 더 많은 기능을 제공하는 인터페이스
    • 이벤트 시스템, aop, transaction 관리, 국제화(i18n), 프로필/환경 변수 등
  • 모든 빈 객체를 eager-loading 개념에 따라 미리 생성하여 앱 시작 시점에 로드
AnnotationConfigApplicationContext
public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
 
	private final AnnotatedBeanDefinitionReader reader;
	private final ClassPathBeanDefinitionScanner scanner;
 
	// ...
}
  • BeanFactoryApplicationContext에 정의된 기능의 구현을 제공하는 클래스
  • 어노테이션을 이용한 클래스로부터 객체 설정 정보를 가져옴

Note

어떤 구현 클래스를 사용하든, 각 구현 클래스는 설정 정보로부터 빈이라고 불리는 객체를 생성하고 그 객체를 내부에 보관한다. 그리고 getBean()를 실행하면 해당하는 빈 객체를 제공한다.

GenericXmlApplicationContext

  • 범용 xml 컨테이너로서 xml로부터 객체 설정 정보를 가져오는 클래스
    • classpath 경로를 기준으로 xml 설정 파일을 찾는 ClassPathXmlApplicationContext와 전체 파일 시스템 경로를 기준으로 xml 설정 파일을 찾는 FileSystemXmlApplicationContext 역할을 모두 수행 가능
  • 스프링 3부터 권장되는 방식이지만, 현재는 잘 사용되지 않음

BeanDefinition

  • 빈의 메타 정보를 담고 있는 추상화 인터페이스
  • BeanDefinitionReader의 구현체에 의해 BeanDefinition를 로드하고 등록
    • 최종적으로 BeanFactory가 이를 기반으로 빈을 생성

@Bean

@Configuration
public class AppConfig {
 
	@Bean
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
 
	// ...
}
  • 설정 클래스 내부의 @Bean 메서드가 반환하는 객체를 컨테이너가 관리하는 빈 객체로 직접 등록
    • @Bean 메서드의 이름으로 빈 객체를 식별
      • name 속성을 사용하여 빈 객체의 이름을 바꿀 수 있음

@Component

@Component // memberServiceImpl이라는 이름으로 빈 등록
public class MemberServiceImpl implements MemberService {
 
	private final MemberRepository memberRepository; // DIP 만족
 
	@Autowired
	public MemberServiceImpl(MemberRepository memberRepository) {
	super();
	this.memberRepository = memberRepository;
 
	// ...
}
  • 스프링이 제공하는 어노테이션을 사용하여, 컨테이너가 이를 자동으로 감지하고 빈으로 등록하도록 표시
  • 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용
    • 빈의 이름을 직접 지정할 수 있음

XML

<?xml version="1.0" encoding="UTF-8"?>
<beans
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 
	<bean id="memberService" class="hello.core.member.MemberServiceImpl">
		<constructor-arg name="memberRepository" ref="memberRepository" />
	</bean>
  
	<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />
	<bean id="orderService" class="hello.core.order.OrderServiceImpl">
		<constructor-arg name="memberRepository" ref="memberRepository" />
		<constructor-arg name="discountPolicy" ref="discountPolicy" />
	</bean>
	<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />
</beans>
  • <constructor-arg><property>를 사용해서 초기화
  • <context:component-scan>를 사용해서 @ComponentScan를 대신할 수 있음

DI (Dependency Injection)

Constructor Injection

@Component
public class MemberServiceImpl implements MemberService {
 
	private final MemberRepository memberRepository; // DIP 만족
 
	@Autowired
	public MemberServiceImpl(MemberRepository memberRepository) {
		super();
		this.memberRepository = memberRepository;
}
  • @Autowired를 사용하여 의존 관계를 생성자를 통해 자동으로 주입받는 방식
    • 객체 변경의 유연성
    • 빈 객체를 생성하는 단계에서 작동
    • 생성자가 한 개만 존재한다면 @Autowired 생략 가능
  • 생성자 호출 시점에 딱 한 번만 호출되는 것을 보장
  • 불변, 필수 관계에 주로 사용
public class MemberServiceImpl implements MemberService {
 
	private final MemberRepository memberRepository; // DIP 만족
 
	public MemberServiceImpl(MemberRepository memberRepository) {
		super();
		this.memberRepository = memberRepository;
	}
}
  • 의존 관계를 생성자를 통해 직접 주입받을 수 있음
  • 스프링 같은 di 프레임워크 없이도 순수한 객체지향 방식으로 구현 가능

Note

@Autowired를 사용할 때 동일한 타입의 빈이 여러 개 존재하면, 스프링 컨테이너는 의존 관계를 해결하기 위해 @Qualifier, @Primary, 그리고 필드나 파라미터 이름 순으로 매칭을 시도한다.

Setter Injection

@Component
public class MemberServiceImpl implements MemberService {
 
	private MemberRepository memberRepository; // DIP 만족
 
	@Autowired
	public setMemberRepository(MemberRepository memberRepository) {
		super();
		this.memberRepository = memberRepository;
}
  • setter를 통해 의존 관계를 주입하는 방식
  • 선택적으로 의존 관계를 설정할 필요가 있는 경우에 사용
    • 빈으로 등록되지 않은 객체 주입

Field Injection

@Component
public class MemberServiceImpl implements MemberService {
 
	@Autowired
	private MemberRepository memberRepository; // DIP 만족
}
  • 필드에 바로 주입하는 방식
  • 외부에서 변경이 불가능하기 때문에 권장되지 않음
    • 테스트 코드 작성 시 불리

Method Injection

@Component
public class MemberServiceImpl implements MemberService {
 
	private MemberRepository memberRepository; // DIP 만족
 
	@Autowired
	public void init(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
}
  • 일반 메서드를 통해서 주입 받는 방식

Note

@Autowired를 사용할 때 @Autowiredrequired 속성, org.springframework.lang.Nullable, java.util.Optional를 이용해서 옵션 처리를 할 수 있다. 주입할 빈이 존재하지 않을 때 required 속성을 사용하면 해당 메서드를 호출하지 않지만, @Nullable를 사용하면 주입할 빈이 존재하지 않더라도 해당 메서드를 호출하고 NULL을 전달한다.

@Component

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
 
	String value() default "";
 
}
  • @ComponentScan의 감지 대상이 되어 빈으로 등록될 수 있도록 표시하는 클래스 레벨 어노테이션
  • 클래스 이름의 첫 글자를 소문자로 변환한 이름이 빈 이름이 됨
    • value 속성을 지정하면 빈 이름을 명시적으로 설정할 수 있음

@Configuration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
 
	@AliasFor(annotation = Component.class)
	String value() default "";
 
	boolean proxyBeanMethods() default true;
 
	boolean enforceUniqueMethods() default true;
}
  • 어노테이션 기반의 설정 클래스로 인식되도록 하는 어노테이션
  • CGLIB를 통해 프록시 객체로 생성되어 빈으로 등록됨
    • @Bean 메서드 간 호출 시에 싱글톤을 보장
    • proxyBeanMethods 속성 값을 false로 설정할 경우 프록시 생성이 비활성화 됨
  • 한 개 이상의 설정 클래스를 사용할 수 있음

@Controller

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
 
	@AliasFor(annotation = Component.class)
	String value() default "";
}
  • 스프링 mvc의 컨트롤러로 등록

@Service

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
 
	@AliasFor(annotation = Component.class)
	String value() default "";
}
  • 서비스 계층 클래스를 컴포넌트로 등록
  • 트랜잭션 처리, aop 적용 등 스프링의 부가 기능을 적용할 수 있는 대상임을 명시적으로 나타냄

@Repository

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
 
	@AliasFor(annotation = Component.class)
	String value() default "";
}
  • dao 계층을 컴포넌트로 등록
  • jdbc의 Exception을 스프링의 DataAccessException으로 자동 변환
    • 내부적으로 PersistenceExceptionTranslationPostProcessor가 처리

@ComponentScan

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
 
	@AliasFor("basePackages")
	String[] value() default {};
	
	@AliasFor("value")
	String[] basePackages() default {};
	
	Class<?>[] basePackageClasses() default {};
	
	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
	
	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
	
	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
	
	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
	
	boolean useDefaultFilters() default true;
	
	Filter[] includeFilters() default {};
	
	Filter[] excludeFilters() default {};
	
	boolean lazyInit() default false;
 
	@Retention(RetentionPolicy.RUNTIME)
	@Target({})
	@interface Filter {
 
		FilterType type() default FilterType.ANNOTATION;
	
		@AliasFor("classes")
		Class<?>[] value() default {};
		 
		@AliasFor("value")
		Class<?>[] classes() default {};
 
		String[] pattern() default {};
	}
}
  • @Component가 붙은 클래스를 자동으로 탐색하여 빈으로 등록하는 어노테이션
    • 기본적으로 @ComponentScan이 선언된 클래스의 패키지를 기준으로 하위 패키지를 모두 탐색
      • basePackages 속성을 지정하면 특정 패키지를 탐색할 수 있음
      • excludeFilters 속성을 지정하면 탐색 대상에서 특정 클래스를 제외할 수 있음
      • includeFilters 속성을 지정하면 @Component가 아닌 클래스도 빈으로 등록할 수 있음
  • 수동 빈 등록과 충돌할 경우 수동 빈 등록이 우선됨
    • 스프링 부트에서는 충돌 시 BeanDefinitionOverrideException 발생
  • 내부적으로 ClassPathScanningCandidateComponentProvider를 사용하여 클래스 경로를 탐색하고, 탐색된 클래스를 BeanDefinition으로 변환하여 등록함
  • xml 기반 설정에서 <context:component-scan>로 대체할 수 있음

Bean의 생명주기

  • 빈 객체의 생성과 의존 관계 주입은 별도의 단계
    • 생성자 주입 방식 제외
    • 빈 객체를 생성하고, 의존 관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있음
      • 빈의 초기화 작업은 의존 관계 주입이 모두 완료된 후 호출해야 함
  • 스프링은 모든 의존 관계가 주입되면 빈의 초기화 시점을 알리고, 컨테이너가 종료되기 직전에는 종료 시점을 알려주는 다양한 기능을 제공

InitializingBean, DisposableBean

public class NetworkClient implements InitializingBean, DisposableBean {
 
	// ...
 
	@Override
	public void afterPropertiesSet() throws Exception {
		connect();
		call("초기화 연결 메시지");
	}
	
	@Override
	public void destroy() throws Exception {
		disconnect();
	}
}
  • 의존 관계가 주입되고 빈이 소멸되기 전에 각각 InitializingBean, DisposableBean 인터페이스의 콜백 메서드가 호출됨
  • 스프링 전용 인터페이스이므로 잘 사용되지 않음
    • 코드가 스프링 전용 인터페이스에 의존됨
    • 초기화, 소멸 메서드의 이름을 변경할 수 없음
    • 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없음

초기화, 소멸 메서드 지정

@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
	NetworkClient networkClient = new NetworkClient();
	networkClient.setUrl("http://hello-spring.dev");
	
	return networkClient;
}
  • 사용자가 정의한 초기화 및 소멸 메서드의 이름을 @BeaninitMethoddestroyMethod 속성 값으로 지정
    • 메서드의 이름을 자유롭게 정의할 수 있으며, 스프링 빈이 스프링 코드에 의존하지 않음
    • 코드가 아닌 설정 정보를 사용하기 때문에 외부 라이브러리에도 적용할 수 있음
  • @BeandestroyMethod 속성의 기본값은 (inferred)로 등록되어 있어 close, shutdown이라는 이름을 갖는 메서드를 자동으로 호출
    • 추론 기능을 사용하지 않을 땐 속성 값을 공백으로 지정하면 됨

@PostConstruct, @PreDestroy

@PostConstruct
public void init() {
	connect();
	call("초기화 연결 메시지");
}
 
@PreDestroy
public void close() {
	disconnect();
}
  • jakarta.annotation에서 제공
    • 스프링이 아닌 다른 컨테이너에서도 동작
    • 외부 라이브러리에 적용할 수 없음
  • 스프링에서 가장 권장하는 방식

Bean Scope

Singleton

class SingletonTest {
 
	@Test
	void singletoneBeanFind() {
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletoneBean.class);
	
	SingletoneBean singletonBean1 = ac.getBean(SingletoneBean.class);
	SingletoneBean singletonBean2 = ac.getBean(SingletoneBean.class);
 
	Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);
 
	ac.close();
	}
 
	@Scope("singleton")
	static class SingletoneBean {
 
		@PostConstruct
		public void init() {
			System.out.println("SingletonBean.init");
		}
 
		@PreDestroy
		public void destroy() {
			System.out.println("SingletonBean.destroy");
		}
	}
}
  • 스크링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 빈을 조회할 때마다 스프링 컨테이너는 항상 같은 인스턴스를 반환

Prototype

class PrototypeTest {
  
	@Test
	void prototypeBeanFind() {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
		
		PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
		PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
		
		Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
 
		ac.close();
	}
	
	@Scope("prototype")
	static class PrototypeBean {
		@PostConstruct
		public void init() {
			System.out.println("PrototypeBean.init");
		}
		
		@PreDestroy
		public void destroy() {
			System.out.println("PrototypeBean.destroy"); // 호출 안 됨
		}
	}
}
  • 빈의 생성, di, 초기화까지만 관여하고 더는 관리하지 않는 매우 좁은 범위의 스코프
    • 종료 시점을 클라이언트에서 관리
  • 빈을 조회할 때마다 스프링 컨테이너는 항상 새로운 인스턴스를 반환
  • 싱글톤 객체에서 프로토타입 객체를 사용하면 싱글톤처럼 관리됨

Web

  • 웹 환경에서만 동작
  • 컨테이너가 해당 스코프의 종료 시점까지 관리

Request

@Component
@Scope(value = "request")
public class MyLogger {
	// ...
}
  • 요청 당 하나씩 생성되고, 요청이 끝나는 시점에 소멸
    • 각각의 요청마다 별도의 빈 인스턴스가 생성되고 관리됨

Session

  • HttpSession과 동일한 생명주기

Application

  • ServletContext와 동일한 생명주기

Web Socket

  • 웹 소켓과 동일한 생명주기

지연 처리

ObjectProvider

@Controller
@RequiredArgsConstructor
public class LogDemoController {
 
	// MyLogger는 request 스코프
	private final ObjectProvider<MyLogger> myLoggerProvider;
	
	@RequestMapping("log-demo")
	@ResponseBody
	public String logDemo(HttpServletRequest request) {
		MyLogger myLogger = myLoggerProvider.getObject();
 
		// ...
 
		return "OK";
	}
}
  • 지정한 빈을 컨테이너에서 대신 찾아주는 dl 서비스를 제공
  • 스프링의 ObjectFactory를 상속받은 확장 인터페이스
  • 조회하고자 하는 타입의 빈이 컨테이너에 등록돼 있으면, ObjectProvider 역시 빈으로 등록됨

JSR-303 Provider

@Controller
@RequiredArgsConstructor
public class LogDemoController {
 
	// MyLogger는 request 스코프
	private final Provider<MyLogger> myLoggerProvider;
	
	@RequestMapping("log-demo")
	@ResponseBody
	public String logDemo(HttpServletRequest request) {
		MyLogger myLogger = myLoggerProvider.get();
 
		// ...
 
		return "OK";
	}
}
  • 별도의 라이브러리(jakarta.inject.Provider)가 필요하지만 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능

Proxy

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
	// ...
}
 
@Controller
@RequiredArgsConstructor
public class LogDemoController {
 
	// MyLogger는 requset 스코프
	private final MyLogger myLoggerr;
	
	@RequestMapping("log-demo")
	@ResponseBody
	public String logDemo(HttpServletRequest request) {
		// ...
 
		return "OK";
	}
}
  • CGLIB 라이브러리를 사용해서 프록시 객체를 생성하고 주입
    • 원본 객체에 대한 조회를 필요한 시점까지 지연해서 처리
  • 프록시 객체는 원본 객체에 대한 위임 로직을 가지고 있으며, 싱글톤처럼 동작