Blocking I/O
하나의 스레드가 I/O에 의해서 차단되어 대기하는 것.
-> 멀티스레딩 기법으로 차단된 시간을 효율적으로 사용할 수는 있으나, 이 방식도 몇 가지 문제점이 있다.
✅ 컨텍스트 스위칭으로 인한 스레드 전환 비용
프로세스 정보를 PCB에 저장, reload 하는 시간 동안에는 CPU가 다른 작업을 하지 못하고 대기한다.
-> 컨텍스트 스위칭이 많으면 CPU의 전체 대기 시간이 길어지기 때문에 성능이 저하된다.
✅ 과다한 메모리 사용으로 인한 오버헤드
일반적인 Java 웹 애플리케이션은 요청당 하나의 스레드를 할당한다.
-> 만약 스레드 내부에서 또 다른 스레드를 추가로 할당하면, 시스템이 감당하기 힘들 정도로 메모리 사용량이 늘어날 수 있다.
✅ 스레드 풀에서 응답 지연
대량의 요청으로 스레드 풀에 유휴 스레드가 없을 경우, 사용 가능한 스레드가 확보되기 전까지 응답 지연이 발생한다.
Non-Blocking I/O
작업 스레드의 종료 여부와 관계없이 요청한 스레드는 차단되지 않는다.
-> 하나의 스레드로 많은 수의 요청을 처리할 수 있다.
Blocking I/O 방식보다 더 적은 수의 스레드를 사용하기 때문에, 앞서 언급한 멀티스레딩 관련 문제들을 극복할 수 있다.
단, CPU를 많이 사용하는 작업이 포함된 경우에는 성능에 악영향을 준다.
-> CPU가 특정 작업에 집중해야 하는데, Non-Blocking I/O라면 계속 들어오는 요청에 대응해야 해서 특정 작업에 집중하지 못한다.
Spring Framework에서의 Blocking I/O와 Non-Blocking I/O
✅ Spring MVC
Blocking I/O 방식.
호출자
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{book-id}")
public ResponseEntity<Book> getBook(@PathVariable("book-id") long bookId) {
URI getBookUri = UriComponentsBuilder.fromUri(baseUri)
.path("/{book-id}")
.build()
.expand(bookId)
.encode()
.toUri();
ResponseEntity<Book> response = restTemplate.getForEntity(getBookUri, Book.class);
Book book = response.getBody();
return ResponseEntity.ok(book);
}
RestTemplate를 사용해서 작업자를 호출한다.
작업자
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{book-id}")
public ResponseEntity<Book> getBook(@PathVariable("book-id") long bookId)
throws InterruptedException {
Thread.sleep(5000);
Book book = bookMap.get(bookId);
return ResponseEntity.ok(book);
}
병목을 주기 위해, 5초 정도 지연시킴.
5번 호출 결과
22:53:52.274 [main] INFO - # 요청 시작 시간: 22:53:52.274108
22:53:57.593 [main] INFO - 22:53:57.593107: book name: IT Book1
22:54:02.622 [main] INFO - 22:54:02.622692: book name: IT Book2
22:54:07.651 [main] INFO - 22:54:07.651671: book name: IT Book3
22:54:12.670 [main] INFO - 22:54:12.670424: book name: IT Book4
22:54:17.689 [main] INFO - 22:54:17.689126: book name: IT Book5
대략 25초 정도 소요됨.
-> 호출 스레드가 매 호출마다 5초씩 병목이 생겼다는 것.
✅ Spring WebFlux
Non-Blocking I/O 방식.
호출자
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{book-id}")
public Mono<Book> getBook(@PathVariable("book-id") long bookId) {
URI getBookUri = UriComponentsBuilder.fromUri(baseUri)
.path("/{book-id}")
.build()
.expand(bookId)
.encode()
.toUri(); // http://localhost:5050/v1/books/{book-id}
return WebClient.create()
.get()
.uri(getBookUri)
.retrieve()
.bodyToMono(Book.class);
}
WebClient를 사용해서 작업자를 호출한다.
작업자
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{book-id}")
public Mono<Book> getBook(@PathVariable("book-id") long bookId)
throws InterruptedException {
Thread.sleep(5000);
Book book = bookMap.get(bookId);
log.info("# book for response: {}, {}", book.getBookId(), book.getName());
return Mono.just(book);
}
병목을 주기 위해, 5초 정도 지연시킴.
5번 호출 결과
22:59:11.023 [main] INFO - # 요청 시작 시간: 22:59:11.023584
22:59:17.274 [reactor-http-nio-1] INFO - 22:59:17.274368: book name: IT Book3
22:59:17.275 [reactor-http-nio-1] INFO - 22:59:17.275635: book name: IT Book2
22:59:17.276 [reactor-http-nio-1] INFO - 22:59:17.276380: book name: IT Book5
22:59:17.277 [reactor-http-nio-1] INFO - 22:59:17.277436: book name: IT Book1
22:59:17.277 [reactor-http-nio-1] INFO - 22:59:17.277915: book name: IT Book4
대략 6초 정도 소요됨.
-> 호출 스레드가 매 호출마다 차단되지 않았다는 것.
Non-Blocking I/O 방식의 통신이 적합한 시스템
✅ Spring WebFlux를 도입하기 위해 고려해야 할 사항
- 학습 난이도
- 리액티브 프로그래밍 경험이 있는 개발 인력 확보
이러한 준비가 되었다면, 개발하려는 애플리케이션이 다음과 같이 Spring WebFlux 기술을 사용하기에 적합한 상황인지 파악하자.
대량의 요청 트래픽이 발생하는 시스템
Spring WebFlux는 상대적으로 적은 컴퓨팅 파워를 사용함으로써 저비용으로 고수준의 성능을 이끌어낸다.
마이크로 서비스 기반 시스템
서비스들 간에 많은 수의 I/O 고려.
스트리밍 또는 실시간 시스템
리액티브 프로그래밍은 일회성 연결 뿐만이 아닌, 끊임없이 들어오는 무한한 데이터 스트림을 효율적으로 처리할 수 있다.
참고자료
https://docs.spring.io/spring-boot/docs/current/reference/html/#web.servlet.spring-mvc
'book > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[리액티브 프로그래밍] 마블 다이어그램(Marble Diagram) (0) | 2024.03.11 |
---|---|
[리액티브 프로그래밍] Reactor 개요 (0) | 2024.03.11 |
[리액티브 프로그래밍] 리액티브 프로그래밍을 위한 사전 지식 (0) | 2024.03.03 |
[리액티브 프로그래밍] 리액티브 스트림즈(Reactive Streams) (0) | 2024.02.19 |
[리액티브 프로그래밍] 리액티브 시스템과 리액티브 프로그래밍 (0) | 2024.02.19 |