- 개선이 필요한 나쁜코드 ? 반복되는 코드
- 프로젝트에서 개선할 만한 부분? 로그인한 유저의 정보를 가져오는 부분
원래 프로젝트에서 로그인 한 유저에 대한 정보를 가져오는 코드는 아래와 같다.
public String main(@CookieValue(required = false, name = "access_token")
String token, Model model) {
if (token != null) {
String userEmail = tokenProvider.getUserEmailByToken(token);
User user = userService.getUserByEmail(userEmail);
model.addAttribute("user", user);
token에서 userEmail정보를 빼온 이후, userEmail 정보를 이용하여 해당하는 유저의 정보를 넘겨받는 형식인데, 우리 프로젝트의 특성상 유저의 정보가 어디서든 필요하고 + 프로젝트의 볼륨이 작지 않다보니 이 대략 5줄의 코드가 모든 컨트롤러에 존재하는 게 상당히 보기 좋지 않은 요인이라고 생각했고 모듈화 시킬 수 있는 방법을 고민했다.
튜터님의 도움을 받아 userArgumentResolver
라는 개념에 대해 알게되었다. 컨트롤러 메서드에서 특정 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩해주는 인터페이스인 HandlerMethodArgumentResolver
를 상속받아 만드는, 로그인 한 유저를 판별하는 Resolver이다.
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return User.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof AnonymousAuthenticationToken)) {
return authentication.getPrincipal();
}
return null;
}
}
supportsParameter()
- 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단합니다.
- 해당 코드에서는 파라미터 클래스 타입이 User.class인 경우 true를 반환합니다.
resolveArgument()
- 파라미터에 전달할 객체를 생성합니다.
- 해당코드에서는 SpringSecurity의 Principal 객체를 가져옵니다.
return User.class.isAssignableFrom(methodParameter.getParameterType());
에서
User는 SpringSecurity에 principal로 등록한 객체를 의미한다.
원래 코드에서는 Java에서 제공하는 User라이브러리를 사용해 유저아이디와 비밀번호, 권한만 넘겨서 인증받는 객체를 principal에 등록했었는데, 로그인 한 유저의 정보 전체를 받아오고 싶었기 때문에 코드를 수정해주었다.
if (authToken.validate()) {
Claims claims = authToken.getTokenClaims();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()})
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
log.debug("claims subject := [{}]", claims.getSubject());
//수정전
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);
//수정후
User principal = userRepository.findByUserEmail(claims.getSubject()).orElseThrow();
return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);
로그인 된 이후, 로그인 된 유저 전체의 정보가 넘어가는 경우가 빈번했기 때문에 토큰이 유효하다면 토큰의 subject값으로 등록해 준 user의 email값을 parameter로 받아서, DB에서 저장 된 정보 자체를 principal 객체로 등록해서 넘겨주었다.
주의사항
could not initialize proxy 에러 발생
Principal객체에 DB에서 가져 온 유저 정보 자체를 담아서 넘겨서, 아래와 같이 다른 서비스에서 User DB에 접근해야할 때, 접근 과정을 줄이는 효과를 보고자했지만, 영속성의 문제로 그건 불가능했다.
(db에 접근하는 과정을 줄이고 싶다면 연관관계 메소드를 사용하지 않는 방식뿐인 것 같다.)
아래 캡쳐를 보면 securityUser로 넘어온 객체는@15260
, 이후 연관관계 메소드를 사용해주기 위해서 DB에 재접근하여 불러온 User 개체는@15710
으로 서로 다른 객체임을 확인할 수 있다.
//자체 생성한 클래스에 원하는 정보를 토큰에서 빼와 담는 식으로 구성
if (authToken.validate()) {
Claims claims = authToken.getTokenClaims();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()})
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
String userNickname = claims.get(NICKNAME_KEY, String.class);
RoleType roleType = RoleType.of(claims.get(AUTHORITIES_KEY).toString());
User principal = userRepository.findByUserEmail(claims.getSubject()).orElseThrow();
log.debug("claims subject := [{}]", claims.getSubject());
return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);
(+)resolver를 사용하기 위해선 webConfig로 resolver를 꼭 등록해줘야한다.
처음에 등록하지 않고 무턱대고 사용했어서 계속 null값만 리턴받는 오류를 마주했었다.
webConfig 클래스를 만들어 꼭 등록해주도록하자
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
}