이렇게 하면 정해놓은 패키지로부터 ‘@Component’어노테이션을 찾아서 빈으로 등록하게 된다. 이 ‘@Component’를 확장한 어노테이션이 몇개 있는데 이것들이 스프링MVC에서 그토록 쓰고 있는 ‘@Controller’,‘@Service’, ‘@Repository’등이다.
자 이제 어노테이션을 이용한 등록을 해본다. 스프링 2.5부터 가능했던 내용이다. 일단 BookService에는 ‘@Service’, BookRepository에는 ‘@BookRepository’를 붙인다. 이러면 저 xml을 읽고 콤포넌트 스캔 태그에서 ‘me.rkaehdaos’하위의 두 빈을 등록한다. 하지만 아직 DI가 되지 않았다. DI를 시키려면 ‘@Autowired’ 어노테이션(혹은 @Inject)등을 사용해서 등록한다. 다시 실행해보면 정상적으로 작동하는 것을 볼 수 있다.
그다음에 나오는 것이 자바 설정 파일이다. 스프링 3.0부터 지원하기 시작했다. 빈 설정을 xml이 아닌 자바 코드에서 할수 없을까 해서 나온 기능이다. 굉장히 유연한 빈 설정 기능을 제공하며 아마 이 기능이 나오지 않았더라면 스프링부트는 절대 지금처럼 나오지 못했을 것이다.
이것도 테스트 해보자. 일단 앞에서 붙였던 어노테이션은 전부 제거한다. 특정 클래스를 만들고 (보통은 이 클래스 이름도 ~~~Config등으로 끝나게 만든다) 거기에 ‘@Configuration’을 붙여주면 그 클래스는 설정 클래스로 인식된다. 그리고 이 설정 클래스 내에서 빈을 등록하게 된다.
@Configuration 을 붙인 자바 설정 클래스
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@Configuration publicclassApplicationConfig { @Bean public BookRepository bookRepository() { returnnewBookRepository(); } @Bean public BookService bookService() { BookServicebookService=newBookService(); bookService.setBookRepository(bookRepository()); // 직접 의존성 주입 return bookService; } }
메소드 이름이 빈의 id가 된다.
메소드 반환 타입이 빈의 타입이 된다.
메소드 안의 리턴 되는 객체가 실제 빈이 될 객체
위에서 직접 의존성 주입하는 방법은 setter를 이용한 방법이며 메소드 파라미터로 주입 받는 것도 가능하다. (ex. public BookService bookService(BookRepository bookRepository) {})
아니면 설정파일에서 주입 정의를 하지 말고 bookService의 멤버 bookRepository에게 ‘@Autowired’를 사용해서도 가능하다.[ 생성자 주입으로는 불가능]
자 예전 책에서나 볼 수 있던 소스에서 점점 요새 코드와 비슷해지고 있다. 이 부분도 문제라고 하면 xml 처럼 일일히 빈을 등록해야한다는 부분이다. 이 자바설정에서도 아까 xml때의 ‘컴포넌트 스캔’을 할수 없을까?
콤포넌트 스캔을 하는 자바 설정 클래스
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@Configuration @ComponentScan(basePackages = "me.rkaehdaos.applicationcontest")//문자열 입력, type safety 문제 여지 있음 @ComponentScan(basePackageClasses =ApplicationcontestApplication.class)//지정 클래스 위치에서부터 하위스캔 publicclassApplicationConfig {
// 콤포넌트스캔을 할것이니 이전 빈 설정은 필요 없음 /* @Bean public BookRepository bookRepository() { return new BookRepository();} @Bean public BookService bookService() { BookService bookService = new BookService(); bookService.setBookRepository(bookRepository()); return bookService; } */ }
‘@ComponentScan’에는 basePackages와 basePackageClasses가 올수 있는데 전자는 문자열로 (패키지가 유니크 한경우 자동완성도 쉽게 지원하나) 타입 세이프트 문제가 있다.
후자는 지정한 클래스 위치로부터 하위 스캔한다.
이 방법이 지금 쓰고 있는 스프링부트와 가장 가까운 방법이라고 봐도 좋을 것이다. 사실 yml을 사용하거나 4.0부터 나온 groovy DSL을 사용해서 더 느슨하고 유연한 설정을 추구하는 방법도 있는데 스프링 부트가 나온이상 사실 이방법에서 정착해도 된다.
스프링 부트를 사용하면 사실 어플리케이션 컨텍스트 자체를 직접 만들어 쓰지 않는다. 스프링 부트 에서 사용하는 @SpringBootApplication은 이미 컴포넌트 스캔도 붙어 있고 ‘@Configration’도 포함하고 있기때문에 사실상 Config 클래스를 새로 만들 필요없이 main메소드를 포함하는 클래스가 곧 자바 설정 클래스가 된다.
핸들러는 과거나 지금이나 빈이어야 한다. (그래야 스프링 프레임워크가 특정 이벤트를 누구한테 전달할지 알 수 있다)
특정 인터페이스 구현이 아니기에 메소드 이름도 마음대로 바꿀 수 있다.
결과는 동일하다.
좀더 자세히 알고 싶으면 API Document를 살펴본다.
특정 이벤트에 대하여 핸들러가 2개 이상이 있다면?
모두 실행된다.
순차적으로 실행된다.
동시에 다른 쓰레드 실행이 아니다.
각 이벤트에서 Thread.currentThread().toString()을 찍어보면 동일하다.
즉 같은 쓰레드에서 순차적으로 실행됨을 알 수 있다.
기본적으로 어떤 핸들러가 먼저 실행될 지는 알수 없다. 다만 순서를 정할 수 있다.
@Order어노테이션을 사용한다.(스프링의 여러 곳에서 많이 쓰이는 어노테이션)
예시
숫자등으로 부여 가능하다. 높은 숫자가 우선 순위
가장 높은 우선순위 : @Order(Ordered.HIGHEST_PRECEDENCE)
숫자를 더하면 더 올라간다: @Order(Ordered.HIGHEST_PRECEDENCE+2)
비동기 동기
기본적으로는 동기(Synchronized)
비동기로 실행하고 싶다면 핸들 메소드에 @Async와 함께 사용한다.
@Async만 붙인다고 비동기로 실행되는 것이 아니라 비동기를 On해야 한다.
@EnableAsync를 하면 이제 비동기가 On되서 비동기로 작동된다.
원래는 쓰레드풀 관련 설정을 더 해야한다. 나중에 Sync에 대해서 더 공부.
비동기인경우 각각의 쓰레드 풀에서 따로 돌고 그 쓰레드 스케쥴링에 달려 있기 때문에 순서는 당연히 보장이 안되며 @Order도 더이상 의미가 없어진다.
스프링이 제공하는 기본 이벤트
ContextRefreshedEvent: ApplicationContext초기화,refresh할 때 발생.
ContextStartedEvent: ApplicationContext를 start()하여 라이프사이클 빈들이 시작 신호를 받은 시점에 발생.
ContextStoppedEvent: ApplicationContext를 stop()하여 라이프사이클 빈들이 정지 신호를 받은 시점에 발생.
ContextClosedEvent: ApplicationContext를 close()하여 싱글톤 빈 소멸 시점에 발생
RequestHandledEvent: HTTP 요청을 처리했을때 발생
이 이벤트들은 ApplicationContext 관한 이벤트이므로 event.getApplicationContext()등으로 ApplicationContext를 꺼낼 수도 있다.
스프링 부트에서는 이런 이벤트들이 더 확장되어서 제공된다.
ResourceLoader
역시 ApplicationContext가 구현하는 인터페이스중 하나로 리소스를 읽어오는 기능을 제공하는 인터페이스다. 따라서 ResourceLoader역할도 할 수 있다.
앞서의 인터페이스와 마찬가지로 ApplicationContext로 @Autowired 받을 수 있지만 ResourceLoader로 받는게 직관적
getResource(“[문자열]”) 이거 하나분인데 안의 문자열이..복잡하다면 복잡.
러너에서 실행해보는 예시
1 2 3 4 5 6 7 8
@Override publicvoidrun(ApplicationArguments args)throws Exception { Resourceresource= resourceLoader.getResource("classpath:sample.txt"); System.out.println(resource.exists()); //해당 파일 존재 여부 출력 System.out.println(resource.getDescription()); //설명 출력 //readString은 자바11부터 추가 되었다. System.out.println(Files.readString(Path.of(resource.getURI()))); }
ApplicationContext 정리
지금까지 ApplicationContext의 인터페이스들을 살펴보았음.
ApplicationContext은 단순 빈 팩토리기능뿐 아니라 리소스로더, 이벤트 퍼플리셔, 환경등 여러 기능을 가지고 있다.
추상화
스프링 레퍼런스의 많은 양을 차지한다. 수많은 추상화가 있다.
Resource 추상화
특징
java.net.URL을 추상화한 것
스프링 내부에서 많이 사용 ex)xml기반 빈설정시 ClassPathXmlApplicationContext(“URL”)에서 이미 사용.
추상화 이유
클래스패스 기준으로 리소스 읽어오는 기능이 없다.
ServletContext를 기준으로 상대 경로로 읽어오는 기능 부재
새로운 핸들러를 등록하여 특별한 URL접미사를 만들어 사용할 수 있지만 구현이 어렵고 편의성 메소드가 부족하다.
예를들어 WebApplicationContext의 경우 ServletContextResource가 쓰이고 이는 웹 어플리케이션루트, 컨텍스트패스부터 찾게 되는데 스프링 부트의 임베디드 톰캣은 컨텍스트 패스가 기본값이 설정되어 있지 않으므로 리소스를 찾을수 없게 된다. 이때 명시적으로 classpath접두사를 사용하면 ClassPathResource가 사용되기 때문에 위의 문제가 사라진다.
Validation 추상화
Valididation: 어플리케이션에서 사용하는 객체 검증용 인터페이스 –> org.springframework.validation.Validator
@Override publicvoidvalidate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "empty","empty title is not allowed"); } }
테스트를 하면 에러 발생 -> 2018-12-23 12:14:25.997 WARN 13200 — [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type ‘java.lang.String’ to required type ‘me.rkaehdaos.demo1.EventDomain’; nested exception is java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘me.rkaehdaos.demo1.EventDomain’: no matching editors or conversion strategy found]
설명 그대로 입력값으로 들어온 숫자(String타입)을 이벤트 타입으로 변환을 못한다.
이에 매칭되는 에디터나 컨버전 전략을 찾지 못했다고 나왔다.
변환하기 위한 PropertyEditor가 필요하다.
PropertyEditor 구현방법
java.beans.PropertyEditor를 implement 한다. * 구현해야할 메소드가 너무 많다.
PropertyEditorSupport를 상속한다. * 구현할 메소드만 구현하면 된다. 보통 getAsText, setAsText를 구현한다.
ExpressionParserparser=newSpelExpressionParser(); Expressionexpression= parser.parseExpression("1+1"); Integervalue2= expression.getValue(Integer.class); System.out.println(value2); ``` * 이 Spel도 위에서 봤던 ConversionService를 사용해서 해당 타입으로 변환한 것이다.
## 스프링 AOP
### 1.기본 전체적으로 * AOP 구현체 제공 * 자바의 AspectJ와 연계할 수 있는 기능도 제공 * 이를 바탕으로 스프링 트랜잭션이나 캐시등의 다른 스프링 기능도 작동 됨 * AOP * Aspect를 모듈화 할 수 있는 프로그래밍 기법 * OOP를 보완하는 수단. * 흩어진 관심사(Concern)들을 모을 수 있다. * 트랜잭션처리, 로깅등의 횡단 관심사 * 용어들 * 많이 어려운 것은 아닌데 안쓰면 햇갈릴 때도 있음 * Aspect : 흩어진 관심사를 모은 하나의 모듈로 Advice와 Pointcut이 들어간다. * Advice : 관심사 작동 하는 부분, 해야할 일 * Target: : AOP가 적용될 객체 * JoinPoint : 합류 지점 * 기본 시점 : 메소드 실행 시점 * 다양한 조인 포인트가 있음 * PointCut : Advice가 적용 될 부분으로 Join포인트중 하나
* AOP 적용 방법 * 컴파일 : 자바파일을 .class로 만들때 바이트코드를 조작해서 .class를 생성 * 로드 타입 : .class를 로딩시 로드타임 위빙을 해서 로딩. 메모리에 해당 기능 생성 * 런타임 : 빈을 만들때(런타임) 프록시 빈을 만든다.
* AOP 구현체 * 많다. 위키피디아를 보면 각 언어별 구현체까지 확인 가능하다. * AspectJ * 엄청나게 많은 조인포인트와 엄청나게 많은 기능을 제공 * 스프링 AOP * 국한적 기능 * **프록시 기반의 AOP** 구현체 * **스프링 빈에만 AOP를 적용** 할 수 있다. * 스프링AOP의 런타임 위빙이 가장 현실적인 방법이다. * AspectJ긔 많은 조인포인트가 필요한 경우 별도의 컴파일러로 컴파일하거나 별도의 자바 에이전트를 설정해서 로드타임 위빙을 사용하는 방법으로 연동한다.
### 프록시기반 AOP * 스프링 AOP는 **프록시 기반의 AOP** 구현체 * **스프링 빈에만 AOP를 적용** 할 수 있다. * 모든 AOP기능을 제공하는 것이 목적이 아니라, 스프링IoC와 연동하여 엔터프라이즈 어플리케이션에서 가장 흔한 문제에 대한 해결책을 제공하는 것이 목적.
* 프록시 패턴 * 디자인 패턴 * 클라이언트는 인터페이스를 바라본다. * 프록시객체는 해당 인터페이스를 구현하며 실제 객체를 바라본다. * 목적: 접근 제어 또는 부가 기능 추가 * 클라이언트 코드와 기본 실제 객체의 코드 변경없이 목적 달성 가능
* 문제점 * 매번 프록시 클래스를 작성해야 하는가? * 여러 클래스 여러 메소드에 적용하려면? * 객체들 관계가 매우 복잡할 때는?
* 스프링 AOP의 해결방법 * 스프링 Ioc컨테이너가 제공하는 기반 시설과 다이나믹 프록시를 혼합해서 복잡한 문제를 심플하게 해결하려 함 * 다이나믹 프록시 * 동적으로(런타임에) 프록시 객체 생성하는 방법 * 자바가 제공하는 방법은 인터페이스 기반 프록시 생성 * CGlib는 클래스 기반 프록시도 지원 * 스프링 IoC : 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록 시켜 준다. * 클라이언트 코드 변경 없음 * AbstractAutoProxyCreator implement BeanPostProcessor * BeanPostProcessor: 빈이 등록후 빈을 가공할 수 있는 라이프 사이클 * 빈이 등록이 되면 AbstractAutoProxyCreator라는 빈 포스트 프로세서로 해당 빈을 감싸는 프록시 빈을 생성후 그 빈을 등록까지 해줌.
### @AOP * 어노테이션 기반의 스프링 @AOP * 보통 start-web에 왠만한건 다 있었지만 AOP는 별도의 의존성 추가가 필요