[TIL] 백엔드 부트캠프 9주차 (2024/09/11 수) 레거시 코드의 테스트 코드 기반 리팩토링

2024. 9. 11. 19:04·TIL 🔖/TIL

💻Today's Schedule

09:00 ~ 10:00 CODEKATA
10:00 ~ 12:00 개인 학습
12:00 ~ 13:00 점심 식사
13:20 ~ 13:30 팀미팅
13:30 ~ 18:00 개인 학습
18:00 ~ 19:00 저녁 식사
19:00 ~ 21:00 개인 학습

✍ Today I Learned

CODEKATA

  • 알고리즘 (62번 옹알이 (2))
 

[프로그래머스/JAVA] 옹알이 (2) String vs StringBuilder

문제 설명 머쓱이는 태어난 지 11개월 된 조카를 돌보고 있습니다. 조카는 아직 "aya", "ye", "woo", "ma" 네 가지 발음과 네 가지 발음을 조합해서 만들 수 있는 발음밖에 하지 못하

fargoewave.tistory.com


개인과제  Level 1-1. Early Return

문제
조건에 맞지 않는 경우 즉시 리턴하여, 불필요한 로직의 실행을 방지하고 성능을 향상시킵니다.
패키지 package org.example.expert.domain.auth.service; 의 AuthService 클래스에 있는 signup() 중 아래의 코드 부분의 위치를 리팩토링해서 해당 에러가 발생하는 상황일 때, passwordEncoder의 encode() 동작이 불필요하게 일어나지 않게 코드를 개선해주세요.

주의사항!
SignupReqeust 클래스의 @NotBlank, @Email과 관계 없이 리팩토링해주세요.
    @Transactional
    public SignupResponse signup(SignupRequest signupRequest) {

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

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

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

        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);
    }
수정한 코드 
  • signupRequest.getEmail() 값이 없을 경우, passwordEncoder.encode(signupRequest.getPassword())가 호출되지 않도록 리팩터링 하려면, 먼저 이메일 존재 여부를 확인한 후에 비밀번호 인코딩을 처리해야 한다. 이를 통해 불필요한 비밀번호 인코딩을 방지할 수 있다.
  • 그동안 과제를 할 때 Level1부터 막혔던 경험이 많아서... 이게 맞나 의심을 했지만?.. 아니라면 나중에 수정하는 것으로..
  • 과제를 수행하며 학습한 것들을 함께 정리한다. 
 @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);
    }
Early Return
  • Early Return은 메서드나 함수의 초반에 특정 조건을 검사하여, 그 조건이 만족되지 않을 경우 즉시 반환하는 방식으로, 이를 통해 불필요한 후속 로직 실행을 방지하고, 코드의 가독성을 높이며 성능을 향상할 수 있다.
  • 여러 가지 조건을 중첩시켜 코드의 복잡도가 증가하는 것을 방지하고, 특정 조건이 만족되지 않으면 바로 반환할 수 있어 코드 흐름을 더 단순하고 명료하게 유지할 수 있다는 장점을 가지고 있다. 
Early Return을 사용할 때 발생할 수 있는 잠재적인 문제
  • 지나치게 많은 반환 지점: Early Return을 너무 많이 사용하면 메서드 내에 여러 개의 반환 지점이 생길 수 있어, 전체 코드의 흐름을 파악하기 어려워질 수 있다. 특히, 조건이 많아질수록 코드가 복잡해져서 디버깅이 어려워질 수 있다.
  • 유지보수의 어려움: 반환 지점이 여러 군데 존재할 경우, 후속 코드에서 변경이 발생할 때 모든 반환 지점에서 일관성을 유지해야 하는 부담이 생기며, 이런 점은 장기적인 코드 유지보수에서 문제가 될 수 있다.
  • 일관성 저하: Early Return이 너무 잦거나 다양한 패턴으로 사용되면, 코드가 일관되게 작성되지 않고, 가독성을 해치는 경우도 발생할 수 있다. 특히 팀 협업 시, 각 개발자가 다른 방식으로 조건문을 구성할 때 일관성 문제가 발생할 수 있다.
Early Return 외에 코드 가독성을 높이기 위한 패턴
  • 가드 클로즈(Guard Clause) : Early Return과 유사한 개념으로, 메서드 초반에 모든 예외적인 조건을 처리해 놓고 정상적인 흐름을 메서드의 마지막에 남겨두는 방식이다. 이는 코드가 깔끔하게 유지되고, 조건에 해당하지 않는 경우 빠르게 처리할 수 있다.
if (!valid) {
    return;
}
// 정상적인 로직은 아래에서 진행
  • 메서드 분리(Extract Method): 복잡한 로직을 작게 쪼개어 각각의 역할을 분리한 메서드로 추출하는 방식으로 각 메서드가 하나의 역할만 담당하게 하여, 가독성과 유지보수성을 높이는 패턴이다. 이를 통해 중복된 로직을 줄일 수 있다.
public void processSignup() {
    if (!isValidEmail()) {
        return;
    }
    encodePassword();
    saveUser();
}
  • 조건부 연산자 사용(Ternary Operator): 단순한 조건 분기에는 if-else 대신 삼항 연산자를 사용해 코드를 간결하게 표현할 수 있다. 다만, 지나치게 복잡하게 쓰면 오히려 가독성이 떨어질 수 있으니, 간단한 경우에만 사용해야 한다.
String result = (condition) ? "true case" : "false case";
  • 디자인 패턴 적용: 특정 로직이 복잡하거나, 변하는 요소가 많다면 전략 패턴이나 상태 패턴 같은 디자인 패턴을 적용하여 코드의 구조를 개선할 수 있다. 이는 각 역할을 분리하고, 상황에 따라 동적으로 변경할 수 있는 유연성을 제공한다.

개인과제 Level 1-2. 불필요한 if-else 피하기

문제
복잡한 if-else 구조는 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다. 불필요한 else 블록을 없애 코드를 간결하게 합니다.
패키지 package org.example.expert.client; 의 WeatherClient 클래스에 있는 getTodayWeather() 중 아래의 코드 부분을 리팩토링해주세요.
public String getTodayWeather() {
        ResponseEntity<WeatherDto[]> responseEntity =
                restTemplate.getForEntity(buildWeatherApiUri(), WeatherDto[].class);

        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("날씨 데이터가 없습니다.");
            }
        }

        String today = getCurrentDate();

        for (WeatherDto weatherDto : weatherArray) {
            if (today.equals(weatherDto.getDate())) {
                return weatherDto.getWeather();
            }
        }

        throw new ServerException("오늘에 해당하는 날씨 데이터를 찾을 수 없습니다.");
    }
수정한 코드
  • 주어진 코드에서 불필요한 else 블록을 제거하여 코드의 가독성을 높일 수 있다. if 블록에서 조건이 충족되지 않으면 바로 예외를 던져 실행 흐름이 종료되기 때문에, else를 사용할 필요가 없다.
        // 상태 코드가 OK가 아닌 경우 즉시 예외 발생
        if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
            throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
        }

        // 날씨 데이터가 없을 경우 예외 발생
        if (weatherArray == null || weatherArray.length == 0) {
            throw new ServerException("날씨 데이터가 없습니다.");
        }
  • Optional을 통해 null 처리를 깔끔하게 처리하고, 예외 발생을 줄일 수 있다
Optional.ofNullable(responseEntity.getBody())
        .filter(arr -> arr.length > 0)
        .orElseThrow(() -> new ServerException("날씨 데이터가 없습니다."));
if-else를 제거할 때 조건의 순서가 중요한 경우
  • 서로 상반된 조건이 있을 때는 조건의 순서가 중요해진다. 예를 들어, 특정 조건이 다른 조건보다 자주 발생하거나 더 중요한 경우, 이를 앞에 배치하는 것이 효율적이다. 예를 들어, 오류가 발생할 가능성이 높은 조건을 먼저 처리해 빠르게 반환하는 것이 좋다.
  • 만약 첫 번째 조건이 더 복잡한 계산을 요구하고, 두 번째 조건이 상대적으로 간단할 경우, 두 번째 조건을 먼저 체크해 성능을 최적화할 수 있다. 예를 들어, 리스트나 배열의 길이를 먼저 체크하고, 그 길이가 0일 때만 추가 작업을 하는 방식이다.
  • 가독성 측면에서도 조건의 순서가 중요하다. 예를 들어, 특정 조건의 우선순위가 분명하다면, 이를 먼저 처리하는 것이 코드 이해에 도움을 줄 수 있다.
if (condition1) {
    return;
}
if (condition2) {
    return;
}

 

개인과제 Level 1-3. 메서드 분리

문제

 

복잡한 로직은 메서드로 분리하고, 메서드 이름만으로 동작을 명확히 이해할 수 있어야 합니다.
패키지 package org.example.expert.domain.user.service; 의 UserService 클래스에 있는 changePassword() 중 아래 코드 부분을 리팩토링해주세요.
if (userChangePasswordRequest.getNewPassword().length() < 8 ||
        !userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
        !userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
    throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
}
수정한 코드 
  • 비밀번호 유효성 검사를 별도의 validateNewPassword() 메서드로 분리했다.
  • changePassword() 메서드는 비밀번호 변경 로직만 처리하고, 유효성 검사는 별도로 처리하여 역할을 명확히 분리한다. 
@Transactional
public void changePassword(long userId, UserChangePasswordRequest userChangePasswordRequest) {
    // 새 비밀번호 유효성 검사 메서드 호출
    validateNewPassword(userChangePasswordRequest.getNewPassword());

    User user = userRepository.findById(userId)
            .orElseThrow(() -> new InvalidRequestException("User not found"));

    if (passwordEncoder.matches(userChangePasswordRequest.getNewPassword(), user.getPassword())) {
        throw new InvalidRequestException("새 비밀번호는 기존 비밀번호와 같을 수 없습니다.");
    }

    if (!passwordEncoder.matches(userChangePasswordRequest.getOldPassword(), user.getPassword())) {
        throw new InvalidRequestException("잘못된 비밀번호입니다.");
    }

    user.changePassword(passwordEncoder.encode(userChangePasswordRequest.getNewPassword()));
}

private void validateNewPassword(String newPassword) {
    if (newPassword.length() < 8 ||
            !newPassword.matches(".*\\d.*") ||
            !newPassword.matches(".*[A-Z].*")) {
        throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
    }
}
메서드 분리를 할 때, 메서드의 역할을 명확히 정의하는 것이 왜 중요한가?
  • 메서드 분리를 통해 각 메서드가 하나의 역할만을 수행하게 되면 코드가 더 간결해지고, 쉽게 이해할 수 있다. 이를 통해 다른 개발자가 코드를 빠르게 파악할 수 있게 한다.
  • 역할이 명확한 메서드는 수정이 필요할 때 영향을 미치는 범위가 적어지고, 특정 기능에 대한 버그를 빠르게 찾고 수정할 수 있다.
  • 명확하게 분리된 메서드는 다른 클래스나 메서드에서도 재사용될 가능성이 높다. 이를 통해 중복 코드를 줄이고 코드 품질을 향상시킬 수 있다.
  • 역할이 분리된 메서드는 독립적으로 테스트하기가 쉬워진다. 각 메서드의 역할이 명확하면 단위 테스트를 통해 개별 기능을 검증하는 것이 더 간단해진.
만약 유효성 검사를 여러 곳에서 재사용할 수 있는 경우 로직 처리 방법
  • 유효성 검사 로직을 별도의 유틸리티 클래스에 분리하여, 필요할 때마다 다양한 곳에서 사용할 수 있다. 예를 들어, PasswordValidator라는 클래스를 만들어 비밀번호 검사를 통합 관리할 수 있다.
  • 유효성 검사를 별도의 서비스나 컴포넌트로 분리하여, 여러 서비스에서 공통으로 사용하는 방식으로 관리할 수 있다. 이를 통해 중복 코드를 줄이고 유지보수성을 높일 수 있다.
  • 자바의 @Valid처럼, 커스텀 어노테이션을 만들어 특정 필드에 적용할 수 있다. 이를 통해 선언적인 방식으로 유효성 검사를 쉽게 적용할 수 있다.
  • 라이브러리 활용: 이미 검증 관련 작업을 하는 라이브러리(Apache Commons Validator, Hibernate Validator 등)를 사용해 일관된 유효성 검사를 적용하고, 여러 프로젝트에서 재사용할 수 있다.

개인과제 Level 1-4. JWT 유효성 검사 로직 수정

문제
JWT 인증은 컨트롤러가 아닌 필터에서 처리하게 하여, 인증과 비즈니스 로직을 분리해야 합니다. 이렇게 하면 코드 중복을 줄이고, 컨트롤러는 본래의 기능에만 집중할 수 있습니다.
패키지 `package org.example.expert.domain.manager.controller;` 의 `ManagerController` 클래스에 있는 `deleteManager()` 에서 JWT 토큰을 직접 해석하여 사용자 정보를 추출하는 방식이 사용되고 있습니다.
JWT 처리 로직과 비즈니스 로직을 분리하고, 인증된 사용자 정보를 더 적절한 방식으로 가져올 수 있도록 `deleteManager()` 메서드를 개선해주세요.
필요 시, `ManagerService` 클래스에 있는 `deleteManager()` 또한 리팩토링해주세요.
@DeleteMapping("/todos/{todoId}/managers/{managerId}")
    public void deleteManager(
            @RequestHeader("Authorization") String bearerToken,
            @PathVariable long todoId,
            @PathVariable long managerId
    ) {
        Claims claims = jwtUtil.extractClaims(bearerToken.substring(7));
        long userId = Long.parseLong(claims.getSubject());
        managerService.deleteManager(userId, todoId, managerId);
    }
수정한 코드 
  •  ManagerController의 deleteManager() 메서드에서 JWT 토큰을 직접 해석하여 사용자 정보를 추출하는 방식이 사용되고 있으며, JWT 검증과 인증 로직이 컨트롤러에 들어가 있다. 컨트롤러는 비즈니스 로직에만 집중해야 하며, 인증 로직은 필터나 다른 적절한 위치에서 처리되어야 한다.
  • AuthUserArgumentResolver 클래스가 이미 존재하기 때문에, JWT 토큰 해석 및 인증 정보 주입은 해당 클래스에서 처리된다. 따라서 컨트롤러에서는 @Auth 어노테이션을 통해 이미 인증된 사용자 정보를 주입받을 수 있다.
@DeleteMapping("/todos/{todoId}/managers/{managerId}")
    public void deleteManager(
            @Auth AuthUser authUser,
            @PathVariable long todoId,
            @PathVariable long managerId
    ) {
        managerService.deleteManager(authUser.getId(), todoId, managerId);
    }
관심사 분리가 필요한 이유
  • 컨트롤러는 본래 비즈니스 로직에 집중해야 하지만, 현재는 JWT 검증과 인증 로직까지 처리하고 있다. 이는 컨트롤러의 책임이 과중해지며 코드의 가독성이 낮아진다. 또한 인증과 관련된 로직이 비즈니스 로직과 섞이면 코드의 유지보수가 어려워진다.
  • 여러 컨트롤러에서 동일한 방식으로 JWT 토큰을 해석하고 사용자 정보를 추출하는 로직이 반복될 수 있다. 이는 코드 중복을 초래하고, JWT 처리 방식이 변경될 경우 모든 컨트롤러를 수정해야 하는 문제가 생긴다.
  • JWT 처리 로직이 각 컨트롤러에 분산되어 있으면, 로직 수정이나 보안 정책 변경 시 여러 곳에서 코드를 수정해야 하는 어려움이 발생할 수 있다. 
  • 인증과 비즈니스 로직이 함께 존재하면 코드의 일관성이 떨어진다. 인증 로직은 특정한 위치(예: 필터, 유틸 클래스)에서 처리되고, 컨트롤러는 비즈니스 로직에 집중하는 것이 코드의 일관성을 유지하는 방법이다.

📝 회고

Level1을 수행하는 것 자체는 정말 금방 했는데 이론적인 학습이 필요하다고 느꼈다.
Early Return 도 들어는 봤지만 정작 왜 필요한지에 대해서는 고민해 본 적이 없었다.
그래서 이번 과제를 진행하며 확실하지 않았던 개념을 돌아보며 다시 한번 학습했다.

개념을 정리하다 보니 추가로 궁금한 것들이 계속 생겼는데, 
그동안 과제를 하며 레벨 채우기에 급급해서 충분한 학습을 하지 못했던 것 같아 
연휴 동안은 과제들을 돌아보며 문제의 출제 의도를 파악해 보는 시간을 다시 가져봐야겠다.

그리고 수준별 수업을 베이직반을 수강 중인데 스탠다드로 올라가게 됐다. 
사실 베이직반의 진도가 다른 반에 비해서 조금 느리다고 느끼긴 했지만 많은 도움을 받았기 때문에
더 배우고 싶은 것들이 많은데...? 평가가 끝나서 그런지 선택권이 없는?......
진도를 따라가는데 무리가 없다고는 하셨지만?... 
아무튼.. 좋은 일이니까~.. 개인과제를 마무리하는 데로 이론반 수업을 부지런히 수강해야겠다. 

하루가 48시간이었으면 좋겠어^^.. 

🔖 Tomorrow's Goal

  • 개인과제 level 2 진행하기
'TIL 🔖/TIL' 카테고리의 다른 글
  • [TIL] 백엔드 부트캠프 10주차 (2024/09/19 목) 아웃소싱 프로젝트 - 설계
  • [TIL] 백엔드 부트캠프 9주차 (2024/09/12 목) 레거시 코드의 테스트 코드 기반 리팩토링 -2
  • [TIL] 백엔드 부트캠프 9주차 (2024/09/10 화) AOP / 테스트코드
  • [TIL] 백엔드 부트캠프 9주차 (2024/09/09 월) 카카오 로그인 / 테스트
fargoe
fargoe
    fargoe
    fargoewave
    fargoe
    GitHub
    전체
    오늘
    어제
    • 분류 전체보기 (166)
      • TIL 🔖 (140)
        • TIL (69)
        • 코딩테스트 (71)
      • DEV (14)
        • Java & Spring (7)
        • MySQL (3)
        • Git&Github (4)
      • 개발지식 (10)
        • 알고리즘 (2)
        • 자료구조 (8)
        • CS (0)
      • 3D (1)
        • Unity (1)
      • ETC (0)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
fargoe
[TIL] 백엔드 부트캠프 9주차 (2024/09/11 수) 레거시 코드의 테스트 코드 기반 리팩토링
상단으로

티스토리툴바