20260304 [TIL] - 코드 개선

2026. 3. 3. 15:22·IL/TIL

0️⃣ 에러 분석

포크 받았는데 yml이나 properties resource 파일 어디 갔을까요. 나 보려고 했는데 일단 넘깁시다.

 

일단 에러를 분석해야 되니까 밑에서부터 차근차근 봅시다.

정의 되어 있지 않단다

그리고 그 위

그래서 jwtUtil이 생성될 수 없었군요?
bean으로 생성이 안 돼서 연관되어 있는 것들이 되지 않았다...... 뭐 그런 얘기 같네요
그래서 나 못해먹겟다까지

 

오케이 대충 알겠고...... 전체 검색으로 찾아보자 무슨 짓을 하셨나요?

?

일단 `@Value` 어노테이션이란?

properties에 보관되어 있는 값을 가져오는 역할을 한대요. 구글링.....

없다 리소스가......

생각해 보니까 어이가 없습니다. 인텔리제이에서 프로젝트 만들면 리소스 자동으로 일단 세팅해 주고 시작하는데 이게 없다구요? ㅎㅎ 처음부터 내 세팅에 맞추려고 확인해 봤는데 없을 때부터 알아봤습니다, 제가.

만들어 주마......

spring:
  application:
    name: expert

  datasource:
    url: jdbc:mysql://localhost:3306/expert
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: 
    password: 

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect

logging:
  level:
    org.hibernate.type: trace

jwt:
  secret:
    key: "PI1v50JKao5QZoxY3J3KpUWavk1y9AKcih3SAoofZ/4="

secret key에다가는 base64 랜덤으로 생성해 달라고 했다. 강의에 나온대로 그 사이트에 들어가서 해야 될 것 같은데.

base64 문자열 랜덤 생성 사이트입니다.

https://www.base64encode.org/

 

Base64 Encode and Decode - Online

Encode to Base64 format or decode from it with various advanced options. Our site has an easy to use online tool to convert your data.

www.base64encode.org

 

아 데이터베이스도 생성해 준다. 까먹지 말자.

성공

 


 

1️⃣ ArgumentResolver 

일단 포스트맨으로 회원가입 로그인 하고 생성을 해 보려고 하니까 오류가 발생한다

일단 ArgumentResolver를 가 보면 

얘가 회색으로 사용 중이지 않다고 뜹니다.

WebConfig 어디 갔지? 이거 등록해 주는 것 있어야 되는데 없네.

package org.example.expert.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthUserArgumentResolver());
    }
}

만들어 주자 등록하자

세상이 밝아졌다
잘 들어온다

성공이다.

 


 

2️⃣ 코드 개선

1. Early Return

앞에 로직들이 굳이.....? 앞에서 실행할 필요가 있나 뒤에서 어차피 이메일 중복체크를 하고 throw 되고 return이 될 거면 앞에 로직들을 먼저 수행할 필요가 없다. 올려.

    @Transactional
    public SignupResponse signup(SignupRequest signupRequest) {
        
        if (userRepository.existsByEmail(signupRequest.getEmail())) {
            throw new InvalidRequestException("이미 존재하는 이메일입니다.");
        }

        String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());

        UserRole userRole = UserRole.of(signupRequest.getUserRole());
        
        User newUser = new User(
                signupRequest.getEmail(),
                encodedPassword,
                userRole
        );
        User savedUser = userRepository.save(newUser);

        String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole);

        return new SignupResponse(bearerToken);
    }

 

2. 불필요한 if-else 피하기

왜 굳이 이렇게 하셨을까나

따로 분리해서 작성을 하면 더 가독성이 좋을 것 같은데요? 위에 if문이 true가 나와서 throw로 던져질 거면...... 

public String getTodayWeather() {
        ResponseEntity<WeatherDto[]> responseEntity =
                restTemplate.getForEntity(buildWeatherApiUri(), WeatherDto[].class);

        if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
            throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
        }

        WeatherDto[] weatherArray = responseEntity.getBody();

        if (weatherArray == null || weatherArray.length == 0) {
            throw new ServerException("날씨 데이터가 없습니다.");
        }

바꿔 분리해. 
+ 그런데 상태코드 먼저 체크를 하고 body를 꺼내야 되지 않을까? 순서도 바꿈
+ 지원님과의 토론...... 맞는 말이다

 

3. Validation

뭘 하고 싶은지는 알겠어요. 길이 확인하고, 숫자 있는지 확인하고 대문자 있는지 확인하고 좋아요.

UserChangePasswordRequest 가 보자.

gradle에 이미 있네. 쓸 수 있는데 왜 안 썼어.

왜 안 썼을까요????

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserChangePasswordRequest {

    @NotBlank
    private String oldPassword;

    @NotBlank
    @Size(min = 8, message = "새 비밀번호는 8자 이상이어야 합니당")
    @Pattern(regexp = ".*\\d.*", message = "새 비밀번호에는 숫자가 포함되어야 합니당")
    @Pattern(regexp = ".*[A-Z].*", message = "새 비밀번호에는 대문자가 포함되어야 합니당")
    private String newPassword;

//    if (userChangePasswordRequest.getNewPassword().length() < 8 ||
//            !userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
//            !userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
//        throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
//    }
}

그리고 컨트롤러에 `@Valid` 어노테이션 붙여 주고, Service에서 저거 삭제하면 끝.

 


 

3️⃣ N+1 문제

어디서 쓰는지 봅시다
오케이

public interface TodoRepository extends JpaRepository<Todo, Long> {

    @EntityGraph(attributePaths = "user")
    Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

    @Query("SELECT t FROM Todo t " +
            "LEFT JOIN FETCH t.user " +
            "WHERE t.id = :todoId")
    Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

    int countById(Long todoId);
}

user랑 연결해 주세요

포스트맨으로 확인해 보니까 fetch join처럼 잘 나간다.

 


 

4️⃣ 테스트코드 연습

1. 테스트 코드 연습 - 1 (예상대로 성공하는지에 대한 케이스)

정상적으로 동작 안 하는데요

디버깅 해 보니까 나오는 왜 반대로 넣고 있어요?

@Test
    void matches_메서드가_정상적으로_동작한다() {
        // given
        String rawPassword = "testPassword";
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // when
        boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);

        // then
        assertTrue(matches);
    }

위치 바꾸면 테스트 통과합니다.

 

2. 테스트 코드 연습 - 2 (예상대로 예외처리 하는지에 대한 케이스)

1번 케이스

나는 Manager not found를 기대했는데

그래서 Todo not found가 어디서 쓰나요. 전체검색을 한다. 

@Transactional
    public CommentSaveResponse saveComment(AuthUser authUser, long todoId, CommentSaveRequest commentSaveRequest) {
        User user = User.fromAuthUser(authUser);
        Todo todo = todoRepository.findById(todoId).orElseThrow(() ->
                new InvalidRequestException("Todo not found"));

        Comment newComment = new Comment(
                commentSaveRequest.getContents(),
                user,
                todo
        );

        Comment savedComment = commentRepository.save(newComment);

        return new CommentSaveResponse(
                savedComment.getId(),
                savedComment.getContents(),
                new UserResponse(user.getId(), user.getEmail())
        );
    }

여기서 사용 중이고 

public class InvalidRequestException extends RuntimeException {
    public InvalidRequestException(String message) {
        super(message);
    }
}

아 애초에 이렇게 해서 음 그럼 바꾸자

@Test
    public void manager_목록_조회_시_Todo가_없다면_IRE_에러를_던진다() {
        // given
        long todoId = 1L;
        given(todoRepository.findById(todoId)).willReturn(Optional.empty());

        // when & then
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> managerService.getManagers(todoId));
        assertEquals("Todo not found", exception.getMessage());
    }

바꾸고 통과

 

2번 케이스

InvalidRequestException이 맞다네요

    @Test
    public void comment_등록_중_할일을_찾지_못해_에러가_발생한다() {
        // given
        long todoId = 1;
        CommentSaveRequest request = new CommentSaveRequest("contents");
        AuthUser authUser = new AuthUser(1L, "email", UserRole.USER);

        given(todoRepository.findById(anyLong())).willReturn(Optional.empty());

        // when
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
            commentService.saveComment(authUser, todoId, request);
        });

        // then
        assertEquals("Todo not found", exception.getMessage());
    }

InvalidRequestException으로 바꿔 준다. 

 

3번 케이스

이건 NullPointerException이 맞다고 하네요.

서비스 로직을 수정하러 가자

user는 잘 들어와 있는데
todo는 null이 뜨네

if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
            throw new InvalidRequestException("일정을 생성한 유저만 담당자를 지정할 수 있습니다.");
        }

이렇게 바꿨다...... null이면 던져라.

성공......

 


 

5️⃣ API 로깅

Interceptor 또는 AOP를 활용해서 어드민 사용자만 접근할 수 있는 특정 API에는 접근할 때마다 접근 로그를 기록해야 합니다.

Interceptor를 쓸지 AOP를 쓸지 고민했는데

일단 Interceptor는
너 관리자야? 맞으면 들어가 아니면 차단. HTTP 요청 단계에서 동작한다. 이런 느낌이고
AOP는 함수 실행 시작~ 함수 실행 끝~ 실행 시간 기록~ 로그 남김~ 이런 느낌입니다.
일단 조금 더 어려워 보이는 AOP로 해 놓고 Interceptor는 시간이 남으면 해 보는 걸로......

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminAspect {
    
}

일단 기본 설정을 하고 

@Slf4j
@Aspect
@Component
public class AdminLogAspect {
}

 

아.... 만들면서 후회했다 Interceptor 할걸..............

 

@Slf4j
@Aspect
@Component
public class AdminLogAspect {

    @Pointcut("@annotation(org.example.expert.domain.common.annotation.AdminAspect)")
    public void adminLog() {}

    @Around("adminLog()")
    public Object logAdmin(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();

        Long userId = (Long) request.getAttribute("userId");
        String url = request.getRequestURI();
        String method = request.getMethod();
        LocalDateTime now = LocalDateTime.now();

        String requestBody = Arrays.toString(joinPoint.getArgs());

        log.info("(Admin req) userId={} time={} method={} url={} body={}",
                userId, now, method, url, requestBody);

        Object result = joinPoint.proceed();

        String responseBody = String.valueOf(result);

        log.info("(Admin res) userId={} time={} method={} url={} body={}",
                userId, LocalDateTime.now(), method, url, responseBody);

        return result;
    }
}

일단 완성 코드. 이건 admin api가 호출될 때마다 자동으로 로그를 남기는 AOP 클래스......

컨트롤러 코드에 직접 로그를 넣지 않아도 요청 -> 로그 찍기 -> 컨트롤러 실행 -> 응답 로그 찍기

이 흐름이 자동으로 만들어진다

  • `@Slf4j` - 롬복이 로그 객체를 만들어 준다. 바로 log.info(..) 사용 가능
  • `@Aspect` - 이 클래스가 AOP 클래스라는 것을 알려 주는 어노테이션 (약간 이 클래스는 다른 메서드 실행에 끼어들기 가능) 이런 느낌인 듯
  • `@Component` - 스프링 bean으로 등록하기 위한 어노테이션
  • `@Pointcut` - AOP에서 어디에 적용할지 결정하는 조건 내가 기본 설정한 @AdminAspect 어노테이션이 붙은 메서드에만 적용된다.
  • `@Around("adminLog()")` - 이건 메서드 실행 전 + 실행 후 두 군데 모두 코드를 끼워 넣겠다는 의미
  • `ProceedingJoinPoint joinPoint` 이 객체는 지금 실행하려는 메서드 정보를 가지고 있다 joinPoint.getArgs() 메서드 파라미터를 가져와서 RequestBody처럼 사용이 가능하다. joinPoint.proceed() 이걸 호출해야 원래 컨트롤러 메서드가 실행이 된다.
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();

AOP에서는 컨트롤러처럼 HttpServletRequest를 파라미터로 받을 수 없다. 그래서 RequestContextHolder로 현재 요청 객체를 꺼내오는 방식을 사용한다.

userId에는 JWT 필터에서 설정해 둔 값이 있다. 

httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));

그래서 AOP에서는 이렇게 꺼내서 쓸 수 있습니다.

아 복붙 힘들다. 캡처로 가자.

AOP에서는 RequestBody를 직접 가져오기 어렵기 때문에 메서드 파라미터 전체를 가져와서 문자열로 변환하고 로그 남김

요청 로그 찍고
원래 컨트롤러 메서드 실행하고
컨트롤러가 반환한 걸 문자열로 바꿔서 로그로
그리고 기록

이렇게 하면

comment를 지울 때 이런 식으로 로그가 찍힌다

 

더보기

여기까지 했는데 또 스탠다드 반에서 다 하니까 AOP 수업을 했다. 왜 항상 제가 하고 나서 수업을 해 주시나요..... 좋은 게 좋은 거지. 수정을 하겠습니다.

package org.example.expert.config;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;
import java.util.Arrays;

@Slf4j
@Aspect
@Component
public class AdminLogAspect {

    @Pointcut("@annotation(org.example.expert.domain.common.annotation.AdminAspect)")
    public void adminLog() {}

    @Around("adminLog()")
    public Object logAdmin(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();

        Long userId = (Long) request.getAttribute("userId");
        String url = request.getRequestURI();
        String method = request.getMethod();

        String params = Arrays.toString(joinPoint.getArgs());
        long start = System.currentTimeMillis();
        LocalDateTime startAt = LocalDateTime.now();


        log.info("[ADMIN API 요청] userId={} time={} method={} url={} params={}",
                userId, startAt, method, url, params);

        try {
            Object result = joinPoint.proceed();

            long durationMs = System.currentTimeMillis() - start;

            log.info("[ADMIN API 응답] userId={} time={} method={} url={} params={} durationMs={}",
                    userId, LocalDateTime.now(), method, url, params, durationMs);

            return result;
        } catch (Exception e) {
            long durationMs = System.currentTimeMillis() - start;

            log.warn("[ADMIN API 에러] userId={} time={} method={} url={} durationMs={} ex={} msg={}",
                    userId, LocalDateTime.now(), method, url, durationMs, e.getClass().getSimpleName(), e.getMessage());

            throw e;
        }
    }
}

 

예외 상황 로그도 기록하게 하고

수행 시간을 보이게 만들었다.

요청 시작 시간을 기록하고
응답 시점에서 수행 시간을 계산.....

try catch를 넣어서 예외 로그를 추가했다.

착한 사람들은 잘 들여다 보면 보입니다 정말로

조금 더 예뻐졌네

 


 

6️⃣ 위 제시된 기능 이외 내가 정의한 문제와 해결 과정

@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;
    private final ObjectMapper objectMapper;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String url = httpRequest.getRequestURI();

        if (url.startsWith("/auth")) {
            chain.doFilter(request, response);
            return;
        }

        String bearerJwt = httpRequest.getHeader("Authorization");

        if (bearerJwt == null) {
            log.warn("인증 헤더 누락: URI={}", url);
            sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "인증이 필요합니다.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);

        try {
            // JWT 유효성 검사와 claims 추출
            Claims claims = jwtUtil.extractClaims(jwt);
            if (claims == null) {
                log.warn("Claims 추출 실패: URI={}", url);
                sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "인증이 필요합니다.");
                return;
            }

            UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));

            httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
            httpRequest.setAttribute("email", claims.get("email"));
            httpRequest.setAttribute("userRole", claims.get("userRole"));

            if (url.startsWith("/admin") && !UserRole.ADMIN.equals(userRole)) {
                log.warn("권한 부족: userId={}, role={}, URI={}", claims.getSubject(), userRole, url);
                sendErrorResponse(httpResponse, HttpStatus.FORBIDDEN, "접근 권한이 없습니다.");
                return;
            }

            chain.doFilter(request, response);
        } catch (ExpiredJwtException e) {
            log.info("JWT 만료: userId={}, URI={}", e.getClaims().getSubject(), url);
            sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "인증이 필요합니다.");
        } catch (SecurityException | MalformedJwtException | UnsupportedJwtException e) {
            log.error("JWT 검증 실패 [{}]: URI={}", e.getClass().getSimpleName(), url, e);
            sendErrorResponse(httpResponse, HttpStatus.BAD_REQUEST, "인증이 필요합니다.");
        } catch (Exception e) {
            log.error("예상치 못한 오류: URI={}", url, e);
            sendErrorResponse(httpResponse, HttpStatus.INTERNAL_SERVER_ERROR, "요청 처리 중 오류가 발생했습니다.");
        }
    }

    private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException {
        response.setStatus(status.value());
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("status", status.name());
        errorResponse.put("code", status.value());
        errorResponse.put("message", message);

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

 

1. [문제 인식 및 정의]

인증 필터가 요청 처리되는 과정에서 중복 실행이 될 수 있다. 인증 에러가 꼬인다든가 그런 위험이 있기 때문에 `OncePerRequestFilter`로 변경해서 1회만 실행할 수 있도록 한다

2. [해결 방안]

얘를
이렇게 바꿔 주고

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html

 

OncePerRequestFilter (Spring Framework 7.0.5 API)

Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) method with HttpServletRequest and HttpServletResponse argument

docs.spring.io

얘를 활용해야겠다

 

2-1. [의사결정 과정]

이거 그대로 유지하면 중복 실행이 될 가능성이 있고 변경하면  HttpServletRequest도 쓸 수 있습니다. 굳이 형변환 안 해도 된다구요.

2-2. [해결 과정]

클래스 선언도 바꾸고 메서드도 바꿨다

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain chain) throws IOException, ServletException {

        String url = httpRequest.getRequestURI();

        if (url.startsWith("/auth")) {
            chain.doFilter(httpRequest, httpResponse);
            return;
        }

        String bearerJwt = httpRequest.getHeader("Authorization");

        if (bearerJwt == null) {
            log.warn("인증 헤더 누락: URI={}", url);
            sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "인증이 필요합니다.");
            return;
        }

        try {
            String jwt = jwtUtil.substringToken(bearerJwt);

            // JWT 유효성 검사와 claims 추출
            Claims claims = jwtUtil.extractClaims(jwt);
            if (claims == null) {
                log.warn("Claims 추출 실패: URI={}", url);
                sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "인증이 필요합니다.");
                return;
            }

            String roleString = claims.get("userRole", String.class);
            UserRole userRole = UserRole.valueOf(roleString);

            httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
            httpRequest.setAttribute("email", claims.get("email"));
            httpRequest.setAttribute("userRole", claims.get("userRole"));

            if (url.startsWith("/admin") && !UserRole.ADMIN.equals(userRole)) {
                log.warn("권한 부족: userId={}, role={}, URI={}", claims.getSubject(), userRole, url);
                sendErrorResponse(httpResponse, HttpStatus.FORBIDDEN, "접근 권한이 없습니다.");
                return;
            }

            chain.doFilter(httpRequest, httpResponse);
        } catch (ExpiredJwtException e) {
            log.info("JWT 만료: userId={}, URI={}", e.getClaims().getSubject(), url);
            sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "인증이 필요합니다.");
        } catch (SecurityException | MalformedJwtException | UnsupportedJwtException e) {
            log.error("JWT 검증 실패 [{}]: URI={}", e.getClass().getSimpleName(), url, e);
            sendErrorResponse(httpResponse, HttpStatus.BAD_REQUEST, "인증이 필요합니다.");
        } catch (Exception e) {
            log.error("예상치 못한 오류: URI={}", url, e);
            sendErrorResponse(httpResponse, HttpStatus.INTERNAL_SERVER_ERROR, "요청 처리 중 오류가 발생했습니다.");
        }
    }

    private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException {
        response.setStatus(status.value());
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("status", status.name());
        errorResponse.put("code", status.value());
        errorResponse.put("message", message);

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

3. [해결 완료]

JWT 인증 필터의 실행 흐름이 불필요하게 중복 실행될 가능성이 줄었습니다. 로그도 중복 안 되고 attribute 덮어쓰기도 리스크도.....

JWT 처리 코드를 try 안에 넣어서 혹시나 잘못된 토큰 때문에 서버가 안 죽게 만들었다.

3-1. [회고]

필터가 한 번 요청할 때 한 번 실행된다고 생각하기 쉽지만 실제로는 디스패치 방식에 따라서 중복 실행될 수도 있고 스프링이 제공하는데 안 쓸 이유가 없다.

3-2. [전후 데이터 비교]

전: request/response 캐스팅 필요했고 중복 실행 가능성이 있었는데

후: 요청당 1회 실행할 수 있으며, HttpServletRequest/Response를 사용할 수 있습니다.


 

7️⃣ 테스트 커버리지

 


 

 

 

저작자표시 비영리 (새창열림)

'IL > TIL' 카테고리의 다른 글

20260310 [TIL] 클라우드 아키텍처 설계 & 배포  (0) 2026.03.10
20260211 [TIL] 외래키로 매핑되어 있는 부모 테이블 row 삭제  (0) 2026.02.11
20260210 [TIL]  (0) 2026.02.10
[TIL] Hibernate Dialect(MySQL8Dialect) 오류 스키마 DDL 경고  (0) 2026.02.08
20260129 [TIL] - Spring 입문 시작에서  (0) 2026.01.29
'IL/TIL' 카테고리의 다른 글
  • 20260310 [TIL] 클라우드 아키텍처 설계 & 배포
  • 20260211 [TIL] 외래키로 매핑되어 있는 부모 테이블 row 삭제
  • 20260210 [TIL]
  • [TIL] Hibernate Dialect(MySQL8Dialect) 오류 스키마 DDL 경고
견지
견지
개발로 개발하는지 새발로 개발하는지 내가 개인 건지 새인 건지 사람인 건지
  • 견지
    개발새발
    견지
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • ... (0)
      • IL (20)
        • TIL (16)
        • WIL (4)
        • MIL (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    DB
    JavaScript
    HTML
    JSP
    java
    oracle
    git
    CSS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
견지
20260304 [TIL] - 코드 개선
상단으로

티스토리툴바