본 글은 천청향로님의 블로그 글을 참고하고 작성되었습니다.
지난 포스트 AOP3에서 AOP의 필요성과 용어에 대해서 알아봤었는데요. 오늘은 직접 구현해보고 그 안에 사용되는 코드에 대해 알아보도록 하겠습니다.
구현 실습
일단 실습환경은 SpringBoot + JPA + H2 Gradle 환경이구요. ApiQuestionController
내 질문 생성, 업데이트폼 요청, 업데이트, 삭제 요청을 RestController를 통해 받는 상황입니다. RestController로 구성한 이유는 HttpStatus값을 이용하여 테스트하기 위함입니다. 그럼 코드를 보도록 하겠습니다.
@RestController
@RequestMapping("/questions")
public class ApiQuestionController {
@Autowired
private QuestionRepository questionRepository;
@PostMapping("")
public ResponseEntity<Void> post(Question question, HttpSession session) {
if(!HttpSessionUtils.isLogin(session)) throw new UnAuthenticationException("로그인이 필요합니다.");
// ..
// 내부 로직
// ..
return new ResponseEntity<Void>(HttpStatus.OK);
}
@GetMapping("/{id}/form")
public ResponseEntity<Void> showUpdateForm(@PathVariable Long id, HttpSession session) {
if(!HttpSessionUtils.isLogin(session)) throw new UnAuthenticationException("로그인이 필요합니다.");
// ..
// 내부 로직
// ..
return new ResponseEntity<Void>(HttpStatus.OK);
}
@PutMapping("/{id}")
public ResponseEntity<Void> updateQuestion(@PathVariable Long id, Question updatedQuestion, HttpSession session) {
if(!HttpSessionUtils.isLogin(session)) throw new UnAuthenticationException("로그인이 필요합니다.");
// ..
// 내부 로직
// ..
return new ResponseEntity<Void>(HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id, HttpSession session) {
if(!HttpSessionUtils.isLogin(session)) throw new UnAuthenticationException("로그인이 필요합니다.");
// ..
// 내부 로직
// ..
return new ResponseEntity<Void>(HttpStatus.OK);
}
}
위 코드를 보면 아래와 같이 Session
을 통해 로그인한 유저인지 확인하는 부분이 '부가적인 기능'의 관점으로 보면 중복됨을 알 수 있습니다.
if(!HttpSessionUtils.isLogin(session)) throw new UnAuthenticationException("로그인이 필요합니다.");
그럼 이 부분을 loginAspect 작성을 통해 해결해보겠습니다(step1 브랜치를 통해 이 과정을 볼 수 있습니다)!
@Aspect
@Component
public class LoginAspect {
private static final Logger log = getLogger(LoginAspect.class);
@Pointcut("execution(* aop.question.ApiQuestionController.*(..)) && args(session,..)")
public void getSession(HttpSession session) {
}
@Around("getSession(session)")
public Object sessionCheck(ProceedingJoinPoint jp, HttpSession session) throws Throwable {
if(!HttpSessionUtils.isLogin(session)) {
log.info("====== aop 예외 발생 ======");
throw new UnAuthenticationException("로그인이 필요합니다.");
}
return jp.proceed();
}
}
위와 같은 Asepct는 만들고 ApiQuestionController
내 세션 확인하는 부분은 모두 지웠습니다. 테스트는 로그인을 하지 않은(세션값이 없는) 유저로 요청을 보냈는데요. AOP내에서 예외가 발생했음을 체크하기 위해 Log값을 찍어놓았습니다. 과연 제대로 작동할까요?
그리고 결과보면 Log값을 통해 AOP가 제대로 호출되고 그 안에서 Session 값을 확인하여 에러를 보낸 것을 확인할 수 있습니다. 그리고 테스트 결과에서 로그인하지 않았을 때 HttpStatus 응답값인 HttpStatus.UNAUTHORIZED
을 제대로 받았음을 확인할 수 있습니다!
위 코드 중 가장 낯선 부분이 여러 개 나오는데요. 이 부분에 대해 잠깐 짚고 넘어갈께요.
어드바이스 종류
위에서 @Around
가 '어드바이스'라고 하는 부분입니다. 어드바이스는 AOP의 '언제' 호출될 지를 의미합니다. @Around
의 경우 포인트컷 표현식으로 지정된 부분에 대해 실행 전, 후로 처리가 가능한 메서드입니다. 보통은 이 @Around
를 많이 사용된다고 하는데요. 이 타입 말고도 총 5가지의 타입이 존재합니다.
@Before
: 어드바이스 타켓 메소드 호출 전에 어드바이스 기능 수행@After
: 성공, 예외 상관없이 어드바이스 타켓 메소드 호출 후에 어드바이스 기능 수행@AfterReturning
: 어드바이스 타켓 메서드가 성공적으로 반환된 후 수행@AfterThrowing
: 어드바이스 타켓 메서드에서 예외 발생시 수행@Around
: 어드바이스 타켓 메서드 실행 전, 후로 수행
여기서 몇 가지 주의할 점이 있는데요. @Around
의 경우 어드바이스 메서드의 매개변수로 ProceedingJoinPoint
타입의 변수를 받고 proceed()
메서드를 호출해야 한다는 것입니다. 이 메서드가 실행되어야 어드바이스 타겟 메서드가 실행이 되거든요!
포인터컷 표현식
위에서 포인트컷 표현식으로 "execution(* aop.question.ApiQuestionController.*(..)) && args(session,..)"
을 썼는데요. 이것과 더불어 다른 종류 몇가지만 더 알아볼게요.
args()
- 타겟 메서드 인자 조건을 명시합니다.
..
은 여러개가 올 수 있다는 뜻이구요. 만약args(session)
까지만 했더라면 매개변수가 한개인 것만 불렀을 것입니다. - 그리고 매개변수를 받고 싶을 때는 사용될 수 있는 표현식이 정해져있습니다. 위와 같이 맨 처음에 둬도 되구요. 또
args(.., session)
과 같이 제일 끝에 두어도 됩니다. 하지만args(.., session, ..)
라는 표현은 안된다는 점을 기억해주세요!
- 타겟 메서드 인자 조건을 명시합니다.
execution()
- 위에서 썻던 표현식 중에 하나죠. execution은 접근제한자, 리턴타입, 인자타입, 클래스/인터페이스, 메서드명, 파라미터타입, 예외타입 등을 조합가능한 가장 세심한 지정자입니다.
- 가장 많이 사용되는 표현식입니다.
within()
- 저희 qna-atdd 프로젝트에도 있었던 표현식이죠.
@Pointcut("within(codesquad.web..*) || within(codesquad.service..*)")
와 같이 사용되었습니다. - 클래스 또는 인터페이스 단위까지 범위 지정이 가능합니다.
@Pointcut("within(codesquad.web.*)
는codesquad.web
에 정의된 클래스들을 의미하고 하위 패키지는 해당하지 않습니다.- 반면
@Pointcut("within(codesquad.web..*)
는codesquad.web
이하 하위 패키지까지 모두 포함합니다.
- 저희 qna-atdd 프로젝트에도 있었던 표현식이죠.
@annotation
- 타켓 메소드에 어노테이션이 지정된 경우
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginCheck { }
- 위와 같이 Annotation을 정의하고 포인트컷 표현식은
@Pointcut("@annotation(LoginCheck) && args(session,..)")
와 같이 사용하면 해당 어노테이션과 인자를 가진 타켓 메서드에 적용이 됩니다.
이 외에도 다양한 포인터컷 표현식들이 있는데요. execution()과 @annotation이 가장 많이 사용되고 있는 것 같습니다. 오늘은 여기까지 하구요! 다음엔 Filter, Interceptor, Resolver, AOP를 단순 비교하면서 살펴보려고 합니다. 비슷한 기능을 하는 것 같지 않나요? 전 좀 헷갈리더라구요..