🧩 TOP 5 회원 찾기 구현
- 스크래치 파일 생성
- 스크래치: 프로젝트와 상관 없이 어떤 특정 메서드나 코드를 수행시켜보고 싶을 때 사용하는 파일
- 프로젝트 폴더가 아니라 스크래치 및 콘솔 탭 - 스크래치 폴더에 생성 된다.
스크래치 파일 저장 위치
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);
}
private static long sumFromOneTo(long input) {
long output = 0;
for (int i = 1; i < input; ++i) {
output = output + i;
}
return output;
}
}
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "api_use_time")
public class ApiUseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
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);
}
ProductController: createProduct 수정
private final ApiUseTimeRepository apiUseTimeRepository;
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();
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
apiUseTime.addUseTime(runTime);
}
System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
- 이 방식의 문제점
- 핵심기능이 100개라면 100개의 핵심기능 모두에 동일한 내용의 부가기능 코드를 추가해야 한다
- 부가기능 코드를 100번 겨우 복붙했는데 수정할 부분이 생기면 100번 반복해서 코드를 수정해야 한다🤯
- 해결책: AOP(Aspect Oriented Programming)를 통해 부가기능을 모듈화
🧩 Spring AOP란
Spring의 AOP 어노테이션
@Aspect: Spring Bean 클래스에만 적용 가능함
- 어드바이스 종류
@Around: 핵심기능 수행 전과 후
@Before: 핵심기능 호출 전
@After: 핵심기능 수행 성공/실패 여부와 상관없이 언제나 동작
@AfterReturning: 핵심기능 호출 성공 시
@AfterThrowing: 핵심기능 호출 실패 시 (예외가 발생한 경우)
- 포인트컷
- 포인트컷 Expression 형태:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
@Pointcut: 포인트컷 재사용 및 결합 가능
Spring AOP 적용
ProductController: createProduct 롤백
UseTimeAop 생성: AOP를 사용해 FolderController, ProductController, NaverApiController에 부가기능을 일괄 추가
▼ 코드 보기
@Slf4j(topic = "UseTimeAop")
@Aspect
@Component
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
this.apiUseTimeRepository = apiUseTimeRepository;
}
@Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
private void product() {}
@Pointcut("execution(* com.sparta.myselectshop.controller.FolderController.*(..))")
private void folder() {}
@Pointcut("execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))")
private void naver() {}
@Around("product() || folder() || naver()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object output = joinPoint.proceed();
return output;
} finally {
long endTime = System.currentTimeMillis();
long runTime = endTime - startTime;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
User loginUser = userDetails.getUser();
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);
}
}
}
}
- AOP를 적용하면 DispacherServlet과 ProductController 사이에 AOP Proxy가 삽입된다.
- DispatcherServlet과 ProductController 입장에서는 변화가 전혀 없다.
joinPoint.proceed()에 의해 원래 호출하려고 했던 함수와 인수가 전달된다.