개발 공부/프로젝트

클로드 코드로 구현하면서 겪은 트러블슈팅

baby-t 2026. 4. 22. 13:32

1편에서 이론을 정리했으니, 이번엔 Claude Code로 실제 구현하면서 겪은 트러블슈팅을 기록합니다.


1. Claude Code란?

터미널에서 실행하는 AI 코딩 도구입니다. 프로젝트 폴더에서 claude 명령어를 실행하면 프로젝트 구조를 파악하고, 파일 생성/수정/삭제까지 직접 해줍니다.

설치는 간단합니다.

Bash
 
npm install -g @anthropic-ai/claude-code
cd 프로젝트폴더
claude

2. 트러블슈팅 1: 계층형 구조가 깨진 문제

Claude Code가 처음 구현한 UserController를 보니 이런 코드가 있었습니다.

Java
    @PostMapping("/reissue")
    public ResponseEntity<TokenInfo> reissue(HttpServletRequest request, HttpServletResponse response) {
        String refreshToken = extractRefreshToken(request);
        RefreshTokenService.ReissueResult result = refreshTokenService.rotate(refreshToken);

        setRefreshCookie(response, result.refreshToken());
        return ResponseEntity.ok(new TokenInfo("Bearer", result.accessToken()));
    }

    private String extractRefreshToken(HttpServletRequest request) {
        if (request.getCookies() == null) throw new InvalidRefreshTokenException();
        return Arrays.stream(request.getCookies())
                .filter(c -> "refreshToken".equals(c.getName()))
                .findFirst()
                .map(Cookie::getValue)
                .orElseThrow(InvalidRefreshTokenException::new);
    }

    private void setRefreshCookie(HttpServletResponse response, String refreshToken) {
        Cookie cookie = new Cookie("refreshToken", refreshToken);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        cookie.setMaxAge(7 * 24 * 60 * 60);
        response.addCookie(cookie);
    }
}

 

컨트롤러에 HTTP 처리 관련 private 메서드들이 생겨버렸습니다. 게다가 LoginFilter에도 똑같은 setRefreshCookie 메서드가 중복으로 존재했습니다. 컨트롤러는 요청을 받아서 서비스로 넘기는 역할만 해야 하는데, 쿠키 처리 로직이 섞여버린 거죠.

💡 해결: 유틸 클래스 분리 및 어노테이션 활용

CookieUtil 유틸 클래스를 만들어서 쿠키 관련 로직을 분리했습니다.

Java
 
public class CookieUtil {
    public static void setRefreshCookie(HttpServletResponse response, String token) { /* ... */ }
    public static void clearRefreshCookie(HttpServletResponse response) { /* ... */ }
}

그리고 @CookieValue 어노테이션으로 더 깔끔하게 쿠키를 받도록 변경했습니다.

Java
// Before
public ResponseEntity<TokenInfo> reissue(HttpServletRequest request, /* ... */) {
    String refreshToken = CookieUtil.extractRefreshToken(request);
    // ...
}

// After
public ResponseEntity<TokenInfo> reissue(
    @CookieValue(value = "refreshToken", required = false) String refreshToken, /* ... */) {
    
    if (refreshToken == null) throw new InvalidRefreshTokenException();
    // ...
}

3. 트러블슈팅 2: 401이 아니라 403이 반환되는 문제

Access Token을 잘못된 값으로 바꾸고 테스트했는데, 예상한 401이 아니라 403이 계속 반환됐습니다. 프론트의 axios interceptor는 401을 감지해야 재발급 로직이 타는데, 403이 오니까 interceptor가 동작하지 않았습니다.

예상대로 돌아가지 않음

원인은 JwtFilter에 있었습니다.

Java
// 문제의 코드
if (jwtUtil.validateToken(token)) {
    // 인증 성공 처리
}

// 토큰이 유효하지 않아도 그냥 다음 필터로 넘겨버림
filterChain.doFilter(request, response);

토큰 검증에 실패해도 그냥 다음 필터로 넘기고 있었습니다. 그러면 SecurityContextHolder에 인증 정보가 없는 채로 Spring Security로 넘어가고, Spring Security가 인증되지 않은 사용자로 판단해 기본값으로 403을 반환한 거죠.

💡 해결: 토큰 검증 실패 시 직접 401 반환

토큰이 유효하지 않으면 직접 401을 반환하도록 수정했습니다.

Java
if (!jwtUtil.validateToken(token)) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}

처음에는 response.sendError(401)을 썼는데 이것도 Spring Security 내부에서 다시 403으로 바뀌는 문제가 있었습니다. response.setStatus(401)로 바꾸니 깔끔하게 해결됐습니다.

4. 최종 동작 확인

테스트 방법은 간단합니다. 브라우저의 localStorage에서 accessToken 값을 아무 문자열로 바꾸고 API 요청을 해보면 됩니다. Network 탭에서 reissue 요청이 자동으로 호출되고, 이후 원래 요청이 성공하면 정상 동작하는 것입니다.

5. 마무리 후기

Claude Code는 정말 대단한 도구입니다. Access Token + Refresh Token + Rotation을 직접 구현하려면 블로그를 찾아보며 하루 종일 걸렸을 텐데, 프롬프트 몇 줄로 빠르게 구현해줬습니다.

근데 AI가 완벽하지 않다는 것도 확인했습니다. 계층형 구조가 깨지는 문제, 403이 반환되는 문제 등은 AI가 스스로 잡아내지 못했습니다. 결국 코드의 동작 원리를 이해하고 있어야 AI의 결과물을 검증하고 개선할 수 있다는 걸 뼈저리게 느꼈습니다.

AI를 잘 활용하는 개발자가 되려면, 오히려 더 깊이 있는 아키텍처 이해가 필요하다는 걸 이번에 다시 한번 깨달았습니다.