이 글에서는 IoC container에게 bean을 만들어달라고 하는 방법이 뭐가 있는지 알아볼거다.
xml을 사용하거나 @Bean
을 bean 생성하는 method에 부착하는 방법이 있는 것은 이전 글들을 읽어봤으면 대충 눈치 챘을 것이다.
그리고 마찬가지로 이전 글들을 봤다면 @ComponentScan
을 이용해서 bean을 탐지하는 방법도 알고 있을 것이다. 몇 번 언급했기 때문. 정확히는 @ComponentScan
에서 @Component
를 탐지해가지고 그것들을 bean으로 만드는 것이다. 이 방법에 대해 좀 더 자세히 알아보자.
@ComponentScan
은 패키지에 있는 @Component
를 스캔해가지고 그것들을 bean으로 만들어달라는 annotation이다. 이 때 패키지를 지정하는 방식이 2가지가 있다.
먼저 밑처럼 패키지 이름을 그대로 사용하는 것이다.
@Configuration
@ComponentScan(basePackages = "com.baeldung.annotations")
class VehicleFactoryConfig {}
@Configuration
@ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
class VehicleFactoryConfig {}
사실 두 방식 다 이전에 소개했다. 그리고 후자가 미묘하게 더 좋을 수 있다고도 말했었다.
그리고 예시는 하나의 패키지에 대해서 했지만 여러개의 패키지에 대해서 하는 것도 가능하다. 이 때는 배열로 전달을 하면 된다.
@ComponentScan({"com.my.package.first","com.my.package.second"})
...
또 아예 주어지지 않은 경우 annotate된 class가 속한 패키지에 대해서 스캔을 수행한다.
또 JAva 8의 Repeatable Annotations를 활용하는게 가능해서 밑과 같은 코드도 유효하다.
@Configuration
@ComponentScan(basePackages = "com.baeldung.annotations")
@ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
class VehicleFactoryConfig {}
@Configuration
@ComponentScans({
@ComponentScan(basePackages = "com.baeldung.annotations"),
@ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
})
class VehicleFactoryConfig {}
<context:component-scan base-package="com.baeldung" />
@Component
class CarUtility {
// ...
}
value
라는 argument로 지정하는 것도 가능하다.@Component(value="engine")
class CarUtility {
// ...
}
@Component
를 포함하는 annotation들에 해당된다. 즉 밑의 annotation들도 이 기능은 전부 지원한다. 그럼 해당 annotation들은 무슨 추가 기능이 있는걸까?DAO Pattern이라는 것이 있다. Java에서 시작한 개념으로 Persistence Layer과 application/business layer의 분리를 위해 사용하는 패턴이다.
이렇게 말하면 어렵지만 사실 간단하다. persistence는 영속이란 뜻을 가지고 있는데, 저 layer은 데이터 영구 보관층, 즉 DB를 의미한다. 그리모 application/business layer은 애플리케이션/비즈니스 로직을 의미, 즉 애플리케이션 자체를 말하는 것이다.
세상에 많은 DBMS가 있고 각각이 데이터를 관리하는 방식이 다 다르다. 이들이 전부 CRUD operation을 지원하긴 하지만 그 방식이 각각 다르다는 것이다. persistence layer측을 언제 바꿀지 모르기 때문에 어떤 DBMS에 대해 이를 고정시키는 것은 좋은 습관이 아니고, 그래서 등장한게 이 DAO다.
DAO 자체가 API로, 하위 DBMS가 뭔지 정확히 모르도록 하면서 business 측에서 CRUD operation을 수행할 수 있도록 해주는 것이다. 그리고 Spring의 @Repository
는 이게 DAO라는 것을 지칭하는거다. 참고로 DAO라고 지칭하는거지 우리가 DAO를 구현해야하는 것은 여전히 마찬가지다(...)
@Repository
class VehicleRepository {
// ...
}
@Component
쓰면 되지 않냐고 할 수 있다. 그런데 추가 기능이 있는데, 바로 저 DAO 안에서 DBMS 건드리다가 그쪽에서 exception이 발생하면 spring의 DataAccessException
의 subclass로 자동으로 번역해주는 기능이다...! 이 경우 PersistenceExceptionTranslationPostProcessor
bean을 따로 만들어줘야 한다.@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
왜냐하면 사실 저 번역 기능을 해주는게 저 class의 bean이기 때문이다. 다만 Spring에서 보통 알아서 셋업해준다.
xml로 저 셋업을 할거면 이렇게 하면 된다.
<bean class=
"org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
@Service
public class VehicleService {
// ...
}
사실 저게 단순 요소가 아니라 서비스 관련 요소구나 라는 가독성을 개선하는것 말고 하는 것은 없다.
그래도 Spring이 저걸 만든 이유는 앞의 @Repository
처럼 추가 기능을 서비스 관련 component에 대해서만 필요로 할 수도 있기 때문.
@Controller
public class VehicleController {
// ...
}
@Configuration
을 가진 class에 @Bean
, 즉 bean을 생성하는 method임을 알리는데 사용하는 annotation 사용이 가능하다.@Configuration
class VehicleFactoryConfig {
@Bean
Engine engine() {
return new Engine();
}
}
@Bean
과 @Configuration
의 관계그런데 사실 @Bean
은 @Configuration
이 없어도 사용이 가능하긴 하다. 하? 정확히는 @Component
가 달려있는 class에서는 @Bean
이 달려있는 method 생성이 가능하다. @Configuration
이 없는 class에서 @Bean
을 사용하는 것을 lite @Bean
mode라고 한다.
이러면 알겠지만 사실 @Configuration
은 단순히 bean을 정의하는 method가 존재하는 곳임을 지칭하는 것이 아니다. 본인이 @Component
가 포함되어 있어서 @Bean
을 사용할 수 있게 하는 것도 맞지만, 그거랑 더불어 inter-bean dependency를 정의하는것도 가능하게 해준다는게 특징이다.
inter-bean dependency랑 어떤 @Bean
method가 같은 class의 @Bean
method를 활용해가지고 dependency 관계를 가지는 것을 말한다. 즉 한 class 안에서 의존관계가 형성되는 것을 말한다. 의존 관계가 형성이 되려면 동일한 instance에 대해서 공유가 되어야 한다는 것이고, 이는 곧 singleton scope의 bean이 형성된다는 것을 의미한다. 밑은 예시 코드. 이 경우 singleton scope를 가지는 bean을 FirstService
, SecondService
가 가지기에 SharedDependency
bean은 1개만 존재한다. 그리고 first랑 second가 shared를 기준으로 의존관계를 형성하게 된다.
@Configuration
public class SomeConfiguration {
@Bean
public SharedDependency sharedDependency() {
return new SharedDependency();
}
@Bean
public FirstService firstService() {
return new FirstService(sharedDependency());
}
@Bean
public SecondService secondService() {
return new SecondService(sharedDependency());
}
}
참고로 여기는 하나의 @Configuration
class에 대해서 수행했지만 타 @Configuration
class에 있는 method를 호출하는 것도 가능하다.
반대로 lite bean의 경우, @Bean
method를 호출할 때마다 동일한 bean이 나오는게 아니라 새로운 bean이 나오게 된다. 이러면 호출할때마다 다른 bean이 나오기에 bean간의 inter-dependency를 형성하는 것이 불가능하다.
그러면 둘이 무슨 용도 차이가 있는것인가? @Configuration
은 환경 설정을 의미한다. 이때 여기서 말하는 환경 설정의 범위는 application 차원에서의 환경 설정인데 그러면 applicaiton 전체에 대해서 여러개의 instance가 존재하면 안되고 하나만 존재해야 하며, 타 method에서 이 configuration과 관련된 bean을 접근하려 할 때 새로 bean을 생성해서 전달하는게 아니라 관련 proxy를 제공해주고 그걸 접근하는 것이 합리적인 디자인이다. (정확히는 CGLIB proxy를 활용하는데 자세한건 넘어가겠다.) 그래서 @Bean
에서 만드는 bean이 singleton scope인 것이다.
그러나 위의 CGLIB 활용 proxy를 설정하는 작업 자체가 application startup time에 영향을 줄 수 있다. 또 CGLIB subclassing을 적용하는 것 자체가 class 디자인에 제약을 주는 경우가 있다. 그러면서 특정 모듈에 다른 모듈의 기능을 넣는 역할을 제공하고 싶거나 모두가 사용할 개별적인 유틸리티를 제공하고 싶은 경우에 lite bean을 사용하면 오버헤드를 최소화하면서 관련 기능 구현이 가능하다. 이럴 때 단순 bean factory 용도로 활용이 된다.
다만 lite bean이 저 의도한 데로'만'사용되고 있는지 아닌지 관리하는거 자체가 매우 피곤하다. 그리고 lite bean으로 구현하려는 용도가 보통 프로그래밍 디자인 차원에서 선호되지 않는 경우거나 다른 안전한 방법이 있는 경우가 많다. 그래서 보통 선호되지 않는다.
lite bean의 예시로는 밑이 있다. 이 경우 SharedDependency
bean이 1개가 아니라 3개가 생성된다.
@Service
public class SomeConfiguration {
@Bean
public SharedDependency sharedDependency() {
return new SharedDependency();
}
@Bean
public FirstService firstService() {
return new FirstService(sharedDependency());
}
@Bean
public SecondService secondService() {
return new SecondService(sharedDependency());
}
}
앞에서 annotation들을 저렇게 구별한 이유가 가독성뿐만이 아니라 추후 각 annotation에 대해서 다른 기능을 지원하기 위해서라고... 내가 적었고 사실 Spring이 그렇게 문서화했다.
이 annotation별 기능을 pointcut이라고 한다. @Pointcut
annotation을 사용하기 때문... 그런데 가만히 생각해보면 우리가 그 pointcut를 만들면 안되나라는 생각을 할 수 있다. 그리고 가능하다.
예를들어 DAO layer의 method들의 실행시간을 측정하고 싶으면 밑과 같이 코드를 짜면 된다.
@Aspect
@Component
public class PerformanceAspect {
@Pointcut("within(@org.springframework.stereotype.Repository *)")
public void repositoryClassMethods() {};
@Around("repositoryClassMethods()")
public Object measureMethodExecutionTime(ProceedingJoinPoint joinPoint)
throws Throwable {
long start = System.nanoTime();
Object returnValue = joinPoint.proceed();
long end = System.nanoTime();
String methodName = joinPoint.getSignature().getName();
System.out.println(
"Execution of " + methodName + " took " +
TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
return returnValue;
}
}
@Aspect
, Aspect Oriented Programming자 갑자기 많이 복잡해지는데 하나하나 알아보도록 하자. 먼저 저 annotation에 대해 얘기하려면 AOP, 즉 Aspect Oriented Programming에 대해 얘기를 안할 수가 없다. AOP는 프로그램 패러다임 중 하나다. (다른 프로그램 패러다임으로 OOP가 있다.)
저게 뭔지 위키백과에 검색해보면 이런 소리를 한다.
In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.
In aspect-oriented software development, cross-cutting concerns are aspects of a program that affect several modules, without the possibility of being encapsulated in any of them.
영어가 힘들텐데 사실 별거 아니다. cross-cutting concern은 비즈니스 로직에 해당되지 않는데 여러 모듈에서 활용하는 중복된 코드를 지칭하는 것이다. 이는 OOP에서 잘 없애지 못하는 개념이고 AOP에서 잘 없애는 개념이다. 왜냐면 AOP가 애초에 저거 담당 패러다임이기 때문. 애초에 저 concern을 AOP에서의 aspect라고 정해버렸다.
자 여기서 잠깐 생각해보자. Spring Application에서 Aspect에 해당할 법한 요소는 뭐가 있을까? 몇가지 생각해보면 성능 측정, 보안, 로그 등이 있다. 이들은 모듈마다 다 필요로 할법한 기능이지만 모듈에 해당하는 기능이라고 보기에는 매우 애매하다.
위의 코드가 무엇인가? 실행시간 측정이라고 했다. 어? Aspect네? 그래서 이걸 갑자기 여기서 사용하고 있는 것이다. 실제로 AOP의 예제로 많이 사용된다.
알겠는데 그래서 대체 어떻게 AOP 형식으로 프로그래밍을 하는건가? Spring에서 AOP 형식으로 프로그래밍하는걸 Spring AOP라고 하며 documentation이 아예 따로 있다. AspectJ를 지원한다.
저걸 다 확인하긴 좀 그러고 간략하게 설명할건데 이를 알려면 proxy pattern이라는 것을 알아야 한다. 뭔가 비슷한 개념을 이전에 봤는데 뭐 저거랑 같은건 아니지만 프록시라는 개념을 그때 이해했으면 됬다. 앞에 소개한 글에서 proxy pattern을 잘 소개했다.
그 글을 보면 알겠지만 proxy pattern은 이미 인터페이스를 implement한 형태로 존재하는 class에 추가 기능을 넣거나 접근 제어를 하고 싶을 때 사용하는 pattern이다. 감싸고 있는 녀석이 프록시. 하지만 이것도 그 글에서 언급한 것처럼 중복 코드가 많은건 여전하다. 즉 이거 자체가 솔루션은 아니고, 이걸 활용한 Spring AOP가 솔루션이다. 이것도 앞에 소개한 글을 참고하면 좋다.
좋다, 그러면 proxy pattern으로 Spring AOP가 런타임 때 bean을 감싸고 거기에 추가기능을 넣고 있으니, 저 성능 측정같은 aspect를 proxy 형성때 감싸도록 지정하면...되겠는데 어떻게 하는가.
뭐 그건 계속 배워나갈건데 일단 aspect에 해당하는 method (및 이것저것)을 포함한 class를 지정해야 하고 거기에 @Aspect
가 쓰인다.
@Pointcut
@Pointcut("within(@org.springframework.stereotype.Repository *)")
public void repositoryClassMethods() {};
@Pointcut
은 어떤 코드의 어떤 시점에 우리가 끼어들고 싶을 때 그 위치를 지칭하는데 사용되며 보통 어떤 method가 실행되는 시점을 나타내는데 사용된다. 이때 정확한 위치를 pointcut expression이라는 녀석으로 지정해야 한다. 여기서 AspectJ의 pointcut designator(PCD)이 등장하고... 뭐 요상한 AsepctJ 문법이 적용되는데 여기서 집중적으로 다루진 않겠다.
괄호 전의 단어가 PCD에 해당하는데 보통 어떤 method 실행을 나타낸다는 뜻의 execution
을 쓰지만 여기서는 within
을 썼다. 이건 무슨 뜻이냐면 AspectJ expression이 포괄하는 영역의 모든 method execution에 대해서 join하라는 뜻. 그래서 코드의 경우에는 @Repository
가 annotate된 class의 모든 method의 실행 시점을 지칭하는 pointcut을 만들기 위해 위처럼 했다.
이 때 @Pointcut
밑에 있는 그냥 일반 코드를 pointcut signature이라고 한다. 아무 implementation이 되지 않은 method인데... 이 method의 이름이 이 @Pointcut
이 가리키는 위치를 대표하는 이름이다.
@Around
and advice그리고 앞의 이름을 여러 annotation이 활용하는데 그 중 하나가 @Around
다. 보면 repositoryClassMethods()
를 보유하고 있는데 이 pointcut에 대해서 '주변에' 무슨 코드를 실행한다는 것을 의미할 때 쓰인다.
그 코드는 밑에 정의되어 있는데 여기서 ProceedingJoinPoint
class에 해당하는 joinPoint
라는 parameter이 있는걸 볼 수 있다. @Around
에서만 쓰이는 녀석인데 저게 @Pointcut
에 의해 가리켜진 시점이다. 그 시점 전에 이제 무슨 코드를 수행하고, joinPoint.proceed()
로 코드 진행 계속 하고, 다시 그 후에 코드를 실행하고 종료하면 어떤 method의 주변에 무슨 코드를 수행하는 것이 가능해지죠.
참고로 다른 advice들도 joinPoint parameter을 가지는게 가능한데 그 녀석들의 class는
JoinPoint
다. 이 둘의 차이점은 이 글을 참고하자.
그래서 @Around
라는 annotation 이름을 가지는 것이다.
참고로 joinPoint.getSignature().getName()
도 하고 있는데 getSignature
은 말 그대로 Signature
을 반환하며, 거기는 또 getName
을 호출하면 join point 관련 method 이름이 나오게 된다. 그러니까 성능 측정한 method 이름이 뭔지 알아내려고 저러고 있는 것이다.
이 @Around
처럼 어떤 pointcut을 기준으로 전이나 후나 주변이나 막 실행하는, 그러니까 aspect에 해당하는 것을 Spring에서는 advice라고 부른다. 참고. 더 다양한 advice들에 대해 알고 싶으면 이 글 참고
여튼, 이 AOP 관련 프로그래밍 문법을 활용하면 특정 @Component
확장 annotation을 가진 class들에 대해서 추가 기능을 넣는 것이 가능하다.