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.service의 ManagerServiceTest 클래스에 있는 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 |