DI (Dependency Injection), AOP (Aspect Oriented Programming)
public UserServiceImpl(DataSource dataSource) {
this.userRepository = new JdbcUserRepository(dataSource);
this.passwordEncode = new BCryptPasswordEncoder();
}
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncode = passwordEncoder;
}
위처럼 클래스 내에서 직접 의존 클래스 인스턴스를 생성하면 생성한 구현 클래스들은 추후에 변경이 어렵다.
클래스간의 결합도가 높다.
아래는 JdbcUserRepository나 BCryptPasswordEncoder 같은 구현 클래스들이 완성되지 않은 상태에서도 개발을 진행할 수 있다.
어떤 클래스가 필요로하는 컴포넌트를 외부에서 생성한 후 내부에서 사용 가능하게 만들어주는 과정 : 의존성 주입(DI)
이러한 의존성 주입을 자동으로 처리하는 기반 : DI 컨테이너
스프링 프레임워크가 제공하는 기능 중 가장 중요한 것이 DI 컨테이너 기능.
DI 컨테이너의 장점 : 컴포넌트간 의존성 해결, 인스턴스의 스코프 관리, 공통 처리 코드를 외부에서 자동으로 끼워넣는 AOP 기능 등
DI : IoC (Inversion of Control) 라고 하는 소프트웨어 디자인 패턴 중 하나.
IoC : 인스턴스 제어의 주도권이 역전된다는 의미. 컴포넌트를 구성하는 인스턴스의 생성과 의존 관계의 연결 처리를 해당 소스코드가 아닌 DI 컨테이너에서 대신 해주기 때문
DI 컨테이너 방식 장점
스프링 프레임워크에서는 ApplicationContext가 DI 컨테이너 역할.
@Configuration과 @Bean 애너테이션을 사용해서 DI 컨테이너에 컴포넌트를 등록.
DI 컨테이너에 등록하는 컴포넌트를 빈이라고 한다.
이 빈에 대한 설정 정보를 빈 정의(Bean definition)라고 한다.
DI 컨테이너에서 빈을 찾아오는 행위를 lookup이라고 한다.
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
빈 정의 방법
<bean> 요쇼의 class 속성에 FQCN(Fully-Qualified Class Name) 을 기술하면 빈 정의됨. <constructor-arg>나 <property>요소를 사용해 의존성 주입.다양한 설정 방식을 지원하기 위한 구현 클래스들이 준비되어 있다.
AnnotationConfigApplicationContext, ClassPathXmlApplicationContext, FileSystemXmlApplicationContext 등
웹 애플리케이션에서는 스프링 MVC를 활용, 이때는 WebApplicationContext를 사용
자바 기반 설정을 위한 클래스를 자바 컨피규레이션 클래스 (java configuration class)라 한다.
@Configuration
public class AppConfig {
@Bean
UserRepository userRepository() {
return new UserRepositoryImpl();
}
...
}
메서드에 매개변수를 추가하는 방법으로 다른 컴포넌트의 의존성을 주입할 수 있다. 인수로 전달될 인스턴스에 대한 빈은 별도로 정의돼 있어야 한다.
<beans>요소 안에 빈 정의를 여러개 한다
<bean>요소에 빈 정의를 한다. id 속성에서 지정한 값이 빈의 이름이 되고, class 속성에서 지정한 클래스가 해당 빈의 구현 클래스다. 이때 class 속성은 FQCN으로 정확하게 기재.
<constructor-arg> 요소에서 생성자를 활용한 의존성을 주입한다. ref 속성에 주입할 빈의 이름을 기재.
빈을 정의하는 애너테이션을 빈의 클래스에 부여하는 방식. 이후 이 애너테이션이 붙은 클래스를 탐색해서 DI 컨테이너에 자동으로 등록. 이 과정을 컴포넌트 스캔이라고 한다.
의존성 주입도 명시적으로 설정하는 것이 아니라 애너테이션이 붙어 있으면 DI 컨테이너가 자동으로 필요로 하는 의존 컴포넌트를 주입. 이러한 주입 과정을 auto wiring 이라 한다.
빈 클래스에 @Component 애너테이션을 붙여 컴포넌트 스캔이 되도록 한다.
생성자에 @Autowired 애너테이션을 부여해서 기본적으로 주입 대상과 같은 타입의 빈을 DI 컨테이너에서 찾아 와이어링 대상에 주입한다.
컴포넌트 스캔을 위해 스캔할 범위를 지정해야 한다. 이는 자바 기반 / XML 기반 설정 방식으로 가능.
@Configuration
@ComponentSacn("com.example.demo")
public class AppConfig {
// 생략
}
@ComponentScan 어노테이션 value 속성이나 basePackages 속성에 컴포넌트를 스캔할 패키지 지정.
XML 방식 : <context:component-scan> 요소의 base-packages 속성에 컴포넌트를 스캔할 패키지 지정.
DI 컨테이너에 등록되는 빈의 이름은 기본적으로 클래스 첫글자를 소문자로 바꾼 이름과 같다.
설정자 기반 의존성 주입 방식 (setter-based dependency injection)
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
생성자 기반 의존성 주입 방식 (constructor-based dependency injection)
@Component
public class Car {
@Autowired
public Car(Engine engine, Transmission transmission) {
this.engine = engine;
this.transmission = transmission;
}
}
컨스트럭터 인젝션을 사용하면 필드를 final로 선언해서 생성 후에 변경되지 않게 만들 수 있다.
필드 기반 의존성 주입 방식 (field-based injection)
public class UserServiceImpl ... {
@Autowired
UserRepository userRepository;
...
}
클래스 로더를 스캔하면서 특정 클래스를 찾은 다음 DI 컨테이너에 등록하는 방법.
기본 설정에서 다음과 같은 에너테이션들이 붙은 클래스들이 탐색 대상이 된다.
탐색 범위를 넓게 하는 것은 성능 면에서 좋지 않다.
DI 컨테이너는 빈 간의 의존 관계를 관리할 뿐 아니라 빈의 생존 기간도 관리. 이를 빈 스코프라고 한다.
DI 컨테이너가 관리하는 빈은 기본적으로 싱글턴.
스프링 프레임워크에서 사용 가능한 스코프
@Bean
@Scope("prototype")
UserService userService() {
...
}
singleton 스코프의 빈이 prototype 스코프의 빈을 주입받으면, prototype 빈은 singleton 빈과 같은 수명을 살게 된다.
룩업 메서드 인젝션으로 해결할 수 있다.
@Lookup
PasswordEncoder passwordEncoder() {
return null;
}
위 메서드를 정의한 클래스 빈의 스코프가 singleton이고 PasswordEncoder 타입 빈의 스코프가 prototype 일 때
위처럼 @Lookup 애너테이션을 붙인 메서드는 DI 컨테이너가 직접 만든 룩업 메서드로 해당 메서드를 override한다. 따라서 @Lookup 애너테이션을 붙일 메서드는 private이거나 final 이면 안된다. 또한 메서드의 매개변수 역시 지정하면 안 된다.
DI 컨테이너는 재정의한 메서드에서 PasswordEncoder 타입 빈을 찾아 꺼내 리턴해준다. 따라서 PasswordEncoder 빈은 정의한 prototype 스코프대로 동작할 수 있다.
룩업 메서드 인젝션 말고 스코프트 프락시 방법으로도 다른 스코프의 빈 주입 문제를 해결할 수 있다.
기존의 빈을 프락시로 감싼 후 이 프락시를 다른 빈에 주입하고, 주입받은 빈에서 이 프락시의 메서드를 호출하면 프락시 내부적으로 DI 컨테이너에서 빈을 룩업하고 룩업된 빈의 메서드를 실행하는 방식이다.
@Bean
@RequestScope(proxyMode = ScopedProxyMode.INTERFACES)
PasswordEncoder passwordEncoder() {
return new ThreadUnsafePasswordEncoder();
}
이렇게 proxyMode 속성으로 프락시를 활성화하면 다른 빈에서 PasswordEncoder 빈을 주입받으면 PasswordEncoder의 프락시가 주입된다. 그리고 주입받은 빈에서 PasswordEncoder 프락시 인스턴스의 메서드를 호출할 때 request 스코프의 인스턴스가 만들어지게 된다.
proxyMode 속성
스코프트 프락시를 적용할 대상 빈이 인터페이스를 가지고 있지 않은 경우 서브클래스 기반의 프락시를 사용해야 한다. 서브클래스 기반의 프락시는 메서드를 오버라이드해야 하기 때문에 메서드나 클래스에 final을 붙일 수 없다.
Configuration 클래스들중 대표 설정 클래스에 @Import 애너테이션을 붙이고 인수에 분할한 설정 클래스들을 나열한다.