Post

⚓ DATT 04 - JWT 기반 로그인 흐름 구현

개요

DATT 프로젝트 1주차 4일차에서는 회원가입 및 로그인 API를 구현하였다.

이번 작업의 핵심은 단순 로그인 기능 구현이 아니라, JWT 기반 인증 흐름을 실제 서비스 구조에 맞게 구성하는 것이었다.

특히 다음 요소들을 중심으로 설계하였다.

  • BCrypt 기반 비밀번호 암호화
  • Validation 기반 입력 검증
  • JWT Access Token 발급
  • 인증 실패 예외 처리 전략
  • 공통 응답 포맷 기반 API 구조

회원가입 API 구현

회원가입 API는 다음 흐름으로 구성하였다.

1
2
3
4
5
6
1. DTO Validation 수행
2. 이메일 중복 검사
3. 닉네임 중복 검사
4. 비밀번호 암호화
5. 회원 저장
6. 응답 반환

회원가입 요청 DTO는 다음과 같이 구성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public record SignupRequest(

        @Email(message = "이메일 형식이 올바르지 않습니다.")
        @NotBlank(message = "이메일은 필수입니다.")
        String email,

        @NotBlank(message = "비밀번호는 필수입니다.")
        @Size(min = 8, max = 30)
        String password,

        @NotBlank(message = "닉네임은 필수입니다.")
        @Size(min = 2, max = 30)
        String nickname
) {
}

Validation 기반 입력 검증 구조

Spring Validation을 통해 요청 데이터 검증을 수행하였다.

핵심은 다음 두 가지이다.

1
@Valid
1
2
3
@NotBlank
@Email
@Size

Controller에서:

1
2
3
public ApiResponse<SignupResponse> signup(
        @Valid @RequestBody SignupRequest request
)

처럼 @Valid를 선언하면 DTO Validation이 자동 수행된다.

검증 실패 시:

1
MethodArgumentNotValidException

이 발생하고, GlobalExceptionHandler에서 이를 처리하도록 구성하였다.

이를 통해 모든 Validation 실패 응답을 공통 포맷으로 통일할 수 있었다.

예시 응답:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "success": false,
  "data": {
    "code": "common.invalid_input",
    "message": "요청 값이 올바르지 않습니다.",
    "status": 400,
    "errors": [
      {
        "field": "email",
        "message": "이메일 형식이 올바르지 않습니다."
      }
    ]
  },
  "error": null
}

BCryptPasswordEncoder 적용 이유

회원가입 시 비밀번호는 평문으로 저장하지 않고 BCrypt 기반 암호화를 적용하였다.

1
2
String encodedPassword =
        passwordEncoder.encode(request.password());

BCrypt를 사용한 이유는 다음과 같다.

  • 단방향 해시 기반
  • Salt 자동 생성
  • Rainbow Table 공격 방어
  • Spring Security 기본 지원
  • 실무 표준 수준의 사용 빈도

특히 BCrypt는 같은 비밀번호라도 매번 다른 해시값이 생성된다.

즉:

1
2
3
password123
→ $2a$10$...
→ 매번 다른 결과

구조로 동작한다.

따라서 로그인 검증 시에는 문자열 비교가 아니라:

1
2
3
4
passwordEncoder.matches(
        rawPassword,
        encodedPassword
);

방식으로 검증해야 한다.


JWT 기반 로그인 흐름

로그인 API는 다음 흐름으로 구성하였다.

1
2
3
4
1. 이메일 조회
2. 비밀번호 검증
3. JWT Access Token 생성
4. Access Token 반환

핵심 구현:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Member member = memberRepository.findByEmail(
        request.email()
).orElseThrow(() ->
        new BusinessException(
                ErrorCode.INVALID_CREDENTIALS
        )
);

if (!passwordEncoder.matches(
        request.password(),
        member.getPassword()
)) {
    throw new BusinessException(
            ErrorCode.INVALID_CREDENTIALS
    );
}

String accessToken =
        jwtProvider.createAccessToken(
                member.getId(),
                member.getRole().name()
        );

JWT 내부에는 다음 정보를 Claim으로 저장하였다.

1
2
- memberId
- role

현재는 DB 조회 없이 Claim 기반 인증 구조로 구현하였다.

이후에는 Refresh Token 및 Redis 기반 인증 구조로 확장할 예정이다.


인증 실패 예외 처리 전략

로그인 실패 시에는 다음 두 경우를 구분하지 않았다.

1
2
- 존재하지 않는 이메일
- 비밀번호 불일치

둘 다:

1
auth.invalid_credentials

로 통일하였다.

이유는 이메일 존재 여부 노출을 방지하기 위해서이다.

만약 다음처럼 분리하면:

1
2
존재하지 않는 이메일입니다.
비밀번호가 일치하지 않습니다.

공격자가 가입된 이메일 목록을 추론할 수 있다.

따라서 인증 실패 응답은 하나로 통일하는 것이 일반적인 보안 전략이다.

최종 ErrorCode:

1
2
3
4
5
INVALID_CREDENTIALS(
    HttpStatus.UNAUTHORIZED,
    "auth.invalid_credentials",
    "이메일 또는 비밀번호가 올바르지 않습니다."
)

JSON Parse 예외 처리

추가로 잘못된 JSON 요청 처리도 보완하였다.

예:

1
2
3
{
  "email": "test@test.com",
}

같은 malformed JSON 요청은:

1
HttpMessageNotReadableException

으로 처리된다.

이를 GlobalExceptionHandler에서 별도 처리하여 공통 응답 포맷으로 통일하였다.


테스트 코드 기반 검증

회원가입 및 로그인 로직은 테스트 코드 기반으로 검증하였다.

테스트 메서드명은 BDD 스타일 네이밍 규칙을 사용하였다.

1
2
3
4
5
givenSignupRequest_whenSignup_thenSuccess()

givenDuplicatedEmail_whenSignup_thenThrowException()

givenValidCredential_whenLogin_thenSuccess()

이를 통해:

1
2
3
4
5
6
7
8
Given
→ 어떤 상황에서

When
→ 어떤 행위를 수행했고

Then
→ 어떤 결과가 발생하는가

를 명확하게 표현할 수 있었다.


마무리

이번 작업을 통해 DATT 프로젝트의 인증 구조가 실제 서비스 형태에 가까워졌다.

특히 다음 요소들을 직접 구현하면서 인증 시스템의 핵심 흐름을 이해할 수 있었다.

  • BCrypt 기반 비밀번호 암호화
  • Validation 기반 입력 검증
  • JWT Access Token 발급
  • 인증 실패 예외 처리 전략
  • 공통 응답 포맷 구조
  • 테스트 코드 기반 검증

이후에는 Refresh Token 및 Redis 기반 인증 구조를 추가하고, JWT 인증 필터와 실제 인증 API 흐름을 완전히 연결할 예정이다.

This post is licensed under CC BY 4.0 by the author.