심화 커리큘럼📚 전체 맵심화 2 컬렉션
05a · 3차 심화과정 · STEP 1 / 5

AOP 로깅

기존 코드 한 줄도 안 건드리고, LogAspect.java 하나로 모든 Service 메서드에 자동 로그 + 실행시간 측정을 추가한다

이 파일의 학습 목적
STEP 1 — AOP는 왜 배우나?

3차 프로젝트까지 완성한 코드에는 한 가지 문제가 있다. Service 메서드가 30개라면 로그를 찍으려면 30군데 같은 코드를 복붙해야 한다. AOP는 이 반복을 완전히 제거한다. 기존 코드 단 한 줄도 건드리지 않고, 파일 하나 추가로 모든 메서드에 자동 로그를 붙이는 것이 STEP 1의 핵심이다.

심화 5단계 중 지금 여기 — STEP 1
📌 지금 여기
STEP 1
AOP 로깅
STEP 2
컬렉션
이론 정리
STEP 3
예외처리
전역 핸들러
STEP 4
@Transactional
readOnly 최적화
STEP 5
@Builder
DTO 생성 개선
3차까지의 문제
  • · Service 메서드마다 log.info() 반복 작성
  • · 실행시간 측정 코드 메서드마다 복붙
  • · 형식 바꾸면 50군데 다 찾아서 수정
  • · 핵심 로직과 로그 코드가 섞여서 가독성 ↓
STEP 1이 해결하는 것
  • · LogAspect.java 파일 1개만 추가
  • · BoardService · UserService · AdminService 전부 자동 적용
  • · 기존 코드 한 줄도 수정 안 해도 됨
  • · 핵심 로직에서 로그 코드 완전 분리
이 파일에서 배울 것: AOP 5가지 핵심 개념(Aspect·Advice·Pointcut·JoinPoint·Proxy) · Advice 종류 · Pointcut 표현식 · @Around 동작 원리 · 실전 코드(LogAspect.java) · 기본/심화/에러 버전 전체
Why AOP
왜 AOP로 로깅을 하는가

일반 로깅은 각 메서드에 직접 System.out.println()이나 log.info()를 써야 한다. Service가 10개, 메서드가 50개라면 50군데 직접 써야 하고, 나중에 형식 바꾸면 50군데 다 찾아서 수정해야 한다. AOP 로깅은 LogAspect.java 딱 하나에만 쓰고, 모든 메서드에 자동으로 적용된다.

❌ 일반 로깅 — 메서드마다 반복
BoardService.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); // 😩 반복
}
✅ AOP 로깅 — 한 곳에만
BoardService.java — 핵심만!
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 하나가
// 모든 메서드에 자동 적용!
Concepts
AOP 핵심 개념 — 5가지
용어한 줄 설명우리 코드에서
Aspect AOP 클래스 전체 — "어디에, 무엇을 끼울지"를 담는 그릇 LogAspect.java 전체
Advice 끼어드는 코드 자체 — 실제로 실행되는 부가 기능 logAround() 메서드 안에 쓴 로그 코드
Pointcut 어떤 메서드에 끼어들지 고르는 필터 — @Around("여기 부분") "execution(* com.example.board.service..*(..))"
JoinPoint 끼어든 순간의 메서드 정보 꾸러미 — 이름, 파라미터 꺼낼 수 있음 ProceedingJoinPoint jp
Proxy 진짜 Service 앞에 스프링이 몰래 끼워넣은 껍데기 대리 객체 우리가 직접 만들지 않음 — 스프링이 자동 생성
Advice 5가지 종류
종류실행 시점실무 빈도우리 프로젝트 사용 여부
@Around ⭐메서드 앞뒤 전부 — 나머지 4개 대체 가능가장 많음✅ 사용 — 시간 측정 + 로그
@Before실행 직전만보통— (필요시 추가 가능)
@After실행 후 항상 (성공/실패 무관)보통
@AfterReturning정상 반환 시만가끔
@AfterThrowing예외 발생 시만가끔
💡 @Around 하나로 나머지 4개를 다 대체할 수 있는 이유

jp.proceed() 위 = @Before 역할 / jp.proceed() 아래 = @After 역할 / try 블록 = @AfterReturning 역할 / catch 블록 = @AfterThrowing 역할. 실무에서 90%는 @Around를 쓴다.

JoinPoint API — 메서드 정보 꺼내기
메서드반환값예시 출력
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()원래 메서드 실행 + 반환값해당 메서드의 반환값 그대로
⚠️ jp.proceed() 를 빠뜨리면?

원래 메서드가 아예 실행되지 않는다. boardService.write()를 호출했는데 DB 저장이 안 되는 상황이 된다. @Around 사용 시 jp.proceed() 호출은 거의 필수다.

Pointcut 표현식
표현식의미예시
execution메서드 직접 지정 — 가장 많이 씀execution(* com.example.board.service..*(..))
@annotation애너테이션으로 지정 — 원하는 메서드에만@annotation(com.example.board.annotation.LogTime)
within패키지/클래스 전체 범위within(com.example.board.service.*)
📌 우리 프로젝트 Pointcut 해석

execution(* com.example.board.service..*(..))
* : 반환 타입 뭐든 / com.example.board.service.. : service 패키지 및 하위 패키지 / * : 클래스명 뭐든 / (..) : 파라미터 뭐든
즉, service 패키지 안의 모든 클래스, 모든 메서드에 자동 적용

Flow
AOP 적용 후 게시글 작성 흐름
BoardController → [AOP Before] → BoardService.write() → [AOP After] → 응답
Controller
BoardController.write() → boardService.write() 호출
실제로는 Proxy가 먼저 받음 — 우리 눈에는 직접 호출처럼 보이지만
Proxy
Proxy(대리 객체)가 요청을 가로챔
스프링이 BoardService 앞에 자동으로 껍데기를 생성 → 모든 요청이 여기 먼저 옴
스프링 자동 생성 — 직접 만들지 않음
AOP Before
LogAspect — jp.proceed() 호출 전 코드 실행
메서드 시작 로그 출력 + 시작 시각 기록
log.info("[START] BoardService.write()") long start = System.currentTimeMillis()
Service
진짜 BoardService.write() 실행
jp.proceed() 호출 → 원래 메서드 실행 → 결과를 Object result에 저장
boardRepository.save(board)
AOP After
LogAspect — jp.proceed() 호출 후 코드 실행
실행 시간 계산 + 완료 로그 출력
log.info("[DONE] write() - {}ms", elapsed)
Controller
원래 반환값(result) 그대로 Controller에 전달
return result → Controller가 최종 ResponseEntity 반환
Full Code
전체 코드
build.gradle — AOP 의존성 추가
build.gradle
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"
}
aspect/LogAspect.java — 신규 생성
📌 파일 위치

src/main/java/com/example/board/aspect/LogAspect.java
aspect 패키지를 새로 만들고 이 파일 하나만 추가한다. 기존 파일은 단 하나도 건드리지 않는다.

aspect/LogAspect.java
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 메서드에 자동으로 로그가 찍힌다.

파라미터까지 찍는 심화 버전 (선택 사항)

기본 버전에서 파라미터 정보도 함께 출력하고 싶다면 아래처럼 확장할 수 있다. 단, 파라미터에 민감한 정보(비밀번호 등)가 포함될 수 있으므로 실무에서는 주의 필요.

aspect/LogAspect.java — 파라미터 포함 버전
@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
*/
에러 발생 시 별도 로그 추가 버전
aspect/LogAspect.java — try-catch 포함 버전
@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 | 삭제 권한이 없습니다.
*/
Q&A
헷갈리기 쉬운 포인트
Q. "AOP 자동 로깅"이라는 표현이 맞나요?

"자동 로깅"은 약간 잘못된 표현이다. 정확히는 "AOP 로깅" = 로깅을 AOP 방식으로 구현한 것. "자동"이라는 말이 틀린 건 아니지만, 처음 설정은 직접 해줘야 한다. Pointcut으로 범위를 지정하면, 그 범위 안의 메서드에 자동 적용되는 것.

Q. @Slf4j는 뭔가요?

Lombok이 제공하는 어노테이션. private static final Logger log = LoggerFactory.getLogger(LogAspect.class); 를 자동 생성해줌. 덕분에 log.info()를 바로 쓸 수 있다. Spring Boot는 기본으로 Logback을 쓰므로 @Slf4j 하나면 충분하다.

Q. AdminService도 자동으로 적용되나요?

Pointcut이 com.example.board.service..* 이면 service 패키지 하위의 모든 클래스에 적용된다. BoardService, UserService, AdminService 전부 자동 적용. 만약 특정 Service만 제외하고 싶다면 && !within(AdminService) 처럼 조건을 추가하면 된다.

Q. Controller에도 AOP를 적용할 수 있나요?

가능하다. Pointcut을 execution(* com.example.board.controller..*(..))로 바꾸거나 ||로 조합하면 된다. 하지만 일반적으로 Service 레이어에만 적용하는 게 관례다. Controller는 요청/응답 처리 담당이고, 실제 비즈니스 로직은 Service에 있기 때문.

심화 커리큘럼📚 전체 맵심화 2 컬렉션