일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- Spring Security
- Spring Cloud OpenFeign
- FeignClients
- JWT
- 사이드 프로젝트
- querydsl
- ExceptionHandlerFilter
- 스레드
- 프로세스
- 멀티태스킹
- Apple 로그인
- Spring Reactive Programming
- 멀티스레드
- asciidoctor
- springboot
- REDIS
- spring boot
- 도메인 주도 설계(DDD) 기반 마이크로서비스(MSA) 모델링
- 네이버클라우드 서버
- 오블완
- OAuth2.0
- 2024년 상반기 회고
- 코드로 배우는 스프링 부트 웹 프로젝트
- 티스토리챌린지
- OpenFeign
- 멀티프로세싱
- microsoft
- 비사이드프로젝트
- ExecutorService
- 비동기
- Today
- Total
기록하기
동기와 비동기, Blocking 과 Non-Blocking 차이 정리 본문
Spring Webflux 를 공부하면서 Reactive Programming 에 대해 학습을 진행하였다.
그 중에서 가장 먼저, 항상 헷갈리는 개념인 동기 vs 비동기, Blocking vs Non-Blocking 에 대해 내용을 정리해보고자 한다.
내용 정리에 앞서 결론적으로 먼저 정리를 해보자면,
- Caller : 호출하는 함수
- Callee : 호출 당하는 함수
- 동기와 비동기의 차이는 Caller 가 Callee 의 결과에 관심을 가지는지 여부
- Blocking 과 Non-Blocking 의 차이는 제어권을 Caller 와 Callee 중 누가 가지고 있는지
이렇게 정리할 수 있으며 위 기준에 따라 구분할 수 있다.
이제 이 내용을 바탕으로 함수 호출 관점에서, I/O 관점에서 내용을 정리해보고자 한다.
함수 호출 관점에서 동기와 비동기
간단한 예제를 작성해보자
Caller 인 main 함수에서 Callee 즉 getResult 함수를 호출하는 구조인데, 내부적으로는 숫자 0에서 +5를 하여 5가 되면 종료되는 로직만 수행하며 해당 로직을 2가지 케이스로 구현해볼 수 있다.
TestA 클래스
@Slf4j
public class TestA {
public static void main(String[] args) {
log.info("함수 호출 관점 A start");
var result = getResult();
var nextValue = result + 5;
assert nextValue == 5;
log.info("함수 호출 관점 A finish");
}
public static int getResult() {
log.info("getResult start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var result = 0;
try {
return result;
} finally {
log.info("getResult finish");
}
}
}
TestB 클래스
@Slf4j
public class TestB {
public static void main(String[] args) {
log.info("함수 호출 관점 B start");
getResult(num -> {
var nextValue = num + 5;
assert nextValue == 5;
});
log.info("함수 호출 관점 B finish");
}
public static void getResult(Consumer<Integer> consumer) {
log.info("getResult start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var result = 0;
consumer.accept(result);
log.info("getResult finish");
}
}
이 2가지 클래스의 차이는 무엇일까?
먼저, TestA 클래스의 수행 순서는 다음과 같다.
- main -> getResult 호출 : Caller 가 Callee 호출
- getResult 가 return result
- main 에서 로직 수행
반면, TestB 의 경우에는 getResult 에 액션을 위임하게 되는데 Callee 가 함수형 인터페이스를 실행하는 것을 확인할 수 있다. 그래서 다음과 같은 수행 순서를 가지게 된다.
- main -> getResult 호출 : Caller 가 Callee 호출
- getResult 에서 함수형 인터페이스를(로직을) 실행
- main 에게 return result
즉, 굵게 작성한 부분이 다른데, TestA 클래스의 경우 main 이 getResult 결과를 기다렸다가 그 결과를 바탕으로 다음 코드를 실행하는 반면, TestB 클래스의 경우 main 이 실행하는 것이 아니라 getResult 에서 실행 후 결과를 반환하는 차이가 있다.
TestA 클래스가 동기 / TestB 클래스가 비동기인데 정리를 해보면 이런 차이가 있음을 알 수 있다.
동기 | 비동기 |
Caller 는 Callee 결과에 관심 유 | Caller 는 Callee 결과에 관심 무 |
Callee 가 전해주는 결과에 따라 다음 action 수행 | Callee 는 결과를 이용해서 callback 수행 |
함수 호출 관점에서 Blocking 과 Non-Blocking
그러면 TestA 와 TestB 의 공통점은 무엇일까?
여기서 이 두 클래스의 공통점은, Caller 가 Callee 가 하는 일이 완료될 때까지 대기한다는 점이다. 바로 제어권을 Callee 가 가지고 있다는 점인데 이런 점에서 이 두 클래스는 Blocking 상태이다.
그렇다면 TestC 클래스를 한 번 살펴보자
TestC 클래스
@Slf4j
public class TestC {
public static void main(String[] args) {
log.info("함수 호출 관점 C start");
var count = 1;
Future<Integer> result = getResult();
while (!result.isDone()) {
log.info("Caller 가 자신의 일을 한다. count : {}", count++);
}
log.info("함수 호출 관점 C finish");
}
public static Future<Integer> getResult() {
var executorService = Executors.newSingleThreadExecutor();
try {
return executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("getResult start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("error : {}", e);
}
var result = 0;
try {
return result;
} finally {
log.info("getResult finish");
}
}
});
} finally {
executorService.shutdown();
}
}
}
이 코드를 실행해보면 다음과 같이 log 가 나온다.
[main] -- 함수 호출 관점 C start
[pool-1-thread-1] -- getResult start
[main] -- Caller 가 자신의 일을 한다. count : 1
[main] -- Caller 가 자신의 일을 한다. count : 2
[main] -- Caller 가 자신의 일을 한다. count : 3
[main] -- Caller 가 자신의 일을 한다. count : 4
...
[pool-1-thread-1] -- getResult finish
[main] -- 함수 호출 관점 C finish
즉, Callee 가 아직 일을 끝내지 않은 상태이지만 Caller 는 log 를 찍는 일을 계속 수행을 하고 있음을 알 수 있다.
앞서 살펴본 TestA, TestB 는 Caller 가 본인 일을 하지 못했지만 TestC 는 본인의 일을 할 수 있다는 점이 차이점이다. 여기서 TestC 가 Non-Blocking 상태이다.
이를 바탕으로 Blocking 과 Non-Blocking 의 차이를 정리해볼 수 있다.
Blocking | Non-Blocking |
Caller 는 Callee 가 완료될때까지 대기 | Caller 는 Callee 를 기다리지 않고 본인의 일 수행 |
제어권은 Callee 에게 있다. | 제어권은 Caller 에게 있다. |
Caller, Callee 별도의 thread 가 필요하지 않다. | Calleer, Callee 별도의 thread 가 필요하다. |
하지만 TestC 는 지속적으로 getResult 의 결과를 확인할 필요가 있다는 점에서 동기 모델로 볼 수 있는데, 비동기 Non-Blocking 모델인 TestD 클래스까지 작성을 해보고, 동기 Blocking / 비동기 Blocking / 동기 Non-Blocking / 비동기 Non-Blocking 을 정리하려고 한다.
TestD 클래스
@Slf4j
public class TestD {
public static void main(String[] args) {
log.info("함수 호출 관점 D start");
getResult(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
var nextValue = integer + 5;
assert nextValue == 5;
}
});
log.info("함수 호출 관점 D finish");
}
public static void getResult(Consumer<Integer> callback) {
var executorService = Executors.newSingleThreadExecutor();
try {
executorService.submit(new Runnable() {
@Override
public void run() {
log.info("getResult start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("error : {}", e);
}
var result = 0;
try {
callback.accept(result);
} finally {
log.info("getResult finish");
}
}
});
} finally {
executorService.shutdown();
}
}
}
TestD 클래스를 실행해보면 다음과 같이 log 가 찍힌다.
[main] -- 함수 호출 관점 D start
[pool-1-thread-1] -- getResult start
[main] -- 함수 호출 관점 D finish
[pool-1-thread-1] -- getResult finish
이를 통해 Caller 인 main 은 getResult 의 결과와 상관없이 본인의 일, 즉 log 찍는 일을 수행하고 있음을 알 수 있다.
그래서 최종적으로 정리를 해보면, 다음과 같다.
동기 | 비동기 | |
Blocking | - Caller 는 Callee 의 일에 관심이 있고(동기), - Callee 에게 제어권이 있으며, Caller 는 아무것도 할 수 없다. |
- Caller 는 Callee 의 일에 관심이 없고(비동기), - Callee 에게 제어권이 있으며, Caller 는 아무것도 할 수 없다. - 결과는 Callee 가 처리한다. |
Non-Blocking | - Caller 는 Callee 의 일에 관심이 있고(동기), - Caller 에게 제어권이 있으며, Caller 는 자기 할 일을 할 수 있다. 다만 지속적으로 Callee 일 확인이 필요하다. |
- Caller 는 Callee 의 일에 관심이 없고(비동기), - Caller 에게 제어권이 있으며, Caller 는 자기 할 일을 할 수 있다. - 결과는 Callee 가 처리한다. |
I/O 관점에서 Blocking 과 Non-Blocking
I/O 관점에서 Blocking, Non-Blocking 을 정리해볼 수 있는데, 먼저 blocking 은 다음과 같은 의미를 가진다.
- CPU-bound blocking : 오랜 시간 일을 한다.
- IO-bound blocking : 오랜 시간 대기를 한다.
여기서 IO-bound blocking 은 스레드가 대부분의 시간을 대기를 하면서 보내게 되는데 IO-bound non-blocking 시에는 Caller 가 다른 일을 할 수 있게 되는 것을 의미한다.
여기서 Blocking 의 경우에는 전파가 되는데, 만약 어떤 함수에서 Blocking 한 함수와 Non-Blocking 한 함수 2개를 호출한다면 바로 응답을 돌려주는 Non-Blocking 이 있어도 Blocking 함수가 있어서 시간이 오래 걸리게 된다. 즉, Caller 는 Blocking 이 된다.
이는 Caller 가 Non-Blocking 이 되려면 모든 함수가 Non-Blocking 이어야 한다는 것을 의미하며, IO-bound blocking 도 발생하면 안 되는 것을 의미한다.
위에서 정리해본 동기, 비동기, Blocking, Non-Blocking 개념 차이를 정리해본 것을 바탕으로 I/O 관점에서 정리를 해보면 다음과 같다.
TestA 동기 Blocking
- Caller 는 Callee 의 결과에 관심이 있다.
- Caller 는 Callee 가 완료할때까지 기다린다.
-> 즉, thread 가 block 되어 wait queue 에서 기다린다.
TestB 동기 Non-Blocking
- Caller 는 Callee 의 결과에 관심이 있다.
- Caller 는 Callee 가 완료할때까지 기다리지 않고, 자신의 일을 한다.
-> wait queue 에 들어가지 않고, recvfrom 을 주기적으로 호출한다.
TestC 비동기 Blocking
- Caller 는 Callee 의 결과 관심 없다.
-> thread 가 주기적으로 확인한다.
TestD 비동기 Non-Blocking
- Caller 는 Callee 의 결과 관심 없다.
- 즉, thread1 은 작업 완료 후 thread2 에 결과 전달한다.
-> 요청은 thread1, 응답은 thread2
표로 정리한 내용을 첨부하며 다음 블로그에서 더 내용을 이어가보겠다.
동기 | 비동기 | |
Blocking | - kernel I/O 작업을 application 이 대기 - 그 후에 이 결과를 이어 받아서 작업 수행 |
|
Non-Blocking | - kernel I/O 작업이 완료되었는지 application 이 주기적으로 확인 - 중간중간에 확인하면서 본인 일 수행 |
- kernel I/O 작업 요청 보내고, application 은 자신의 일 수행 - 작업 완료 시 -> kernel 이 signal, callback 을 호출 |
'Server > Spring Boot' 카테고리의 다른 글
OAuth2.0 Microsoft 소셜 로그인 (5) | 2024.11.07 |
---|---|
Spring Cloud OpenFeign 적용기 (0) | 2023.08.26 |
[사이드 프로젝트 - 비사이드] 13기 참여 후기 (0) | 2023.04.25 |
Spring Boot OAuth2.0 애플 로그인 & 탈퇴 구현 (4) | 2023.01.05 |
[사이드 프로젝트 - 비사이드] Spring Boot + JPA + Querydsl 적용 (0) | 2022.12.28 |