기존 코드 한 줄도 안 건드리고, LogAspect.java 하나로 모든 Service 메서드에 자동 로그 + 실행시간 측정을 추가한다
3차 프로젝트까지 완성한 코드에는 한 가지 문제가 있다. Service 메서드가 30개라면 로그를 찍으려면 30군데 같은 코드를 복붙해야 한다. AOP는 이 반복을 완전히 제거한다. 기존 코드 단 한 줄도 건드리지 않고, 파일 하나 추가로 모든 메서드에 자동 로그를 붙이는 것이 STEP 1의 핵심이다.
일반 로깅은 각 메서드에 직접 System.out.println()이나 log.info()를 써야 한다.
Service가 10개, 메서드가 50개라면 50군데 직접 써야 하고, 나중에 형식 바꾸면 50군데 다 찾아서 수정해야 한다.
AOP 로깅은 LogAspect.java 딱 하나에만 쓰고, 모든 메서드에 자동으로 적용된다.
public void write(BoardDto dto, User user) { // 😩 반복 코드 log.info("write() 시작"); long start = System.currentTimeMillis(); // 핵심 로직 Board board = new Board(); board.setTitle(dto.getTitle()); boardRepository.save(board); log.info("write() 완료: {}ms", System.currentTimeMillis() - start); // 😩 반복 } public void delete(Long id, User user) { // 😩 또 반복 log.info("delete() 시작"); long start = System.currentTimeMillis(); // 핵심 로직... log.info("delete() 완료: {}ms", System.currentTimeMillis() - start); // 😩 반복 }
public void write(BoardDto dto, User user) { // ✨ 핵심 로직만! Board board = new Board(); board.setTitle(dto.getTitle()); boardRepository.save(board); } public void delete(Long id, User user) { // ✨ 핵심 로직만! Board board = boardRepository.findById(id)...; boardRepository.delete(board); } // LogAspect.java 하나가 // 모든 메서드에 자동 적용!
| 용어 | 한 줄 설명 | 우리 코드에서 |
|---|---|---|
| Aspect | AOP 클래스 전체 — "어디에, 무엇을 끼울지"를 담는 그릇 | LogAspect.java 전체 |
| Advice | 끼어드는 코드 자체 — 실제로 실행되는 부가 기능 | logAround() 메서드 안에 쓴 로그 코드 |
| Pointcut | 어떤 메서드에 끼어들지 고르는 필터 — @Around("여기 부분") | "execution(* com.example.board.service..*(..))" |
| JoinPoint | 끼어든 순간의 메서드 정보 꾸러미 — 이름, 파라미터 꺼낼 수 있음 | ProceedingJoinPoint jp |
| Proxy | 진짜 Service 앞에 스프링이 몰래 끼워넣은 껍데기 대리 객체 | 우리가 직접 만들지 않음 — 스프링이 자동 생성 |
| 종류 | 실행 시점 | 실무 빈도 | 우리 프로젝트 사용 여부 |
|---|---|---|---|
| @Around ⭐ | 메서드 앞뒤 전부 — 나머지 4개 대체 가능 | 가장 많음 | ✅ 사용 — 시간 측정 + 로그 |
| @Before | 실행 직전만 | 보통 | — (필요시 추가 가능) |
| @After | 실행 후 항상 (성공/실패 무관) | 보통 | — |
| @AfterReturning | 정상 반환 시만 | 가끔 | — |
| @AfterThrowing | 예외 발생 시만 | 가끔 | — |
jp.proceed() 위 = @Before 역할 / jp.proceed() 아래 = @After 역할 / try 블록 = @AfterReturning 역할 / catch 블록 = @AfterThrowing 역할. 실무에서 90%는 @Around를 쓴다.
| 메서드 | 반환값 | 예시 출력 |
|---|---|---|
jp.getSignature().getName() | 실행된 메서드 이름 | "write", "delete" |
jp.getSignature().getDeclaringTypeName() | 클래스 풀네임 | "com.example.board.service.BoardService" |
jp.getArgs() | 파라미터 배열 | [BoardDto@1a2b, User@3c4d] |
jp.getTarget().getClass().getSimpleName() | 클래스 단순 이름 | "BoardService" |
jp.proceed() | 원래 메서드 실행 + 반환값 | 해당 메서드의 반환값 그대로 |
원래 메서드가 아예 실행되지 않는다. boardService.write()를 호출했는데 DB 저장이 안 되는 상황이 된다. @Around 사용 시 jp.proceed() 호출은 거의 필수다.
| 표현식 | 의미 | 예시 |
|---|---|---|
| execution | 메서드 직접 지정 — 가장 많이 씀 | execution(* com.example.board.service..*(..)) |
| @annotation | 애너테이션으로 지정 — 원하는 메서드에만 | @annotation(com.example.board.annotation.LogTime) |
| within | 패키지/클래스 전체 범위 | within(com.example.board.service.*) |
execution(* com.example.board.service..*(..))
→ * : 반환 타입 뭐든 / com.example.board.service.. : service 패키지 및 하위 패키지 / * : 클래스명 뭐든 / (..) : 파라미터 뭐든
즉, service 패키지 안의 모든 클래스, 모든 메서드에 자동 적용
jp.proceed() 호출 → 원래 메서드 실행 → 결과를 Object result에 저장return result → Controller가 최종 ResponseEntity 반환dependencies {
// ... 3차 기존 의존성 그대로 유지 ...
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-webmvc"
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "io.jsonwebtoken:jjwt-api:0.12.3"
runtimeOnly "io.jsonwebtoken:jjwt-impl:0.12.3"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:0.12.3"
compileOnly "org.projectlombok:lombok"
runtimeOnly "org.mariadb.jdbc:mariadb-java-client"
annotationProcessor "org.projectlombok:lombok"
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.springframework.boot:spring-boot-starter-security"
// ↓ STEP 1 — AOP 의존성 추가 (이것만 추가하면 됨)
implementation "org.springframework.boot:spring-boot-starter-aop"
}
src/main/java/com/example/board/aspect/LogAspect.java
aspect 패키지를 새로 만들고 이 파일 하나만 추가한다. 기존 파일은 단 하나도 건드리지 않는다.
package com.example.board.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect // ① "나는 AOP야!" 선언 @Component // ② 스프링 빈으로 등록 (이 둘은 항상 함께 붙임) @Slf4j // ③ Lombok 로거 자동 생성 — log.info() 사용 가능 public class LogAspect { /** * Pointcut: * execution(* com.example.board.service..*(..)) * ↑ ↑ ↑ * 모든 반환타입 service 패키지 하위 전체 모든 파라미터 * * → service 패키지 안에 있는 모든 클래스, 모든 메서드에 자동 적용 */ @Around("execution(* com.example.board.service..*(..))") public Object logAround(ProceedingJoinPoint jp) throws Throwable { // 1. 메서드 정보 꺼내기 String className = jp.getSignature().getDeclaringType().getSimpleName(); String methodName = jp.getSignature().getName(); // 2. 시작 로그 + 시각 기록 long start = System.currentTimeMillis(); log.info("[START] {}.{}()", className, methodName); // 3. 원래 메서드 실행 — 이 줄이 없으면 메서드가 실행되지 않음! Object result = jp.proceed(); // 4. 실행 시간 계산 + 완료 로그 long elapsed = System.currentTimeMillis() - start; log.info("[DONE] {}.{}() - {}ms", className, methodName, elapsed); // 5. 원래 반환값 그대로 돌려줌 return result; } }
# 게시글 작성 요청 시 INFO c.e.board.aspect.LogAspect : [START] BoardService.write() INFO c.e.board.aspect.LogAspect : [DONE] BoardService.write() - 23ms # 게시글 목록 조회 시 INFO c.e.board.aspect.LogAspect : [START] BoardService.getBoardList() INFO c.e.board.aspect.LogAspect : [DONE] BoardService.getBoardList() - 8ms # 로그인 시 (UserService도 자동 적용됨) INFO c.e.board.aspect.LogAspect : [START] UserService.login() INFO c.e.board.aspect.LogAspect : [DONE] UserService.login() - 412ms # 관리자 유저 삭제 시 INFO c.e.board.aspect.LogAspect : [START] AdminService.deleteUser() INFO c.e.board.aspect.LogAspect : [DONE] AdminService.deleteUser() - 31ms
BoardService, UserService, AdminService, BoardController, UserController, AdminController — 단 한 파일도 수정하지 않았다. LogAspect.java 하나만 추가했을 뿐인데, Pointcut에 걸리는 모든 Service 메서드에 자동으로 로그가 찍힌다.
기본 버전에서 파라미터 정보도 함께 출력하고 싶다면 아래처럼 확장할 수 있다. 단, 파라미터에 민감한 정보(비밀번호 등)가 포함될 수 있으므로 실무에서는 주의 필요.
@Around("execution(* com.example.board.service..*(..))") public Object logAround(ProceedingJoinPoint jp) throws Throwable { String className = jp.getSignature().getDeclaringType().getSimpleName(); String methodName = jp.getSignature().getName(); Object[] args = jp.getArgs(); // 파라미터 배열 long start = System.currentTimeMillis(); log.info("[START] {}.{}() args={}", className, methodName, Arrays.toString(args)); Object result = jp.proceed(); long elapsed = System.currentTimeMillis() - start; log.info("[DONE] {}.{}() - {}ms", className, methodName, elapsed); return result; } /* 출력 예시: [START] BoardService.write() args=[BoardDto(title=테스트), User(username=user1)] [DONE] BoardService.write() - 23ms */
@Around("execution(* com.example.board.service..*(..))") public Object logAround(ProceedingJoinPoint jp) throws Throwable { String className = jp.getSignature().getDeclaringType().getSimpleName(); String methodName = jp.getSignature().getName(); long start = System.currentTimeMillis(); log.info("[START] {}.{}()", className, methodName); try { Object result = jp.proceed(); long elapsed = System.currentTimeMillis() - start; log.info("[DONE] {}.{}() - {}ms", className, methodName, elapsed); return result; } catch (Exception e) { long elapsed = System.currentTimeMillis() - start; // 에러 발생 시 별도 로그 — warn 레벨로 찍어서 구분 log.warn("[ERROR] {}.{}() - {}ms | {}", className, methodName, elapsed, e.getMessage()); throw e; // 예외는 다시 던져줘야 함 — GlobalExceptionHandler로 전달됨 } } /* 에러 시 출력 예시: [START] BoardService.delete() [WARN] BoardService.delete() - 5ms | 삭제 권한이 없습니다. */
"자동 로깅"은 약간 잘못된 표현이다. 정확히는 "AOP 로깅" = 로깅을 AOP 방식으로 구현한 것. "자동"이라는 말이 틀린 건 아니지만, 처음 설정은 직접 해줘야 한다. Pointcut으로 범위를 지정하면, 그 범위 안의 메서드에 자동 적용되는 것.
Lombok이 제공하는 어노테이션. private static final Logger log = LoggerFactory.getLogger(LogAspect.class); 를 자동 생성해줌. 덕분에 log.info()를 바로 쓸 수 있다. Spring Boot는 기본으로 Logback을 쓰므로 @Slf4j 하나면 충분하다.
Pointcut이 com.example.board.service..* 이면 service 패키지 하위의 모든 클래스에 적용된다. BoardService, UserService, AdminService 전부 자동 적용. 만약 특정 Service만 제외하고 싶다면 && !within(AdminService) 처럼 조건을 추가하면 된다.
가능하다. Pointcut을 execution(* com.example.board.controller..*(..))로 바꾸거나 ||로 조합하면 된다. 하지만 일반적으로 Service 레이어에만 적용하는 게 관례다. Controller는 요청/응답 처리 담당이고, 실제 비즈니스 로직은 Service에 있기 때문.