본문 바로가기

Programming/Framework

DAY 150. Spring Annotation

 

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;
}