Spring 심화 주차 개인 과제

하늘·2025년 4월 17일

Spring 부트캠프

목록 보기
18/18

Lv 1. 코드 개선

1. 코드 개선 퀴즈 - Early Return

@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);
}
if (userRepository.existsByEmail(signupRequest.getEmail())) {
    throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}

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

@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 피하기

    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 블록을 없애 코드를 간결하게 합니다.

답:

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

3. 코드 개선 퀴즈 - Validation

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

패키지 package org.example.expert.domain.user.service; 의 UserService 클래스에 있는 changePassword() 중 아래 코드 부분을 해당 API의 요청 DTO에서 처리할 수 있게 개선해주세요.


UserChangePasswordRequest

@NotBlank
@Pattern(
        regexp = "^(?=.*[A-Z])(?=.*\\d).{8,}$",
        message = "새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다."
)
private String newPassword;

UserController

 @PutMapping("/users")
public void changePassword(@Auth AuthUser authUser, @RequestBody @Valid UserChangePasswordRequest userChangePasswordRequest) {
    userService.changePassword(authUser.getId(), userChangePasswordRequest);
}

Lv 2. N+1 문제

  • JPQL 특정 기능을 사용하여 N+1 문제를 해결하고 있는 TodoRepository가 있습니다. 해당 Repository가 어떤 기능을 활용해서 N+1을 해결하고 있는지 분석 해보세요.
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") 
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
idtitleuser
1TIL 작성5
2코드카타 하기2
3강의 다 듣기5

위에 Todo데이트를 user 정보랑 같이 가져오는거

LEFT JOIN FETCH

단어
LEFT JOINTodo에 user가 없어도 Todo는 가져온다.
FETCHUser도 같이 한 번에 가져오기
t.userTodo가 갖고 있는 user정보

ORDER BY t.modifiedAt DESC

  • Todo를 수정한 시간 기준으로 가장 최근에 수정된 것부터 나열
메서드의미
findAllBy전체 다 가져옵니다.
OrderBy....DescmodifiedAt 기준으로 내림차순 정렬(최근 → 오래된 순)

(Pageable pageable)

  • 몇 번째 페이지를 가져올지 한 페이지에 몇 개씩 가져올지 절할 수 있게 해주는 파라미터
@Query("SELECT t FROM Todo t " +
        "LEFT JOIN FETCH t.user " +
        "WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

LEFT JOIN FETCH t.user

  • Todo와 연결된 user도 한번에 같이 가져온다

    WHERE t.id = :todoId

  • 특정 id 값 하나만 조회

  • :todoId는 파라미터로 넘기는 값

    Optional< Todo >

  • Todo가 있을 수 있고 없을 수 있어서 Optional로 감싼 것

이를 동일한 동작을 하는 @EntityGraph 기반의 구현으로 수정해주세요.

@EntityGraph

  • 필요한 데이터를 한 번에 같이 가져오게 하는 도구

  • @EntityGraph를 안쓰면 Todo를 가져온 다음에 다시 Todo에 연결된 user를 하나씩 따로 가져와야함(지연로딩 = N + 1 문제 발생)

  • @EntityGraph를 쓰면 Todo랑 연결된 user를 동시에 가져옴

    @EntityGraph로 구현하면

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

@EntityGraph(attributePaths = {"user"})
Optional<Todo> findById(Long todoId);

Lv 3. 테스트코드 연습

1. 테스트 코드 연습 - 1

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

@ExtendWith(SpringExtension.class)
class PasswordEncoderTest {

@InjectMocks
private PasswordEncoder passwordEncoder;

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

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

    // then
    assertTrue(matches);

passwordEncoder.matches(encodedPassword, rawPassword);

이부분에서 자물쇠(암호화된 비밀번호) 에 열쇠(내가 입력한 비밀번호) 를 넣어야 열리는데 위에 코드처럼 하면 열쇠를 자물쇠로 착각하고 자물쇠로 열쇠를 열려는 상황이여서

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

위에 처럼 바꾸면 된다.

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

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

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

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

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

      InvalidRequestException exception = assertThrows(InvalidRequestException.class,
            () -> managerService.getManagers(todoId));
    assertEquals("Manager not found", exception.getMessage());

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

1번케이스

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

// 기존
public void manager목록조회시_Todo가없다면NPE에러를_던진다()

// 변경
public void manager목록조회시_Todo가없다면예외가발생한다()

2번 케이스

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

// 기존
assertEquals("Todo not found", exception.getMessage());

// 변경
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new ServerException("Todo not found"));

3번 케이스

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

org.example.expert.service.MaanagerService
if (todo.getUser() == null) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}

2개의 댓글

comment-user-thumbnail
2025년 4월 24일

아니 이게 산삼보다 귀하다는 글인가요

답글 달기
comment-user-thumbnail
2025년 4월 25일

잘 배우고 가요 !

답글 달기