강의 링크
모든 내용을 정리하지는 않았다.
<? 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">
<bean id="bookService"
class="me.whiteship.springapplicationcontext.BookService">
//1.여기까지만 하면 BookService bean 만들고 끝.
//2.그러므로 아래와 같이 property를 생성해서 주입해줘야 한다.
<property name="bookRepository" ref="bookRepository" />
//3. name은 setter에서 가져온 것이고, ref는 레퍼런스로 다른 bean을 참조한다는 것
//4. 그러므로 ref에는 name(setter)에 들어갈 수 있는 다른 bean의 id가 와야한다.
<bean>
<bean id="bookRepository"
class="me.whiteship.springapplicationcontext.BookService"/>
위 처럼 bean 설정을 하면, 아래와 같이 ApplicationContext를 만들어서 bean 설정을 사용하면 된다.
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
String[] beanDefinitionNames = context.getBeanDefinitionNames();
System.out.println(Arrays.toString(beanDefinitionNames);
BookService bookService = (BookService) context.getBean("bookService");
System.out.println(bookService.bookRepository != null);
// 결과 : true
// 이유 : bookService는 위의 <bean id="bookService" 이하의 코드에서
bookRepository 의존성을 주입을 받았기 때문에
하지만, 이 방법의 단점은 일일히 <bean id=""...>
처럼 bean으로 등록하는 것이 번거롭다.
그래서 등장한 것이 <context:component-scan...>
<? 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">
<context:component-scan base-package="me.whiteshp.springapplicationcontext"/>
// 1.의미 : 나는 me.whiteship... 패키지부터 bean을 스캐닝해서 등록하겠다.
// 2.@Component를 확장한 애너테이션들(@Repository, @Service)을 사용해서 bean으로 등록할 수 있다.
// 3.하지만 bean으로만 등록되고 의존성 주입은 안된다.
// 4.의존성 주입은 @Autowired 나 @Inject를 통해 받을 수 있다.
@Repository
public class BookRepository {
}
@Service
public class BookService {
@Autowired
BookRepository bookRepository;
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
이제 1.2.에서 작성한 DemoApplication을 실행하면,
application.xml을 읽어들이기는 하지만, xml에 등록되어있는 component-scan 기능을 사용해서
bean들을 me.whiteship...패키지 이하에서 애너테이션들을 스캐닝해서 등록해준다.
@Configuration
public class ApplicationConfig {
@Bean
public BookRepository bookRepository() {
return new BookRepository();
}
// 방식1. 메소드를 호출해서 의존성 주입
@Bean
public BookService bookService() {
BookService bookService = new BookService();
bookService.setBookRepository(bookRepository());
return bookService;
}
// 방식2. 메소드 파라미터로 의존성 주입받기
@Bean
public BookService bookService(BookRepository bookRepository) {
BookService bookService = new BookService();
bookService.setBookRepository(bookRepository);
return bookService;
}
// 방식3. 의존성 주입을 직접하지 않고, @Autowired를 쓰는 방법
@Bean
public BookService bookService(BookRepository bookRepository) {
return new BookService();
}
// 방식3. BookService는 아래와 같이 변경
/* public class BookService {
@Autowired
BookRepository bookRepository;
...
} */
위와 같이 java 설정 파일로 만든 것을 application context로 사용하기 위한 방법은 아래와 같다.
public class DemoApplication {
public static void main(String[] args) {
//ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
// 1.ApplicationConfig 클래스를 bean 설정으로 사용하겠다는 것.
// 2.ApplicationConfig에서 @Bean이 달린 bean 정의들을 읽어서 bean들로 등록하고,
코드를 작성한 대로 의존성을 주입해준다.
하지만, 이 방식의 경우에도 @Bean을 하나씩 정의해야 하므로 불편하다. 그래서 application.xml에서 처럼 Component scan을 이용하는 방법이 있다.
@Configuration
@ComponentScan(basePackagesClasses = DemoApplication.class) //1.DemoApplication의 위치에서부터 ComponentScan을 해라
public class ApplicationConfig {
}
이 방법이 스프링부트와 가장 가까운 방법이다.
그런데 스프링부트에서는 @SpringBootApplication
을 붙이면 DemoApplication에서 ApplicationContext이 필요없고, ApplicationConfig.java도 필요없다.
@SpringBootApplication 안에는 @ComponentScan 과 @Configuration이 이미 붙어있기 때문에,
아래의 코드가 사실상 bean 설정파일이다.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
}
}
@Service
public class BookService {
BookRepository bookRepository;
@Autowired
public BookService(BookRepository bookRespository) {
this.bookRepository = bookRepository;
}
}
이 상태에서 어플리케이션을 실행하면 BookRepository 빈을 찾지 못해서 실행 실패.
아래와 같이 @Repository 혹은 @Component 어노테이션을 붙여줘야 한다.
@Repository
public class BookRepository {
}
@Service
public class BookService {
BookRepository bookRepository;
@Autowired
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
위의 생성자를 통한 의존성 주입 방식은 빈을 못찾아서 실패했지만,
setter의 경우 BookRepository 없이도 BookService의 인스턴스를 생성을 할 수 있다.
하지만 @Autowired 때문에 의존성을 주입하려고 시도하기 때문에 실패한다.
이 때는 @Autowired(required = false)
를 주면 의존성 주입을 선택적으로 할 수 있다.
그러면 BookService의 인스턴스는 만들어져서 빈으로 등록이 되고,
BookRepository는 의존성 주입이 안된 상태로 빈으로 등록이 된 것이다.
@Service
public class BookService {
@Autowired(required = false)
BookRepository bookRepository;
}
이 경우 생성자를 사용한 의존성 주입은 빈을 만들때에도 개입이 된다.
생성자로 전달받아야 하는 타입의 bean이 없으면 무조건 인스턴스를 만들지 못하고, BookService도 등록이 안된다.
반면, setter나 필드로 의존성을 주입할 때는 @Autowired(required = false)를 사용하여
BookService가 BookRepository의 의존성 없이도 bean으로 등록되도록 할 수 있다.
예를 들어 아래와 같이 2개의 동일한 타입의 Repository가 있을 때,
public interface BookRepository {
}
@Repository
public class MyBookRepository implements BookRepository {
}
@Repository
public class KeesunBookRepository implements BookRepository {
}
아래와 같이 bookRepository 의존성을 주입하려 하면, 주입을 못해준다.
2개 중에 어떤 것을 주입해야 하는지 모르기 때문에.
@Service
public class BookService {
@Autowired
BookRepository bookRepository;
}
이 경우, 아래와 같이 @Primary
와 @Qualifier
어노테이션으로 정해줄 수 있다.
@Primary
- 여러가지 중에 주로 사용할 것이다.@Qualifier
- bean의 id를 명시해준다. (@Service, @Repository 등의 애노테이션이 붙은 클래스의 bean id는 클래스명과 같고, 앞 글자만 소문자)
@Service
public class BookService {
@Autowired @Primary
BookRepository bookRepository;
}
@Service
public class BookService {
@Autowired @Qualifier("keesunBookRepository")
BookRepository bookRepository;
}
해당 타입의 빈을 모두 주입받을 수도 있다.
@Service
public class BookService {
@Autowired
List<BookRepository> bookRepositories;
}
디폴트는 싱글톤이다.
싱글톤은 어플리케이션 전반에 걸쳐서 해당 빈의 인스턴스가 오직 한개 뿐인 것.
때문에 아래의 어플리케이션 실행 결과 인스턴스의 레퍼런스가 같다.
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
Single single;
@Autowired
Proto proto;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(proto);
System.out.println(single.getProto());
}
}
@Component
public class Single {
@Autowired
private Proto proto;
public Proto getProto() {
return proto;
}
}
@Component
public class Proto {
}
또한 모든 싱글톤 스코프의 빈은 기본값이 applicationContext를 만들 때 만들어지게 되어있다.
그러므로 어플리케이션이 구동될 때 시간이 더 걸릴 수 있다.
대부분의 경우 싱글톤 스코프를 쓰지만, 그 외의 경우(Request, Session, WebSocket, ...)에는 prototype 스코프와 유사하다.
프로토타입은 매번 새로운 인스턴스를 만들어서 사용하는 것이다.
@Component @Scope("prototype")
public class Proto {
}
이제 Proto의 빈을 받아 올 때마다 새로운 인스턴스가 생성된다. (빈을 받아 올 때만)
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
ApplicationContext ctx;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(ctx.getBean(Proto.class));
System.out.println(ctx.getBean(Proto.class));
System.out.println(ctx.getBean(Proto.class));
System.out.println(ctx.getBean(Single.class));
System.out.println(ctx.getBean(Single.class));
System.out.println(ctx.getBean(Single.class));
}
}
위에서 Proto는 3번 모두 다른 인스턴스 레퍼런스가 찍힐 것이고,
Single은 3번 모두 같은 인스턴스 레퍼런스가 찍힐 것이다.
만약 아래 처럼 prototype bean이 singleton bean을 참조하면?
- 아무 문제없다.
- 프로토타입의 빈은 새롭지만 싱글톤의 빈은 항상 같으므로.
@Component @Scope("prototype")
public class Proto {
@Autowired
Single Single;
하지만 반대로 Singleton scope의 bean이 prototype scope의 bean을 참조하면?
- 프로토타입 빈이 업데이트가 안된다.
세 가지 방법이 있다.
- scope-proxy
- Object-Provider
- Provider (표준)
아래와 같이 proxyMode를 설정할 경우, 프로토타입 빈은 업데이트 된다.
(proxyMode의 디폴트 값은 proxy를 사용하지 않는 것이다.)
@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Proto {
}
proxyMode를 쓴다는 것은 클래스를 프록시로 감싸는 것이다.
왜 프록시로 감싸는 것인가? 싱글톤 인스턴스가 prototype 빈 인스턴스를 직접 참조하면 안되기 때문에.
프록시를 거쳐서 참조해야 한다.
왜 프록시를 거쳐서 참조해야 하는가?
직접 참조하면 이 prototype 빈 인스턴스를 매번 새로운 인스턴스로 바꿔줄 수 없기 때문에. 매 번 새로운 인스턴스로 바꿔줄 수 있는 프록시로 감싸도록 한다.
prototype 빈을 상속받은 클래스를 만들어서 프록시를 만들어주는 CG라이브러리 라는 third-party 라이브러리가 있다.
이 라이브러리는 클래스도 프록시를 만들어줄 수 있게 해준다.
원래 Java JDK 안에 있는 다이나믹 프록시는 인터페이스의 프록시 밖에 못만든다.
때문에 위의 코드에서 처럼 proxyMode = ScopedProxyMode.TARGET_CLASS
는
CG라이브러리를 이용해서 클래스를 상속받은 프록시를 만들라고 지시하는 것이다.
만약 인터페이스가 있었다면 proxyMode = ScopedProxyMode.INTERFACE
를 지시하여
JDK의 인터페이스 기반의 프록시를 만들어 사용
@Component
public class Single {
@Autowired
private ObjectProvider<Proto> proto;
public Proto getProto() {
return proto.getIfAvailable();
이 방법은 코드에 스프링 코드가 들어가기 때문에 선호하지 않는다.
프로파일은 bean들의 묶음이며, 환경이다.
각각의 환경에 따라 bean을 다르게 사용해야 할 경우, 또는 특정 환경에서만 어떠한 bean을 등록해야하는 경우에 사용한다.
프로파일은 ApplicationContext의 Environment를 사용한다. ApplicationContext가 bean factory의 역할만 하는 것은 아니다. ApllicationContext가 상속받고있는 EnvironmentCapable 인터페이스의 getEnvironment()를 사용하여 Environment를 가져올 수 있다. 그리고 environment.getActiveProfile()을 통해 현재 활성화된 프로파일을 가져올 수 있다.
@Component
public class AppRunner implements ApllicationRunner {
@Autowired
ApplicationContext ctx;
@Override
public void run(ApplicationArguments args) throws Exception {
Environment environment = ctx.getEnvironment();
System.out.println(Arrays.toString(environment.getActiveProfiles());
System.out.println(Arrays.toString(environment.getDefaultProfiles()); // Default
}
}
아래의 예시는 테스트 프로파일일 때만 사용되는 빈 설정 파일이다.
test 라는 프로파일로 어플리케이션 실행하기 전까지는 아래의 설정이 적용이 안된다.
@Configuration
@Profile("test")
public class TestConfiguration {
@Bean
public BookRepository bookRepository() {
return new TestBookRepository();
}
}
@Repository
@Profile("test")
public class TestBookRepository implements BookRepository {
}
@Repository
@Profile("!Prod") //Prod 프로파일이 아닌 프로파일
public class TestBookRepository implements BookRepository {
}
@Repository
@Profile("!Prod & test") //Prod 프로파일이 아니면서 test인 프로파일
public class TestBookRepository implements BookRepository {
}
- 프로퍼티는 다양한 방법으로 정의할 수 있는 설정값
- Environment의 역할은 프로퍼티 소스 설정 및 프로퍼티 값 가져오기
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
ApplicationContext ctx;
@Autowired
BookRepository bookRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
Environment environment = ctx.getEnvironment();
System.out.println(environment.getProperty("app.name")); // VM options에 -Dapp.name=spring5 명령
System.out.println(environment.getProperty("app.about")); //app.properties에 정의된 app.about
}
}
@SpringBootApplication
@PropertySource("classpath:/app.properties") //Environment를 통해 프로퍼티를 추가하는 방법
public class Demospring51Application {
public static void main(String[] args) {
SpringApplication.run(Demospring51Application.class, args);
}
}
- StandardServletEnvironment의 우선순위
- ServletConfig 매개변수
- ServletContext 매개변수
- JNDI (java:comp/env/)
- JVM 시스템 프로퍼티 (-Dkey="value")
- JVM 시스템 환경 변수 (운영 체제 환경 변수)
org.springframework.core.io.Resource
- java.net.URL을 추상화 한 것.
- 스프링 내부에서 많이 사용하는 인터페이스
- 클래스패스 기준으로 리소스 읽어오는 기능 부재
- ServletContext를 기준으로 상대 경로로 읽어오는 기능 부재
- 새로운 핸들러를 등록하여 특별한 URL 접미사를 만들어 사용할 수는 있지만 구현이 복잡하고 편의성 메소드가 부족하다.
- Resource의 타입은 location 문자열과 ApplicationContext의 타입에 따라 결정된다.
- ClassPathXmlApplicationContext -> ClassPathResource
- FileSystemXmlApplicationContext -> FileSystemResource
- WebApplicationContext -> ServletContextResource
- ApplicationContext의 타입에 상관없이 리소스 타입을 강제하려면 java.net.URL 접두어(+classpath:) 중 하나를 사용할 수 있다.
- classpath:me/whiteship/config.xml -> ClassPathResource
- file:///some/resource/path/config.xml -> FileSystemResource
- 객체 그래프를 조회하고 조작하는 기능 제공
- Unified EL과 비슷하지만, 메소드 호출을 지원하며, 문자열 템플릿 기능도 제공
- OGNL, MVEL, JBOss EL 등 자바에서 사용할 수 있는 여러 EL이 있지만, SpEL은 모든 스프링 프로젝트 전반에 걸쳐 사용할 EL로 만들었다.
- 스프링 3.0부터 지원
- #{'표현식'}
- ${프로퍼티}
- 표현식은 프로퍼티를 포함할 수 있지만, 프로퍼티는 표현식을 포함할 수 없다.
- #{${my.data} + 1}
@Component
public class AppRunner implements ApplicationRunner {
@Value("#{1 + 1}")
int value;
@Value("#{'hello ' + 'world'}")
String greeting
@Value("#{1 eq 1}")
boolean trueOrFalse;
@Value("hello")
String hello;
@Value("${my.value}")
int myValue;
@Value("#{${my.value} eq 100}")
boolean isMyValue100;
// bean 참조하기 (sample 이라는 bean의 data)
@Value("#{sample.data}")
int sampleData;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(value);
System.out.println(greeting);
System.out.println(trueOrFalse);
System.out.println(hello);
System.out.println(myValue);
System.out.println(isMyValue100);
Spring AOP는 AOP의 구현체 제공
Java의 AOP구현체인 AspectJ와 연동해서 사용할 수 있는 기능 제공
AOP는 OOP를 보완하는 수단으로, 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법
- Aspect : 모듈
- Target : 적용되는 대상
- Advice : 해야할 일들
- Join point : 모듈이 끼어드는 지점
- Pointcut : 어디에 적용해야 하는지
- AOP 구현체
- Java에서는 AspectJ와 Spring AOP
- 컴파일 시점 (AspectJ)
- 로드 타임 시점 (AspectJ)
- 런타임 시점 (Spring AOP)
- 프록시 기반의 AOP 구현체
- 스프링 bean에만 AOP를 적용할 수 있다.
- 모든 AOP 기능을 제공하는 것이 목적이 아니라, 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제에 대한 해결책을 제공하는 것이 목적
- 기존 코드 변경없이 접근 제어 또는 부가 기능 추가
- 매 번 프록시 클래스를 작성해야 하는가?
- 여러 클래스, 여러 메소드에 적용하려면?
- 객체들 관계도 복잡하고..
- 스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 여러 복잡한 문제 해결
- 동적 프록시: 동적으로 프록시 객체를 생성하는 방법
- 자바가 제공하는 방법은 인터페이스 기반 프록시 생성
- CGlib은 클래스 기반 프록시도 지원
- 스프링 IoC: 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록 시켜준다.
- 클라이언트 코드 변경 없음
- AbstractAutoProxyCreator
@Component
@Aspect
public class PerfAspect {
@Around("@annotation(PerfLogging)") //@PerfLogging 애노테이션이 붙은 곳에 적용
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
log begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
@Before("bean(simpleEventService)") //bean의 모든 메서드에 앞서(@Before) 적용
public void hello() {
System.out.println("hello");
}
}
@Documented //javaDoc 만들때 사용
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS) //이 어노테이션을 .class 파일까지도 유지
public @interface PerfLogging {
}