본문 바로가기
프로젝트/AnswerDev

[AnswerDev] 2. 비동기 처리

by kim-dev 2024. 8. 15.
반응형

 

지금까지 스프링부트로 개발했을 때에는, DB나 API 요청이 많으면 서비스가 그 요청을 완료할 때까지 잠시 멈췄었다.
요청을 모두 순차적으로 처리하고, 한 번에 하나의 요청만 처리했기 때문이다.

그래서 서비스의 성능을 향상시키기 위해서는 요청을 처리하는 속도를 높이거나, 동시에 여러 작업을 처리할 수 있게 해줘야 했다. 그래서 비동기 처리를 허용했다.

 

// AnswerDevApplication.java

@EnableAsync
@SpringBootApplication
public class AnswerDevApplication {

	public static void main(String[] args) {
		SpringApplication.run(AnswerDevApplication.class, args);
	}

}

자신의 Application.java에서 @EnableAsync 어노테이션을 붙여주면, 이제 해당 서비스는 비동기적으로 가동될 준비가 된 것이다.

 

이제 비동기적으로 작동시키고자 하는 메서드 위에 @Async 메서드를 붙여주면 해당 메서드가 비동기적으로 동작한다.

// UserService.java

@Transactional
@Async
public CompletableFuture<SignUpServerDto> signUp(SignUpClientDto dto) {

    ...

    SignUpServerDto result = new SignUpServerDto(accessToken, refreshToken);
    return CompletableFuture.completedFuture(result);
}

여기서 주의해야 할 것은, 비동기로 돌아가는 메서드는 void 혹은 CompletableFuture<결과값>를 반환해야 한다.

void는 결과값으로 아무 것도 반환하지 않을 때 사용되는 것인데, CompletableFuture<>은 무엇일까?

쉽게 말하면, 비동기 메서드는 그 메서드가 완료될 때까지 기다리지 않기 때문에, 해당 메서드가 완료되지 않은 채로 메서드의 결과값을 반환한다. 그래서 아직 해당 결과값이 미정이라는 의미로 CompletableFuture<>로 감싸서 반환하는 것이다.

 

그렇다면 이 결과값을 받는 쪽에서는 어떻게 받아서 사용해야 할까?
그렇다. 해당 메서드가 온전히 완료될 때까지 기다린 후 결과값이 제대로 반환되었을 때 그 값을 사용하면 되는 것이다.

// UserController.java

@PostMapping("/signup")
public SignUpServerDto signUp(@RequestBody SignUpClientDto dto) throws ExecutionException, InterruptedException {
    return userService.signUp(dto).get();
}

CompletableFuture<>의 .get() 함수를 사용하면, 해당 처리를 완료할 때까지 잠깐 동기적으로 수행한 후 결과값을 받아온다.

CompletableFuture<SignUpServerDto>를 그대로 받을 수도 있겠지만, 그렇게 사용하면 결과값이 온전하지 않겠지?

 

 

비동기로 처리하면 성능 차이가 눈에 보일 정도로 확실히 빠르게 작동시킬 수 있다.

그러니... 웬만하면 @Async와 CompletableFuture<>을 적극 활용해서 비동기적으로 구현하자.

 

 

 

추가)

@Transactional
@Async
public CompletableFuture<ResponseTokenServerDto> login(LoginClientDto dto) {
     String username = dto.getUsername();
     String password = dto.getPassword();

     User theUser = userRepository.findByUsername(username);
    if (theUser == null) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "아이디 혹은 비밀번호가 잘못되었습니다.");
    }

    if (passwordEncoder.matches(password, theUser.getPassword())) {
        String accessToken = jwtUtil.createToken(theUser.getId(), JwtEnum.ACCESSTOKEN);
        String refreshToken = jwtUtil.createToken(theUser.getId(), JwtEnum.REFRESHTOKEN);

        ResponseTokenServerDto result = new ResponseTokenServerDto(accessToken, refreshToken);
        return CompletableFuture.completedFuture(result);
    } else {
        throw new ResponseStatusException((HttpStatus.BAD_REQUEST, "아이디 혹은 비밀번호가 잘못되었습니다."));
    }
}

이런 로그인 메서드를 간단히 구현했다.
여기서 의문이 든 건... theUser을 DB에서 가져오는데, DB조회는 상대적으로 시간이 오래 걸린다.
그런데 메서드를 비동기적으로 처리해 나가면... 가져오는 데 시간이 오래 걸리는 theUser을 가져오는 것을 완료하지 못하고 바로 아래의 theUser == null 비교를 처리하게 될 텐데, 그러면 비교 연산이 제대로 작동하지 않는 게 아닐까?

 

결과는 비교 연산이 정상적으로 작동한다는 것이었다.
@Async는 메서드 내의 모든 작업을 비동기적으로 처리한다는 의미가 아니라, 메서드 자체를 비동기적으로 수행하겠다는 의미였다.
즉 theUser을 DB에서 가져오는 작업은 동기적으로 수행이 되기 때문에, theUser을 가져오고 나서야 아래 라인으로 넘어가면서 null인지 비교하게 되는 것이었다.

 

NestJS의 경우에는 비동기 함수를 선언하면, 내부의 모든 작업이 비동기적으로 처리되어 await 예약어를 자주 사용했어야 했는데, 스프링부트는 그게 아니라서 편리한 것 같다.