공부하며 작성한 포스트입니다. 틀린 내용이나 부실한 설명이 있다면 알려주세요.😁
"컴퓨팅에서 관점 지향 프로그래밍(Aspect Oriented Programming)은 횡단 관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 적인 프로그래밍 패러다임이다" - 위키백과
객체지향 설계 방식을 충분히 따르더라도, 여러 클래스에 공통된 기능이 흩어져서 존재할 수 있는데, 이렇게 흩어진 공통 기능들을 관심사 라고 한다. 이러한 관심사를 모듈화 하고, 쓰이는 곳에 필요할 때 연결함으로써 유지보수 혹은 재사용성을 더욱 더 높이는것. 그리고 OOP를 더욱 OOP스럽게 보완해주는것이 AOP이다.
위의 그림에 있는 블럭 중, 주황,파랑,빨강색의 블럭이 Aspect이다.AOP에서 사용하는 여러 용어의 의미는 다음과 같다.
AOP를 사용하기 전과 후의 가장 유명한 예제를 보자. 객체를 실행하고, 그 실행하는 데 걸린 시간을 출력하는 예제이다.
public interface EventService {
public void created();
public void operation();
public void deleted();
}
@Component
public class SimpleServiceEvent implements EventService{
@override
public void created(){
long begin = System.currentTimeMillis();
try{
Thread.sleep(1000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Created");
System.out.println(System.currentTimeMillis() - begin);
}
@override
public void operation(){
long begin = System.currentTimeMillis();
try{
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Operation");
System.out.println(System.currentTimeMillis() - begin);
}
@override
public void deleted(){
long begin = System.currentTimeMillis();
try{
Thread.sleep(3000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Delete");
System.out.println(System.currentTimeMillis() - begin);
}
EventService 인터페이스를 만들고 구현체를 생성했다. 각 메소드는 실행하는 데 소요된 시간을 출력할것이다.
이제 이 클래스를 사용하는 코드를 생성해보자.
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
EventService eventService;
@Override
public void run(ApplicationArguments args) throws Exception{
eventService.create();
eventService.operation();
eventService.delete();
}
}
위와 같은 코드를 실행하면 아마 다음과 같은 output이 나올것이다.
Created
1006
Operation
2008
Delete
3011
이처럼 우리는 객체 지향 설계 방식을 충분히 지켰음에도 불구하고, 아래와 같은 수행 시간을 기록하는 코드는 여러 곳에서 중복되고 있다는것을 확인했다. 예제에서는 10줄 이내의 간단한 코드이지만, 인증, 로깅, 보안에 관련된 코드가 중복이 된다면, 코드가 굉장히 길어지게 될 것이고, 메소드의 수정이 일어난다면 수정 작업도 만만치 않을것이다.
중복이 발생한 코드
long begin = System.currentTimeMillis(); ... System.out.println(System.currentTimeMillis() - begin;
이제 이 코드를 Aspect를 사용해 AOP방식으로 개선하자.
@Component
@Aspect //Aspect 정의
public class PerfAspect {
@Around("execution(* com.example..*.EventService.*(..))") // 해당 패키지에 적용
// Advice 정의 (*Around -> 메서드 실행 전, 후 실행)
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis(); // 메소드 실행 전에 작동
Object retVal = pjp.proceed(); // proceed() -> 메소드 실행 *Around 사용 시 반드시 있어야 함.
System.out.println(System.currentTimeMillis() - begin); //메소드 실행 후 작동
return retVal;
}
}
위와 같은 Aspect를 정의하고, 중복이 발생한 코드를 지우면 다음과 같은 코드가 된다.
@Component
public class SimpleServiceEvent implements EventService{
@override
public void created(){
try{
Thread.sleep(1000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Created");
}
@override
public void operation(){
try{
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Operation");
}
@override
public void deleted(){
try{
Thread.sleep(3000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Delete");
}
다시 AppRunner를 실행해보면, 동일한 결과가 나오게 되는것을 알 수 있다.
Aspect를 생성하는 과정에서 처음 보는 문법이 있을텐데, 제일 처음 보이는 @Around는 어드바이스이다. 메소드가 무엇을, 언제 할지 의미하고있습니다. 여기서 "무엇"이란 logPerf()
메소드입니다.
"언제"는 @Around가 되는데, "언제"의 경우, 총 5가지의 타입이 존재한다.
시점 | 설명 |
---|---|
@Before | 메소드가 호출되기 전에 기능을 수행 |
@After | 메소드의 결과에 관계 없이, 메소드가 완료되면 수행 |
@Around | 메소드 호출 전,후 기능 수행 |
@AfterReturning | 메소드가 성공적으로 결과를 반환하면 수행 |
@AfterThrowing | 메소드 실행 중 예외를 던지게 되면 수행 |
파란 박스의 "execution(* com.blogcode.board.BoardService.getBoards(..))"
는 어드바이스의 value이다. 이 문자열을 포인트컷 표현식이라고 하는데, 포인트컷 표현식은 지정자, 타겟명세 둘로 나눌 수 있고, 총 9가지가 있지만, Annotation, execution방식을 가장 많이 사용한다.
지정자 | 설명 |
---|---|
execution() | 접근제한자, 리턴타입, 인자타입, 클래스/인터페이스, 메소드명, 파라미터타입, 예외타입 등을 전부 조합가능한 지정자 |
@annotation | 타겟 메소드에 특정 어노테이션이 지정된 경우 |
AOP를 적용함으로써, 중복이 발생한 코드를 지울 수 있게 된 것이다. 그리고 기존에는 각 함수의 기능 외에도 실행 시간 측정이라는 책임을 맡게 됨으로써, 단일 책임 원칙에도 어긋났었던 코드를 좀 더 간결하고 본인의 역할에만 집중할 수 있게 만들 수 있다..