반응형
상황
개인적으로 하는 프로젝트에서 AOP를 썼었다.
Refresh token을 쓰고 있었고, 이를 레디스에 저장하고 있었다. 토큰 속에는 닉네임을 subject 로 가지고 있었다.
문제는 닉네임을 수정하면 리프레쉬 토큰에는 변경이 되지 않아서 로그인이 안 되는 것. 그래서 레디스 속 리프레쉬 토큰을 변경해주어야 했고, 변경된 리프레쉬 토큰을 응답 쿠키에 담아주어야 했다.
또 유저 삭제 시에도 리프레쉬 토큰을 레디스에서 삭제하고, 응답 쿠키도 삭제해야했다.
이를 AOP로 구현한 코드는 아래와 같다.
간략하게 설명하면,
- @Pointcut 으로 각각 닉네임 수정 시점, 유저 삭제 시점 을 지정해주었고,
- @Around 로 수정 직후, 삭제 직후에 리프레쉬 토큰 처리를 해주었다.
@Slf4j(topic = "AuthAop")
@Aspect
@Component
@RequiredArgsConstructor
public class AuthAop {
private final JwtUtil jwtUtil;
private final RedisUtil redisUtil;
private final HttpServletResponse response;
@Pointcut("execution(* com.yunstudio.insight.domain.user.controller.UserController.changeNickname(..))")
private void forChangeNicknameMethod() {
}
@Pointcut("execution(* com.yunstudio.insight.domain.user.controller.UserController.deleteUser(..))")
private void forSoftDeleteUserMethod() {
}
@Around("forChangeNicknameMethod()")
public Object changeNickname(ProceedingJoinPoint joinPoint) throws Throwable {
// 유저 닉네임 변경 로직 처리
Object result = joinPoint.proceed();
// 응답 객체 변환
CommonResponse<UserChangeNicknameRes> commonResponse = (CommonResponse<UserChangeNicknameRes>) result;
UserChangeNicknameRes userChangeNicknameRes = commonResponse.getData();
// 변경 전후 닉네임
String oldNickname = userChangeNicknameRes.oldNickname();
String newNickname = userChangeNicknameRes.newNickname();
log.info("변경 전 닉네임 : {}", oldNickname);
log.info("변경 후 닉네임 : {}", newNickname);
// 변경 전 닉네임 로그아웃 처리
redisUtil.setUserLogout(oldNickname);
// 액세스 토큰 처리
addAccessTokenInHeader(newNickname);
// 리프레쉬 토큰 처리
changeRefreshToken(newNickname);
return result;
}
private void addAccessTokenInHeader(String newNickname) {
// 변경 후 닉네임으로 액세스 토큰 발생
String accessToken = jwtUtil.setTokenWithBearer(jwtUtil.createAccessToken(newNickname, UserRole.USER.getAuthority()));
// 응답 객체에 추가
response.addHeader(JwtUtil.ACCESS_TOKEN_HEADER, accessToken);
}
private void changeRefreshToken(String newNickname) {
// 변경 후 닉네임으로 리프레쉬 토큰 발생
String refreshToken = jwtUtil.createRefreshToken(newNickname, UserRole.USER.getAuthority());
// 리프레쉬 토큰 쿠키 생성
Cookie refreshTokenCookie = jwtUtil.createRefreshTokenCookie(refreshToken);
// 응답 객체에 쿠키 추가
response.addCookie(refreshTokenCookie);
// 변경 후 닉네임으로 레디스 로그인 처리
redisUtil.setUserLogin(newNickname, refreshToken);
}
@Around("forSoftDeleteUserMethod()")
public Object softDeleteUser(ProceedingJoinPoint joinPoint) throws Throwable {
// 유저 삭제 로직 처리
Object result = joinPoint.proceed();
// 응답 객체 변환
String softDeletedNickname = getSoftDeleteUserNicknameInResult((CommonResponse<UserDeleteRes>) result);
log.info("softDeletedNickname : {}", softDeletedNickname);
// 리프레쉬 토큰 쿠키 삭제
Cookie removeRefreshTokenCookie = new Cookie(JwtUtil.REFRESH_TOKEN_HEADER, null);
response.addCookie(removeRefreshTokenCookie);
// 레디스에서 로그아웃 처리
redisUtil.setUserLogout(softDeletedNickname);
return result;
}
private String getSoftDeleteUserNicknameInResult(CommonResponse<UserDeleteRes> result) {
UserDeleteRes userDeleteRes = result.getData();
return userDeleteRes.nickname();
}
}
문제
일단 문제점이 좀 있다.
- 닉네임 수정, 닉네임 삭제 처리가 한 클래스에 있다.
- 포인트컷 execution이 문자열로 되어 있어서 만약에라도 메소드명이 수정된다면 작동이 되지 않는다.
- @Around 는 핵심기능 실행 전후를 처리할 때 쓰인다. 하지만 위 로직들은 실행 직후에 처리하니 @After 를 써야한다.
- controller 로 리턴되는 객체를 가져다가 AOP를 적용하고 있었다. 서비스 직후로 좁혔다.
- 그리고 좀 더 솔직히 말하면 AOP까지 안 가고 서비스 내에서 처리하면 될 것 같은데 그냥 AOP 쓰고 싶었다.
해결
우선 닉네임 수정, 유저 삭제 어노테이션을 만들어 주었다.
// 닉네임 수정 어노테이션
package com.yunstudio.insight.domain.user.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ChangeRefreshTokenInRedis {
}
// 유저 삭제 어노테이션
package com.yunstudio.insight.domain.user.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface DeleteRefreshTokenInRedis {
}
그 다음 닉네임 수정 AOP 클래스, 유저 삭제 AOP 클래스를 나눠서 만들어 주었다.
// 닉네임 변경 AOP
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class ChangeRefreshTokenAop {
private final JwtUtil jwtUtil;
private final RedisUtil redisUtil;
private final HttpServletResponse response;
@AfterReturning(
value = "@annotation(com.yunstudio.insight.domain.user.aop.annotation.ChangeRefreshTokenInRedis)",
returning = "result"
)
public UserChangeNicknameRes changeNickname(UserChangeNicknameRes result) {
// 변경 전후 닉네임
String oldNickname = result.oldNickname();
String newNickname = result.newNickname();
log.info("변경 전 닉네임 : {}", oldNickname);
log.info("변경 후 닉네임 : {}", newNickname);
// 변경 전 닉네임 로그아웃 처리
redisUtil.setUserLogout(oldNickname);
// 액세스 토큰 처리
addAccessTokenInHeader(newNickname);
// 리프레쉬 토큰 처리
changeRefreshToken(newNickname);
return result;
}
private void addAccessTokenInHeader(String newNickname) {
// 변경 후 닉네임으로 액세스 토큰 발생
String accessToken = jwtUtil.setTokenWithBearer(jwtUtil.createAccessToken(newNickname, UserRole.USER.getAuthority()));
// 응답 객체에 추가
response.addHeader(JwtUtil.ACCESS_TOKEN_HEADER, accessToken);
}
private void changeRefreshToken(String newNickname) {
// 변경 후 닉네임으로 리프레쉬 토큰 발생
String refreshToken = jwtUtil.createRefreshToken(newNickname, UserRole.USER.getAuthority());
// 리프레쉬 토큰 쿠키 생성
Cookie refreshTokenCookie = jwtUtil.createRefreshTokenCookie(refreshToken);
// 응답 객체에 쿠키 추가
response.addCookie(refreshTokenCookie);
// 변경 후 닉네임으로 레디스 로그인 처리
redisUtil.setUserLogin(newNickname, refreshToken);
}
}
// 유저 삭제 AOP
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DeleteRefreshTokenAop {
private final RedisUtil redisUtil;
private final HttpServletResponse response;
@AfterReturning(
value = "@annotation(com.yunstudio.insight.domain.user.aop.annotation.DeleteRefreshTokenInRedis)",
returning = "result"
)
public UserDeleteRes softDeleteUser(UserDeleteRes result) {
String softDeletedNickname = result.nickname();
log.info("softDeletedNickname : {}", softDeletedNickname);
// 리프레쉬 토큰 쿠키 삭제
Cookie removeRefreshTokenCookie = new Cookie(JwtUtil.REFRESH_TOKEN_HEADER, null);
response.addCookie(removeRefreshTokenCookie);
// 레디스에서 로그아웃 처리
redisUtil.setUserLogout(softDeletedNickname);
return result;
}
}
그리고 서비스에 위의 AOP를 적용했다.
// 서비스 클래스 내의 AOP 적용 메서드
@Transactional
@ChangeRefreshTokenInRedis
public UserChangeNicknameRes changeNickname(User user, String newNickname) {
// 변경 전 닉네임
String oldNickname = user.getNickname();
// 닉네임이 존재하는지 검증
validateNewNickname(newNickname);
// 닉네임 변경
user.changeNickname(newNickname);
userRepository.save(user);
return new UserChangeNicknameRes(oldNickname, newNickname);
}
@Transactional
@DeleteRefreshTokenInRedis
public UserDeleteRes deleteUser(User user) {
// 소프트 딜리트
userRepository.delete(user);
return new UserDeleteRes(user.getNickname());
}
정리
변경점은 다음과 같다.
- @Around -> @AfterReturning 을 이용해서 메인로직이 성공적으로 처리완료하면 AOP가 적용되도록 수정했다. returning 속성은 이후 메서드의 매개변수를 어떤 변수명으로 받을 것인지 명시해뒀다.
- 사용하지 않는 JoinPoint 매개변수를 삭제했다.
- @Pointcut 어노테이션을 사용하지 않았다. 따로 뺄 만큼 시점을 구분할 필요 없이 어노테이션으로 해결가능해졌기 때문.
- AOP 메서드의 요청 및 응답 타입을 Object에서 구체적인 타입들(DTO)로 수정했다.
- 기존의 컨트롤러 직후 시점에서 서비스 직후 시점으로 범위를 좁혔다.
반응형
'TIL ✍️' 카테고리의 다른 글
24/07/31(수) 89번째 TIL : EC2 Ubuntu 사용자 데이터 스크립트 (2) | 2024.07.31 |
---|---|
24/07/30(화) 88번째 TIL : Access token & Refresh token 요청/응답 방식과 양식 (1) | 2024.07.30 |
24년 6월 24일(월요일) - 86번째 TIL : Querydsl 일대다 조회 시 중복 요소 처리 (0) | 2024.06.24 |
24년 6월 22일(토요일) - 85번째 TIL : 요청 DTO 검증 시 정규표현식으로 하기 (0) | 2024.06.23 |
24년 6월 20일(목요일) - 84번째 TIL : JPA 에서 SoftDelete 처리 (0) | 2024.06.20 |