본문 바로가기
ㄴ Spring Webflux/스프링으로 시작하는 리액티브 프로그래밍

Ch03 Blocking I/O 와 Non-Blocking I/O

by 경아ㅏ 2024. 8. 28.

 

Blocking I/O

 

  • Spring MVC 
  • RedisTemplate 을 사용하여 동기적으로 API 응답을 주고 받는 방식
  • 요청 받은 작업 스레드가 작업을 완료하기 전까지 요청 스레드는 차단
  • 따라서 여러 작업을 차단없이 수행하려면 멀티 스레드를 사용해야 하는데 잦은 컨텍스트 스위칭, 스레드를 위한 메모리 할당 증가 등의 문제 존재

 

e.g. 클라이언트에서 본사 API 로 특정 책에 대한 정보를 요청할 때 본사 서버에서 다시 지점 서버로 요청하는 과정이 있다면,

 

 

ClientApplication

 

@SpringBootApplication
public class ClientApplication {

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

    @Bean
    // Application 시작하자마자 실행되는 메서드
    public CommandLineRunner run() {
        return (String... args) -> {
            for (int i = 1; i <= 5; i++) {
                Book book = this.getBook(i);
                log.info("{}: book name: {}", LocalTime.now(), book.getName());
            }
        };
    }

    // RestTemplate 으로 http://localhost:8080/v1/books/{book-id} API 요청
    private Book getBook(long bookId) {
        RestTemplate restTemplate = new RestTemplate();

        URI getBooksUri = UriComponentsBuilder.fromUri(baseUri)
                .path("/{book-id}")
                .build()
                .expand(bookId)
                .encode()
                .toUri();

        ResponseEntity<Book> response =
                restTemplate.getForEntity(getBooksUri, Book.class);
        Book book = response.getBody();
		
        // 본점 서버(1차 서버)로 부터 데이터를 가져오기 전까지 요청 스레드 차단
        return book;
    }
}

 

 

본사 서버(1차 Network I/O)

 

@RestController
@RequestMapping("/v1/books")
public class SpringMvcHeadOfficeController {
    
    private final RestTemplate restTemplate;

    URI baseUri = UriComponentsBuilder.newInstance().scheme("http")
            .host("localhost")
            .port(7070)
            .path("/v1/books")
            .build()
            .encode()
            .toUri();

    public SpringMvcHeadOfficeController(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @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(); // http://localhost:7070/v1/books/{book-id}

        // 2차 API 서버(지점 서버)로 요청 전송        
        ResponseEntity<Book> response = restTemplate.getForEntity(getBookUri, Book.class);
        Book book = response.getBody();

        // 지점 서버에서 요청 응답이 오기 전까지 해당 스레드 차단
        return ResponseEntity.ok(book);
    }
}

 

 

지점 서버(2차 Network I/O)

 

@RestController
@RequestMapping("/v1/books")
public class SpringMvcBranchOfficeController {
    private Map<Long, Book> bookMap;

    @Autowired
    public SpringMvcBranchOfficeController(Map<Long, Book> bookMap) {
        this.bookMap = bookMap;
    }

    // bookMap 에서 특정 book-id 에 대한 Book 객체를 찾아 리턴
    @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);
    }
}

 

 

Non-Blocking I/O

 

  • Spring WebFlux
  • WebClient 를 이용하여 비동기적으로 API 응답을 주고받는 방식
  • 요청 받은 스레드가 작업을 완료하지 않았더라도 요청 스레드는 차단되지 않기 때문에 실행 시간 단축 가능
  • 대량의 트래픽 요청을 받거나, MSA 기반 시스템에서 NW I/O 사이의 지연을 단축시키고자 할 때 유용

 

ClientApplication

 

@SpringBootApplication
public class ClientApplication {

    private URI baseUri = UriComponentsBuilder.newInstance().scheme("http")
        .host("localhost")
        .port(6060)
        .path("/v1/books")
        .build()
        .encode()
        .toUri();

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

    @Bean
    public CommandLineRunner run() {
    	return (String... args) -> {
        	for (int i = 1; i <= 5; i++) {
            	this.getBook(i)
                    .subscribe( // Mono 로 전달받은 Publisher 를 subscribe 로 데이터 처리
                     	book -> { log.info("{}: book name: {}",LocalTime.now(), book.getName()); }
                    );
                }
        };
    }

    private Mono<Book> getBook(long bookId) {
        URI getBooksUri = UriComponentsBuilder.fromUri(baseUri)
            .path("/{book-id}")
            .build()
            .expand(bookId)
            .encode()
            .toUri(); // http://localhost:6060/v1/books/{book-id}

        // WebClient로 본사 서버에 데이터 요청, Mono(Publisher) 객체 리턴
        return WebClient.create()
                .get()
                .uri(getBooksUri)
                .retrieve()
                .bodyToMono(Book.class);
	}
}

 

 

본사 서버(1차 Network I/O)

 

@RequestMapping("/v1/books")
@RestController
public class SpringReactiveHeadOfficeController {
    
    URI baseUri = UriComponentsBuilder.newInstance().scheme("http")
            .host("localhost")
            .port(5050)
            .path("/v1/books")
            .build()
            .encode()
            .toUri();
    
    @Autowired
    public SpringReactiveHeadOfficeController() {}

    @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}

        // 2차 서버로 요청 전송 + Mono 객체 전달
        return WebClient.create()
                .get()
                .uri(getBookUri)
                .retrieve()
                .bodyToMono(Book.class);
    }
}

 

 

지점 서버(2차 Network I/O)

 

@RequestMapping("/v1/books")
@RestController
public class SpringReactiveBranchOfficeController {
    
    private Map<Long, Book> bookMap;

    @Autowired
    public SpringReactiveBranchOfficeController(Map<Long, Book> bookMap) {
        this.bookMap = bookMap;
    }

    @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);
        // Mono 객체 만들어 리턴
        return Mono.just(book);
    }
}

 

 

Spring MVC (Blocking I/O)로 5번 API 요청시 1번 응답을 받는데 걸린 시간(5s) * 5번 = 25s,

Spring WebFlux(Non-Blocking I/O)로 5번 API 요청시 요청 스레드가 차단 되지 않아 전체 요청을 수행하는데 5s 소요


신기하ㄷ ㅏ ...

댓글