Spring

Spring - 심화 주차 과제 세션

윤승 2025. 4. 21. 22:03

과제 실습 코드 

 

GitHub - younseung-Lee/spring-advanced

Contribute to younseung-Lee/spring-advanced development by creating an account on GitHub.

github.com

 

 

✅  기능 요구 사항

일주일 간 강의와 과제 세션이 있다 보니 시간이 촉박하여, 필수 기능까지만 구현했다..

해설세션을 듣고 나머지 도전기능을 알아 볼 생각이다. 

Lv1. 코드 개선은 저번 블로그에 정리하였다.

https://lys5654.tistory.com/52

 

Early Return 패턴

Early Return 패턴: 조건이 맞지 않으면 일찍 반환(early return) 해서 아래 코드의 실행을 피하는 구조이다. ✅ Early Return 패턴을 사용하면 좋은 점중첩된 if 줄이기가독성이 향상된다.불필요한 연산을

lys5654.tistory.com

 

 

Lv2. N+1 문제 
  • TodoRepository에서 JPQL의 fetch join을 사용해 N+1 문제 해결 중.
  • 동일한 로직을 @EntityGraph 기반으로 변경.

기존 코드

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

 

기존에 코드를 보면 JPQL 명시적 fetch join을 사용하여 Page<Todo>로 반환하고 있다. 

하지만 이 방법을 사용하면 세밀한 제어와 즉시 로딩이 가능하다는 장점이 있지만, 단점으로는 fetch join이 Pageable과 충돌할 수 가 있고, 문자열 기반 쿼리로 실수도 가능하다는 점이다.

 

수정 코드(@EntityGraph 사용)

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

 

@EntityGraph 어노테이션을 사용하면 Spring Data JPA가 내부적으로 fetch join을 사용해 user를 함께 조회할 수 있도록 해준다.

그리고 Pageable과 함께 사용 시 더 안정적이고, 유지보수가 쉽다.

 

언제 @EntityGraph를 쓰는 게 좋을까?

  • 반복적으로 동일한 fetch join이 필요한 경우
  • 간단한 관계 조회 (ex. 단순한 @ManyToOne, @OneToOne)
  • JPQL 없이 명시적인 쿼리 작성 없이 빠르게 선언적으로 fetch join을 쓰고 싶을 때

 

Lv3. 테스트코드 연습

 

테스트 코드 기능 수정을 하기에 앞서 간단하게 테스트 구조에 대 알아보자.

Given – "테스트를 위한 준비 단계"

  • 테스트를 실행하기 위해 필요한 데이터, 환경, 객체 등을 준비하는 단계

 

When – "실제 동작을 수행하는 단계"

  • 테스트하고자 하는 기능 또는 메서드를 실행하는 단계

Then – "결과를 검증하는 단계"

  • 실행 결과가 기대한 대로 동작했는지 검증(assert) 하는 단계

"준비(Given) → 실행(When) → 검증(Then)" 순서로 테스트의 흐름을 가진다.

 

✅  테스트 코드 연습 1

기존 코드

 

 

아직 테스트 코드에 대해 익숙하지가 않아서 뭐가 문제인지 감이 안 잡혔다. 

근데 에러원인을 찾아보니 정말 간단한 에러였던 것이다..

 

테스트 내에서 비밀번호 일치 여부를 확인하는 matches() 메서드를 사용하는 부분에서 예상한 결과(true)가 아닌 false가 반환되고 있었다.

원인을 찾아보니, 첫 번째 인자는 사용자가 입력한 *rawPassword이고, 두 번째 인자는 암호화된 encodedPassword입니다.

그런데 테스트 코드를 확인해보니, 이 두 인자의 순서가 반대로 들어가 있었던 것이 문제였다...!

 

수정 코드

passwordEncoder.matches(rawPassword, encodedPassword); // 올바른 인자 순서

이런 단순하지만 인자 순서 실수로 인해 테스트가 실패하는 경우가 종종 있을 것 같다..

 

✅  테스트 코드 연습 2-1

기존 코드

현재 이 코드 문제로는 테스트 메서드 이름은 NullPointerException을 기대하지만, 실제 서비스 코드에서는 InvalidRequestException을 던지고 있다. 또한 메시지 역시 실제 메시지와 동일하지 않은 것을 알 수 있다.

 

수정 결과:

  • 메서드 이름 변경: 매니저_저장시_투두가_없다면_InvalidRequestException_예외를_던진다() {}
  • 실제 코드의 메시지 내용과 일치하도록 수정: "manager not found" → "Todo not found"

✅  테스트 코드 연습 2-2

테스트 코드 2-2 문제도 2-1과 비슷한 에러이기에 간단하게 InvalidRequestException 대신 ServerException을 던지도록 수정해주었다.

 

수정 코드:

   new InvalidRequestException("Todo not found"));   ->  new ServerException("Todo not found"));

 

✅  테스트 코드 연습 2-3

테스트 코드 2-3 문제 테스트 패키지 org.example.expert.domain.manager.serviceManagerServiceTest 클래스에 있는 todo의_user가_null인_경우_예외가_발생한다() 테스트가 성공할 수 있도록 서비스 로직을 수정해야 한다.

 

기존코드

    @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("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
        }

        User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
                .orElseThrow(() -> new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다."));

        if (ObjectUtils.nullSafeEquals(user.getId(), managerUser.getId())) {
            throw new InvalidRequestException("일정 작성자는 본인을 담당자로 등록할 수 없습니다.");
        }

        Manager newManagerUser = new Manager(managerUser, todo);
        Manager savedManagerUser = managerRepository.save(newManagerUser);

        return new ManagerSaveResponse(
                savedManagerUser.getId(),
                new UserResponse(managerUser.getId(), managerUser.getEmail())
        );
    }

 

기존 이 코드에서 todo.getUser()가 이미 null인 상태이기 때문에,
todo.getUser(). getId()를 호출하는 순간 NullPointerException(NPE)가 발생한다.

 

해결 방법

  • todo.getUser()가 null인지 먼저 확인한 뒤에 getId()를 호출하도록 해야 한다. 
if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
    throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}

 

  • todo.getUser()가 null인 경우 → 예외 발생
  • 또는, todo.getUser(). getId()와 현재 사용자 ID가 일치하지 않는 경우 → 예외 발생

 

'Spring' 카테고리의 다른 글

아웃소싱 프로젝트 - 소프트 딜리트(SoftDelete) 트러블 슈팅  (1) 2025.04.29
Servlet Filter  (1) 2025.04.22
Early Return 패턴  (1) 2025.04.17
JPA 연관관계  (1) 2025.04.16
HttpMessageConverter  (1) 2025.04.15