본문 바로가기
프로그래밍/Springboot-토이프로젝트

【게시판-번외04】AOP를 적용한 로그출력

by 코이킹 2022. 9. 14.
반응형

안녕하세요 코이킹입니다.
이번 포스트는 AOP를 적용한 로그 출력에설정에 대한 내용입니다. 

 

개념적인 내용은 거의 없으므로, 개념적인 내용을 원하신다면 AOP를 키워드로 구글 검색하셔서 다른 자료를 참고해주시면 감사하겠습니다. 

 

1. 목표

- AOP라는 키워드를 알기

- 스프링 부트에서 AOP를 적용하여 로그를 출력하도록 설정할 수 있다.

 

2. AOP란?

AOP를 구글에서 검색해보시면 위키백과에 검색해보시면 다음과 같은 검색 결과를 얻을  수 있을 겁니다. 

컴퓨팅에서 관점 지향 프로그래밍(aspect-oriented programming, AOP)은 횡단 관심사(cross-cutting concern)의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다. 

저의 경우 동일한 처리(횡단 관심사)를 서로 다른 컴포넌트에서 실시할 경우에는 동일한 처리를 하나의 모듈로 묶어서 처리한다면 더 효율적인 구현이 가능할 수 있다고 이해했습니다. 

 

스프링부트에서도 AOP개념을 쉽게 적용할 수 있는 기능을 제공하고 있기에, 스프링 부트를 의 기능 활용해서 AOP개념을 적용한 로그 출력을 구현해 봤습니다. 

 

3. AOP개념을 적용한 로그출력 설정

1) AOP 로그출력 설정 : /template-springboot/src/main/java/com/sb/template/aop/CommonAopLog.java

package com.sb.template.aop;

import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.aspectj.util.GenericSignature.ClassSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import lombok.extern.slf4j.Slf4j;


@Slf4j
@Component
@Aspect
public class CommonAopLog {

	@Pointcut("execution(* com.sb.template.controller.*.*(..))")
	public void pointCut() {}

	@Pointcut("@annotation(com.sb.template.annotation.Timer)")
	public void timerPointCut() {}

	@Before("pointCut()")
	public void beforeProcess(JoinPoint joinPoint) {
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		Method method = methodSignature.getMethod();
		Object[] objs = joinPoint.getArgs();
		log.info("Start-{} ", methodSignature.getDeclaringTypeName() + "." + method.getName());
		log.info("Request Param : {} ", objs);
	}

	@AfterReturning(value = "pointCut()", returning = "returnValue")
	public void afterReturning(JoinPoint joinPoint, Object returnValue) {
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		Method method = methodSignature.getMethod();
		log.info("End-{} ", methodSignature.getDeclaringTypeName() + "." + method.getName());
		log.info("Response Param : {} ", returnValue);
	}

	@Around("timerPointCut()")
	public Object timerProcess(ProceedingJoinPoint proceedingJoinPoint) {
		StopWatch stopWatch = new StopWatch();
		Object res = new Object();

		MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
		Method method = methodSignature.getMethod();

		Object[] objs = proceedingJoinPoint.getArgs();
		log.info("Start-{} ", methodSignature.getDeclaringTypeName() + "." + method.getName());
		log.info("Input : {} ", objs);

		try {
			stopWatch.start();
			res = proceedingJoinPoint.proceed();
			log.info("End-{} ",  methodSignature.getDeclaringTypeName() + "." + method.getName());
			log.info("Output : {} ", res);
			stopWatch.stop();
			log.info("Process execution time  : {} ", stopWatch.getTotalTimeSeconds());
			return res;
		} catch(Throwable throwable) {
			log.error("Process-Error-{} , Message : {} ",
					method.getName(), throwable.getMessage());
			return res;
		}
	}

}

- 21행 : 스프링부트에서 사용할 빈으로 등록- 22행 : AOP적용하겠다는 선언 - 25~29행 : 포인트 컷(CommonAopLog에서 로그를 출력할 곳을 지정한 것)을 설정했습니다. 25행의 경우 컨트롤러 클래스가 들어있는 패키지를 지정해서 모든 컨트롤러 클래스에서 AOP를 사용한 로그를 출력할 수 있도록 설정했으며, 28행의 경우 Timer라는 어노테이션이 선언되어 있는 곳에서  AOP를 사용한 로그를 출력할 수 있도록 설정했습니다. 

- 31~38행 : 컨트롤러에서 받은 요청의 요청 파라미터와 바디의 데이터를 확인할 수 있는 로그를 출력하는 메서드로, @Before가 선언되어 있어 컨트롤러의 요청에 매칭 되는 메서드가 실행되기 전에 beforeProcess메서드가 실행 되게 됩니다. 

- 40~46행 : 컨트롤러에서 받은 요청의 응답을 보낼때의 데이터를 확인할 수 있는 로그를 출력하는 메서드로, @AfterReturning가 선언되어 있어 컨트롤러의 요청에 매칭 되는 메서드가 실행된 후에 afterReturning메서드가 실행 되게 됩니다. 

 

- 48~73행 : Timer어노테이션이 선언된 곳의 처리시간과 매개변수로 사용된 데이터, 리턴값을 출력하는 메서드입니다. 

 

 

2) AOP적용지점을 선언하기 위한 커스텀 어노테이션 : /template-springboot/src/main/java/com/sb/template/annotation/Timer.java

package com.sb.template.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {

}

 

3) 컨트롤러/template-springboot/src/main/java/com/sb/template/controller/BoardController.java

 

 - 이전 Log4j설정 시에 테스트용으로 작성한 로그 출력 코드를 제거했습니다. 

 

4) 서비스 : /template-springboot/src/main/java/com/sb/template/service/BoardService.java

package com.sb.template.service;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;

import com.sb.template.annotation.Timer;
import com.sb.template.entity.Board;
import com.sb.template.enums.BoardType;
import com.sb.template.repo.BoardRepository;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class BoardService {

	@Autowired
	private BoardRepository boardRepository;

	@Timer
	public List<Board> getAllBoard() {

		List<Board> res = boardRepository.findAll();
		if (res == null) return null;

		log.debug("Data from DB : {}", res);

		return res;
	}

	@Timer
	public Board createBoard(Board board) {

		return boardRepository.save(board);
	}

	@Timer
	public Board getBoardOne(int boardNo) {

		Optional<Board> board = boardRepository.findById(boardNo);

		if (board.isEmpty()) {
			return null;
		}

		return board.get();
	}

	@Timer
	public void updateBoardForm(int boardNo, Model model) {
		Optional<Board> board = boardRepository.findById(boardNo);

		if (board.isEmpty()) {
			return ;
		}

		model.addAttribute("boardTypes", BoardType.getBoardTypes());
		model.addAttribute("board", board.get());
	}

	@Timer
	@Transactional
	public Board updateBoard(int boardNo, Board updatedBoard) {

		Optional<Board> res = boardRepository.findById(boardNo);

		if (res.isEmpty()) {
			return null;
		}

		Board board = res.get();
		board.setType(updatedBoard.getType());
		board.setTitle(updatedBoard.getTitle());
		board.setContents(updatedBoard.getContents());

		Board endUpdatedBoard = boardRepository.save(board);
		return endUpdatedBoard;
	}

	@Timer
	public void deleteBoard(int boardNo) {

		Optional<Board> res = boardRepository.findById(boardNo);

		if (res.isEmpty()) {
			return ;
		}

		boardRepository.delete(res.get());
	}
}

- 25, 36, 42, 54, 66, 85행 : 커스텀 어노테이션을 사용하여 어노테이션이 사용된 메서드에 AOP 로그 출력을 적용했습니다.

 

4. 동작확인 

※ 위의 캡처는 글 상세보기 요청 시의 로그입니다. 

컨트롤러와 서비스에서 AOP를 사용한 로그가 제대로 출력되고 있는 것을 확인할 수 있습니다.

 

5. 전체 소스코드 

https://github.com/leeyoungseung/template-springboot/tree/feature/setting_for_log4j2

 

GitHub - leeyoungseung/template-springboot

Contribute to leeyoungseung/template-springboot development by creating an account on GitHub.

github.com


 AOP를 적용한 로그 출력에 대한 포스트는 이상으로 마치겠습니다.

다음 포스트는 유효성 검사에 대한 내용이 되겠습니다. 

반응형

댓글