SPARTA 과제/SPRING

심화) LV 0~ LV 4 필수 문제풀이 과정

kjw81024 2026. 3. 4. 00:08

0. 에러분석

 

일단 제일 아래에 있는 오류부터 확인했다 

Could not resolve placeholder 'jwt.secret.key' in value "${jwt.secret.key}"

jwt.secret.key 라는 자리에 해당 값을 찾을 수 없는듯 하다

-> IllegarArgumentException

 

해당 오류는 jwtUtil 에서 발생했기 때문에 Component annotation 이 붙은 JwtUtil class 가 정상적으로 만들어지지 않았고 

-> BeanCreationException

 

jwtUtil 을 참조하고 있는 FilterConfig 에서는 의존성이 만족되지 못해 오류가 발생한다

-> UnsatisfiedDependencyException 

 

해당 부분까진 트러블 슈팅으로 메모해둔 부분이라 순서를 알겠는데 WebServerException 과 ApplicationContextException 은 잘 모르겠어서 일단 처음 발생한 IllegarArgumentException 을 해결해주도록 한다.

 

해당 화면에서 필드를 설명하면

SecretKey 는 JWT 의 서명을 생성하고 검증할 때 쓰는 비밀키 -> 원래 application.properties 에 적혀있음 

key 는 secretKey 를 변환한 실제 키 객체임 

 

init 에서는 bytes 를 key 를 초기화해서 값을 넣어줌

application.properties 에서 가져온 문자열 secretKey 를 디코딩해서 원래 배열로 변환하는 역할 

 

찾아보니 application 파일이 존재하지 않기 때문에 이전 프로젝트에서 썼던 파일을 새로 생성

심화주차는 application.propertise 가 아닌 application.yml 파일을 사용했기 때문에 해당 파일을 복사해왔다

 

파일 위치 -> src/main/resources/application.yml 

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/nbcam
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true


jwt:
  secret:
    key: happyjavawithtakoiloveyoutakolivewithmeforeverplzbabocatcutecat # 여기를 임의로 설정했다

너무 짧게 설정하면 위 같은 오류가 뜨니 어느정도의 길이를 가지게 secret key 를 설정해준다 

 

application 파일만 추가하니까 추가로 있던 WebServerException 과 ApplicationContextException이 사라진걸 보니까 다 연결된 오류인 것 같다! 

웹서버에서 문제가 생기면 Spring container 가 초기화가 안 돼서 생기는 문제

 

코드 돌아가는 김에 diagram 뽑아서 확인하고 넘어가기 

 


1. ArgumentResolver 

 

ArgumentResolver 와 관련한 자료를 찾아봤다

https://breakcoding.tistory.com/402

 

[Spring] HandlerMethodArgumentResolver에 대해 알아보자

안녕하세요 오늘은 Spring에서 제공하는 정말 편리한 기능인 HandlerMethodArgumentResolver에 대해 알아보겠습니다. HandlerMethodArgumentResolver는 무엇이고 어떤 경우에 사용하는 것이 좋을까요? 예제를 보며

breakcoding.tistory.com

 

client 가 header 에 token 을 담아서 request 를 보낼 때 원래는 @RequestHeader String token 이 있고

해당 token 을 decode (파싱) 해서 client 의 정보를 뽑고 객체를 사용함 

 

 이 때 특정 어노테이션을 하나 생성하고, HandlerMethodArgumentResolver 를 구현한 클래스를 만들고 등록하면 

토큰을 파싱하고 객체를 return, 즉 사용자 정보를 얻는 과정을 쉽게 만들 수 있음

 

이 때 필요한게 회색 줄 친 클래스들이다 

 

다시 코드로 돌아가서 어느 클래스가 구현이 잘 되지 않았는지 확인했다

# 1. 특정 어노테이션 class 

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}
# 2. HandlerMethodArgumentResolver 를 구현한 클래스

public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
	# 후략

일단 해당 클래스에 bean 으로 등록하는 어노테이션이 빠져있으므로 추가

 

또 HandlerMethodArgumentResolver 를 구현한 클래스를 등록해주는 클래스가 존재하지 않는다

-> WebConfig 클래스 만들어주기 

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final AuthUserArgumentResolver authUserArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers){
        resolvers.add(authUserArgumentResolver);
    }
}

debug
postman

 

Controller 에서 확인을 해보니 Signup 과 Signin 둘 다 bearerToken 을 return 하고, 그대로 잘 작동한다


2. 코드개선


(1) Early Return 

@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());
    # 후략

Encoding 하는 코드와 String 형태로 받아온 UserRole 을 Enum 형태로 바꿔주는 메서드 앞으로 email 중복체크 로직을 이동

 

확인

duplicated email
debuging 화면

미리 table 에 값을 삽입하고 똑같은 값으로 요청을 보냈을 때 debug 를 찍어보니까 

SignupRequest 를 확인한 후 encoding 하는 로직 없이 바로 error 가 던져지는 것을 확인할 수 있음 


(2) unnecessary IF - ELSE

 

if else 문이 중첩되어 나타나게 되면 코드 가독성이 떨어지기 때문에 불필요하게 사용하는것을 지양해야함

 

기존 코드를 살펴보면 getTodayWeather method는 Todo 를 저장할 때 당일의 날씨를 return 해주는 역할

 

responseEntity 전체를 가져와서

요청이 성공했는지 확인 ( getStatusCode 를 통한 OK 체크)

가져와졌다면 getBody 를 통한 DTO 체크

WeatherDTO 리스트를 가져와서 Entity.getBody 를 통해서 가져와서 null 인지 확인, 비어있는지 확인

# 날씨 데이터를 가져오는 로직이 OK 로 성공했는지 확인
# 해당 if 문을 통과 했다는 것은 요청 자체가 성공했다는 의미 
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
    throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
}

# Body 를 가져옴
# 이 때 responseEntity 가 Null 이면 NPE 가 뜨겠지만 위에서 확인을 했고
# getBody method 자체는 nullable 이기 때문에 아래 if 문으로 확인
WeatherDto[] weatherArray = responseEntity.getBody();

# 날씨 데이터를 조회하는 요청이 성공 
# -> null 이거나 공백인지 확인 하는 로직은 따로 작성해주기
if (weatherArray == null || weatherArray.length == 0) {
    throw new ServerException("날씨 데이터가 없습니다.");
}

 

else 문을 풀어주는것과 동시에 getBody 부분을 getStatus 를 확인한 다음으로 옮겨서 불필요한 메서드 호출을 방지했다


(3) Validation

service 계층에서 password 를 확인해주던걸 DTO 로 옮겨줬다

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserChangePasswordRequest {

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

 


3. N+1 problem 

query 문을 주석처리 하고 아래 코드를 method 위에 추가했다

@EntityGraph(attributePaths = {"user"})

 

강의자료에서 EntityGraph 는 Page로 return 하는 게 안 된다고 해서 더 찾아봤는데 다른 자료에서 EntityGraph 도 Page 를 사용해줄 수 있다고.. 나와있어서 일단 체크하고 패스

 

 


4. 테스트 코드 

(1) 정상작동 되도록 확인

matches method 는 매개변수 순서가 raw 이후 encoded 를 넣어줘야함 

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

 

Java HotSpot(TM) 64비트 서버 VM 경고: 부트스트랩 클래스 경로가 추가되었기 때문에 부트 로더 클래스에만 공유가 지원됩니다

테스트는 잘 통과했는데 저 warning은 뭐지 싶어서 일단 papago 를 돌려봤는데 별건 아니고 

 

JVM 을 실행할 때 옵션을 추가한 게 있으면 발생하는 오류라고 한다

error 가 아니라 warning 이므로 문제가 있을 수도 있다고 sharing 해주는거니까 크게 신경쓰지 않아도 됨 

(IntelliJ 가 classLoader 를 건드려서 생기는 문제!)


(2) 예상대로 예외처리하도록 수정

1) 

실제 코드에서 던지는 메시지가 Todo not found 이므로 Test 에서도 Manager -> Todo 로 수정

빨강색 보기싫군. .

 

2) 

현재 ServerException 으로 던지고 있는걸 InvalidRequestException 으로 수정해주기

 

3)

if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
    throw new InvalidRequestException("일정을 생성한 유저만 담당자를 지정할 수 있습니다.");
}

해당 if 문에서 오류가 발생

 

처음보는 method 가 있어서 찾아봤다

https://jh-tr.tistory.com/285#google_vignette

 

[Java] ObjectUtils 클래스 메서드 사용법, 예제 (isEmpty, nullSafeEquals, isArray)

ObjectUtils 클래스는 Spring Framework에서 제공하는 유틸리티 클래스로서, 객체와 배열에 관련된 다양한 작업을 수행할 수 있습니다. ObjectUtils 클래스에서 자주 사용하는 ObjectUtils.isEmpty , ObjectUtils.nullS

jh-tr.tistory.com

 

ObjectUtils.nullSafeEquals(obj1, obj2)

obj1 과 obj2 가 모두 null 이면 true 반환 (null == null 느낌인듯)

하나가 null 이면 그냥 equals method 를 호출, NullPointerException 을 발생 Xx

 

-> user.getId 가 null 이든 todo.getUser().getId() 가 null 이든 NullPointerException 이 발생하지 않는데 대체 왜 NullPointerException 이 발생하는가. . 

 

todo.getUser() 에서 NPE 발생

애초에 todo.getUser() 가 null 이면 getId() 호출 순간 null 이 발생하게 됨

 

if (todo.getUser() == null){
    throw new InvalidRequestException("일정을 생성한 유저만 담당자를 지정할 수 있습니다.");
}

위 코드로 한 번 더 확인해줘야함 

message 가 동일하니까 하나의 if문으로 합쳐줄까 고민했는데 더 가독성이 떨어지는 기분이라 일단 따로 작성했다