# 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