스프링 입문을 위한 자바 객체 지향의 원리와 이해 책의 내용을 정리하여, 코드에 적용시킨 예제입니다.
목적 : 카드 추가, 수정, 삭제, 이동 등의 history를 로그로 남기기 위해 AOP를 적용하도록 한다.
"스프링 ID가 의존성에 대한 주입이라면, 스프링 AOP는 로직 주입이라고 볼 수 있다."
객체지향에서 로직(코드)이 있는 곳은 메서드 안쪽이므로, AOP는 메서드에 코드를 주입할 수 있다.
매서드에서 코드를 주입할 수 있는 곳
Around : 메서드 전 구역
Before : 메서드 시작 전
After : 메서드 종료 후
AfterReturning : 메서드 정상 종료 후
AfterThrowing : 메서드에서 예외가 발생하면서 종료된 후
Pointcut : 횡단 관심사를 적요할 타깃 메서드를 선택하는 지시자 (메서드 선택 필터) "타깃 클래스의 타깃 메서드 지정자"
스프링 AOP는 Aspect를 메서드에만 지정할 수 있으므로 "타깃 메서드 지정자"라고 할 수 있지만, 다른 AOP프레임 워크에서는 속성 등에도 Aspect를 적용할 수 있기때문에 Aspect 적용 위치 지정자(지시자)
가 맞는 표현이다.
코드에서 "execution(* com.codesquad.todo2.domain.project.ProjectService.addCard(..))"
스프링 프레임워크가 관리하는 빈의 모든 메서드
(광의) Aspect 적용이 가능한 모든 지점
(협의) 호출된 객체의 메서드
코드에서 ProceedingJoinPoint joinPoint
Pointcut이 특정한 하나의 메서드에 지정된 것이 아니라 여러 클래스의 동일한 메서드가 지정될 수 있다.
@Before("execution(* runSomething())")
이 Pointcut 일때,
aaa.runSomething() 메서드를 호출한 상태라면 → Joinpoint는 aaa객체의 runSomething() 이 된다.
bbb.runSomething() 메서드를 호출한 상태라면 → Joinpoint는 bbb객체의 runSomething() 이 된다.
jointpoint 파라미터를 이용하면 실행 시점에 실제 호출된 메서드가 무엇인지, 실제 호출된 메서드를 소유한 객체가 무엇인지, 호출된 메서드의 파라미터는 무엇인지 등의 정보를 확인할 수 있다.
joinPoint.getArgs() 로 본래 메서드가 받은 매개 변수를 받아온다.
Advice (Aspect Method)의 리턴 타입이 Original Method의 리턴 값과 타입이 맞아야 한다. ★
Around 에서 joinPoint.proceed() 에 대한 시점 이 중요
proceed()를 기준으로 이전 코드는 before, 이후 코드는 after로 구분된다.
proceed() 의 리턴 값은 Object 이다. 이는 Aspect 로 연결된 Original Method 의 리턴 값을 받을 수 있다.
logAddCard(), logDeleteCard(), logReorderCard()
메서드 의존성 추가
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.9.5'
compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.5'
관심사 분리
@Component
: 빈으로 등록한다.
빈
이 설정되는 이유 : 객체의 생성과 의존성 주ㅊ입을 스프링 프레임워크에 위임하기 위해서
ProjectService의 addCard(), softDeleteCard(), reorderCard() 들은 AOP 적용 대상이기 때문에 빈 등록 필요
LogAspect는 AOP의 Aspect 이기 때문에 등록할 필요가 있다.
@Aspect
: 이 클래스를 이제 AOP에서 사용하겠다.
@Around
: 대상 메서드 전역에서 이 메서드를 실행하겠다. // 런타임에 주입된다.
@Aspect
@Component
public class LogAspect {
@Autowired
private LogService logService;
@Autowired
private ProjectService projectService;
@Around("execution(* com.codesquad.todo2.domain.project.ProjectService.addCard(..))") //Pointcut
public Object logAddCard(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Long projectId = (Long) args[0];
Long categoryId = (Long) args[1];
Long userId = (Long) args[3];
CardDto cardDto = (CardDto) joinPoint.proceed(); //형변환 필요
Long cardId = cardDto.getId();
String cardTitle = cardDto.getTitle();
String dstCategoryTitle = logService.findCategoryTitleById(categoryId);
Project project = projectService.findProjectByIdOrHandleNotFound(projectId);
Log log = new Log(userId, cardId, cardTitle, null, dstCategoryTitle, "added");
project.addLog(log);
projectService.saveProject(project);
return cardDto;
}
@Around("execution(* com.codesquad.todo2.domain.project.ProjectService.softDeleteCard(..))")
public Object logDeleteCard(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Long projectId = (Long) args[0];
Long categoryId = (Long) args[1];
Long cardId = (Long) args[2];
Long userId = (Long) args[3];
boolean returnValue = (boolean) joinPoint.proceed(); //형변환 필요-본래 메서드의 리턴타입과 동일
String cardTitle = logService.findCardTitleById(cardId);
String srcCategory = logService.findCategoryTitleById(categoryId);
Project project = projectService.findProjectByIdOrHandleNotFound(projectId);
Log log = new Log(userId, cardId, cardTitle, srcCategory, null, "removed");
project.addLog(log);
projectService.saveProject(project);
return returnValue;
}
@Around("execution(* com.codesquad.todo2.domain.project.ProjectService.reorderCard(..))")
public Object logReorderCard(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Long projectId = (Long) args[0];
Long dstCategoryId = (Long) args[1];
CardIds requestBody = (CardIds) args[2];
Long userId = (Long) args[3];
Long cardId = requestBody.getCardId();
Project project = projectService.findProjectByIdOrHandleNotFound(projectId);
Category srcCategory = projectService.findCategoryByCardIdFromProject(project, cardId); //이전 카테고리
String sourceCategoryTitle = srcCategory.getTitle();
boolean returnValue = (boolean) joinPoint.proceed(); // 기준이 되는 시점
String cardTitle = logService.findCardTitleById(cardId);
project = projectService.findProjectByIdOrHandleNotFound(projectId);
String dstCategoryTitle = logService.findCategoryTitleById(dstCategoryId); //목적지 카테고리
if (sourceCategoryTitle.equals(dstCategoryTitle)) {
sourceCategoryTitle = null;
}
Log log = new Log(userId, cardId, cardTitle, sourceCategoryTitle, dstCategoryTitle, "moved");
project.addLog(log);
projectService.saveProject(project);
return returnValue;
}
}
오랜만에 올리셨네요! 나중에 참고해보겠습니다.
근데 저는 @Aspect를 사용하는 경우를 거의 보지 못한 것 같아서, 잘 사용하는지는 모르겠어요.
지난 2주간 고생 많으셨습니다!