Annotation
- JDK5부터 추가된 기능으로 자바 코드에 추가적인 정보를 제공하는 메타데이터이다.
- 비즈니스 로직에 영향을 주지는 않지만, 컴파일 과정에서 유효성 체크, 코드를 어떻게 컴파일하고 처리할지 알려주는 정보를 제공한다.
- 어노테이션을 클래스, 메소드, 변수, 매개변수 등에 추가할 수 있다.
어노테이션으로 Spring AOP 구현 방식 테스트를 진행하는 실습을 해보자.
- 어노테이션 기반이여도 Aspect 역할하는 클래스는 Bean으로 등록되어야 한다.
<bean id="characterAspect" class="com.kh.aop.aspect.CharacterAspect" />
- AspectJ 어노테이션을 사용한 Aspect 적용을 위한 프록시 설정 (어노테이션 기반으로 AOP를 적용할 수 있도록 활성화 하는 태그)
- XML 설정 파일에 다음과 같이 설정한다.
- autoporxy 설정을 추가하면, 클래스 중에 @Aspect라고 붙어있는 Bean 이 있으면, PointCut이 적용된 타겟 객체를 자동으로 Proxy 객체로 만들겠다는 설정
<aop:aspectj-autoproxy proxy-target-class="true"/>
▶ [ proxy-target-class="true" ]
- 프록시를 만들 때, 일반적인 클래스 같은 경우는 AOP를 적용하면 프록시를 만들지만,
인터페이스를 상속하고 있는(구현하고 있는) 클래스 같은 경우는 실제로 Advice를 실행시킬 때 해당 클래스 타입으로 Proxy 객체를 만드는 것이 아니라, 인터페이스를 가지고 Proxy 객체를 만든다.
- 따라서 이 속성이 적용되어 있으면 Proxy 객체로 만들고 싶은 객체가 인터페이스를 구현하고 있어도 인터페이스로 Proxy를 만드는 게 아니라 클래스를 가지고 만들어라는 뜻이다.
- 스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 Bean 객체가 인터페이스를 구현하고 있으면, 인터페이스를 이용해서 프록시를 생성한다.
- Bean 객체가 인터페이스를 구현하고 있을 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하고 싶다면 proxy-target-class="true" 속성을 추가한다.
- 내가 Aspect로 활용할 java 클래스에 가서 상단에 @Aspect 어노테이션(일반적인 자바 클래스가 아니라 Aspect임을 나타내는 @어노테이션)을 붙인다.
- Aspect에는 Advice와 PointCut이 있는데, 포인트 커트를 한 번만 정의하고 필요할 때마다 참조할 수 있는 @Pointcut 어노테이션을 제공한다
- @Before("pointcut"), @After, @AfterThrowing, @AfterReturning, @Around 지원
[AspectJ 포인트커트 표현식]
- Spring AOP 에서 포인트커트는 AspectJ(AOP 프레임워크)의 포인트 커트 표현식을 이용해서 정의한다.
- Spring AOP는 동적 프록시를 지원하기 때문에 구조적으로 다르다.
- Spring AOP 에서 지원되는 AspectJ 포인트 커트 표현식(종류는 한정적이다.)
1) execution([접근지정자] 리턴타입 [클래스이름].메소드명(파라미터)) : 메소드 실행에 대한 조인 포인트를 지정한다.
-> 접근지정자 : public, private 등, 생략 가능
-> 리턴타입 : 메소드의 반환값을 의미한다.
-> 클래스이름 : 클래스의 풀 패키지명이 포함된 이름을 적어준다, 생략 가능
" * " : 메소드에서 리턴하는 타입의 모든 값을 표현한다.
" .. " : 매개 값의 개수가 0개 이상을 의미한다. 매개 값이 없다면 생략할 수 있다.
2) args(파라미터) : 타겟 메소드에 전달되는 파라미터 값을 Advice에도 전달하기 위한 파라미터를 지정한다. 이 때, args(파라미터) 이름과 advice 메소드의 매개값인 파라미터 이름이 동일해야 한다.
3) bean(빈ID) : 포인트 커트 표현식 내에서 빈ID로 특정 빈을 지정할 수 있다.
4) @annotation(어노테이션이름(풀패키지명)) : 주어진 어노테이션을 갖는 조인 포인트를 지정한다.
중복되는 PointCut 코드를 줄이기 위해서
1) xml 방식에서는 <aop:point cut> 태그를 작성했다면,
<aop:pointcut
id="questPointCut"
expression="excectuion(* com.kh.aop.character.Character.quest(..))" /)>
2) @Annotation 방식에서는 @Pointcut 어노테이션을 메소드에 붙이고 사용하고 싶은 곳에서 메소드 호출하듯이 작성하고 매개값은 동일하게 맞춘다.
- 여기에서 메소드는 단순히 마커 역할을 한다. 따라서 몸체가 없는 메소드 형식이다.
- @Pointcut 어노테이션에서 매개 값을 받기 때문에 메소드에서도 같은 이름으로 매개 값을 받는다.
@Pointcut("execution(* com.kh.aop.character.Character.quest(..)) && args(questName)")
public void questPointCut(String questName) {
}
지정하고,
@AfterThrowing(
value = "questPointCut(questName)",
throwing = "exception"
)
이렇게 @어노테이션에서 value로 참조한다.
@어노테이션을 이용한 Bean 자동 등록 & Aspect 등록 실습을 해보자.
01. src > main > java > aop에 owner 패키지의 Owner.java 와 pet 패키지의 Cat/Dog/Pet.java 준비
02. src > test > java에 com.kh.aop.owner 패키지의 OwnerTest 만들기
- 테스트 클래스에서 스프링 기능을 확장해서 사용하도록
@ExtendWith(SpringExtension.class) // JUnit5 환경에서는 SpringExtension, JUnit 4라면 SpringRunner
- 스프링에서 쓸 ApplicationContext에 설정 정보를 가지고 있는 설정 파일 지정
- src > main > java에 자바 파일로 설정 파일들이 들어간다.
@ContextConfiguration(class = {RootConfig.class})
를 작성한다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {RootConfig.class})
class OwnerTest {
@Autowired
private Owner owner;
@Test
public void creat() {
assertThat(owner).isNotNull();
assertThat(owner.getName()).isNotNull();
assertThat(owner.getAge()).isPositive().isGreaterThan(0);
}
@Test
public void barkTest() throws Exception {
assertThat(owner.getPet()).isNotNull();
assertThat(owner.getPet().bark()).isNotNull();
}
}
03. com.kh.aop.conifg에 RootConfig 파일 작성
- 이 자바 파일은 스프링 ApplicationContext에서 사용할 자바 설정 파일임을 알려주는 @어노테이션 작성
@Configuration
@ComponentScan("com.kh.aop")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class RootConfig {
}
- 이 후, Test.java 에서 RootConfig 클래스의 정보를 쓸 수 있도록 @ContextConfiguration어노테이션의 classes 속성에 지정하면 된다.
- 테스트 메소드를 만들어서 통과하면, RootConfig.class 파일을 정상적으로 읽어서 ApplicationContext를 만들었다는 뜻이된다.
04. OwnerTest.java에 Owner라는 Bean 등록
- ApplicationContext에서 Owner 객체가 있으면 주입받아서 가져올 수 있도록 @Autowired 어노테이션을 붙인다.
- 그러나 ApplicationContext 안에 Owner 라는 객체는 아직 없다. 따라서, 이 상황에서 테스트 돌리면 NoSuchBeanDefinitionException 이라고 Bean을 찾지 못해서 발생하는 에러가 출력된다.
- 따라서 예외가 발생하지 않도록 하는 설정을 한다. @Autowird(required = false) 이렇게 하면 일단 NULL이 들어가서 예외는 면하지만 우리가 원하는 테스트는 아닌 상태임
- 그래서 Owner가 NULL이 아니도록 만들어보자.
- Java 방식은 JavaConfig만 사용하는데, @어노테이션 방식은 클래스에 @어노테이션을 붙여서 Bean으로 만든다.
05. Owner.java
- 어노테이션 기반으로 자바 설정이나 xml 설정을 사용하지 않고 @어노테이션을 통해서 ApplicationContext가 관리하는 Bean으로 등록한다.
- 이 때 사용하는 @어노테이션은 바로, @Component이다.
- 값은 @Value로 주입한다. (붙이지 않은 상태에서 테스트 돌리면 null 나옴 WHY? 기본 생성자만 가지고 Bean을 만들기 때문에, 필드의 값을 초기화 하지 않은 상태이기 때문에 JVM이 설정한 기본 값만 가지고 있는 상태)
@Component
public class Owner {
@Value("이산아")
private String name;
@Value("20")
private int age;
@Autowired
@Qualifier("dog")
private Pet pet;
}
- 그러나 @Component만 붙였다고 해서 Bean으로 바로 등록되는 것이 아니기 때문에 RootConfig.java에 @ComponentScan("com.kh.aop") 을 작성한다.
- 이 의미는 "어노테이션 기반으로 Bean 생성을 활성화 할 것이고, 어느 패키지 부터 @컴포넌트이 붙어있는 걸 찾고(Scan), Bean으로 만들면 돼" 라는 뜻이다.
- 이 때, Pet pet 필드 같은 경우는 ApplicationContext를 통해서 Pet이라는 형태의 객체가 있으면 주입받고 싶기 때문에 @Autowired 어노테이션을 붙인다. 이 때도 NoSuchBeanDefinitionException 에러가 발생한다. WHY? Pet 타입의 Bean이 아직 없기 때문이다.
06. Cat.java
@Component
public class Cat implements Pet {
@Value("고영희")
private String name;
@Override
@NoLogging
public String bark() throws Exception {
// ▼ 예외 상황을 일부러 발생시켜기 위한 코드
// if(true) {
// throw new Exception();
// }
return "야옹";
}
}
- @Component를 통해서 바로 Bean으로 등록될 수 있도록 한다.
07. Dog.java
@Component
public class Dog implements Pet {
@Value("갱얼쥐")
private String name;
@Override
@Repeat(count = 3)
public String bark() {
return "멍멍";
}
}
- 여기까지만 하면 또 NoUniqueBeanDefinitionException : No qualifying bean of type 'com.kh.aop.pet.Pet avilable: exepected single matching bean but found 2: cat, dog ' 에러 발생! WHY? Pet 타입의 객체가 2개 존재하고 있기 때문에 구체적으로 어느 pet bean을 Owner에 주입할지 모르기 때문이다. 그래서 Pet.java에 가서 내가 정하고 싶은 Bean을 @Qualifier("cat") 지정한다.
[참고]
- cat, dog 이 ID들은 별도로 지정하지 않으면 클래스의 이름을 소문자로 작성한 것대로 정해진다.
- 메소드 이름을 가지고 Bean을 지정할 때는 @Bean을 사용한다.
-> 자바 config는 메소드를 만들고 메소드가 리턴하는 것을 Bean으로 등록할 수 있도록 함
08. OwnerAspectjava 만들기
- cf. XML 방식에서는 root-context.xml에 <aop:aspectj-autoproxy> 태그를 써서 어노테이션을 사용한 Aspect 적용이 가능하도록 프록시를 자동으로 생성하도록 설정 했었다.
- 자바 방식에서는 RootConfig.java 파일에 @EnableAspectJAutoProxy(proxyTargetClass = true) 를 설정하면 된다.
- 즉, 어노테이션 기반으로 Aspect를 만들겠다고 선언한 것
- 그리고 OwnerAspect를 횡단 관심사를 가지고 있는 Aspect로 등록하기 위해서 @Aspect 어노테이션을 붙인다.
- 이게 끝이 아니고! ApplicationContext에서 관리하는 Bean이 될 Aspect를 선언해야 한다. 그래야 ApplicationContext가 Bean을 활용해서 실제 타겟 객체를 감싸는 Proxy 객체를 만들어준다.
- 따라서 여기에도 @Component를 작성한다. (Bean이자 Aspect가 되는 것)
- pet에 있는 bark() 메소드 전후로 일어날 기능을 정의하도록 @Around 를 만들어보자.
@Aspect
@Component
public class OwnerAspect {
// ▼ 언제 Advice를 수행할 지 PointCut을 지정
@Around("execution(* com.kh.aop.pet.*.bark())")
// ▼ bark() 호출에 대해서 ID로 특정 bean만 실행하도록 bean 아이디를 넘겨주는 지정자와 그것의 부정
@Around("execution(* com.kh.aop.pet.*.bark()) && !bean(dog)")
// ▼ @NoLogging 어노테이션이 붙어있으면 해당 Advice에 대상이 되지 않도록 하는 지정자
@Around("execution(* com.kh.aop.pet.*.bark()) && !@annotation(com.kh.aop.annotation.NoLogging)")
// ▼ Advice에서 할 기능
public String barkAdvice(ProceedingJoinPoint jp) {
// 반드시 ProceedingJoinPoint 를 매개값으로 가져야 실제 타겟 메소드를 호출할 수 있다.
String result = null;
try {
// before
System.out.println("손!");
// 타겟 객체의 메소드를 호출해서 변수에 담는다.
// bark() 메소드에서는 return 값이 있기 때문이다.
result = (String)jp.proceed();
// after returning
System.out.println(result);
} catch (Throwable e) {
// after throwing
System.out.println("그럼 까까먹을까?");
}
return result;
}
[ 정상 수행 출력 결과 ]
손!
야옹
[ 예외 상황에서 출력 결과 ]
손!
그럼 까까먹을까?
- 특정 어노테이션이 메소드 위에 붙어 있으면 해당 Advice가 실행되도록 해보자.
09. src > main > src > aop > annotation 패키지에 annotation 생성, 이름은 NoLogging
// @Target : 어노테이션을 적용할 위치(대상)를 지정한다. (@어노테이션을 어디어디에 붙일 수 있느냐, 배열 형태로)
@Target({ElementType.METHOD, ElementType.TYPE})
// @Retention : 어노테이션의 유효범위(어느 시점까지 어노테이션이 영향을 미치는지 결정)를 지정할 때 사용한다.
// RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해서 참조가 가능하다.
// RetentionPolicy.CLASS : 컴파일러 클래스를 참조할 때까지 유효하다. (실제 실행시에는 JVM에서 참조할 수 없다.)
// RetentionPolicy.SOURCE : 코드상에서만 유효하다.(컴파일하면 사라짐)
@Retention(RetentionPolicy.RUNTIME)
// @Inherited : 부모 클래스에서 어노테이션을 선언하면 자식 클래스에도 상속된다.
//@Inherited
public @interface NoLogging {
}
- @Override : 컴파일러에게 이 메소드는 인터페이스를 구현하고 있는 추상메소드를 재정의하는 것이라는 정보를 제공하는 역할
- @Repeat : 작성한 어노테이션 내용대로 반복
@Retention(RUNTIME)
@Target(METHOD)
public @interface Repeat {
int count() default 1;
}
'Programming > Framework' 카테고리의 다른 글
DAY 153. Spring MVC 패턴 - 회원 조회 (0) | 2022.01.19 |
---|---|
Day 151. Spring MVC - 프로젝트 만들기 개요 (0) | 2022.01.17 |
DAY 149. Spring AOP(Aspect Oridented Programming) (0) | 2022.01.14 |
DAY 148. Spring DI - Annotation (0) | 2022.01.13 |
DAY 147. Spring DI(Dependency Injection) 의존성 주입 (0) | 2022.01.12 |