
우테코 4차 데모데이에서는 이전 데모데이와는 다르게 크루들이 직접 돌아다니며 서비스를 이용해보고 이에 대한 피드백을 받을 수 있다. 데모데이 이후 <방끗에 대한 솔직한 의견을 들려주세요>에 적힌 많은 피드백들을 나열해보았고 이중 꼭 필요하다고 생각되는 기능들을 레벨 4동안 구현/보안하고자 했다.

그중 하나가 레벨 4에 추가된 비회원 기능이다. 4차 데모데이까지 방끗 서비스는 카카오 로그인 기능만 제공하고 있었으며 페이지에 접근하기 위해서는 반드시 로그인을 해야 했다. 데모데이를 준비하며 팀 내에서도 체크리스트 작성 페이지전까지는 로그인 기능을 풀자, 아니다에 대해 여러 의견이 있었지만 당시에는 프론트와 백엔드간의 일정 조율이 힘들어서 단순하게 모든 API가 인증된 사용자만 접근할 수 있도록 개발을 진행했다. 마음 한편으로는 그래도 많이들 우리 서비스를 한 번 이용해보지 않을까하는 생각이 있었다.
하지만 생각보다 훨씬 더 로그인 기능이 사용자 유입에 장애물이 되었다. 크루들이 랜딩 페이지에서 카카오 로그인 버튼을 누르기 전 멈칫하는 순간을 자주 보았다. 나조차도 다른 팀의 서비스를 구경할 때 회원가입/로그인이 필요한 서비스이면 귀찮아서 접속하지 않게 된다는 걸 깨달았다.
방끗처럼 사용자에게 아직 익숙하지 않은 서비스라면, 서비스의 매력을 충분히 보여주어 사용자가 직접 경험해보고 싶도록 만드는 것이 중요하다. 초반에 많은 유저들이 부담없이 서비스를 경험할 수 있도록 비회원 기능의 도입이 필수적이라고 생각했다.
지금까지는 비회원 기능이라고 표현했지만, 이제는 방끗 둘러보기로 표현을 대체한다. 비회원으로 다른 쇼핑몰처럼 무언가 특정 행위를 할 수 있는 것이 아닌 로그인하지 않은 사용자도 특정 페이지로 접근할 수 있도록 권한을 푸는 작업이기 때문이다. 이번 글에서는 기존의 코드를 어떻게 변경했는지 설명한다.
모든 API는 인증된 사용자만 접근할 수 있다. 컨트롤러에 도달하기 전 ArgumentResolver에서 User객체를 반환하는데 이때 토큰이 존재하지 않으면 예외를 발생시킨다.
@Component
public class AuthPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
...
@Override
public boolean supportsParameter(MethodParameter parameter) {
return User.class.isAssignableFrom(parameter.getParameterType())
&& parameter.hasParameterAnnotation(AuthPrincipal.class);
}
@Override
public User resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
if (request.getCookies() == null) {
throw new BangggoodException(ExceptionCode.AUTHENTICATION_COOKIE_EMPTY);
}
String token = extractToken(request.getCookies());
return authService.extractUser(token);
}
컨트롤러는 @AuthPrinciapl 어노테이션을 사용한다.
@PostMapping("/checklists")
public ResponseEntity<Void> createChecklist(@AuthPrincipal User user,
@Valid @RequestBody ChecklistRequest checklistRequest) {
long checklistId = checklistManageService.createChecklist(user, checklistRequest);
return ResponseEntity.created(URI.create("/checklists/" + checklistId)).build();
}
@GetMapping("/checklists/{id}")
public ResponseEntity<SelectedChecklistResponse> readChecklistById(@AuthPrincipal User user,
@PathVariable("id") Long checklistId) {
return ResponseEntity.ok(checklistManageService.readChecklist(user, checklistId));
}
예시용 체크리스트 때문에 어노테이션을 제거하는 것만으로 문제를 해결할 수 없었다. 예시용 체크리스트는 서비스에 대한 사용자의 이해를 돕기 위해 디폴트로 추가된 체크리스트이다. 회원가입시 사용자별로 생성된다. 방끗 둘러보기를 구현하면서 로그인하지 않은 사용자도 해당 체크리스트를 볼 수 있도록 구현되길 원했다.

체크리스트를 전부 조회하는 /checklists 는 유저의 id를 기반으로 체크리스트를 조회한다. 로그인하지 않은 사용자는 id값이 없기 때문에 해당 API를 요청하면 예외가 발생하는 상황이다.
먼저 떠올린 방법은 어드민 사용자를 만드는 것이다. 어드민 사용자가 예시용 체크리스트를 가지고 있도록 하고 이를 모든 사용자(로그인하지 않은 사용자까지도)에게 보여주는 것이다. 하지만 서비스단의 코드가 너무 많이 변경됐다. 체크리스트를 조회할 때 Checklist뿐만 아니라 여러 서비스에서 엔티티를 조회하는데 이때 어드민 유저의 체크리스트도 조회하기 위한 코드가 필요한 모든 서비스에 추가돼야 한다. 또한 예시용 체크리스트도 좋아요, 삭제, 편집 기능이 모두 가능한데 지금의 구조로는 일반 유저가 접근할 수 없다. 자신의 체크리스트가 아니므로 401 UnAuthorized 예외가 발생한다. 마지막으로 예시용 체크리스트는 언제든 다른 방식으로 대체될 수 있다고 생각해서 이를 위해 서비스의 많은 코드가 변경되는 것이 좋은 방법이 아니라 생각했다.
서비스쪽을 최대한 변경하지 않는 방법을 고민하다보니 컨트롤러보다 앞단에서 문제를 해결해보고자 했다. @AuthPrincipal외에 @UserPrinciapl를 추가로 도입했다.
게스트 유저는 애플리케이션이 실행될 때 최초로 한 번 저장되며 자신만의 예시용 체크리스트와 질문들을 가지고 있다. 로그인하지 않은 사용자가 특정 API를 호출할 때 @UserPrincipal이 붙어있다면 게스트 유저가 할당되어 게스트 유저의 체크리스트와 질문들을 조회한다. 그러나 체크리스트 작성과 같이 반드시 토큰이 있어야만 호출 가능한 API에는 @AuthPrincipal를 사용해 토큰이 없으면 예외가 발생하도록 한다.
<@AuthPrincipal 어노테이션을 사용하는 API 요청>

<@UserPrincipal 어노테이션을 사용하는 API 요청>

참고
해당 PR에서 구현 방식을 확인해볼 수 있다.
맨 하단에 각 설명에 따른 사진이 바뀐 것 같아요!