프로젝트에 세션 기반 로그인을 사용하면서 현재 로그인한 사용자의 세션 정보를 가져와 유효성을 검증하는 코드가 중복으로 들어갔다. 이러한 문제는 코드의 유지 보수성을 저하시킨다.
이번 글은 커스텀 어노테이션과 HandlerMethodArgumentResolver 개념을 간단하게 알아보고 적용 과정을 기록했다.
모든 API에서 현재 로그인한 사용자 정보가 필요한 것은 아니다. 따라서 사용자 정보를 필요로 하는 특정 API에서만 바인딩 되게 하기 위해 어노테이션을 커스텀 한다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
@interface를 사용해서 커스텀 어노테이션을 만든다. 코드를 자세히 살펴보면 다음과 같다.
Target
어노테이션이 생성될 위치
파라미터로 사용하기 위해 ElementType.PARAMETER로 설정했다.
Retention
어노테이션의 라이프 사이클(생명 주기)를 설정한다.
애플리케이션의 실행 동안 유지하기 위해 RUNTIME으로 설정한다.
Strategy interface for resolving method parameters into argument values in the context of a given request.
HandlerMethodArgumentResolver
는 인터페이스로, 메서드의 매개변수를 인자값으로 변경시켜주는 역할을 한다.
해당 Resolver를 사용한 이유는 파라미터로 사용되는 인자값에 공통적으로 처리해야 되는 로직을 적용시키기 위함이다.
HandlerMethodArgumentResolver
는 두 개의 메서드를 구현해야 한다.
boolean supportsParameter(...)
주어진 메서드의 parameter가 현재 구현 중인 Argument Resolver에서 지원하는 타입인지 검사
Object resolveArgument(...)
메소드의 파라미터를 argument로 바인딩
CurrentUserResolver
구현
@Component
@RequiredArgsConstructor
public class CurrentUserResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
private final MemberRepository memberRepository;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class)
&& parameter.getParameterType().equals(Member.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
SessionInfo sessionInfo = (SessionInfo) httpSession.getAttribute("sessionInfo");
if (sessionInfo == null) throw new NoSuchMemberException("잘못된 세션 정보입니다.");
Long memberId = sessionInfo.getId();
return memberRepository.findById(memberId).orElseThrow(
NoSuchMemberException::new);
}
}
supportsParameter()
컨트롤러 메서드에 커스텀한 CurrentUser 어노테이션이 붙었는지, 파라미터로 붙은 객체의 타입이 Member인지 검사한다.
resolveArgument()
HTTP 요청에서 세션 정보를 가져온다.
세션 정보에서 사용자의 ID(PK)를 통해 사용자의 정보를 조회한다.
유효한 사용자인 경우 Member 객체를 반환한다.
(참고) Member & SessionInfo 코드
@Entity
@NoArgsConstructor
@Getter
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String password;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SessionInfo {
private Long id;
private String email;
}
WebConfig
코드
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final CurrentUserResolver currentUserResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserResolver);
}
}
WebMvcConfigurer를 구현한 WebConfig에 이전에 만든 CurrentUserResolver를 등록한다.
ProblemController
@Operation(summary = "문제 등록")
@PostMapjaping
public ResponseEntity<Void> save(@CurrentUser Member member,
@RequestBody @Valid ProblemSaveRequest request) {
problemService.save(member, request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
ProblemService
public void save(Member member,
ProblemSaveRequest request) {
Problem problem = request.toEntity(member);
problemRepository.save(problem);
}