객체지향 프로그래밍(OOP)을 보완하는 수단
여러 곳에서 쓰이는 공통 기능을 모듈화하고, 쓰이는 곳에 필요할 때 연결함으로써
유지보수/재사용에 용이하도록 프로그래밍 하는 것
흩어진 관심사(Crosscutting Concerns)를 Aspect 로 모아서 모듈화하고, 핵심적인 기능에서 분리하여 재사용
→ 객체지향 프로그래밍(OOP)을 통하여 더욱 객체지향적으로 만들어주는 기술
→ concerns들과 모듈들이 뒤섞여 있는데, 모듈은 모듈끼리 / concerns은 concerns끼리 분리한다.
모듈화
- 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것
- 예시: 핵심적인 관점은 비즈니스 로직으로, 부가적인 관점은 핵심 로직을 실행하기 위해 행해지는 DB 연결, 로깅, 파일 입출력 등...
흩어진 관심사(Crosscutting Concerns)
- 소스 코드상에서 계속 반복해서 사용되는 부분들
- 클래스 A, B, C 에서 공통적으로 나타나는 색깔 블록은 중복되는 메서드, 필드, 코드 등...
참고: 관심사 분리 (SoC, separation of concerns)
(프록시 패턴)
참고: 프록시 (Proxy, Proxy Server), 즉시로딩, 지연로딩
AOP 적용 전의 해결방법
만약 노란색 기능을 수정하고 싶다면, 각각 클래스의 노란색 기능을 일일이 수정해줘야 한다.
→ SOLID 원칙 위배 + 유지 보수 면에서 불리
AOP 적용 후의 해결방법
(Aspect 로 모듈화 한 것)
Aspect 의 기능(흩어진 기능들을 모음)을 통해, 각각 Concern 별로 Aspect를 만들어주고
어느 클래스에서 사용하는지 입력해준다.
aspect
주황색, 파란색, 빨간색처럼 모듈화 시켜놓은 블럭
위의 방식처럼, 분리해서 독립된 클래스로 만든 부가기능
AOP의 기본 모듈
싱글톤 형태의 객체로 존재
기능
- Advice(부가기능) + PointCut(advice를 어디에 적용시킬 것인지 결정)
- 핵심기능 코드 사이에 침투된 부가기능을 독립적인 aspect로 구분해 낼수 있다.
- 구분된 부가기능 aspect를 런타임 시에 필요한 위치에 동적으로 참여하게 할 수 있다.
Concern
(색칠된 부분)
서로 다른 클래스라고 하더라도, 비슷한 기능을 하는 부분 (비슷한 메서드, 비슷한 코드 등...))
Advice
해야 할 일, 기능
Target에 제공할 부가기능을 담고 있는 모듈
Weaving
- Pointcut에 의해서 결정된 Target의 Join point에 부가기능(advice)를 삽입하는 과정
- AOP가 핵심기능(Target)의 코드에 영향을 주지 않으면서, 필요한 부가기능(advice)를 추가할 수 있도록 해주는 핵심적인 처리 과정
각 모듈에는 Advice 와 Pointcut 이 들어있다.
Pointcut
어디에 적용해야 하는지 (= 시작점)
(예시 : A라는 클래스의 Go라는 메서드)
Advice를 적용할 Target의 메서드를 선별하는 정규표현식
(해당 표현식은 execution으로 시작하고, 메서드의 Signature를 비교하는 방법을 주로 이용)
Advice가 적용될 메서드를 정의
각 모듈에는 Advice 와 Pointcut 이 들어있다.
Target
각각 클래스
→ 즉, 적용이 되는 대상 (부가기능을 부여할 대상)
(예시 : 클래스 A, B, C)
핵심 기능을 담고 있는 모듈
Aspect(부가기능)와 target(핵심 기능)은 완전히 분리되어 있고
런타임 시, aspect -> target -> aspect 이렇게 호출한다.
Join point
끼어들 지점
(예시 : 메서드를 실행할 때, 필드에서 값을 가져갈 때 등...)
Advice가 적용될 수 있는 위치
스프링 컨테이너에 AOP 담당 객체임을 알린다.
타겟 메서드의 Aspect 실행 시점을 지정할 수 있는 어노테이션
@Before
어드바이스 타겟 메소드가 호출되기 전에 어드바이스 기능을 수행
'핵심기능' 호출 전 (ex. Client의 입력값 Validation 수행)
@After
타겟 메소드의 결과에 관계없이(성공, 예외 관계없이), 타겟 메소드가 완료되면 어드바이스 기능을 수행
'핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (Try, Catch의 finally()처럼 동작)
@AfterReturning
타겟 메소드가 성공적으로 결과값을 반환 후(정상적 반환 이후), 어드바이스 기능을 수행
'핵심기능' 호출 성공시에만 (함수의 return값 사용 가능)
@AfterThrowing
타겟 메소드가 수행 중 예외를 던지게 되면, 어드바이스 기능을 수행
'핵심기능' 호출 실패 시, 즉 예외(Exception) 발생한 경우만 동작 (ex. 예외 발생했을 때 개발자에게 email)
@Around
어드바이스가 타겟 메소드를 감싸서, 타겟 메소드 호출 전/후(메소드 실행 전후)에 어드바이스 기능을 수행
"핵심기능" 수행 전과 후 (@Beafore + @After)
@Around("within(bit.your.prj.controller.*)")
횡단 관심사항의 대상 지정과 적용 시점을 지정
- pointcut : 횡단 관심사항 적용 대상 지정 (execution, within, bean)
- Advice : 횡단 관심사항 적용 시점 (예시 : 메서드 실행 전/후, 리턴 시, 예외 발생 시)
"Class A 에 Perf 메서드가 있고,
Hello 라는 Aspect 가 있고,
Class A 의 Perf 메서드가 실행 되기 전에는 항상 Hello를 출력해야한다." 고 가정하자.
자바 파일을 클래스 파일로 만들 때
바이트 코드들을 조작하여, 조작된 바이트 코드들을 생성
A.java 파일이 A.class로 변환될 때
A.class 파일에 Hello를 출력하는 메서드가 포함되어 있어야 한다.
A.java는 순수하게 A.class로 컴파일 되었지만,
A.class를 로딩하는 시점에 Hello를 출력하는 메서드를 끼워넣는 방법
A의 바이트코드는 변함이 없지만,
로딩하는 JVM 메모리 상에서는 Perf라는 메서드 전에, Hello를 출력하는 메서드가 같이 포함된 상태로 로딩된다.
// 예시 1
// ? 는 생략 가능
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
// 예시 2
@Around("execution(public * com.sparta.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
// 예시 3 : 모든 공개 메서드 실행
execution(public * *(..))
// 예시 4 : set 다음 이름으로 시작하는 모든 메서드 실행
execution(* set*(..))
// 예시 5 : AccountService 인터페이스에 의해 정의된 모든 메서드의 실행
execution(* com.xyz.service.AccountService.*(..))
// 예시 6 : service 패키지에 정의된 메서드 실행
execution(* com.xyz.service.*.*(..))
// 예시 7 : 서비스 패키지 또는 해당 하위 패키지 중 하나에 정의된 메서드 실행
execution(* com.xyz.service..*.*(..))
// 예시 8 : 서비스 패키지 내의 모든 조인 포인트
within(com.xyz.service.*)
// 예시 9 : 서비스 패키지 또는 하위 패키지 중 하나 내의 모든 조인 포인트
within(com.xyz.service..*)
// 예시 10 : AccountService 프록시가 인터페이스를 구현하는 모든 조인 포인트
this(com.xyz.service.AccountService)
// 예시 11 : AccountService 대상 객체가 인터페이스를 구현하는 모든 조인 포인트
target(com.xyz.service.AccountService)
// 예시 12 : 단일 매개변수를 사용하고 런타임에 전달된 인수가 Serializable과 같은 모든 조인 포인트
args(java.io.Serializable)
// 예시 13 : 대상 객체에 @Transactional 애너테이션이 있는 모든 조인 포인트
@target(org.springframework.transaction.annotation.Transactional)
// 예시 14 : 실행 메서드에 @Transactional 애너테이션이 있는 조인 포인트
@annotation(org.springframework.transaction.annotation.Transactional)
// 예시 15 : 단일 매개 변수를 사용하고 전달된 인수의 런타임 유형이 @Classified 애너테이션을 갖는 조인 포인트
@args(com.xyz.security.Classified)
// 예시 16 : tradeService 라는 이름을 가진 스프링 빈의 모든 조인 포인트
bean(tradeService)
// 예시 17 : 와일드 표현식 *Service 라는 이름을 가진 스프링 빈의 모든 조인 포인트
bean(*Service)
modifiers-pattern
public
private
*
return-type-pattern
void
String
List<String>
*****
declaring-type-pattern
클래스명 (패키지명 필요)
com.sparta.springcore.controller.*
: controller 패키지의 모든 클래스에 적용
com.sparta.springcore.controller..
: controller 패키지 및 하위 패키지의 모든 클래스에 적용
method-name-pattern(param-pattern)
1) 함수명
addFolders
: addFolders() 함수에만 적용
add*
: add 로 시작하는 모든 함수에 적용
2) 파라미터 패턴 (param-pattern)
(com.sparta.springcore.dto.FolderRequestDto)
: FolderRequestDto 인수 (arguments) 만 적용
()
: 인수 없음
(*)
: 인수 1개 (타입 상관없음)
(..)
: 인수 0~N개 (타입 상관없음)
@Component
@Aspect
public class Aspect {
@Pointcut("execution(* com.sparta.springcore.controller.*.*(..))")
private void forAllController() {}
>
@Pointcut("execution(String com.sparta.springcore.controller.*.*())")
private void forAllViewController() {}
>
@Around("forAllContorller() && !forAllViewController")
public void saveRestApiLog() {
...
}
>
@Around("forAllContorller()")
public void saveAllApiLog() {
...
}
}
타겟 메서드의 실행 시간을 측정하는 경우,
implementation 'org.springframework.boot:spring-boot-starter-aop'
Spring AOP 를 사용하기 위해 의존성을 추가
// 방법 1 : 경로 지정 방식
@Component // Spring AOP 는 Bean에서만 동작하므로, @Component 등... 을 통해 스프링 Bean으로 등록 후 사용해야 한다.
@Aspect // 해당 클래스가 Aspect 라는 것을 명시
public class PerfAspect {
// "com.example 밑의 모든 클래스에 적용하고, EventService 밑의 모든 메서드에 적용하라."
// logPerf() 메서드 : @Around 의 execution을 통해 Advice를 적용할 범위 지정 가능
@Around("execution(* com.example..*.EventService.*(..))")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object reVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return reVal;
}
}
// 방법 2 : 특정 어노테이션이 붙은 포인트에 해당 Aspect를 실행하는 방식
@Component
@Aspect
public class PerfAspect {
// @Around 의 @annotation(PerfLogging) : 해당 메서드(logPerf())를 적용시킬 특정 메서드에 @PerfLogging 을 붙이기만 하면 logPerf() 기능이 동작
@Around("@annotation(PerfLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
// 방법 3 : 특정 Bean 전체에 해당 기능을 적용하는 방식
@Component
@Aspect
public class PerfAspect {
// 적용될 빈(simpleServiceEvent)을 직접 명시한다. → 그럼 해당 빈이 가지고 있는 모든 public 메서드에 해당 기능이 적용된다.
@Around("bean(simpleServiceEvent)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
// Controller 예시
@Auth
@GetMapping("/products")
public Page<Product> getProducts ( ... ) {
...
}
각 모듈의 실행시간을 측정하여, 사용자의 서비스 사용시간 저장
//AOP 사용해 모든 Controller 에 부가기능 추가
@Slf4j
@Aspect //스프링 빈 (Bean) 클래스에만 적용 가능
@Component
@RequiredArgsConstructor
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
//포인트컷
@Around("execution(public * com.sparta.myselectshop.controller..*(..))")
public synchronized Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
Object output = joinPoint.proceed(); //controller 의 로직 부분이 실행됨
return output;
} finally {
long endTime = System.currentTimeMillis(); // 측정 종료 시간
long runTime = endTime - startTime; // 수행시간 = 종료 시간 - 시작 시간
// 로그인 회원이 없는 경우, 수행시간 기록하지 않음
Authentication auth = SecurityContextHolder.getC
ontext().getAuthentication();
if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
// 로그인 회원 정보
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
apiUseTime = new ApiUseTime(loginUser, runTime); // 로그인 회원의 기록이 없으면
} else {
apiUseTime.addUseTime(runTime); // 로그인 회원의 기록이 이미 있으면
}
log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
}
}
API 사용시간 = Controller 에 요청이 들어온 시간 - Controller 에 응답이 나간 시간
예시
Controller 에 요청이 들어온 시간: 9시 10분 30초
Controller 에 응답이 나간 시간: 9시 10분 33초 이라고 가정한다면,
= API 사용시간은 '3초'
Intellij 메뉴에서 File > New > Scratch File → Java 선택
사용 시간 측정 코드
//수행시간 측정
class Scratch {
public static void main(String[] args) {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
// 함수 수행
long output = sumFromOneTo(1_000_000_000);
// 측정 종료 시간
long endTime = System.currentTimeMillis();
//소요시간 = 측정 종료 시간 - 측정 시작 시간
long runTime = endTime - startTime;
System.out.println("소요시간: " + runTime);
}
//sumFromOneTo() 함수의 수행시간: 1 에서 "입력된 숫자"까지의 합계를 구하는 함수
private static long sumFromOneTo(long input) {
long output = 0;
for (int i = 1; i < input; ++i) {
output = output + i;
}
return output;
}
}
API 사용시간을 저장할 테이블 설계
ApiUseTime
@Getter
@Entity
@NoArgsConstructor
public class ApiUseTime {
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@OneToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
@Column(nullable = false)
private Long totalTime;
public ApiUseTime(User user, long totalTime) {
this.user = user;
this.totalTime = totalTime;
}
public void addUseTime(long useTime) {
this.totalTime += useTime;
}
}
public interface ApiUseTimeRepository extends JpaRepository<ApiUseTime, Long> {
Optional<ApiUseTime> findByUser(User user); //user 를 기준으로 찾는다
}
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
private final ApiUseTimeRepository apiUseTimeRepository;
// 관심 상품 등록하기
//@Secured(UserRoleEnum.Authority.ADMIN)
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
return productService.createProduct(requestDto, userDetails.getUser());
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원 정보
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
// 로그인 회원의 기록이 없으면
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
// 로그인 회원의 기록이 이미 있으면(새롭게 쓰는게 아니라, 덮어쓰기) --> ApiUseTime 테이블에 추가되어있음
apiUseTime.addUseTime(runTime);
}
log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
// 관심 상품 조회하기
...
// 관심 상품 최저가 등록하기
...
// 상품에 폴더 추가
...
}
어플리케이션 전체에 흩어진 공통 기능이 하나의 장소에서 관리됨
→ 효율적인 유지보수
다른 Service 모듈들이 본인의 목적에만 충실하고, 그 외 사항들은 신경쓰지 않아도 됨
중복되는 코드 제거
높은 생산성
재활용성 극대화
변화 수용이 용이
참고: AOP란 무엇인가
참고: [Spring] AOP란? 용어 정리, 사용 방법
참고: [Spring] AOP란?
참고: [Spring] AOP(Aspect Oriented Programming)란? 스프링 AOP란?
참고: [Spring] AOP란 - (AOP, Spring AOP, AOP 어노테이션)
참고: [Spring] AOP(Aspect Oriented Programming)란 무엇일까?
참고: [Spring] 스프링 AOP Pointcut 표현식
참고: [Spring] 스프링 AOP 포인트컷(Pointcut) 표현식 정리