🤖 Computer Science

비동기 프로그래밍

date
Dec 15, 2023
slug
asynchronous-programming
author
status
Public
tags
Java
비동기
summary
비동기 프로그래밍이란 무엇인지 알아봅시다
type
Post
thumbnail
제목을 입력해주세요_-001 (6).png
category
🤖 Computer Science
updatedAt
Dec 17, 2023 10:31 AM
오늘은 비동기 프로그래밍 에 관해 함께 알아보는 시간을 갖도록 하겠습니다.
비동기 프로그래밍에 대해 잘 알기 위해서는 동기비동기 의 차이를 알아야 합니다.
이 둘의 차이는 무엇일까요?

동기 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("Task 1 완료"); } 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 amin(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 를 활용해서 구현한 비동기 처리의 예시입니다.
아까와는 다르게 task1task2 가 별도의 스레드에서 실행되기 때문에, 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초가 소요됨을 알 수 있죠.
notion image
그림으로 그려보면 다음과 같은 방식으로 동작함을 알 수 있습니다.
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 의 실행 스레드의 컨텍스트에서 이루어져서 동작 스레드의 차이가 없기는 합니다.
notion image
그림으로 그려보면 다음과 같은 형태로 동작함을 알 수 있습니다.
callermaincalleegetResult 를 호출하고, 함수형 인터페이스의 실행이 되고 난 후
그 결괏값이 main 으로 돌아갑니다.
A 와 다른 점은 caller 가 직접 실행하는 실행의 주체가 되는 것이 아닌, callee 에게 실행의 주체를 위임한 형태임을 알 수 있습니다.

두 클래스의 차이점?

두 클래스의 차이점은 이렇게 정리할 수 있습니다.
A
B
maingetResult 의 결과에 관심이 있다.
maingetResult 의 결과에 관심이 없다.
maingetResult 의 결과를 통해 다음 코드를 실행한다.
getResult 는 결과를 이용해서 함수형 인터페이스를 실행한다.
이를 통해 알 수 있는 사실은 A와 같은 모델은 callercallee 의 결과에 관심이 있는 모양입니다.
callercallee 의 결과를 이용해서 다음 동작을 수행해야 하는 절차에 따라 동작하기 때문입니다.
이것은 동기식으로 실행되는 코드의 특징입니다.
반대로, B 와 같은 모델은 callercallee 의 결과에는 관심이 없는 모양입니다.
callee 는 결과를 이용해서 callback 을 수행합니다. 따라서 절차에 상관 없이 동작할 수 있는 구조가 만들어지게 됩니다.
이것이 비동기식으로 실행되는 코드의 특징입니다.
 
오늘은 동기/비동기 프로그래밍의 특징과 차이점에 대해서 간단히 알아보았는데요,
이 이해를 더 깊이해줄 blockingnon-blocking 에 대한 글도 작성해서 더 깊이 이해하는 시간을 꼭 갖도록 하겠습니다~!