스프링 부트, 입문!

스프링 부트, 입문!

쉽고 빠르게 배우는, 스프링 부트 첫걸음!

30 AOP, 관점 지향 프로그래밍

# AOP, 관점 지향 프로그래밍 ## 미션 AOP 개념을 설명하고, 이를 실제 코드로 훈련하시오. ![홍팍-스프링-부트-입문--미션](http://drive.google.com/thumbnail?export=view&sz=w960&id=1eOI1buTFYpcxj-CP9KMsgBCVGhjmKOXG) ## 테스트용 DB 설정 - H2, memory db #### ../resources/application.properties ``` # 09강: h2 DB, 웹 콘솔 설정 spring.h2.console.enabled=true # 15강: data.sql 적용을 위한 설정(스프링부트 2.5 이상) spring.jpa.defer-datasource-initialization=true # 17강: JPA 로깅 설정 ## 디버그 레벨로 쿼리 출력 logging.level.org.hibernate.SQL=DEBUG ## 이쁘게 보여주기 spring.jpa.properties.hibernate.format_sql=true ## 파라미터 보여주기 logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE ## 고정 url 설정 ## 30강 spring.datasource.url=jdbc:h2:mem:testdb # 28강: PostgreSQL 연동 #spring.datasource.url=jdbc:postgresql://localhost:5432/firstproject_db #spring.datasource.username=postgres #spring.datasource.password=postgres spring.datasource.data=classpath:/data.sql ## 30강 spring.datasource.initialization-mode=always spring.jpa.hibernate.ddl-auto=create-drop ``` ## 03:18 댓글 생성 입출력 로깅 - slf4j, log.info() #### ../service/CommentService ``` ... @Slf4j @Service public class CommentService { @Autowired private CommentRepository commentRepository; @Autowired private ArticleRepository articleRepository; ... @Transactional public CommentDto create(Long articleId, CommentDto dto) { log.info("입력값 => {}", articleId); log.info("입력값 => {}", dto); // 게시글 조회(혹은 예외 발생) Article article = articleRepository.findById(articleId) .orElseThrow(() -> new IllegalArgumentException("댓글 생성 실패! 대상 게시글이 없습니.")); // 댓글 엔티티 생성 Comment comment = Comment.createComment(dto, article); // 댓글 엔티티를 DB로 저장 Comment created = commentRepository.save(comment); // DTO로 변경하여 반환 // return CommentDto.createCommentDto(created); CommentDto createdDto = CommentDto.createCommentDto(created); log.info("반환값 => {}", createdDto); return createdDto; } ... } ``` ## 06:01 입력값 로깅 AOP - @Aspect, @Componenet, @Pointcut, @Before, @AfterReturning, JoinPoint #### ../service/CommentService ``` ... @Slf4j @Service public class CommentService { @Autowired private CommentRepository commentRepository; @Autowired private ArticleRepository articleRepository; ... @Transactional public CommentDto create(Long articleId, CommentDto dto) { // 게시글 조회(혹은 예외 발생) Article article = articleRepository.findById(articleId) .orElseThrow(() -> new IllegalArgumentException("댓글 생성 실패! 대상 게시글이 없습니.")); // 댓글 엔티티 생성 Comment comment = Comment.createComment(dto, article); // 댓글 엔티티를 DB로 저장 Comment created = commentRepository.save(comment); // DTO로 변경하여 반환 return CommentDto.createCommentDto(created); } ... } ``` #### ../aop/DebuggingAspect ``` package com.example.firstproject.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component @Slf4j public class DebuggingAspect { @Pointcut("execution(* com.example.firstproject.service.CommentService.create(..))") private void cut() {} @Before("cut()") public void loggingArgs(JoinPoint joinPoint) { // 입력값 가져오기 Object[] args = joinPoint.getArgs(); // 클래스명 String className = joinPoint.getTarget() .getClass() .getSimpleName(); // 메소드명 String methodName = joinPoint.getSignature() .getName(); // 입력값 로깅하기 for (Object obj : args) { log.info("{}#{}의 입력값 => {}", className, methodName, obj); } } } ``` ## 13:58 반환값 로깅 AOP #### ../aop/DebuggingAspect ``` package com.example.firstproject.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component @Slf4j public class DebuggingAspect { @Pointcut("execution(* com.example.firstproject.service.CommentService.create(..))") private void cut() {} ... @AfterReturning(value = "cut()", returning = "returnObj") public void loggingReturnValue(JoinPoint joinPoint, Object returnObj) { // 클래스명 String className = joinPoint.getTarget() .getClass() .getSimpleName(); // 메소드명 String methodName = joinPoint.getSignature() .getName(); // 반환값 로깅 log.info("{}#{}의 반환값 => {}", className, methodName, returnObj); } } ``` ## 17:16 AOP 대상 범위 변경 - @Pointcut, execution #### ../aop/DebuggingAspect ``` ... @Aspect @Component @Slf4j public class DebuggingAspect { @Pointcut("execution(* com.example.firstproject.service.CommentService.*(..))") private void cut() {} ... } ``` ## 19:05 수행 시간 측정 - Custom Annotation, Pointcut, @Around, ProceedingJoinPoint, StopWatch #### ../annotation/RunningTime ``` package com.example.firstproject.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RunningTime { } ``` #### ../aop/PerformanceAspect ``` package com.example.firstproject.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; @Aspect @Component @Slf4j public class PerformanceAspect { @Pointcut("@annotation(com.example.firstproject.annotation.RunningTime)") private void enableRunningTime() {} @Pointcut("execution(* com.example.firstproject..*.*(..))") private void cut() {} @Around("cut() && enableRunningTime()") public void loggingRunningTime(ProceedingJoinPoint joinPoint) throws Throwable { // 메소드 수행 전 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 메소드 수행 Object returningObj = joinPoint.proceed(); // 메소드 종료 후 stopWatch.stop(); String methodName = joinPoint.getSignature() .getName(); log.info("{}의 총 수행 시간 => {} sec", methodName, stopWatch.getTotalTimeSeconds()); } } ``` #### api/CommentApiController ``` ... @RestController public class CommentApiController { ... // 댓글 삭제 @RunningTime @DeleteMapping("/api/comments/{id}") public ResponseEntity<CommentDto> delete(@PathVariable Long id) { // 서비스에게 위임 CommentDto deletedDto = commentService.delete(id); // 결과 응답 return ResponseEntity.status(HttpStatus.OK).body(deletedDto); } } ``` ## 🔥 구글링 훈련하기 - AOP 장점 - @Aspect - @Pointcut 표현식 - @Before - AOP JoinPoint 사용법 - @AfterReturning - 스프링 커스텀 어노테이션 - @Around - ProceedingJoinPoint - 스프링 StopWatch