CODING/스파르타 내일배움캠프 TIL

Spring 심화 주차 개인 과제 트러블슈팅 TIL 모음

codingTrip 2025. 2. 25. 21:24

2025.02.24(월)

Spring 심화 주차 개인 과제 트러블슈팅

필수

Lv 1-3. Validation

사실 이전 개인과제에서 정규식을 적용하지 않고 기본으로 주어지는 Validation을 사용했기 때문에

정규식에 대해 익숙하지 않았던 것은 사실이다.

따라서 블로그로 검색해서 정규식에 대한 개념을 이해하고,

내가 이해한 바를 정규식으로 만들어 아래와 같이 테스트를 진행했다.

정규식 사이트에서 테스트 진행

https://regexr.com/

새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.

나는 위의 조건에 대해

8자 이상이고,

숫자와 대문자는 무조건 포함해야 하고,

나머지 소문자와 특수문자도 입력 가능하게 해야한다고 이해했다.

따라서 위와 같은 정규식으로 만들었다.

 

Lv 2. N+1 문제

먼저 각각의 개념에 대해 파악하는 것이 중요하다고 생각했다.

 

N+1 문제란?

JPA가 연관된 엔티티를 조회할 때 추가적인 쿼리를 반복적으로 실행하기 때문에 발생하는 문제

하나의 쿼리로 N개의 객체를 로딩한 후,

각 객체에 연관된 데이터를 추가로 조회하는 개별 쿼리가 N번 실행되면서 총 N+1번의 쿼리가 발생하는 문제

 

Fetch Join이란?

연관된 엔티티나 컬렉션을 SQL 한번으로 조회할 수 있도록 해주는 기능

한 번의 쿼리로 연관된 엔티티들을 함께 로드할 수 있음

 

@EntityGraph란? 

Data JPA에서 fect 조인을 어노테이션으로 사용할 수 있도록 만들어 준 기능

 

=> N+1 문제를 해결하는 방법에는 Fetch Join, @EntityGraph등이 있다.

과제에서는 @EntityGraph로 변경하는 것이었기 때문에 아래와 같이 수정했다.

사실 users(테이블명)인지 user(엔티티 필드명)인지 혼란스러웠다.

검색을 통해 엔티티 필드명이라는 것을 찾을 수 있었다.

@EntityGraph(attributePaths = {"user"})

 

Lv 3. 테스트코드 연습 -2-3

nullSafeEquals

https://docs.spring.io/spring-framework/docs/1.2.x/javadoc-api/org/springframework/util/ObjectUtils.html#nullSafeEquals(java.lang.Object,%20java.lang.Object)

둘 다 null이면 true

둘 다 null이 아니면 equals()

둘 중 하나가 null이면 false를 반환한다.

if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
    	throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}

따라서 위와 같은 조건을 보면서

'둘 중 하나가 null이니까 InvalidRequestException이 발생할 것 같은데 왜 안 되는 거지?'

라는 의문이 들었고, 이에 시간을 많이 쓰게 되었다.

 

if (!ObjectUtils.nullSafeEquals(
        user.getId(), 
        todo.getUser().getId())
) {
    	throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}

정확한 원인 분석을 위해 위의 코드처럼 줄을 나눠 보았다.

org.opentest4j.AssertionFailedError: Unexpected exception type thrown, 
Expected :class org.example.expert.domain.common.exception.InvalidRequestException
Actual   :class java.lang.NullPointerException
Caused by: java.lang.NullPointerException: Cannot invoke "org.example.expert.domain.user.entity.User.getId()" because the return value of "org.example.expert.domain.todo.entity.Todo.getUser()" is null

 

그랬더니 todo.getUser().getId()이 부분에서 문제가 발생했음을 파악할 수 있었다.

todo에서 getUser가 null이고, 이를 getId()를 하다보니

NullPointerException이 발생한 것이었다.

 

따라서 아래와 같이 기존 if문 조건에 todo.getUser가 null인 경우도 추가했다.

if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
    	throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}

 

 

내일 테스트코드 실전 특강이 있을 예정인데

열심히 잘 들어서 과제를 잘 마무리 해야겠다. 

 


2025.02.25(화)

Spring 심화 주차 개인 과제 트러블슈팅

도전

Lv 4. API 로깅 - Interceptor

Interceptor를 사용해서 구현하고자 했다.

아래의 코드처럼 실행하니

권한이 ADMIN임에도 equals가 false가 되었다.

if (!UserRole.ADMIN.equals(userRole)){
    log.error("미인증 사용자 요청");
    response.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
    return false;
}

이 조건문이 잘 실행되어야 제대로 관리자 권한인지 체크할 수 있는데... 하면서 원인을 찾아보았다.

 

원인은 각 비교군의 타입이 다르기 때문이었다.

UserRole.ADMIN은 Enum 타입이고

userRole은 String 타입으로 강제 형변환했으므로 String 타입이었다.

String userRole = (String) request.getAttribute("userRole");

따라서 Enum -> String 타입으로 형변환하거나

String -> Enum 타입으로 형변환 해주어야 했다.

나의 경우는 Enum -> String 타입으로 형변환하기로 했다.

 

Enum.name() vs Enum.toString()

if (!UserRole.ADMIN.name().equals(userRole)){
    log.error("미인증 사용자 요청");
    response.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
    return false;
}

Enum -> String 타입으로 형변환하는 방법은 크게 2가지가 있는데

나는 이중에서 Enum.name()을 사용했다.

그 이유는 name()은 final 메소드이므로 재정의할 수 없지만 toString()은 재정의가 가능하기 때문이다.

재정의가 불가능한 것이 더 안전하다고 판단했다.

출처 : https://velog.io/@nhj2927/Java-Enum.name-vs-Enum.toString

 

Lv 5. 위 제시된 기능 이외 ‘내’가 정의한 문제와 해결 과정

회원가입 후, 바로 JWT가 발급되는 점

1. 문제 인식 및 정의

 : 회원가입 후, 바로 RequestBody에 JWT 발급됨

 

이를 문제로 인식한 이유 :

 - 보안 문제: 사용자의 인증 정보가 아직 완전히 검증되지 않은 상태에서 토큰이 생성될 가능성이 존재함
현재 코드에서는 서비스에서 아래와 같이 이미 존재하는 이메일인지 확인하는 검증 로직만 존재함

if (userRepository.existsByEmail(signupRequest.getEmail())) {
    throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}

해당 코드에는 구현되어있지 않으나

추후 탈퇴 계정 확인 후 차단, 회원가입 시도 -> 관리자 승인 후 회원가입 완료 등의 기능 구현 시 문제 발생 가능성 존재함

- 로그인 시에도 JWT 발급:  로그인 시에 사용자의 이메일, 비밀번호 등을 검증한 후 JWT를 발급하는 것이 더 안전함

- 불필요한 토큰 생성: 회원 가입 후 사용자가 바로 로그인 하지 않으면, 불필요한 토큰 생성 가능성 존재함

- 토큰 관리의 어려움: JWT는 한 번 발급되면 서버에서 무효화하기 어려움.  따라서 회원 가입 시점에 발급된 토큰은 관리가 어려울 가능성 존재함.


2. 해결 방안
2-1. 의사결정 과정

회원 가입 후에는 JWT를 발급하지 않고, 로그인 시에만 발급하는 것으로 수정


2-2. 해결 과정

회원 가입 후 ResponseBody에는 회원의 아이디, 이메일, 역할 정보만 보여주는 것으로 수정

* 수정된 파일 목록
org.example.expert.domain.auth.dto.response.SignupResponse

/** 수정 후
 * 회원 가입 후 ResponseBody에는 회원의 아이디, 이메일, 역할 정보만 보여주는 것으로 수정
 */
private final Long id;
private final String email;
private final UserRole userRole;

public SignupResponse(Long id, String email, UserRole userRole) {
    this.id = id;
    this.email = email;
    this.userRole = userRole;
}

 

org.example.expert.domain.auth.service.AuthService

/** 수정 후
 * 회원 가입 후 ResponseBody에는 회원의 아이디, 이메일, 역할 정보만 보여주는 것으로 수정
 */
return new SignupResponse(savedUser.getId(),savedUser.getEmail(),savedUser.getUserRole());


3. 해결 완료
3-1. 회고

처음에는 '왜 회원가입 후, 로그인 후에도 JWT가 발급되지?' '단순히 보통 로그인 시에 JWT가 발급되지 않나?'

라는 막연한 생각과 단순한 의문만 가지고 있었으나

좀 더 회원가입 직후 JWT 발급 시 발생 가능한 문제점들을 찾아보니

더욱 나의 생각에 확신을 가질 수 있었다.

앞으로는 궁금한 부분에 대해 의문을 갖고 찾아보는 습관을 들여야겠다.

 

3-2. 전후 데이터 비교

수정 전 수정 후

 

Lv 6. 테스트 커버리지

이 단계는 완벽하게 소화할 것이라고 생각하지는 않고,

다만 일단 오늘 배운 테스트코드 실습 세션 자료를 따라 쳐보면서

테스트코드를 작성하는 것에 익숙해지고자 한다.

 

could not prepare statement [Table "USERS" not found (this database is empty); SQL statement:

출처 : https://stackoverflow.com/questions/72959168/spring-boot-h2-db-for-testing-throws-table-not-found-and-sql-syntax-error

원인은 application.properties에 mysql 관련 설정을 해준 것이 원인인 것으로 보인다.

default로 테스트 코드는 H2를 사용하므로 해당 부분을 주석처리 해주고 실행했다.

 

main과 test를 분리해서 따로 실행하는 부분은 추후 더 알아봐야겠다.