Post
SpringBoot AOP활용 Log 추가
Spring Boot 환경에서 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)를 활용하여 API 요청을 자동으로 데이터베이스에 기록하는 로깅 시스템을 넣어서 기록합니다.
반복적인 로깅 코드를 모든 컨트롤러 메소드에 작성하는 것은 비효율적이며, 코드의 가독성을 해치고 유지보수를 어렵게 만듭니다. AOP를 사용하면 이러한 ‘횡단 관심사(cross-cutting concerns)’를 핵심 비즈니스 로직과
분리하여 프로젝트를 훨씬 깔끔하게 관리할 수 있습니다.
최종 목표
- 모든 API 요청(@Controller의 모든 메소드 호출)을 가로채서,
- 요청 정보(URI, HTTP 메소드, 요청자 IP 등)를,
- SystemLog 객체에 담아,
- 데이터베이스에 자동으로 저장한다.
1. 의존성 추가 (build.gradle)
가장 먼저 spring-boot-starter-aop 의존성을 추가하여 AOP를 사용할 수 있는 환경을 설정합니다.
// build.gradle
dependencies { // … 다른 의존성들 implementation ‘org.springframework.boot:spring-boot-starter-aop’ // … }
2. 로그 데이터 모델 생성 (SystemLog.java)
API 호출 정보를 담을 SystemLog 클래스를 정의합니다. Lombok을 사용하여 간결한 코드를 유지합니다.
CREATE TABLE SYSTEM_LOG ( LOG_ID NUMBER PRIMARY KEY, SABUN VARCHAR2(100), ACTION_TYPE VARCHAR2(50) NOT NULL, REQUEST_URL VARCHAR2(255), IP_ADDRESS VARCHAR2(50), SUCCESS_YN CHAR(1) DEFAULT ‘Y’, ERROR_MESSAGE VARCHAR2(2000), CREATED_AT DATE DEFAULT SYSDATE );
CREATE SEQUENCE SEQ_SYSTEM_LOG START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;
COMMENT ON TABLE SYSTEM_LOG IS ‘시스템 전체 활동 로그’; COMMENT ON COLUMN SYSTEM_LOG.LOG_ID IS ‘로그 ID’; COMMENT ON COLUMN SYSTEM_LOG.SABUN IS ‘작업 수행 사용자 ID’; COMMENT ON COLUMN SYSTEM_LOG.ACTION_TYPE IS ‘작업 유형 (LOGIN, LOGOUT, CREATE, UPDATE, DELETE 등)’; COMMENT ON COLUMN SYSTEM_LOG.REQUEST_URL IS ‘요청 URL’; COMMENT ON COLUMN SYSTEM_LOG.IP_ADDRESS IS ‘요청자 IP 주소’; COMMENT ON COLUMN SYSTEM_LOG.SUCCESS_YN IS ‘성공 여부 (Y/N)’; COMMENT ON COLUMN SYSTEM_LOG.ERROR_MESSAGE IS ‘에러 메시지 (실패 시)’; COMMENT ON COLUMN SYSTEM_LOG.CREATED_AT IS ‘로그 생성일시’;
@Getter @Setter @Builder public class SystemLog { private Long logId; private String sabun; private String actionType; private String requestUrl; private String ipAddress; private char successYn; private String errorMessage; private Date createdAt; }
3. 데이터베이스 저장을 위한 Mapper와 Service 구현 [ SystemLogMapper (Interface \& XML) ]
MyBatis를 사용하여 SystemLog 객체를 데이터베이스에 저장할 매퍼 인터페이스와 XML을 작성합니다.
SystemLogMapper.java
@Mapper public interface SystemLogMapper { void insertSystemLog(SystemLog systemLog); }
SystemLogMapper.xml
<insert id=“insertSystemLog” parameterType=“org.kms.ssms.model.SystemLog”> <selectKey keyProperty=“logId” resultType=“long” order=“BEFORE”> SELECT SEQ_SYSTEM_LOG.NEXTVAL FROM DUAL </selectKey> INSERT INTO SYSTEM_LOG ( LOG_ID, SABUN, ACTION_TYPE, REQUEST_URL, IP_ADDRESS, SUCCESS_YN, ERROR_MESSAGE, CREATED_AT ) VALUES ( #{logId}, #{sabun}, #{actionType}, #{requestUrl}, #{ipAddress}, #{successYn, jdbcType=CHAR}, #{errorMessage}, SYSDATE ) </insert>
LogService
Aspect가 직접 Mapper를 호출하는 대신, 서비스 계층을 두어 역할을 분리합니다.
@Service @RequiredArgsConstructor public class LogService {
private final SystemLogMapper systemLogMapper;
@Transactional public void saveLog(SystemLog log) { systemLogMapper.insertSystemLog(log); } }
4. AOP Aspect 구현 (ApiLoggingAspect.java)
이제 AOP의 핵심인 Aspect를 구현할 차례입니다.
- @Aspect: 이 클래스가 Aspect임을 선언합니다.
- @Component: Spring 컨테이너가 이 Aspect를 빈으로 등록하도록 합니다.
- @Pointcut: Advice(로직)를 적용할 지점(Join Point)을 정의합니다. 여기서는 org.kms.ssms.controller 패키지
하위의 모든 클래스와 메소드를 대상으로 합니다.
- @Around: 메소드 실행 전후, 또는 예외 발생 시점에 코드를 실행할 수 있는 가장 강력한 Advice입니다.
!within으로 LoginController쪽은 빼서 따로 구현합니다 (로그인 전에 진행되기 때문에 해당 시점의 로그인 ID를 담지 못할 수 있음)
@Aspect @Component @RequiredArgsConstructor public class ApiLoggingAspect {
private final LogService logService;
@Pointcut(“(@annotation(org.springframework.web.bind.annotation.PostMapping) || “ + “@annotation(org.springframework.web.bind.annotation.PutMapping) || “ + “@annotation(org.springframework.web.bind.annotation.DeleteMapping)) \&\& “ + “!within(org.kms.ssms.controller.common.LoginController)”) public void crudApi() {}
@AfterReturning(pointcut = “crudApi()”, returning = “result”) public void logApiCall(JoinPoint joinPoint, Object result) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); Principal principal = request.getUserPrincipal(); String sabun = (principal != null) ? principal.getName() : “anonymousUser”;
SystemLog log = SystemLog.builder() .sabun(sabun) .actionType(joinPoint.getSignature().getName()) .requestUrl(request.getRequestURI()) .ipAddress(request.getRemoteAddr()) .successYn(‘Y’) .build();
logService.saveLog(log); } }
전체 동작 흐름
1. 클라이언트가 API를 호출합니다.
2. 요청이 DispatcherServlet을 거쳐 org.kms.ssms.controller의 특정 메소드로 향합니다.
3. ApiLoggingAspect의 @Around Advice가 컨트롤러 메소드 실행을 가로챕니다.
4. HttpServletRequest에서 URI, Method, IP 등의 정보를 추출합니다.
5. 추출한 정보로 SystemLog 객체를 생성하고 LogService를 통해 DB에 저장합니다.
6. pjp.proceed()를 호출하여 원래의 컨트롤러 메소드를 실행합니다.
7. 컨트롤러 메소드의 실행 결과가 클라이언트에게 반환됩니다.
댓글