이론

비동기 프로그래밍? 동시성 제어? - with Java

김비니 2025. 1. 13. 16:05

들어가며

안녕하세요, 개발자 비니입니다.

최근 동시성이 요구되는 환경이 많아지면서, 비동기 및 동기 처리에 대한 중요성이 높아지고 있죠?

활발한 커뮤니티, 게임 채팅등 많은 곳에서 쓰이고 있어요.

 

이번에 Java에서 이러한 개념들이 어떻게 구현되는지,

남겨보려 합니다. 이전에는 C/C++에서 Mutex와 세마포어만 사용했었거든요!


비동기 프로그래밍이란

"작업이 완료될 때까지 기다리지 않고, 다른 작업을 동시에 수행할 수 있도록 하는 프로그래밍"

쉽게 말해, 자원 활용을 최적화하면서 애플리케이션 응답을 빠르게 받아내는 거죠.

 

간단히 예를 들어보면,

우리가 웹 사이트에 사진을 업로드하는 동안, 메시지나 영상을 볼 수 있는 기능을 구현하는 거죠!

 

더 쉽게 예를 들어보면,

우리가 워드 문서를 열고 컴퓨터가 읽고 있는 동안

키보드로 작업을 할 수 있는 것과 같아요.

 

그냥, 병렬 작업을 할 수 있어요.

좌 동기 / 우 비동기


동시성 제어

"여러 작업이 동시에 실행될 때 발생할 수 있는 충돌이나 데이터 불일치를 방지하기 위한 방법"

주로 스레드나 프로세스 간의 상호작용에서 중요하게 다뤄집니다.

 

말이 어려우니, 이것도 예시를 들어보면

두 개의 스레드가 하나의 변수에 접근하여 값을 변경하려 할 때 나타나는 충돌을

방지하기 위한 기법이에요.

 

더 쉽게 예를 들어보면,

  1. "1000"이라는 값을 가진 "Money"  변수가 있다고 가정해 볼게요.
  2. "A"와 "B" 스레드가 동시에 "Money" 변수를 가지고 와요, 얼마가 남았는지 보려고요!
  3. "A" 스레드에서 Money 변수로부터 1000원을 출금하겠다는 요청을 보내요.
    • 이때, 출금은 A 스레드가 가져온 Money에서 1000을 빼고, 반환해요.
  4. "B" 스레드에서도 Money 변수로부터 1000원을 출금하겠다는 요청을 보내요.
  5. "A" 스레드와 "B" 스레드 모두 각각 1000원의 값을 가지게 됩니다.
    하지만 Money는 "0"이 되어요. 와! 1000원이 복사가 되었죠?

 


Java에서의 비동기 프로그래밍과 동시성 제어

여기 예제에서는 온라인 JAVA 컴파일러를 통해 해 보도록 할게요.

각각 한 개 정도씩 예제와 함께 테스트해 봅시다!

(JAVA) 비동기 프로그래밍

CompletableFuture

자세하게는 다루지 않겠지만, 간단히 예시로만 알아보도록 할게요.

JAVA5부터 Futer가 추가되면서, 비동기 작업에 대한 결괏값을 얻을 수 있게 되었어요.

CompletableFuter는 이러한 Futer의 완료 후 콜백을 받을 수 있는 메서드예요.

Futer를 외부에서 종료시킬 수도 있고, ~초동안 콜백이 없으면 강제종료! 이런 코드를 구현할 수 있는 코드입니다.

 

Futer와 Comple... 를 자세하게 다루는 건 추후 하나의 글로 만나고,

실제 구현을 통해 비동기를 이해해 봅시다.

import java.util.*;
import java.lang.*;
import java.io.*;
import java.util.concurrent.CompletableFuture;

class Main {
    public static void main(String[] args) {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("A");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("B");
        future.join();
    }
}

 

  • runAsync
    • 반환값이 없음 (반환값이 있는 경우에는 SupplyAsync를 사용하면 됩니다!)
    • 비동기로 작업을 실행함
  • Threed Sleep
    • 인자 ms 만큼 스레드를 대기합니다. (2000ms == 2s)
    • 이걸 쓰려면 반드시 <예외처리>가 있어야만 합니다.
      없으면 컴파일 에러가 나옵니다...
  • . join
    • 스레드가 종료되기까지 메인 스레드를 끝내지 않고 기다립니다!

위 코드, 만약 스레드가 없는 상태에서 pringln이 동작한다고 하면

A
B


이렇게 반환되겠지만,

위 스레드를 실행하게 되면

B
A

 

이렇게 실행됩니다.

어디 한번 볼까요?

 

어때요, 정말 B-A로 출력이 되지요?

 

이 외에도 ExcutorService라는 메서드도 있어요.
스레드 풀을 이용한 비동기 작업을 할 수가 있죠.

원리 자체는 비슷합니다! 

 

(JAVA) 동시성 제어

Synchronized

스레드의 "임계 구역"을 설정하는 방법입니다.

쉽게 말해, 변수를 "동시에"접근할 수 없도록 해 주는 방법입니다.

 

멀티-스레딩 기법에 대해서는 다음 포스팅에서 알아보고,

지금은 Synchronized를 통한 임계구역 설정법만 알아봅시다.

 

import java.util.*;

class Draw {
    public synchronized void withdraw(int amount) {
        if (Main.money >= amount) {
            Main.money -= amount;
            System.out.println(Thread.currentThread().getName() + " 출금 성공! 남은 금액: " + Main.money);
        } else {
            System.out.println(Thread.currentThread().getName() + " 출금 실패! 잔액 부족");
        }
    }
}

class Main {
    public static int money = 1000;
    
    public static void main(String[] args) {
        Draw account = new Draw();

        Thread t1 = new Thread(() -> account.withdraw(1000), "A 스레드");
        Thread t2 = new Thread(() -> account.withdraw(1000), "B 스레드");

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("남은 금액: " + money);
    }
}

 

아까 동시성 제어에 대해 이야기할 때,

A와 B의 뱅킹 서비스를 예로 들어서 말씀드렸어요.

이 녀석이요!

 

하지만 이번 코드에서는, 

Synchronized를 통해서 전역 변수인 "Money"에 한 번에 한 스레드만 접속할 수 있도록 설정되었답니다.

 

Synchronized가 적용된 메서드는

여러 개의 스레드가 실행되더라도, 순차적으로 앞서 실행된 스레드가 끝날 때까지 후발 스레드들은 대기하게 됩니다.

 

그럼 자연스레, A의 계산이 끝난 후, Money의 값이 변경된 후에 B가 접근하게 되겠죠!

그럼 결과는?

 

자, 어떤가요?

이번엔 두 번째로 실행된 스레드 B는 출금에 실패 한 모습을 볼 수 있어요.

  1. A 스레드B 스레드가 동시에 withdraw(1000)을 호출합니다.
  2. 두 스레드 중 하나(예: A 스레드)가 먼저 진입합니다.
  3. A 스레드가 출금을 완료하고 money는 0이 됩니다.
  4. 이후 B 스레드가 진입하지만, 잔액이 부족하여 출금에 실패합니다.

그럼... JavaScript에서는?

싱글 스레드 기반이지만, 비동기 작업을 효율적으로 처리하기 위한 방법도 존재해요.

  • 콜백 함수: 비동기 작업 후 실행될 함수를 전달하는 방법
  • Promise: 비동기 작업의 완료/실패를 처리하는 방법
  • async/await: 비동기 작업을 동기적으로 작성할 수 있는 방법
  • 동시성 제어 라이브러리: Node.js의 async-mutex 활용 방법

Javascript에서의 비동기 작업은 내용이 방대하니,

이것도 별도의 포스팅으로 찾아오도록 할게요.


정리하자면

  1. 비동기 프로그래밍이란 
    • 자원을 효율적으로 활용하여 빠른 응답성을 유지
    • 여러 작업을 동시에 수행 (병렬 처리)
  2. 동시성 제어란
    • 여러 스레드가 공유 자원에 접근할 때 충돌을 방지
    • (예시) 두 스레드가 동시에 Money 변수를 수정할 때 발생하는 문제
  3. JAVA에서의 활용
    • 비동기 프로그래밍 : CompletableFuture 등.
      • 비동기 작업의 결과를 쉽게 처리할 수 있도록 도와주는 메서드
    • 동시성 제어 : synchronized 등
      • 임계 구역(CS)을 설정하여 동시성 문제를 해결하는 방법

마치며

비동기 프로그래밍과 동시성 제어는 고성능 애플리케이션 개발에 필수적인 요소입니다.

우리가 데이터 하나를 로드하는 동안, 아무것도 할 수 없다면

사용자들은 그만큼 기다리다가 빠져나가게 되겠죠.

이를 잘 활용한다면, 더욱 효율적이고 안정적인 시스템을 구축할 수 있습니다.

 

다양한 동기화 기법을 실전에 적용해 보면서 나타나는 많은 트러블 문제들도 분명 있어요.

실전만큼 좋은 게 없답니다. 저도 이제 JAVA에도 서슴없이 사용해 봐야겠어요!

그럼 다들, 파이팅입니다! 💫