Lv 0. 프로젝트 세팅 - 에러 분석
문제 :
프로젝트를 실행했으나, 특정 에러로 인해 애플리케이션 실행에 실패했습니다! 발생한 에러의 원인을 정확히 분석하고 실행 가능하게 만들어주세요!
무슨 에러가 있을지 확인을 먼저 하기 위해 실행을 해본다!


자 에러코드가 있는 걸 발견했으니 무슨 오류인지 정확히 확인하기 위해 Caused by를 찾아주면 된다.
(Caused by 부분을 캡쳐하지 못해서 설명으로 대체하겠다.)
에러가 났던 이유는 application.properties가 없었기 때문이다!
application.properties는 스프링 애플리케이션 설정 파일이다.
DB설정이나 JWT 설정 등을 설정하는 파일이기 때문에 매우 중요하다!

그럼 문제를 알았으니 해결해 주기 위해 파일을 생성해준다.
resources를 자바가 아닌 메인에서 만들어준 이유는 코드가 아닌 프로그램이 사용하는 파일을 넣어줄 것이기 때문이다.
여기서 궁금했던 점!! 왜 하필 이름이 resources일까?
바로 Spring Boot가 자동으로 읽는 기본 설정 파일 위치이기 때문이다.
Spring Boot는 기본적으로 아래와 같이 자동으로 읽도록 설정되어 있다!
java 코드 → src/main/java설정/파일 → src/main/resources
그렇다면 다른 이름으로 할 수 있을까? 할 수 있다!
대신 설정을 따로 해줘야 된다고 한다!
하지만 읽히지 않는 문제가 생길 수도 있고, 나는 사용하지 않을 것이기 때문에 설명은 생략한다!
그럼 다시 문제로 돌아와서
application.properties를 만들면 끝일까?
당연하게도 아니다!
위에서도 말했 듯 application.properties는 스프링 애플리케이션 설정 파일이기 때문에 설정들을 해줘야 한다!

자 꼭 필요한 기본적인 설정들과 jwt 시크릿 키까지 넣어줬다!
이제 내가 발견했던 실행 할 때의 오류는 해결했으니 다른 문제가 있는지 다시 실행을 해준다.

다행히도 에러가 사라졌다!
그런데 저 WARN이 눈에 들어온다! 저건 왜 있는 걸까?
2026-03-04T19:51:35.330+09:00 WARN 20504 --- [ main]
JpaBaseConfiguration$JpaWebConfiguration :
spring.jpa.open-in-view is enabled by default.
Therefore, database queries may be performed during view rendering.
Explicitly configure spring.jpa.open-in-view to disable this warning
내용을 보면 이런 내용이다!
이런 경우에는 spring.jpa.open-in-view=false 라는 설정을 넣어주면 된다!

spring.jpa.open-in-view는 DB 연결을 언제까지 유지할지 결정하는 옵션이다.
기본값이 true라서 원한다면 따로 지정해서 바꿔줘야 한다!
간단하게 비교해보자면
true는 Lazy Loading 에러가 거의 발생하지 않아 개발이 편리하다.
하지만 Controller에서도 DB 쿼리가 실행될 수 있어 성능 문제나 N+1 문제가 발생할 수 있다.
false는 Service 계층까지만 DB 연결을 유지하기 때문에 성능 관리와 쿼리 통제가 가능하고 계층 구조가 명확해진다.
하지만 Controller에서 Lazy Loading을 하면 LazyInitializationException이 발생할 수 있다.
Lazy Loading: 필요할 때 DB 조회
N+1 문제: Lazy Loading 때문에 쿼리가 너무 많이 발생
LazyInitializationException: DB 연결 없어서 Lazy Loading 못함
이것까지 수정하고 나면 아주 예쁜 실행창이 기다리고 있다!

Lv 1. ArgumentResolver
문제 :
패키지 org.example.expert.config; 에 위치한 AuthUserArgumentResolver의 로직이 현재 동작하지 않고 있습니다.
AuthUserArgumentResolver 가 정상적으로 기능할 수 있도록 해주세요.
자 그럼 어떤 문제가 있어서 동작하지 않는지 살펴보자!

Spring은 기본적으로 Resolver를 자동 등록하지 않는다!
그래서 스프링이 자동으로 ArgumentResolver를 바로 인지하지 못해서 작동이 안 된 것 같다!
그럼 제대로 작동시키기 위해 WebConfig에서 등록을 해보자!

스프링 MVC 설정에 들어가서, 컨트롤러 파라미터를 해석하는 Resolver 목록에 AuthUserArgumentResolver를 추가 등록한다.
자세한 코드 및 설명은 사진을 참고하면 된다!

Resolver 목록에 AuthUserArgumentResolver를 등록해 주고 다시 확인해 보면 처음과 달리 흰색으로 바뀐 걸 확인할 수 있다!
Lv 2. 코드 개선
문제 :
Early Return, 필요한 if-else 피하기, Validation
1. 코드 개선 퀴즈 - Early Return
코드 개선을 위해 수정해야 할 부분을 찾아보자!

지금 코드에서 리팩토링 해주면 좋을 부분이다!
코드는 위에서 아래로 실행된다.
그렇기 때문에 지금과 같은 코드 흐름이면 encode() 실행되고 그다음에 이메일 중복 체크가 진행된다.
그러면 이메일이 중복이라 어차피 실패할 가입인데도 비밀번호를 해싱(암호화)이 진행된다.
그럼 이제 우리는 위치 리팩토링을 하여 이메일 중복 체크가 먼저 진행되도록 해주면 된다!

위에서 이야기했던 대로
조금 더 효율적인 코드를 위해 encode()가 쓸데없이 실행되는 걸 막기 위해,
두 코드의 자리를 바꿔 이메일 중복 체크가 먼저 진행되도록 리팩토링 해주었다!
// 이미 가입된 이메일이면 회원가입 중단을 위한 코드
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}
// 비밀번호를 암호화하기 위한 코드
String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());
위와 같이 빠르게 예외를 던지고 즉시 종료 시키는 것을 Fail Fast라고 한다!
Fail Fast은 문제가 있으면 즉시 실패시키는 방식이다!
성능 효율 좋고 버그 찾기가 쉽다.
Fail Fast과는 다르게 문제가 발생해도 프로그램이 계속 동작하도록 하는 방식이 있다.
바로 Fail Safe이다.
Fail Safe
는 에러 대신 기본 사용자 생성한다. 그래서 오류가 있어도 시스템 계속 동작한다!
그리고 서비스 중단을 방지한다.
2. 리팩토링 퀴즈 - 불필요한 if-else 피하기
불필요한 else 블록을 찾아보자!

이 부분에서 else 부분이 불필요한 것 같다!
throw가 실행되면 메서드가 바로 종료되기 때문에, 그다음 코드는 자동으로 else처럼 동작하기 때문에 불필요하다!
그래서 else를 사용하는 것이 오히려 가독성이 떨어진다.
그리고 또 다른 리팩토링 해야 할 부분!
API가 실패인데 body를 먼저 꺼내는 건 의미가 없다!
그렇기 때문에 WeatherDto [ ]도 순서를 바꿔주어 좀 더 자연스럽게 해 주면 좋을 것 같다!

사진과 같이 불필요한 else를 삭제해주고 순서도 리팩토링 해주면 끝난다!
// 날씨 데이터를 사용하기 전에 정상적인 데이터인지 검사하는 코드
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
}
// API 응답의 body(실제 데이터)를 가져와서 weatherArray에 저장
WeatherDto[] weatherArray = responseEntity.getBody();
// 날씨 데이터가 있는지 확인하는 코드
if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}
3. 코드 개선 퀴즈 - Validation
자 이번에는 주어진 코드를 해당 API의 요청 DTO에서 처리할 수 있게 개선해 보자!

그렇다면 이 부분이 어떤 내용을 담고 있길래 DTO로 보내려는 걸까?
// 조건들은 새 비밀번호의 길이가 8자보다 작거나
userChangePasswordRequest.getNewPassword().length() < 8 ||
// 문자열 어딘가에 숫자가 하나라도 포함되어 있거나
!userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
// 대문자가 포함되어있지 않으면
!userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*"))
// 위 규칙을 만족하지 않으면 예외를 발생
throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
간단하게 정리해 보았다!
그렇다면 줄여서 설명해 보면
DTO에 새 비밀번호로 변경할 때 조건과 그 조건을 작성해 주면 될 것 같다.
그리고 서비스로 들어오기 전에 검증이 먼저 터져야 하니까, 컨트롤러에서 @Valid를 붙여줘야 한다!




사진과 같이 컨트롤러와 디티오를 변경해 주면 완료해 주면 끝난다!
Lv 3. N+1 문제
문제 :
JPQL fetch join을 사용하여 N+1 문제를 해결하고 있는 TodoRepository가 있습니다.
이를 동일한 동작을 하는 @EntityGraph 기반의 구현으로 수정해 주세요.
위에서 간단하게 설명을 적어놨던 N+1 문제!
이번에는 자세하게 다뤄보겠습니다!
N+1 문제는
목록을 한 번(1번) 조회했는데, 그 목록의 각 항목(N개)에 딸린 연관 데이터를 가져오느라 쿼리가 N번 추가로 나가는 문제다.
그래서 총 쿼리 수가 1 + N이 돼서 N+1이라고 부른다.
(여기서 쿼리는 데이터베이스(DB)에 데이터를 요청하거나 수정하는 명령문을 말한다.)
N+1 문제가 발생하는 핵심 이유는 연관관계가 LAZY로 되어 있고, 목록 조회 후에 연관 데이터를 접근할 때 발생한다.
그렇다면 LAZY와 EAGER는 무엇일까?
LAZY는 데이터를 처음부터 가져오지 않고 실제로 필요할 때 가져오는 방식이고,
EAGER는 처음 조회할 때 연관 데이터도 같이 가져오는 방식이다.
@EntityGraph는 무엇일까?
fetch join(Todo를 가져올 때 User도 한 번에 같이 가져오게 하는 조인)을 어노테이션 방식으로 표현하는 것이다!
이 메서드로 조회할 때는 user도 같이 로딩해 줘라고 지정하는 것이다.
그럼 이론을 어느 정도 알아봤으니 코드를 한번 살펴보자!


저 부분을 이제 @EntityGraph를 사용하여 바꿔주면 된다.
그리고 코드를 보니 또 리팩토링 해줄 부분이 있다.
Optional<Todo> findByIdWithUser(Long todoId);
이 메서드는 fetch join 때문에 만들어줬다.
fetch join은 JPA가 자동으로 만드는 쿼리가 아니라 쿼리 자체가 완전히 다르기 때문에 새 메서드가 필요하다.
그런데 EntityGraph는 쿼리는 그대로 두고, 연관 데이터를 같이 로딩하라고 JPA에게 알려주는 기능이기 때문에
JpaRepository 안에 이미 기본으로 제공되는 메서드 findById()를 붙여주면 된다!
그렇기 때문에 레포지토리와 서비스에서 메서드를 변경해줘야 한다!


이렇게 @EntityGraph를 넣어주고 메서드까지 변경을 완료해 주면 끝이다!
Lv 4. 테스트코드 연습
문제 : 테스트 코드 연습
1. 테스트 코드 연습 - 1

이 테스트 코드는 비밀번호를 encode 하면 암호화되는지와matches로 비교하면 true가 나오는지를 검증하는 코드이다!
의도대로 성공할 수 있도록 수정해 보자!
지금 보이는 문제는 2개가 있다.
첫 번째 문제 : matches() 인자 순서가 잘못됐다!
// PasswordEncoder.matches(사용자가 입력한 평문 비밀번호, DB에 저장된 암호화 비밀번호)
boolean matches = passwordEncoder.matches(CharSequence rawPassword, String encodedPassword);
보통 위와 같은 형식으로 작성해 준다.
그런데 지금 작성된 코드를 보면 이런 식으로 되어있다.
boolean matches = passwordEncoder.matches(encodedPassword, rawPassword);
이렇게 순서가 바뀌면 의미가 완전히 달라져서 항상 실패하게 된다!
그렇기 때문에 이 부분을 수정해주어야 한다.
두 번째 문제 : @InjectMocks로 PasswordEncoder를 만들 수 없다!
@InjectMocks
private PasswordEncoder passwordEncoder;
@InjectMocks는
필드에 필요한 의존성을 주입해서 객체를 만들어주는 용도이기 때문에 어떤 구현체를 생성해야 하는지 알 수 없다.
그런데 PasswordEncoder는 보통 인터페이스이다. 즉, 구현체가 필요하다.
(구현체는 인터페이스가 정의한 기능을 실제로 동작하게 만드는 클래스이다.)
그렇기 때문에 암호화/검증을 하는지 확인하기 위해, 실제 구현체를 써주어야 한다.
그리고 직접 객체를 생성해 줄 예정이기 때문에 @ExtendWith(SpringExtension.class)을 지워주겠다!
@ExtendWith(SpringExtension.class)는 JUnit 테스트를 Spring 환경에서 실행하도록 해주는 어노테이션이다.
그래서 우리가 직접 객체를 만들어 줄 경우 필요가 없게 된다!
위에서 이야기했던 것들을 수정해 보자!


테스트까지 해주면 잘 돌아가는 것을 볼 수 있다!
1. 테스트 코드 연습 - 2
1번 케이스

자 이게 우리가 수정해야 할 테스트 코드이다.
문제가 되는 테스트 코드와 테스트 코드 메서드명을 수정해 주면 된다.
어디가 문제인지 정확히 확인하기 위해 서비스 로직도 확인해 주자!

서비스 로직을 확인해 보니 테스트 코드와는 다른 부분이 있다.
new InvalidRequestException("Todo not found")
이 부분에서 우리는 두 가지 수정사항을 발견하였다.
Todo가 없다면 InvalidRequestException 에러가 나온다는 것과
"Manager not found"가 아니라 "Todo not found"라는 점!
서비스 로직에 맞춰서 테스트 코드와 테스트 메서드명을 수정해 주자!

이렇게 수정을 해주면 된다!
2번 케이스

이어서 두 번째로 수정해줄 테스트 코드를 살펴보자!
어디가 문제일지 서비스 로직도 같이 확인해 주겠다.

테스트 코드에는
assertThrows(ServerException.class, ...)
라고 되어있는데 실제 서비스 로직에는
InvalidRequestException("Todo not found")
코드가 사용되고 있다!
그리고 가독성을 위해
long todoId = 1;
이 부분도 같이 수정해 주겠다.
자바는 자동으로 long으로 형변환한다.
즉 내부에는
long todoId = (long) 1;
이렇게 되어있다.
그렇기 때문에 필수적으로 바꿔줘야 하는 건 아니지만
명확하게 long 타입이라는 걸 표시하여 코드 읽는 사람이 바로 이해할 수 있게 해 주겠다.
그러면 이 부분들을 서비스 로직에 맞춰 테스트 코드를 수정해 주겠다.

이렇게 해주면 수정이 끝났다!
3번 케이스

이번에는 테스트 코드에 맞춰서 서비스 로직을 수정해 보자.
테스트 코드를 확인해 줬으니 서비스 로직도 확인하자.

서비스 로직을 보니 todo.user가 null인 경우를 체크해 주는 부분이 없다.
if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId()))
이 부분은 현재 로그인 유저와 Todo를 만든 유저가 일치하는지만 체크해주는 코드이다.
그렇기 때문에 todo.user에 null이 들어가면 NullPointerException(NPE)이 발생한다.
NullPointerException(NPE) null인 객체를 사용하려고 할 때 발생하는 에러다.
NPE를 예외처리 하지 않았을 경우
현재 메서드 실행 중단→ 호출한 메서드로 전파→ 계속 전파→ 결국 서버 에러로 이어진다.
그렇기 때문에 todo.user에 대해 null 체크를 해줘야 한다.

위에서 발견한 문제를 수정해 주면 끝이다!
이렇게 필수과제들이 끝났다.
과연 우리들이 어떤 걸 배우길 바라면 만든 과제일까?
아마도 문제를 분석하고 서비스 로직을 이해할 수 있도록 도와주기 위한 과정이 아니었을까 짐작해 본다.
이번 과제 덕분에
실제 개발 과정에서의 테스트 수정, 원인 분석, 코드 수정 등의 과정을 경험해 볼 수 있어서 좋았다.
'💻 Backend > Bootcamp 과제' 카테고리의 다른 글
| CH 5 플러스 Spring_코드 개선 과제 (0) | 2026.04.06 |
|---|---|
| 클라우드_아키텍처 설계 & 배포 트러블슈팅 (0) | 2026.03.11 |