1. 핵심 목표: WebFlux 환경에 맞는 인증 파이프라인 구축
AuthIntegrationTests
package com.example.distributed;
import com.example.distributed.domain.User;
import com.example.distributed.dto.LoginRequest;
import com.example.distributed.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Transactional
public class AuthIntegrationTests {
@Autowired
private WebTestClient webTestClient;
@Autowired
private ObjectMapper objectMapper;
private static final String VALID_USER_ID = "valid_user";
private static final String VALID_PASSWORD_PLAINTEXT = "correct_password_123";
private static final String WRONG_PASSWORD_PLAINTEXT = "11111111_CompletelyDifferent";
@TestConfiguration
static class TestDataSetup {
@Bean
public User testUser(UserRepository userRepository, PasswordEncoder passwordEncoder) {
String encodedPassword = passwordEncoder.encode(VALID_PASSWORD_PLAINTEXT);
User testUser = new User(VALID_USER_ID, encodedPassword);
return userRepository.save(testUser);
}
}
@Test
@DisplayName("유효한_자격증명으로_로그인_요청에_성공해야_하고_JWT를_받아야_한다")
void 유효한_자격증명으로_로그인_요청에_성공해야_하고_JWT를_받아야_한다() throws Exception {
LoginRequest loginRequest = new LoginRequest(VALID_USER_ID, VALID_PASSWORD_PLAINTEXT);
webTestClient.post().uri("/login")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(loginRequest)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.token").exists()
.jsonPath("$.token").isNotEmpty();
}
@Test
@DisplayName("잘못된_비밀번호로_로그인_시도_시_401_에러가_발생해야_한다")
void 잘못된_비밀번호로_로그인_시도_시_401_에러가_발생해야_한다() throws Exception {
LoginRequest wrongRequest = new LoginRequest(VALID_USER_ID, WRONG_PASSWORD_PLAINTEXT);
webTestClient.post().uri("/login")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(wrongRequest)
.exchange()
.expectStatus().isUnauthorized();
}
@Test
@DisplayName("인증되지_않은_상태로_보호된_리소스에_접근_시_401_에러가_발생해야_한다")
void 인증되지_않은_상태로_보호된_리소스에_접근_시_401_에러가_발생해야_한다() throws Exception {
webTestClient.post().uri("/api/protected")
.exchange()
.expectStatus().isUnauthorized();
}
}
WebTestClient 활용
실제 서버를 띄운 것과 유사한 환경에서 인증 흐름을 테스트
- WebTestClient: 논블로킹 방식으로 API에 요청을 보내고 응답을 검증하는 도구로, WebFlux 환경 테스트에 최적화되어 있음.
- 테스트 시나리오: 유효한 로그인(JWT 발급), 잘못된 비밀번호(401), 토큰 없는 접근(401) 등 발생 가능한 모든 상황을 자동화하여 검증
AuthController
package com.example.distributed.controller;
import com.example.distributed.dto.LoginRequest;
import com.example.distributed.dto.LoginResponse;
import com.example.distributed.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@RestController
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/login")
public Mono<ResponseEntity<LoginResponse>> login(@RequestBody LoginRequest request) {
return Mono.fromCallable(() ->
userService.authenticateAndGenerateToken(
request.getUsername(),
request.getPassword()
)
)
.subscribeOn(Schedulers.boundedElastic())
.map(jwtToken -> {
LoginResponse response = LoginResponse.builder()
.success(true)
.message("로그인 성공 및 JWT 토큰 발급")
.token(jwtToken)
.build();
return ResponseEntity.ok(response);
})
.onErrorResume(RuntimeException.class, e -> {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage()));
});
}
}
비동기 로그인 API
동기식으로 작동하던 기존 로직을 Mono로 감싸 비동기 스트림으로 전환
- Mono.fromCallable & Schedulers.boundedElastic(): [DB 조회나 비밀번호 매칭처럼 CPU/IO 소모가 큰 동기 로직이 WebFlux의 이벤트 루프를 방해하지 않도록, 별도의 스레드 풀(boundedElastic)에서 실행되도록 격리
- onErrorResume: 인증 실패 시 발생하는 예외를 401 Unauthorized 상태 코드로 변환하여 클라이언트에게 명확한 에러를 반환
1. 유효한_자격증명으로_로그인_요청에_성공해야_하고_JWT를_받아야_한다
올바른 사용자 정보로 로그인 인증에 성공하는지 확인, 그리고 로그인이 성공했을 시에 응답으로 JWT 토큰을 정확히 발급받는지를 검증한다. 비동기 환경에서 사용자 조회, 비밀번호 비교, 토큰 생성까지 성공하는지를 확인
2. 잘못된_비밀번호로_로그인_시도_시_401_에러가_발생해야_한다
잘못된 비밀번호가 입력되었을 때 인증 실패 로직 작동 확인. UserService가 null을 반환하고 AuthController가 401로 올바르게 변환하는지를 확인
3. 인증되지_않은_상태로_보호된_리소스에_접근_시_401_에러가_발생해야_한다
JWT 토큰 없이 보호된 경로로 요청을 보내면 Security로 진입한다. JwtAuthenticationFilter가 요청 헤더 검사하고 토큰을 찾지 못하면 SecurityConfig 설정에 따라 접근 불가 결정이 내려지고 401을 생성하고 반환함을 확인
'분산 처리 환경' 카테고리의 다른 글
| 스프링 분산 처리 환경 9: Resilience4j 서킷 브레이커 적용 (0) | 2025.11.21 |
|---|---|
| 스프링 분산 처리 환경 8: Spring Cloud Gateway, JWT 인가 필터 및 경로별 라우팅 검증 테스트 (1) | 2025.11.19 |
| 스프링 분산 처리 환경 6: Spring Cloud Gateway (0) | 2025.11.15 |
| 스프링 분산 처리 환경 5: DB 연동 (0) | 2025.11.13 |
| 스프링 분산 처리 환경 4: BCryptPasswordEncoder (1) | 2025.11.12 |