일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 연습문제
- Scaffold
- 디프만16기
- 디프만
- pub.dev
- nGrinder
- 자료구조
- Redis
- Oidc
- 운영체제
- depromeet
- Kafka
- 부하 테스트
- 코딩테스트
- Spring
- exception
- dip
- C언어
- 코딩
- flutter
- kakao
- 코드 트리
- Kotlin
- java
- 코드트리
- Sharding
- 코딩 테스트
- c
- OAuth
- AOP
- Today
- Total
Nick Dev
[Outstagram] AOP를 통해 횡단 관심사(cross-cutting concern) 걷어내기 본문
고민하게 된 계기
프로젝트 전반에서 현재 로그인된 유저의 정보를 가져오는 일이 많다
- '나의 게시물', '게시물 작성' 등 많은 로직에서 현재 세션에 있는 유저 정보를 필요로 함
필요로 할 때마다 세션에서 유저 정보 가져오는 것은 중복된 코드가 너무 많아짐 -> 한 곳에서 처리하자!!
초기 해결 방식
ArgumentResolver
를 통해 공통 관심사를 걷어내자
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
/**
* @Login 애노테이션 붙어있으면서 UserDTO 타입이면 해당 ArgumentResolver 사용
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation =
parameter.hasParameterAnnotation(Login.class);
boolean hasUserType =
UserDTO.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasUserType;
}
/**
* 컨트롤러 호출되기 직전에 호출되서 세션에서 유저 정보 찾아서 반환해준다. 없으면 null 리턴
*/
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest)
webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(LOGIN_USER);
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* LoginMemberArgumentResolver 등록
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
Argument Resolver
- 컨트롤러 메서드가 호출될 때, 필요한 매개변수를 해석해주는 역할
supportsParameter
메서드 구현
해당하는 애노테이션과 타입이 맞는지 확인해서 특정 행위를 할지 말지 결정
즉,
resolveArgument
수행 여부를 결정하는 메서드임애노테이션이
@Login
이고, 애노테이션을 받는 타입이UserDTO
라면resolveArgument
를 수행함
resolveArgument
메서드 구현
supportsParameter
가true
라면 실행되는 메서드로, 실제 세션에서 유저 정보를 조회해 리턴해준다현재 요청의 세션에서 유저 세션 정보를 가져옴
- 이때,
getSession(false)
로 가져옴 - 디폴트가
true
인데, 만약 세션이 없다면 새로 생성해서 가져오기 때문에, 이를 방지하기 위해false
로 가져옴
- 이때,
📌문제점
Argument Resolver로 구현한
@Login
은 컨트롤러 메서드의 매개변수를 동적으로 처리해줌그래서 서비스 계층 등의 다른 계층에서는
@Login
애노테이션을 사용할 수 없음그럼 어디서든지 사용할 수 있는 애노테이션으로 만들기 위해서는 Argument Resolver가 아니라 AOP로 구현하는게 맞다!
AOP 도입
AOP란?
AOP는 로직(code) 주입이라고 생각하며 편함
특정 메서드 실행을 기점으로 앞 뒤로 로직을 주입하고 메서드를 진행
이때, 주입될 코드(
Advisor
)들을@Aspect
에 정의해놓고, 어떤 메서드(Pointcut
)에 어떤 시점(Advice
)에 주입할지 명시하면 된다AOP를 도입함으로써, 자연스럽게 SRP를 지키게 된다
하나의 메서드가 하나의 역할을 할 수 있도록 만들어줌
핵심 관심사만 남겨두고 공통 관심사는 AOP를 통해 코드 주입해주면 되니까
AOP를 도입한 이유
컨트롤러 메서드의 매개변수뿐만 아니라 서비스 계층 등 다른 계층에서도
@Login
애노테이션을 사용하기 위해서 AOP 도입
AOP 코드
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* *(.., @com.outstagram.outstagram.common.annotation.Login (*), ..))")
public void loginRequired() {
}
@Around("loginRequired()")
public Object checkSession(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("AOP - @Login Check Started");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
HttpSession session = request.getSession(false);
if (session == null) {
log.info("AOP - @Login Check Result - session empty");
throw new ApiException(ErrorCode.UNAUTHORIZED_USER);
}
UserDTO user = (UserDTO) session.getAttribute(LOGIN_USER);
if (user == null) {
log.info("AOP - @Login Check Result - user empty");
throw new ApiException(ErrorCode.UNAUTHORIZED_USER);
}
/*
joinPoint 안는 현재 실행중인 메서드에 대한 정보들을 담고 있음(메소드 이름, 타입, 파라미터)
getArgs() : 현재 메서드에 전달된 파라미터들을 객체 배열 형태로 리턴
이 파라미터들 중에서 타입이 UserDTO인 것을 찾아 현재 세션의 유저 정보를 넣어줄 것임
*/
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof UserDTO) {
args[i] = user;
}
}
/*
@LoginSession UserDto user -> 이 user에 현재 session에 있는 유저를 넣어준다
위에서 변경한 파라미터를 적용하려면 파라미터 배열을 proceed() 메서드에 전달해야 함
*/
return joinPoint.proceed(args);
}
}
@Aspect
- 해당 클래스가
@Pointcut
- 어떤 메서드가 Advice의 대상이 되어야 하는지 알려주는 애노테이션
@Pointcut("execution(* *(.., @com.outstagram.outstagram.common.annotation.Login (*), ..))")
public void loginRequired() {
}
@Login
애노테이션이 적용된 파라미터를 포함하는 모든 메서드가loginRequired()
메서드(Advice
)의 대상이 되도록 지정함- 즉,
@Login
이 달려있는 모든 메서드는loginRequired()
를 실행하게 한다
(실제 로직은checkSession
에 정의되어 있음)
@Around
- 실제
Advice
를 정의하는 부분으로 지정된JoinPoint
주위에 추가적인 로직을 실행할 수 있게 함
@Around("loginRequired()")
public Object checkSession(ProceedingJoinPoint joinPoint) throws Throwable {
// 세션 검사 로직 구현
}
loginRequired()
에 정의된 메서드들이 실행될 때마다checkSession()
메서드가 호출된다
=> 즉,@Login
애노테이션이 포함된 메서드를 실행할 때마다checkSession()
메서드가 실행된다는 말
checkSession()
메서드
현재
request
의session
을 꺼내서 유저가 로그인했는지 확인한다- 세션 꺼낼 때,
false
로 꺼내기 (true
로 꺼내면 세션 없다면 새로 생성해서 반환함) - 만약 유저가 최근에 로그인을 했다면, 세션에 유저 정보가 남아 있을 것
- 세션 만료가 지났거나, 세션에 유저 정보가 없다면 예외 발생시킴
- 세션 꺼낼 때,
joinPoint
- 실제 호출된 메서드에 대한 정보(어떤 객체 소유의 메서드인지, 호출된 메서드의 파라미터는 무엇인지...)를 확인할 수 있음
- 이 정보들을 순회하면서 type이
UserDTO
라면 해당 파라미터에 세션에서 찾은user
객체를 넣어준다
proceed()
메서드를 통해 위에서 변경한 파라미터를 실제 메서드에 적용- 이 메서드 호출하지 않으면 위에서 주입한
user
객체가 실제 메서드에 반영되지 않음
- 이 메서드 호출하지 않으면 위에서 주입한
정리
Argument Resolver -> AOP로 리팩토링한 이유
@Login
애노테이션을 컨트롤러 메서드이 매개변수로만 사용하지 않고, 다른 여러 계층의 메서드의 매개변수로도 사용하기 위해서 AOP로 리팩토링 진행
'Outstagram' 카테고리의 다른 글
[Outstagram] 왜 같은 클래스에서 @Cacheable 달린 메서드 호출하면 씹힐까... (0) | 2024.12.12 |
---|---|
[Outstagram] kafka를 활용한 피드 push model 구현 과정 (0) | 2024.12.12 |
[Outstagram] 무한 스크롤 구현하려다 Snowflake ID 도입한 이야기 (0) | 2024.12.12 |
[Outstagram] 좋아요 동시성 문제 해결 (1) | 2024.12.11 |
[Outstagram] 이미지 처리 추상화 (0) | 2024.12.11 |