POJO 구성 방식
11. 스프링 환경 및 프로파일마다 다른 POJO 로드
자바 구성 클래스를 여러 개 생성하고 각 클래스마다 POJO 인스턴스 / 빈을 묶는다. 이렇게 작성한 구성 클래스에 @Profile 을 붙임으로써 프로파일 별로 다른 빈 인스턴스 구성을 할 수 있다.
POJO 의 초깃값은 애플리케이션의 제작 단계에 따라 달라질 수 있다. 예를 들어 개발 단계나 운영 단계에서 사용하고자 하는 DB 의 경로가 달라질 수 있고, 이러한 설정들을 달리 적용해야 할 빈들이 존재할 수 있다. 이런 경우에 @Profile 을 사용해 개발자가 시나리오에 맞는 구성 클래스를 읽어 들여 실행하게 할 수 있다.
간단한 예시로, 자바 구성 클래스에 @Profile 을 붙여 여러 버전으로 나누는 코드를 작성한다.
@Configuration
@Profile("develop")
public class ConfigurationDevelop {
@Bean
public Product product() {
// ...
}
}
@Configuration
@Profile("deploy")
public class ConfigurationDeploy {
@Bean
public Product product() {
// ...
}
}
위와 같이 구성 클래스를 @Profile 을 나눠 작성한다. @Profile 의 속성으로 위의 예제에서는 하나의 속성값만을 정의했지만, 두 개 이상의 속성값을 정의하는 것도 가능하고 이 경우는 {} 로 값들을 묶어 정의할 수 있다. 이렇게 작성한 프로파일들을 활성화하기 위해서 어플리케이션 콘텍스트 환경을 가져온 뒤 다음과 같이 setActiveProfiles() 메소드를 호출한다.
AnnotationConfigApplicationContext context = ...;
context.getEnvironment().setActiveProfiles("develop");
혹은 자바 런타임 플래그로 프로파일을 명시하는 것도 가능한데, 'develop' 프로파일을 활성화하려면 다음 플래그를 추가하는 것도 가능하다.
-Dspring.profiles.active=develop
어떤 프로파일도 로드되지 않는 불상사를 막기 위해 기본 프로파일을 지정하는 것이 가능하다. 어플리케이션 콘텍스트 환경의 메소드를 통해 이를 지정하려면 setDefaultProfiles() 메소드를 사용할 수 있고, 플래그로 지정하고자 할 경우는 spring.profiles.active 대신 spring.profiles.default 로 사용할 수 있다.
12. POJO 에게 IoC 컨테이너 리소스 알려주기
빈이 IoC 컨테이너 리소스를 인지하게 하려면 Aware 인터페이스를 구현한다. 스프링은 이 인터페이스를 구현한 빈을 감지해 대상 리소스를 세터 메소스로 주입한다. 자주 쓰이는 스프링 Aware 인터페이스는 다음과 같다.
Aware 인터페이스 | 대상 리소스 타입 |
BeanNameAware | IoC 컨테이너에 구성한 인스턴스의 빈 이름 |
BeanFactoryAware | 컨테이너 서비스를 호출하는 데 사용 |
ApplicationContextAware | 컨테이너 서비스를 호출하는 데 사용 |
MessageSourceAware | 텍스트 메시지 해석하는 데 사용 |
ApplicationEvent PublisherAware |
애플리케이션 이벤트를 발행하는 데 사용 |
ResourceLoaderAware | 외부 리소스를 로드하는 데 사용 |
EnvironmentAware | ApplicationContext 인스턴스에 묶인 org.springframework.core.env.Environment 인스턴스 |
Aware 인터페이스의 세터 메소드는 스프링이 빈 프로퍼티를 설정한 후, 초기화 콜백 메소드를 호출하기 이전에 호출한다. 순서를 정리하면 다음과 같다.
- 생성자나 팩토리 메소드를 호출해 빈 인스턴스 생성
- 빈 프로퍼티에 값, 빈 레퍼런스 설정
- Aware 인터페이스에 정의한 세터 메소드 호출
- 빈 인스턴스를 빈 후처리기의 postProcessBeforeInitialization() 메소드로 넘겨 초기화 콜백 메소드 호출
- 빈 인스턴스를 빈 후처리기의 postProcessAfterInitialization() 메소드로 넘김
- 컨테이너가 종료되면 폐기 콜백 메소드 호출
NOTE
스프링 최신 버전에서는 @Autowired 를 붙여 ApplicationContext 를 손쉽게 가져올 수 있어 굳이 Aware 인터페이스를 구현할 필요가 없다. 프레임워크나 라이브러리 개발 시에는 Aware 인터페이스를 구현하는 게 나을 수 있지만, 스프링에 종속된 인터페이스를 정말 구현할 필요가 있는지 따져보는 것이 중요하다.
13. 어노테이션을 활용한 AOP 적용
AOP (Aspect-Oriented Programming ; 관점 지향 프로그래밍).
AOP 에 대한 개념은 따로 포스팅하도록 하겠다. 우선 스프링에서 어노테이션을 활용해 어떻게 AOP 를 적용하는 지를 먼저 정리하려 한다.
자바 클래스에 @Aspect 를 붙이고 메소드 별로 적절한 어노테이션을 붙여 Advice 로 만들어 Aspect 를 정의할 수 있다. 적용 가능한 Advice 어노테이션은 @Before, @After, @AfterReturning, @AfterThrowing, @Around 가 있다. IoC 컨테이너에서 Aspect 어노테이션 기능을 활성화하기 위해서는 구성 클래스 중 하나에 @EnableAspectJAutoProxy 를 붙인다.
Aspect 는 여러 타입과 객체에 공통 관심사(예 ; 로깅, 트랜잭션 관리) 를 모듈화한 자바 클래스로, @Aspect 를 붙여 표시한다. AOP 에서 말하는 Aspect 란 어디에서 (PointCut), 무엇을 할 것인지 (Advice) 를 합쳐 놓은 개념이다. 어드바이스는 @Advice 를 붙인 단순 자바 메소드이고, 포인트컷은 어드바이스에 적용할 타입 / 객체를 찾는 표현식이다.
@Before 어드바이스
Before 어드바이스는 특정 프로그램 실행 시점 이전의 공통 관심사를 처리하는 메소드이다.
@Aspect
@Component
public class CalculatorLoggingAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Before("execution(* Calculator.add(..))")
public void logBefore() {
log.info("The method add() begins");
}
}
위와 같이 @Before 어노테이션의 값으로 포인트컷 표현식을 적용할 수 있다. 이 포인트컷 표현식은 Calculator 인터페이스의 add() 메소드 실행을 가리킨 것이다. 앞부분의 와일드카드(*) 는 모든 수정자(public, protected, private), 모든 반환형을 매치함을 의미한다. 인수 목록 부분에 쓴 두 점(..) 은 인수 개수는 몇 개든 좋다는 의미이다.
NOTE
@Aspect 만 붙여서는 스프링이 자동으로 감지하지 않기 때문에, 해당 POJO 마다 개별적으로 @Component 를 붙여야 한다.
포인트컷으로 매치한 실행 지점을 JoinPoint 라고 한다. 포인트컷은 여러 조인포인트를 매치하기 위해 지정한 표현식이고 이렇게 매치된 조인포인트에서 해야 할 일이 바로 어드바이스이다.
어드바이스가 현재 조인포인트의 세부 내용에 접근하려면 JoinPoint 형 인수를 어드바이스 메소드에 선언해야 한다. 그러면 메소드명, 인수값 등 자세한 조인포인트 정보를 조회할 수 있다. 다음과 같이 클래스명, 메소드명에 와일드카드를 적용하면 모든 메소드에 예외없이 포인트컷을 적용할 수 있다.
@Aspect
@Component
public class CalculatorLoggingAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Before("execution(* *.*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("The method {}() begins with {} ",
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()));
}
}
@After 어드바이스
After 어드바이스는 조인포인트가 끝나면 실행되는 메소드이다. 조인포인트가 정상 실행되든, 도중 예외가 발생하든 상관없이 실행된다.
@Aspect
@Component
public class CalculatorLoggingAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
@After("execution(* *.*(..))")
public void logAfter(JoinPoint joinPoint) {
log.info("The method {}() ends", joinPoint.getSignature().getName());
}
}
@AfterReturning 어드바이스
After 어드바이스는 조인포인트의 실행 성공 여부와 관계없이 동작한다. 조인포인트가 값을 반환할 경우에만 메소드를 실행하고자 한다면, 다음과 같이 AfterReturning 어드바이스로 대체할 수 있다.
@Aspect
@Component
public class CalculatorLoggingAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
@AfterReturning(
pointcut = "execution(* *.*(..))",
returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
log.info("The method {}() ends with {}", joinPoint.getSignature().getName(), result);
}
}
조인포인트가 반환한 결과값을 받아오려면 @AfterReturning 의 returning 속성으로 지정한 변수명을 어드바이스 메소드의 인자로 지정한다. 스프링 AOP 는 런타임에 조인포인트의 반환값을 이 인수에 넣어 전달한다. 이 때 포인트컷 표현식은 pointcut 속성으로 따로 지정해야 한다.
@AfterThrowing 어드바이스
After Throwing 어드바이스는 조인포인트 실행중 예외가 발생할 경우에만 실행된다. 작동 원리는 @AfterReturning 과 같고, 발생한 예외는 @AfterThrowing 의 throwing 속성에 담아 전달할 수 있다. 자바 언어에서 Throwable 은 모든 에러/예외 클래스의 상위 타입이므로 다음과 같이 어드바이스를 적용하면 조인포인트에서 발생한 에러/예외를 모두 가져올 수 있다.
@Aspect
@Component
public class CalculatorLoggingAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
@AfterThrwoing(
pointcut = "execution(* *.*(..))",
throwing = "result")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
log.error("An exception {} has been thrown in {}()", e, joinPoint.getSignature().getName());
}
}
@Around 어드바이스
Around 어드바이스는 가장 강력한 어드바이스이다. 이 어드바이스는 조인포인트를 완전히 장악하기 때문에 앞서 살펴본 어드바이스를 모두 Around 어드바이스로 조합할 수 있다. 심지어 원본 조인포인트를 언제 실행할 지, 실행 자체를 할지 말지, 계속 실행할 지 여부도 제어할 수 있다.
다음은 Before, After Returning, After Throwing 어드바이스를 Around 어드바이스로 조합한 예이다. Around 어드바이스의 조인포인트 인자형은 ProceddingJoinPoint 로 고정돼 있고, JoinPoint 하위 인터페이스인 ProceedingJoinPoint 를 이용하면 원본 조인포인트를 언제 진행할 지 그 시점을 제어할 수 있다.
@Aspect
@Component
public class CalculatorLoggingAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Around("execution(* *.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("The method {}() begins with {}",
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()));
try {
Object result = joinPoint.proceed();
log.info("The method {}() ends with {}", joinPoint.getSignature.getName(), result);
return result;
} catch (IllegalArgumentException e) {
log.error("Illegal Argument {} in {}()", Arrays.toString(joinPoint.getArgs()),
joinPoint.getSignature().getName());
throw e;
}
}
}
#Reference.
'Study > Spring' 카테고리의 다른 글
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #6 (0) | 2021.09.24 |
---|---|
[Spring] AOP 란? (0) | 2021.09.24 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #4 (0) | 2021.09.21 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #3 (0) | 2021.09.17 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #2 (0) | 2021.09.16 |