본문 바로가기

Spring

WebFlux

 

 

 

 

 


 

web-flux 경험하다 

spring-gateway 서버를 만드는데

spring-web 을 사용하지 않고

spring-web-flux를 사용한다

 

그래서 인증서버를 호출하는 함수, redis 조회하는 함수를 사용하는데

인텔리제이가 비동기로 작성하라고 한다.

 

Mono를 사용하라는데 

어라라?

if문을 사용을 못하네?

함수형으로 처리를 해야한다...

 

web-flux 학습의 필요성을 느끼다

3년 전에 web flux 책을 읽었었는데

그때는 필요성을 못 느낀 상태로 공부를 했어서 그런가보다~ 하고 술술 넘어 갔는데

이번에 spring-gateway 서버를 만들면서

비동기 처리를 위해 함수를 작성하다보니 욕심이 생긴다.

 

그래서 Web Flux 관련해서 공부한 것을 아래에 정리 해보려고 한다.


web-flux 왜 비동기인가?

애초에 왜 동기를 쓰지 않고 비동기를 쓰는거야?

spring-gateway 는 인증서버, 모노리틱 서버의 요청을 받는, 즉 모든 요청을 받는 수문장 같은 역할이다.

따라서 모든 요청을 받기 때문에 해야하는 일은 간단하게 검증 후 라우팅을 하는 역할이다.

 

요청이 많은데 동기로 처리하다보면 다른 요청을 계속 기다려?

아니지 답장이 오면 그때 처리하고, 다른 요청들도 계속해서 받아야지

그래서 비동기로 처리를 하는 것이다.

 

 

 

 

비동기를 쓰는 이유는 thread 때문이겠지?

기존의 spring-web은 동기로 처리하고 있어.

하나의 request가 곧 하나의 thread 인거지

그래서 thread를 관리하고, thread pool을 관리해야해.

공통으로 작업하는 변수가 있으면 각자의 thread에서 공통값을 통일하기 위해

, thread-safe 하도록

main thread memory에 즉시 값을 대입하는 둥.. 그런 역할을 하지.

 

 

 

 

spring-gateway는 비지니스 로직에 집중하기보다는 정말로 라우팅에 집중한다고 생각하면 됨.

비지니스 로직은 다른 서버에서 동기적으로 처리하고.

spring-gateway는 빨리 빨리 라우팅해주는 거지

그래서 적은 수의 thread로 많은 일을 처리 할 수 있는거야.

코드도 비동기적으로 작성하면 좋겠지?

 

 

 


 

web-flux 익숙하지 않다

함수형 프로그래밍으로 작성

기존에 쓰던 코드를 못쓴다. 가령 if문 던가..

if로 살던 내가.. switchIfEmpty() .filter()

public Mono<String> getRoleFromRedisMono(String userId, String role) {
		// 1. 레디스에서 role 값 가져옴, 없으면 인증 서버에서 요청해서 받는다
		final String redisKey = REDIS_USER_ROLE_KEY + userId;
		return Mono.justOrEmpty((String) redisTemplate.opsForValue().get(redisKey))
				// 값이 없을 때 비동기 작업 수행
				.switchIfEmpty(
						// 2. AuthService에서 역할을 가져오고 Redis에 저장(인증서버)
						authService.getRoleFromAuthService(userId)
				)
				// 3. 요청된 역할과 저장된 역할이 일치하는지 확인
				.filter(storedRole -> storedRole.equals(role))
				// 일치하지 않거나 오류가 발생한 경우
				// 4. 역할이 일치하면 반환
				.switchIfEmpty(Mono.error(new IllegalStateException("Role mismatch or not found")));
	}


	public Optional<String> getRoleFromRedis(String userId, String role) {
		String redisKey = REDIS_USER_ROLE_KEY + userId;
		String storedRole = (String) redisTemplate.opsForValue().get(redisKey);

		// 1. Redis에 저장된 역할이 없거나, 공백 문자열인 경우
		if (storedRole == null || storedRole.isBlank()) {
			log.info("Role for user {} not found in Redis, fetching from AuthService", userId);

			// 2. AuthService에서 역할을 가져옴
			storedRole = String.valueOf(authService.getRoleFromAuthService(userId));
		}

		// 3. 요청된 역할과 저장된 역할이 일치하는지 확인
		if (!role.equals(storedRole)) {
			log.warn("Role mismatch for user {}: expected {}, but found {}", userId, role, storedRole);
			return Optional.empty();
		}

		// 4. 역할이 일치하면 Optional로 반환
		return Optional.of(storedRole);
	}

 

 

 

동기 스타일

 

// Spring-Web 스타일	

// 3. role 권한 체크 할 때
		if (!roleService.checkUserRole(userId, role)) {
			return handleInvalidToken(exchange, "User role validation failed for token: " + token, HttpStatus.FORBIDDEN);
		}
        
  public boolean checkUserRole(String userId, String role) {

		// 권한 validation
		if (!UserRoleEnum.isValidRole(role)) {
			log.warn("Invalid role: {}", role);
			return false;
		}

		// CUSTOMER 는 role 체크 안한다
		if (Objects.equals(role, UserRoleEnum.CUSTOMER.getAuthority())) {
			return true;
		}

		// OWNER, MANAGER, MASTER 레디스 role 체크
		return redisService.getRoleFromRedis(userId, role).isPresent();
	}

 

 

비동기 스타일

 // 3. role 권한 체크 - 비동기 방식으로 변경
    return roleService.checkUserRoleMono(userId, role)
        .flatMap(hasRole -> {
            if (!hasRole) {
                return handleInvalidToken(exchange, "User role validation failed for token: " + token, HttpStatus.FORBIDDEN);
            }

            // 로그인 && 헤더가 있는 경우
            if (path.equals("/auth/v1/users/login")) {
                // 로그인이 성공했다고 가정하고 응답을 완료
                return exchange.getResponse().setComplete();
            }

            // custom header 생성
            ServerWebExchange modifiedExchange = authService.setCustomHeader(exchange,
                    HeaderResponseDto.builder()
                            .token(token)
                            .userId(userId)
                            .role(role)
                            .build());

            // 변경된 요청으로 체인 필터 진행
            return chain.filter(modifiedExchange);
        })
        .onErrorResume(e -> {
            log.error("Error occurred during role validation", e);
            return handleInvalidToken(exchange, "An error occurred while validating user role: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        });
}


public Mono<Boolean> checkUserRoleMono(String userId, String role) {

		// 권한 validation
		if (!UserRoleEnum.isValidRole(role)) {
			log.warn("Invalid role: {}", role);
			return Mono.just(false);  // 비동기적으로 false 반환
		}

		// CUSTOMER 는 role 체크 안한다
		if (Objects.equals(role, UserRoleEnum.CUSTOMER.getAuthority())) {
			return Mono.just(true);  // 비동기적으로 true 반환
		}

		// OWNER, MANAGER, MASTER 레디스 role 체크
		return redisService.getRoleFromRedisMono(userId, role)
				.map(storedRole -> {
					boolean hasRole = storedRole.equals(role);
					if (!hasRole) {
						log.warn("Permission denied for user: {} with role: {}", userId, role);
					}
					return hasRole;
				})
				.defaultIfEmpty(false)  // 값이 없을 경우 false 반환
				.onErrorResume(e -> {
					log.error("Error checking user role", e);
					return Mono.just(false);  // 에러 발생 시 false 반환
				});
	}