[내일배움캠프] - DAY17 코드 개선 과제

2026. 3. 4. 00:01·TIL & 트러블 슈팅

코드 개선(Spring Code Refator)

오늘은 코드 개선 과제를 진행하는 글을 담아볼까 합니다. 단순히 “동작하는 코드”를 만드는 것이 아니라, 더 읽기 쉽고, 유지보수하기 좋으며, 확장 가능한 코드로 다듬는 과정에 초점을 맞춰 개선을 진행했습니다.


1. 프로젝트 세팅 - 에러 분석

요구 사항: 프로젝트를 실행했으나, 특정 에러로 인해 애플리케이션 실행에 실패했습니다! 발생한 에러의 원인을 정확히 분석하고 실행 가능하게 만들어주세요! (에러는 여러 개일 수 있습니다)

 

처음 프로젝트를 실행하였을 때 아래와 같은 오류가 발생하였습니다.

 

Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'filterConfig' defined in file [C:\Users\User\IdeaProjects\spring-advanced\build\classes\java\main\org\example\expert\config\FilterConfig.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed

 

 

문제를 확인해보니 application.yml 파일이 존재하지 않았고, JWT에서 사용하는 secret key 값 또한 설정되어 있지 않아 발생한 오류였습니다. JwtUtil 클래스에서는 @Value("${jwt.secret.key}")를 통해 설정 값을 주입받도록 되어 있는데, 해당 프로퍼티가 정의되지 않으면 Bean 생성 과정에서 의존성 주입에 실패합니다. 그 결과 JwtUtil Bean이 정상적으로 생성되지 않았고, 이를 생성자 주입받는 FilterConfig 또한 함께 초기화에 실패하면서 UnsatisfiedDependencyException이 발생했습니다. 즉, 설정 파일 누락으로 인해 JWT 관련 Bean 생성이 실패하면서 애플리케이션이 시작 단계에서 종료된 문제였습니다.

 

 

resources 디렉터리 내부에 application.yml 파일을 생성한 뒤 JWT key 값을 추가했습니다. 이후 애플리케이션을 다시 실행했지만, 이번에는 또 다른 오류가 발생했습니다.

Failed to configure a DataSource
'url' attribute is not specified
Failed to determine a suitable driver class

 

해당 오류는 DB 연결 정보가 설정되지 않아 DataSource 구성이 실패하면서 발생했습니다. application.yml에 데이터베이스 접속 정보를 추가하고 DB를 생성한 뒤 정상적으로 실행되었습니다.

 

 


2. ArgumentResolver

요구 사항: 패키지 org.example.expert.config;에 위치한 AuthUserArgumentResolver의 로직이 현재 동작하지 않고 있습니다. AuthUserArgumentResolver가 정상적으로 기능할 수 있도록 해주세요.

 

문제를 확인해보니 AuthUserArgumentResolver 클래스는 이미 구현되어 있었지만, 스프링 Bean으로 등록되지 않았고 MVC 컨테이너에도 추가되지 않아 실제로는 사용되지 않는 상태였습니다. 그 결과 컨트롤러에서 @Auth와 AuthUser를 사용하더라도 해당 ArgumentResolver가 동작하지 않았으며, 인증 정보가 정상적으로 주입되지 않는 문제가 발생했습니다.

 

 

이에 따라 @Component를 통해 Bean으로 등록하고, WebConfig에서 MVC 설정에 ArgumentResolver를 추가하여 정상적으로 동작하도록 처리했습니다.


3. 코드 개선

3-1. Early Return

 

요구사항: 패키지 org.example.expert.domain.auth.service; 의 AuthService 클래스에 있는 signup() 중 아래의 코드 부분의 위치를 리팩토링해서

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

 

위의 에러가 발생하는 상황일 때, passwordEncoder의 encode() 동작이 불필요하게 일어나지 않게 코드를 개선해주세요.

 

여기서 한 가지 짚고 넘어가야 할 점이 있습니다. 코드는 항상 위에서 아래로 순차적으로 실행되기 때문에, 기존 코드 구조에서는 예외가 발생하기 전에 이미 passwordEncoder.encode()가 수행되고 UserRole 객체 또한 생성되고 있었습니다. 즉, 이메일 중복과 같이 사전에 검증할 수 있는 조건이 있음에도 불필요한 연산과 객체 생성이 먼저 이루어져, 쓸모없는 데이터가 생성되고 프로그램 성능에 부담을 줄 수 있는 구조였습니다. 이에 따라 불필요한 작업이 실행되지 않도록 검증 로직을 상단으로 이동시키는 방식으로 리팩토링하여 처리했습니다.

 

 

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

 

요구사항: 패키지 org.example.expert.client;의 WeatherClient 클래스에 있는 getTodayWeather() 중 아래의 코드 부분을 리팩토링해주세요.

WeatherDto[] weatherArray = responseEntity.getBody();
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
    throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
} else {
    if (weatherArray == null || weatherArray.length == 0) {
        throw new ServerException("날씨 데이터가 없습니다.");
    }
}

 

해당 코드는 if-else 구조로 작성되어 있어 조건이 중첩되면서 가독성이 떨어지고, 불필요한 들여쓰기로 인해 로직 파악이 어려운 문제가 있었습니다. 예외가 발생하면 즉시 메서드가 종료되므로 else 블록을 사용할 필요가 없다고 판단했습니다. 따라서 불필요한 if-else를 제거하고, 조건을 각각 독립적으로 검사하도록 리팩토링하여 코드의 가독성과 유지보수성을 개선했습니다.

 

 

 

3-3. Validation

 

요구사항: 패키지 org.example.expert.domain.user.service;의 UserService 클래스에 있는 changePassword()

중 아래 코드 부분을 해당 API의 요청 DTO에서 처리할 수 있게 개선해주세요.

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

 

해당 코드는 서비스 계층에서 비밀번호 길이와 패턴을 조건문으로 직접 검증하고 있었는데, 이러한 방식은 비즈니스 로직과 입력 값 검증 로직이 혼재되어 코드 가독성과 책임 분리가 떨어지는 문제가 있었습니다. 입력 값 검증은 서비스가 아닌 요청 DTO 단계에서 처리하는 것이 더 적절하다고 판단했습니다. 따라서 DTO에 @Pattern과 같은 Bean Validation 어노테이션을 적용하여 유효성 검증을 수행하도록 변경했고, 기존 서비스에 존재하던 조건문은 제거하여 로직을 단순화했습니다.

 

 


4. N+1 문제

요구사항: 기존 JPQL fetch join으로 구현된 N+1 문제 해결 로직을 @EntityGraph 기반 조회 방식으로 변경하여 동일한 성능을 유지하도록 리팩토링하세요.

 

문제 요구사항에 맞게 JPQL fetch join 기반 @Query를 제거하고 @EntityGraph를 적용하여 동일하게 N+1 문제를 해결하도록 리팩토링했습니다.

// 기존 코드
public interface TodoRepository extends JpaRepository<Todo, Long> {

    @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
    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);
}

 


5. 테스트 코드 연습

1. 테스트 코드 연습 - 1

 

문제: 테스트 패키지 org.example.expert.config;의 PasswordEncoderTest 클래스에 있는 matches_메서드가_정상적으로_동작한다() 테스트가 의도대로 성공할 수 있게 수정해 주세요.

 

 

기존에 있던 테스트가 정상적으로 동작하지 않았던 이유는 matches() 메서드의 파라미터 순서를 반대로 전달했기 때문입니다. Spring Framework 의 PasswordEncoder는 matches(rawPassword, encodedPassword) 순서로 비교를 수행합니다. 즉, 첫 번째에는 평문 비밀번호, 두 번째에는 암호화된 비밀번호가 들어가야 합니다. 하지만 기존 코드에서는 encodedPassword, rawPassword 순서로 전달하여 항상 일치하지 않는 결과가 발생했습니다. 따라서 두 파라미터의 위치를 올바른 순서로 변경하여 문제를 해결하였고, 이후 테스트가 정상적으로 통과하도록 수정하였습니다.


2. 테스트 코드 연습 - 2

케이스(1)

 

문제: 테스트 패키지 org.example.expert.domain.manager.service; 의 ManagerServiceTest의 클래스에 있는 manager_목록_조회_시_Todo가_없다면_NPE_에러를_던진다() 테스트가 성공하고 컨텍스트와 일치하도록 테스트 코드와 테스트 코드 메서드명을 수정해 주세요.

 

💡Hint!
던지는 에러가 NullPointerException이 아니므로 메서드명 또한 수정되어야 해요!

 

 

먼저 테스트가 실패한 원인을 확인하기 위해 위 사진 처럼 managerService 내부 로직을 확인하였습니다. 서비스에서는 Todo를 찾지 못했을 때 NullPointerException이 아닌 InvalidRequestException을 발생시키고 있으며, 예외 메시지도 "Todo not found"로 처리하고 있었습니다.

 

 

하지만 기존 테스트는 NPE 발생을 기대하도록 작성되어 있어 실제 동작과 맞지 않아 실패가 발생했습니다. 따라서 서비스 로직에 맞게 테스트가 InvalidRequestException을 기대하도록 수정하고, 예외 메시지와 테스트 메서드명도 함께 변경하여 일관성을 맞추었습니다.


케이스(2)

 

문제: 테스트 패키지 org.example.expert.domain.comment.service;의 CommentServiceTest의 클래스에 있는comment_등록_중_할일을_찾지_못해_에러가_발생한다() 테스트가 성공할 수 있도록 테스트 코드를 수정해 주세요.

 

먼저 테스트 실패 원인을 확인하기 위해 commentService의 예외 처리 로직을 직접 확인하였습니다. 서비스 코드를 살펴보니 Todo를 찾지 못하는 경우 ServerException이 아닌 InvalidRequestException을 발생시키도록 구현되어 있었습니다. 하지만 기존 테스트에서는 ServerException 발생을 기대하고 있어 실제 서비스 동작과 예외 타입이 일치하지 않았고, 이로 인해 테스트가 실패하고 있었습니다.

 

 

따라서 서비스 로직에 맞추어 테스트의 기대 예외를 InvalidRequestException으로 변경하였고, 예외 타입과 메시지를 일치시켜 정상적으로 테스트가 통과하도록 수정하였습니다.


케이스(3)

 

문제: 테스트 패키지 org.example.expert.domain.manager.service의 ManagerServiceTest 클래스에 있는 todo의_user가_null인_경우_예외가_발생한다() 테스트가 성공할 수 있도록 서비스 로직을 수정해 주세요.

 

 

테스트가 실패 원인 분석을 위해 먼저 saveManager() 로직을 직접 확인해보았습니다. 테스트에서는 Todo의 user가 null인 상황을 만들어 예외가 발생하는지를 검증하고 있었는데, 기존 코드에서는 이에 대한 null 체크 없이 바로 todo.getUser().getId()를 호출하고 있었습니다. 이 때문에 의도한 InvalidRequestException이 아닌 NullPointerException이 발생하며 테스트가 실패했습니다.

 

 

그래서 todo.getUser() == null 조건을 먼저 추가해 사용자 존재 여부를 선검증하도록 수정하였고, user가 없을 경우 명시적으로 InvalidRequestException을 발생시키도록 변경하여 테스트와 로직이 일치하도록 개선했습니다.


6. API 로깅

요구사항: 어드민만 접근할 수 있는 다음 두 API에 대해 로깅을 적용합니다.

  • CommentAdminController의 deleteComment()
  • UserAdminController의 changeUserRole()

구현 방식

1. Interceptor

  • HttpServletRequest를 통해 요청 정보를 사전 처리합니다.
  • 요청한 사용자가 어드민 권한인지 확인합니다.
  • 어드민이 아닐 경우 예외를 발생시켜 접근을 차단합니다.
  • 인증이 성공하면 요청 시각과 요청 URL을 로그로 기록합니다.

 

2. AOP

  • @Around 어노테이션을 사용하여 어드민 API 실행 전후에 로깅을 수행합니다.
  • 다음 정보를 로그에 기록합니다.
    • 요청 사용자 ID
    • API 요청 시각
    • 요청 URL
    • 요청 본문(RequestBody) 
    • 응답 본문(ResponseBody)

요청 및 응답 데이터는 JSON 형식으로 변환하여 기록하며, 로깅은 Logger를 사용하여 구현합니다.


Interceptor 구현

어드민 API에 접근했을 때 요청 정보를 기록하기 위해 Interceptor를 구현했습니다.

 

 

먼저 @Component를 사용해 해당 클래스를 스프링 빈으로 등록했습니다. 이렇게 해야 이후 설정 클래스에서 Interceptor로 등록해서 사용할 수 있습니다. HandlerInterceptor를 구현하면 여러 메서드를 사용할 수 있는데, 여기서는 요청이 컨트롤러로 전달되기 전에 실행되는 preHandle() 메서드를 사용했습니다. 메서드 내부에서는 HttpServletRequest를 통해 요청 URL을 가져왔고, LocalDateTime.now()를 사용해 요청이 들어온 시간을 확인했습니다. 이렇게 가져온 값들을 log.info()를 사용해 로그로 남기도록 했습니다. 어드민 API에 접근할 때 언제 어떤 URL로 요청이 들어왔는지 확인할 수 있도록 하기 위함입니다. 마지막으로 true를 반환하여 요청이 정상적으로 컨트롤러까지 전달되도록 했습니다.

 

Interceptor 등록

Interceptor는 클래스를 만든 것만으로는 동작하지 않습니다. 스프링이 요청을 처리하는 과정에 직접 등록해줘야 실제로 실행됩니다. 그래서 WebMvcConfigurer를 구현한 WebConfig에서 Interceptor를 추가했습니다.

 

 

WebMvcConfigurer를 구현하여 스프링 MVC의 설정을 커스터마이징할 수 있도록 했습니다. 이 인터페이스를 사용하면 ArgumentResolver나 Interceptor 같은 기능을 직접 등록할 수 있습니다. addInterceptors() 메서드에서는 앞에서 만든 AdminInterceptor를 실제 요청 흐름에 등록했습니다. registry.addInterceptor(adminInterceptor)를 통해 인터셉터를 추가하고, addPathPatterns("/admin/**")를 사용해 /admin으로 시작하는 API 요청에만 인터셉터가 동작하도록 설정했습니다. 이렇게 설정하면 어드민 관련 API에 요청이 들어올 때마다 AdminInterceptor의 preHandle()이 먼저 실행되며, 이 시점에서 요청 URL과 요청 시간을 로그로 기록할 수 있습니다.


AOP를 활용한 로깅

 

Interceptor는 컨트롤러에 들어오기 전 요청 정보를 확인하는 역할을 합니다. 하지만 실제 메서드 실행 전후의 데이터까지 확인하기에는 한계가 있기 때문에, 이번에는 AOP를 활용해 요청과 응답 데이터를 함께 로깅하도록 구현했습니다.

 

 

@Around 어노테이션을 사용해 특정 메서드 실행 전과 후에 로직이 동작하도록 설정했습니다. execution 표현식을 사용하여 deleteComment()와 changeUserRole() 두 개의 어드민 API 메서드를 대상으로 지정했습니다. 메서드 내부에서는 먼저 LocalDateTime.now()를 사용해 API 요청 시각을 기록합니다. 그 다음 joinPoint.getArgs()를 통해 컨트롤러 메서드로 전달된 파라미터 값(RequestBody)을 가져와 로그로 남겼습니다. joinPoint.proceed()는 실제 컨트롤러 메서드를 실행하는 부분입니다. 이 코드를 기준으로 앞은 메서드 실행 전 로직, 뒤는 실행 후 로직이 됩니다. 마지막으로 result를 로그로 기록하여 API 실행 결과(ResponseBody)도 함께 확인할 수 있도록 했습니다. 이렇게 AOP를 적용하면 어드민 API가 호출될 때 요청 시각, 요청 데이터, 응답 데이터까지 한 번에 로그로 확인할 수 있습니다.


API 로깅 테스트

Interceptor와 AOP를 통해 어드민 API 로깅을 구현한 후, 포스트맨을 사용하여 테스트 할 수 있지만 이번 과제에서 배운 테스트 코드를 활용하여 API 로깅이 정상 동작하는지 확인을 위해 간단한 테스트 코드를  아래와 같이 작성해 보았습니다.

 

 

@SpringBootTest를 사용해 스프링 컨텍스트를 로드한 상태에서 테스트가 실행되도록 설정했습니다. 이렇게 하면 실제 애플리케이션과 유사한 환경에서 API 요청을 테스트할 수 있습니다. @AutoConfigureMockMvc는 컨트롤러 테스트를 위해 MockMvc를 자동으로 설정해주는 어노테이션입니다. 여기서 addFilters = false 옵션을 사용한 이유는 JWT 필터와 같은 인증 필터를 제외하고 테스트를 진행하기 위해서입니다. 이번 테스트의 목적은 인증이 아니라 API 로깅이 정상적으로 동작하는지 확인하는 것이기 때문입니다. 테스트에서는 MockMvc를 사용해 /admin/comments/1 경로로 DELETE 요청을 보내도록 설정했습니다. 그리고 응답 상태 코드가 200 OK인지 확인하여 API가 정상적으로 실행되는지 검증했습니다. 이 테스트를 실행하면 해당 API가 호출되면서 Interceptor와 AOP에서 설정한 로그가 함께 출력되는지 확인할 수 있습니다.


트러블 슈팅

1. API 로깅 테스트 중 에러 발생

API 로깅을 구현 후 해당 로직이 정상으로 동작하는지 확인하기 위해 아래와 같이 테스트 코드를 작성했었습니다.

 

@SpringBootTest
@AutoConfigureMockMvc
class AdminApiAspectTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("deleted_comment")
    void deleted_comment_test() throws Exception {

    // given

    // when
        mockMvc.perform(delete("/admin/comments/1"))

    // then
                .andExpect(status().isOk());
    }

}

 

 

테스트 코드를 작성한 뒤 실행해보니 위 사진과 같은 오류가 발생하였습니다. 아래에서 한번 로그들을 확인하면서 에러의 원인을 간단히 정리해보겠습니다.

 

WARN  JwtFilter : 인증 헤더 누락: URI=/admin/comments/1

Expected :200
Actual   :401

Body = {"code":401,"message":"인증이 필요합니다.","status":"UNAUTHORIZED"}

 

JWT 필터에서 인증 헤더가 존재하지 않아 요청이 차단되고 있었습니다. 해당 테스트는 API 로깅(AOP)이 정상적으로 동작하는지 확인하는 목적이었기 때문에 인증 로직까지 함께 테스트할 필요는 없는 상황이었습니다. 하지만 프로젝트에는 이미 JWT 인증 필터가 적용되어 있었기 때문에 테스트 요청이 필터에서 먼저 차단되고 있었습니다. 즉, 테스트 코드에서 /admin/comments/1 API를 호출하더라도 Authorization 헤더가 존재하지 않기 때문에 JwtFilter에서 401 응답을 반환하게 된 것이었습니다. 이 문제를 해결하기 위해 어떤 방법이 있을지 찾아보다가 구글 검색을 통해 해결 방법을 확인할 수 있었습니다. 검색해본 결과 테스트 코드에서@AutoConfigureMockMvc(addFilters = false) 옵션을 추가하면 JWT 인증 필터를 거치지 않고 테스트를 실행할 수 있다는 것을 알게 되었습니다.

 

 

따라서 해당 옵션을 적용하여 JWT 인증을 우회한 상태로 테스트를 진행했고, 이후 정상적으로 테스트가 동작하는 것을 확인할 수 있었습니다.

 


이번 TIL을 마무리하며..

사실 이번 과제를 진행하면서 가장 고민이 되었던 부분은, 그동안은 프로젝트를 직접 만들어보는 경험만 해봤지 다른 사람이 작성한 프로젝트 코드를 깊게 읽어본 경험은 거의 없었다는 점이었습니다. 이번 과제를 통해 기존 코드를 하나씩 살펴보면서 이해해야 했기 때문에 코드를 읽고 구조를 파악하는 연습이 중요하다는 것을 느낄 수 있었습니다.

'TIL & 트러블 슈팅' 카테고리의 다른 글

[내일배움캠프] - DAY19 AOP(Aspect Oriented Programming)에 대해 알아보자  (0) 2026.03.06
[내일배움캠프] - DAY18 테스트  (0) 2026.03.04
[내일배움캠프] - DAY16 일정관리 앱 만들기-2  (1) 2026.02.12
[내일배움캠프] - DAY15 일정관리 앱 만들기  (0) 2026.02.04
[내일배움캠프] - DAY14 자료구조와 알고리즘-2  (0) 2026.01.27
'TIL & 트러블 슈팅' 카테고리의 다른 글
  • [내일배움캠프] - DAY19 AOP(Aspect Oriented Programming)에 대해 알아보자
  • [내일배움캠프] - DAY18 테스트
  • [내일배움캠프] - DAY16 일정관리 앱 만들기-2
  • [내일배움캠프] - DAY15 일정관리 앱 만들기
N_HYUN
N_HYUN
안녕하세요! 현이의 개발 공부방입니다.
  • N_HYUN
    현이의 개발 공부방
    N_HYUN
  • 전체
    오늘
    어제
    • 분류 전체보기 (29)
      • FrontEnd (0)
      • BackEnd (0)
        • Java (0)
      • DataBase (1)
      • TIL & 트러블 슈팅 (24)
      • Etc (4)
        • Theoretical Computer Scienc.. (1)
        • Web & CS (3)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
N_HYUN
[내일배움캠프] - DAY17 코드 개선 과제
상단으로

티스토리툴바