지난 몇 년간 개발자로 일하면서 로그 보기는 개발의 시작이자 끝이였습니다.
개발뿐 아니라 유지보수 상황에서도 위 두가지 질문은 끊임없이 반복되었고, 이때마다 "로그"를 보고 문제를 해결하곤 했습니다.
로그는 시스템의 모든 중요한 활동, 특히 API 호출에 대한 상세한 기록을 남기는 것입니다. 이러한 기록이 시스템 유지보수, 개발, 확장에 가장 중요한 단서가 됩니다.
@PostMapping("/news/{newsId}/like")
public ResponseEntity<Map<String, Object>> toggleLike(...) {
log.info("사용자 {}가 {}번 뉴스에 '좋아요'를 눌렀습니다.", userId, newsId);
return ResponseEntity.ok(response);
}
위와 같은 방식은 실제 업무 중 프로젝트에서도 활용되는 아주 흔한 방식입니다. 로그가 필요한 중요한 코드 주변에 로그를 남기는 코드를 작성하는 방식입니다. 하지만 이 방식은 다음과 같은 명백한 단점을 가집니다.
이번에는 프로젝트의 꽃! 로그를 기록하는 코드를 진행 중인 프로젝트에 추가하겠습니다. 객체지향적이면서도 간단하게 컨트롤러의 메소드마다 직접 로그를 기록하는 방법으로 진행해보겠습니다.
AOP(Aspect-Oriented Programming)는 애플리케이션의 여러 부분에 공통적으로 나타나는 부가 기능(예: 로깅, 트랜잭션, 보안)을 공통 모듈로 분리하여 관리하는 프로그래밍 패러다임입니다. 저는 이번 프로젝트에서 AOP 관점을 활용하여 '활동 로그 기록'이라는 공통 관심사를 모든 컨트롤러의 비즈니스 로직으로부터 완벽하게 분리하기로 결정했습니다.
구현할 아키텍처는 아래와 같습니다.
커스텀 어노테이션 생성: 로그를 기록하고 싶은 컨트롤러 메소드에 붙여주기만 하면 되는 어노테이션을 직접 생성해 보겠습니다.
공통 로깅 클래스 구현: 로깅 어노테이션이 붙은 메소드의 실행을 '가로채서(Advice)', 로그 기록이라는 공통 로직을 실행 전후에 실행시키는 AOP 모듈을 만듭니다.
동작: 특정 API가 호출되면, Spring은 해당 컨트롤러 메소드에 직접 생성한 로깅 어노테이션이 붙어있는지 확인합니다. 만약 붙어있다면, 메소드를 직접 실행하기 전에 ActivityLogAspect를 먼저 실행하여 로그를 기록하고, 그 후에 원래의 메소드를 실행합니다.
이 방식을 통해 메소드의 핵심 로직을 건드리지 않고 로그를 남길 수 있으며, 추후 로깅 정책 개선 시 공통 로깅 클래스만 관리하면 되어 효율적입니다.
기본적으로 프로젝트에 AOP 관련 의존성을 추가합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
로그를 적용할 대상을 지정하기 위한 어노테이션을 직접 생성합니다.
@Target(ElementType.METHOD) // 메소드에만 적용 가능
@Retention(RetentionPolicy.RUNTIME) // 런타임에도 정보 유지
public @interface LogActivity {
String value() default ""; // 활동 설명을 위한 속성
}
공통 로깅 클래스인 ActivityLogAspect 클래스를 구현합니다.
package com.ccp.simple.aop;
import jakarta.servlet.http.HttpServletRequest;
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.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
@Slf4j
public class ActivityLogAspect {
@Around("@annotation(logActivity)")
public Object logActivity(ProceedingJoinPoint joinPoint, LogActivity logActivity) throws Throwable {
// 1. 요청 정보 수집
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ip = request.getRemoteAddr();
String method = request.getMethod();
String url = request.getRequestURI();
String activity = logActivity.value();
// 2. 사용자 정보 수집
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userId = (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal()))
? authentication.getName() : "GUEST";
// 3. 로그 기록 (메소드 실행 전)
log.info("[Activity Log] User: {}, IP: {}, Method: {}, URL: {}, Action: {}", userId, ip, method, url, activity);
try {
// 4. 원래의 타겟 메소드 실행
Object result = joinPoint.proceed();
// 5. 로그 기록 (메소드 성공 후)
log.info("[Activity Log] Completed: {} - Success", activity);
return result;
} catch (Throwable throwable) {
// 6. 로그 기록 (메소드 실패 시)
log.error("[Activity Log] Failed: {} - Error: {}", activity, throwable.getMessage());
throw throwable;
}
}
}
API 요청시 받을 수 있는 기본적이면서 중요한 정보를 모두 수집할 수 있도록 하였습니다. 또한 메소드 전, 후, 실패 시 모두 로그가 남을 수 있도록 설정하였습니다.
@LogActivity("뉴스 검색")
@GetMapping("/news/search")
public List<NewsResponseDto> searchNews(@RequestParam("q") String query) {
return newsService.searchNews(query);
}
@LogActivity 을 달고 내용을 작성해줍니다.실제로 아래와 같이 로그 정보가 남는 것을 확인할 수 있었습니다. 해당 로그 정보만으로도 어떠한 메소드가 실행되었고, 어떠한 이유로 에러가 났는지 명확하게 확인할 수 있었습니다.
2025-09-11T13:58:38.126+09:00 INFO 26216 --- [nio-8080-exec-1] com.ccp.simple.aop.ActivityLogAspect : [Activity Log] User: anonymousUser, IP: 0:0:0:0:0:0:0:1, Method: POST, URL: /api/login, Action: 사용자 로그인
2025-09-11T13:58:42.820+09:00 INFO 26216 --- [nio-8080-exec-3] com.ccp.simple.aop.ActivityLogAspect : [Activity Log] Completed: 뉴스 목록 조회 - Success
2025-09-11T13:58:38.412+09:00 ERROR 26216 --- [nio-8080-exec-1] com.ccp.simple.aop.ActivityLogAspect : [Activity Log] Failed: 사용자 로그인 - Error: 401 UNAUTHORIZED "Invalid credentials"
AOP 방식으로 로그 남기기를 적용한 결과, 아래와 같은 두 가지 큰 성과를 얻었습니다.
향상된 코드 품질: 로그 기록 코드를 개별적으로 메소드 안에 작성하지 않아도 됩니다. 컨트롤러는 오직 자신의 핵심 책임에만 집중할 수 있게 되어, 코드의 가독성과 유지보수성이 비약적으로 향상되었습니다.
일관된 로그: 모든 API 호출에 대해 "누가, 어디서, 무엇을, 어떻게" 했는지에 대한 일관된 형식의 로그가 자동으로 기록됩니다. 이는 서비스 운영 중 문제 추적과 사용자 행동 분석에 매우 중요한 기록이 됩니다.
Spring AOP를 활용한 로깅 방식은 여러 모듈에 걸쳐 반복적으로 나타나는 작업을 따로 분리하여 관리합니다. 간단하지만 객체 지향적 프로그래밍을 확실하게 해볼 수 있는 기능 구현이였습니다.