Hyeok의 웹 개발 블로그

<2025.04.21> [Spring - advanced] 코드 개선 & 트러블 슈팅 본문

TIL/Spring

<2025.04.21> [Spring - advanced] 코드 개선 & 트러블 슈팅

Yhyeok 2025. 4. 21. 15:45

✅ 코드 개선

 - Early Return

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

UserRole userRole = UserRole.of(signupRequest.getUserRole());

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

이렇게 되어있던 코드를 리팩토링을 통해 passwordEncoder의 encode() 동작이 불필요하게 일어나지 않게 수정했다.

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

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

UserRole userRole = UserRole.of(signupRequest.getUserRole());

 


✅ 불필요한 if- else 제거

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 되어있어서 가독성이 떨어지고 깔끔하지 않습니다. 그래서 if - else 를 제거하고

여러개의 if 문으로 나열했습니다.

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

 


✅ Validation

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

    @NotBlank(message = "기존 비밀번호를 입력하세요.")
    private String oldPassword;
    @NotBlank(message = "새 비밀번호를 입력하세요.")
    @Size(min = 8, message = "새 비밀번호는 8자 이상이어야 합니다.")
    @Pattern(regexp = ".*\\d.*",message = "비밀번호에 숫자가 포함되어야 합니다.")
    @Pattern(regexp = ".*[A-Z].*", message = "비밀번호에 대문자가 포함되어야 합니다.")
    private String newPassword;
}

 

- 첫 번째 코드를 해당 API의 요청 DTO 에서 처리하기 위해 RequestDto의 코드를 위와 같이 수정을 했습니다.

 



✅ N+1 문제 개선

@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

 

- fetch join 대신 @EntityGraph 사용해서 N+1 해결 

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

✅ TestCode -1

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

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

        // then
        assertTrue(matches);
    }
}

 

When 부분에  rawPassword 와 encodedPassword 의 순서를 바꿔 테스트가 의도대로 작동 하도록 수정하였다.

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

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

        // then
        assertTrue(matches);
    }
}

 

아래의 코드가 config 패키지에 있는 PasswordEncoder 클래스이다.

@Component
public class PasswordEncoder {

    public String encode(String rawPassword) {
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
    }

    public boolean matches(String rawPassword, String encodedPassword) {
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified;
    }
}

 


 

✅ TestCode -2

위와 같은 코드를 아래와 같이 수정하여 TestCode가 정상 작동 되게 했습니다.

@Test
public void manager_목록_조회_시_Todo가_없다면_InvalidRequestException_을_던진다() {
    // 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());
}

✅ TestCode -3

 

위 코드를 정상 작동 하게 하기위해 CommentService 클래스에 saveComment 매서드를 확인했습니다.

@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
    );

 

1. ServerException 대신 InvalidRequestException을 사용하고 있기 때문에 TestCode에도 같이 맞춰줍니다. 
2. findById (anyLong) 부분을 맞춰주기위해 findById(todoId) 로 수정해 줍니다.

 

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

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

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

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

✅ TestCode - 4

위 코드를 성공 시키기 위해 서비스 로직을 수정했습니다.

@Transactional
public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
    // 일정을 만든 유저
    User user = User.fromAuthUser(authUser);
    Todo todo = todoRepository.findById(todoId)
            .orElseThrow(() -> new InvalidRequestException("Todo not found"));
    
    if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
        throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
    }

기존 ManagerService에 작성 되어있던 코드입니다. 

@Transactional
public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
    // 일정을 만든 유저
    User user = User.fromAuthUser(authUser);
    Todo todo = todoRepository.findById(todoId)
            .orElseThrow(() -> new InvalidRequestException("Todo not found"));
    /**
     * todo.getUser() == null 코드 추가
     */
    if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
        throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
    }

  

- todo.getUser() == null || 코드를 추가하여 TestCode가 정상 작동하게 수정하였습니다.

 


✅ 트러블슈팅

  - TestCode를 수정하고 실행하였을 때, 실행이 안되는 오류가 발생하였습니다. 그래서 원인을 알아보니, 설정을 안한 것이 문제 였습니다.

 

설정 에서 Build -> Build Tools -> Gradle 에 들어가서 그림에 파란 박스 안에 설정을 intellij IDEA로 변경해주었습니다.

위 설정을 해도 gradle-wrapper.properties 가 없어서 실행이 안되는 오류가 발생해서 파일을 생성해주었습니다.

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

 

 

이렇게 설정을 하고 난 후에는 TestCode가 정상적으로 작동하는 것을 확인했습니다.

 

 

* 전체 코드 : https://github.com/yunhyeok-Lee/spring-advanced.git

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

<2025.05.01> 영속성 컨텍스트  (2) 2025.05.01
<2025.04.22> 배달 기능 프로젝트 피드백  (1) 2025.04.22
<2025.04.17> Formatter  (0) 2025.04.17
<2025.04.15> HttpMessageConverter  (2) 2025.04.15
<2025.03.31> IOC / DI  (0) 2025.03.31