오늘은 비동기 프로그래밍
에 관해 함께 알아보는 시간을 갖도록 하겠습니다.
비동기 프로그래밍에 대해 잘 알기 위해서는 동기
와 비동기
의 차이를 알아야 합니다.
이 둘의 차이는 무엇일까요?
동기 vs 비동기
동기(Synchronous) 와 비동기(Asynchronous) 는 데이터 처리와 작업 실행 방식을 나타내는 용어입니다.
동기
먼저 동기 방식은 작업들이 순차적으로 진행됩니다. 즉, 한 작업이 완료되기 전에는 다음 작업이 시작되지 않습니다. 이 방식은 작업의 순서가 중요할 때 유용한 방식입니다. 예를 들어, 어떤 데이터를 먼저 처리하고 그 결과를 이용해서 다음 작업을 수행할 경우에 적합하겠죠?
하지만 이렇게 동기적으로 작업이 처리될 경우 전체 작업의 효율성이 떨어질 수 있습니다. 하나의 작업이 지연되면 전체 작업의 진행 또한 지연되기 때문입니다.
public class SynchronousExample {
public static void main(String[] args) {
task1();
task2();
}
private static void task1() {
System.out.println("Task 1 시작");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task1 완료");
}
private static void task2() {
System.out.println("Task 2 시작");
System.out.println("Task 2 완료");
}
}
// 실행 결과
// Task 1 시작
// Task 1 완료
// Task 2 시작
// Task 2 완료
위 코드는 자바 코드로 알아본 동기 작업의 예시입니다.
task1
함수는 2초의 소요 시간이 필요하고 task2
함수는 바로 실행이 완료되지만, task1이 종료된 이후에 task2가 실행됩니다. 따라서 task2 의 실행에도 2초가 필요한 것이지요.
비동기
비동기 방식은 여러 작업들이 동시에 실행될 수 있습니다. 한 작업이 완료되기를 기다리지 않고 다른 작업이 시작될 수 있다는 뜻이지요. 이 방식은 작업 완료 시간이 불규칙하거나, 다른 작업과 독립적으로 실행되는 작업이 실행되어야 할 때 유용합니다.
비동기 방식은 시스템 자원을 보다 효율적으로 사용할 수 있게 해주지만, 작업들 사이의 동기화나 순서를 관리하는데 어려움이 있을 수 있습니다.
public class AsynchronousExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("Task 1 시작");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task 1 완료");
});
Thread thread2 = new Thread(() -> {
System.out.println("Task 2 시작");
System.out.println("Task 2 완료");
});
thread1.start();
thread2.start();
}
}
// 실행 결과
// Task 1 시작
// Task 2 시작
// Task 2 완료
// Task 1 완료
비동기 처리의 대표적인 구현 방법 중 하나인 Thread
를 활용해서 구현한 비동기 처리의 예시입니다.
아까와는 다르게 task1 과 task2 가 별도의 스레드에서 실행되기 때문에, task1 의 동작이 끝나기를 기다리지 않고, 거의 동시에 task2 의 작업이 시작됩니다.
이렇게 살펴보면 감이 잘 안올 수 있을 것 같은데요.
동기와 비동기의 차이점에 대해 더 자세히 알아보기 위해서 함수 관점에서의 동기와 비동기의 차이
에 대해 설명해 드리겠습니다.
함수 관점에서의 동기와 비동기의 차이
@Slf4j
public class A {
public static void main(String[] args) {
log.info("main start");
var result = getResult();
var nextValue = result + 1;
assert nextValue == 1;
log.info("main 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");
}
}
}
이 A
클래스를 살펴보면, getResult
라는 함수를 실행시키고 그 결과를 검증하는 코드임을 알 수 있습니다.
또한 getResult 메서드는 1초라는 실행시간이 소요됨을 알 수 있죠.
그리고 이 코드는 동기 방식으로 동작하기 때문에 main 함수 또한 1초가 소요됨을 알 수 있죠.
그림으로 그려보면 다음과 같은 방식으로 동작함을 알 수 있습니다.
main 함수가 caller
의 역할을 하여 callee
역할인 getResult 를 호출하여 결괏값을 반환받고
최종적으로 해당 값이 1이 맞는지 확인을 완료하였습니다.
@Slf4j
public class B {
public static void main(String[] args) {
log.info("Start main");
getResult(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
var nextValue = integer + 1;
assert nextValue == 1;
}
});
log.info("Finish main");
}
public static void getResult(Consumer<Integer> cb) {
log.info("Start getResult");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var result = 0;
cb.accept(result);
log.info("Finish getResult");
}
}
다음으로 B
클래스를 살펴보면, getResult 함수를 호출하는 것은 비슷한데, 매개변수로 Consumer
라는 함수형 인터페이스를 넘겨줍니다.
그리고 그 함수형 인터페이스 내에 실제 main 에서 실행되어야 할 로직이 존재합니다.
그리고 getResult 함수는 1초를 쉰 이후, 결과를 반환하는 것이 아닌 인자로 받은 인터페이스의 accept
를 실행합니다.
이 때, 함수형 인터페이스를 사용하면, 해당 인터페이스의 실행은 그것을 호출한 스레드의 컨텍스트에서 이루어집니다.
지금은 main 내에서 실행하고 있으니 main 의 실행 스레드의 컨텍스트에서 이루어져서 동작 스레드의 차이가 없기는 합니다.
그림으로 그려보면 다음과 같은 형태로 동작함을 알 수 있습니다.
caller 인 main 이 callee 인 getResult 를 호출하고, 함수형 인터페이스의 실행이 되고 난 후
그 결괏값이 main 으로 돌아갑니다.
A 와 다른 점은 caller 가 직접 실행하는 실행의 주체가 되는 것이 아닌, callee 에게 실행의 주체를 위임한 형태임을 알 수 있습니다.
두 클래스의 차이점?
두 클래스의 차이점은 이렇게 정리할 수 있습니다.
A | B |
main은 getResult의 결과에 관심이 있다. | main은 getResult의 결과에 관심이 없다. |
main은 getResult의 결과를 통해 다음 코드를 실행한다. | getResult는 결과를 이용해서 함수형 인터페이스를 실행한다. |
이를 통해 알 수 있는 사실은 A와 같은 모델은 caller 가 callee 의 결과에 관심이 있는 모양입니다.
caller 가 callee 의 결과를 이용해서 다음 동작을 수행해야 하는 절차에 따라 동작하기 때문입니다.
이것은 동기식으로 실행되는 코드의 특징입니다.
반대로, B 와 같은 모델은 caller 가 callee 의 결과에는 관심이 없는 모양입니다.
callee 는 결과를 이용해서 callback
을 수행합니다. 따라서 절차에 상관 없이 동작할 수 있는 구조가 만들어지게 됩니다.
이것이 비동기식으로 실행되는 코드의 특징입니다.
오늘은 동기/비동기 프로그래밍의 특징과 차이점에 대해서 간단히 알아보았는데요,
이 이해를 더 깊이해줄 blocking
과 non-blocking
에 대한 글도 작성해서 더 깊이 이해하는 시간을 꼭 갖도록 하겠습니다~!
'Computer Science > 프로그래밍 이론 💬' 카테고리의 다른 글
일급 컬렉션이란? (0) | 2024.02.07 |
---|---|
Dependency Injection이란? (0) | 2024.02.06 |
제어의 역전(IOC) 이란? (0) | 2024.02.06 |
객체지향 프로그래밍이란? (0) | 2024.02.02 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆