-
@Aspect (AOP) 적용하기Spring 2022. 6. 9. 00:21
김영한 강사님의 스프링 고급편을 듣고,
이번 withEmployee 프로젝트 리팩토링을 진행하면서 프로젝트에 AOP를 구현하고 싶었기에
공부한 내용을 바탕으로 프로젝트에 클래스 깊이를 나타내면서 로그를 나타내는 부분을 AOP를 적용하기로 했다.
AOP
AOP 는 Aspect Oriented Programming(관점 지향 프로그래밍) 으로 핵심기능 관점과 부가적인 기능 관점을 나누어 보고 흩어진 부가기능 관심사를 모듈화 하여 핵심비지니스에서 분리하여 관리하는 것을 말하며 여러 오브젝트에 산재해서 나타나는 공통적인 기능인 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 점을 보완한다.
스프링에서의 AOP
스프링 AOP 는 런타임시에 프록시를 생성하는 프록시 패턴 기반의 AOP 구현체 로 스프링 빈에만 AOP를 적용 가능하며 타겟의 인터페이스가 존재하는 경우 JDK동적 프록시를 이용하여 인터페이스 기반의 프록시를 생산하고 인터페이스가 존재하지 않을 때에는 CGLIB(구체 객체을 상속하여 메소드를 오버라이딩 하는 방식)을 이용하여 프록시를 만든다. 스프링 AOP의 경우 기본 설정은 JDK 동적 프록시를 사용하도록 되어있다.
스프링 AOP의 주요 목적은 횡단 관심사를 적용할 대상을 제어 하고 부가기능 을 구현하는 것이다.
- Advisor = Pointcut(적용대상) + Advice(부가기능)
스프링 AOP는 @Aspect어노테이션을 이용하여 구현한다.
- Aspect
- 다중 객체에 영향을 주는 횡단관심사의 모듈화
- Spring AOP에서는 aspect는 정규 클래스나 @Aspect 어노테이션(@Aspect 스타일)으로 주석처리된 정규 클래스를 사용
- Joinpoint
- Advice가 적용될 위치
- Advice
- 실제 부가기능을 담은 구현체
- Pointcut
- JointPoint의 상세 스펙 정의
- 구체적으로 Advice가 실행될 지점 지정
- Target
- Aspect를 적용하는 대상
- Weaving
- 다른 애플리케이션 타입이나 advised객체를 생성하기 위한 객체를 가지는 aspect 연결
- 이것은 컴파일 시점(예를 들어, AspectJ 컴파일러를 사용하여), 로그시점 또는 런타임에 수행될수 있다
- 런타임시 직조(weaving)를 수행한다.
의존관계
먼저 스프링 AOP를 적용하기 위해서 다음의 의존관계를 추가해준다.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.7.0</version> </dependency>
부가기능 구현
표현하고자 하는 부가기능을 구현할 클래스를 생성한다.
먼저 메서드 깊이와 요청을 구분하기위한 아이디를 가진 TraceId 객체와 이를 포함한 상태를 나타내는 TraceStatus 객체를 만들어준다.
import java.util.UUID; import lombok.Getter; @Getter public class TraceId { private String id; private int level; public TraceId() { this.id = createId(); this.level = 0; } public TraceId(String id, int level) { this.id = id; this.level = level; } private String createId() { return UUID.randomUUID().toString().substring(0, 8); } public TraceId createNextId() { return new TraceId(id, level + 1); } public TraceId createPreviousId() { return new TraceId(id, level - 1); } public boolean isFirstLevel() { return level == 0; } }
import lombok.Getter; @Getter public class TraceStatus { private TraceId traceId; private Long startTimeMs; private String message; public TraceStatus(TraceId traceId, Long startTimeMs, String message) { this.traceId = traceId; this.startTimeMs = startTimeMs; this.message = message; } }
기능을 구현할 인터페이스를 만들어준다.
public interface LogTrace { TraceStatus begin(String message); void end(TraceStatus status); void exception(TraceStatus status, Throwable e); }
실제 기능 구현클래스를 만들어준다. 트레이스 아이디를 담는 스레드로컬을 이용해 동시성문제를 방지하고, 요청을 구분할 것이다. 요청의 마지막 단계에서 아이디를 홀더에서 제거해준다.
import lombok.extern.slf4j.Slf4j; @Slf4j public class ThreadLogTrace implements LogTrace { private static final String START_PREFIX = "-->"; private static final String COMPLETE_PREFIX = "<--"; private static final String EX_PREFIX = "<X-"; private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>(); @Override public TraceStatus begin(String message) { syncTraceId(); TraceId traceId = traceIdHolder.get(); Long startTimeMs = System.currentTimeMillis(); log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message); return new TraceStatus(traceId, startTimeMs, message); } private void syncTraceId() { TraceId traceId = traceIdHolder.get(); if (traceId == null) { traceIdHolder.set(new TraceId()); } else { traceIdHolder.set(traceId.createNextId()); } } private static String addSpace(String prefix, int level) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < level; i++) { sb.append((i == level - 1) ? "|" + prefix : "| "); } return sb.toString(); } @Override public void end(TraceStatus status) { complete(status, null); } @Override public void exception(TraceStatus status, Throwable e) { complete(status, e); } private void complete(TraceStatus status, Throwable e) { Long stopTimeMs = System.currentTimeMillis(); long resultTimeMs = stopTimeMs - status.getStartTimeMs(); TraceId traceId = status.getTraceId(); if (e == null) { log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs); } else { log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString()); } releaseTraceId(); } private void releaseTraceId() { TraceId traceId = traceIdHolder.get(); if (traceId.isFirstLevel()) { traceIdHolder.remove(); } else { traceIdHolder.set(traceId.createPreviousId()); } } }
생성한 LogTrace를 빈으로 등록해준다.
@Configuration public class LogConfig { @Bean public LogTrace logTrace() { return new ThreadLogTrace(); } }
Aspect 생성
이제 생성된 부가기능을 Aspect로 만들어 적용대상과 기능을 구현하도록 하자.
Pointcut 어노테이션을 이용해서 적용대상을 따로 구현해주고 @Around를 이용해 Advice를 적용할 대상을 메소드로 표현하여 해당 기능 대상을 지칭하도록 했다.
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class LogAop { private final LogTrace logTrace; public LogAop(LogTrace logTrace) { this.logTrace = logTrace; } @Pointcut("execution(* com.yunhalee.withEmployee..*Controller*.*(..))") public void controller() { } @Pointcut("execution(* com.yunhalee.withEmployee..*Service*.*(..))") public void service() { } @Pointcut("execution(* com.yunhalee.withEmployee..*Repository*.*(..))") public void repository() { } @Around("controller() || service() || repository()") public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { TraceStatus status = null; try { status = logTrace.begin(joinPoint.getSignature().toShortString()); Object result = joinPoint.proceed(); logTrace.end(status); return result; } catch (Throwable e) { e.printStackTrace(); logTrace.exception(status, e); throw e; } } }
실행
이제 앱을 실행시켜 요청을 보내면 다음과 같이 트랜잭션 아이디와 깊이 정보를 생성하여 보여주며 적용되는 것을 확인할 수 있다!
추가로 실제 앱을 배포할 때에는 application-prod.properties등으로 실행파일을 구분하여 로깅 레벨, 저장 위치등을 설정할 수 있다.
(참고한 사이트 !!!!!)
https://ttl-blog.tistory.com/448
https://engkimbs.tistory.com/746
http://ldg.pe.kr/framework_reference/spring/ver2.x/html/aop.html
https://seypark.tistory.com/105
'Spring' 카테고리의 다른 글
[AWS + JENKINS + SONARQUBE] Spring 프로젝트 CI/CD 구현하기 1) CI 구현하기 (Jenkins 설치 및 실행, Github연동(ssh, webhook)) (0) 2022.09.17 에러 페이지 반환하기 (Feat.ErrorController) (0) 2022.08.29 Springboot와 React(Axios)에서 컨트롤러 prefix 수정하기 (0) 2022.05.30 PUT vs PATCH (0) 2022.03.21 Repository Layer의 단위테스트 작성 (0) 2022.03.11