학습
Ruby 비동기 프로그래밍 (Concurrency)
Ruby 비동기 프로그래밍 (Concurrency) — 고급
비동기 프로그래밍 (Concurrency)
1. 도입 (왜 배워야 하는가)
비동기 프로그래밍은 현대 소프트웨어 개발에서 매우 중요한 기술입니다. 특히 웹 애플리케이션이나 모바일 앱과 같은 비즈니스 환경에서는 동시에 여러 작업을 처리해야 할 필요가 자주 발생합니다. 예를 들어, 쇼핑몰 웹사이트에서는 사용자가 상품을 검색하는 동안 백그라운드에서 재고를 확인하고, 결제 정보를 처리하는 등의 여러 작업이 동시에 이루어져야 합니다. 이러한 비동기 처리가 없다면 사용자는 불필요하게 대기해야 하며, 이는 사용자 경험을 저하시키고 비즈니스 기회를 놓칠 수 있습니다.
비동기 프로그래밍을 모르면 다음과 같은 문제가 발생할 수 있습니다:
- 사용자 경험이 저하되어 이탈률이 증가함.
- 서버 리소스의 비효율적 사용으로 성능 저하.
- 다양한 작업을 순차적으로 처리하여 응답 속도가 느려짐.
- 스레드(Thread): 프로세스 내에서 실행되는 최소 실행 단위. 여러 스레드가 동시에 실행되며 자원을 공유합니다.
- 프로세스(Process): 독립적으로 실행되는 프로그램. 각 프로세스는 고유의 메모리 공간을 가집니다.
2. 핵심 개념 설명
비동기 프로그래밍은 프로그램이 동시에 여러 작업을 수행할 수 있는 능력을 의미합니다. 이는 일반적으로 '스레드'와 '프로세스'라는 개념을 통해 구현됩니다.
비유
비동기 프로그래밍을 요리하는 예로 비유해 보겠습니다. 요리사는 여러 요리를 동시에 준비할 수 있습니다. 예를 들어, 스프를 끓이는 동안 샐러드를 만들고, 고기를 굽는 작업을 동시에 진행할 수 있습니다. 이와 같이 비동기 프로그래밍은 여러 작업을 동시에 수행할 수 있는 방법입니다.
| 용어 | 설명 |
|---|---|
| 스레드 | 프로세스 내에서 실행되는 경량 프로세스 |
| 프로세스 | 독립적으로 실행되는 프로그램 |
| 비동기 | 작업이 완료될 때까지 대기하지 않고 진행 |
3. 기본 코드 예제
Ruby에서 비동기 프로그래밍을 구현하기 위해 Thread 클래스를 사용할 수 있습니다. 다음은 기본적인 스레드를 사용하는 예제입니다.
# 새로운 스레드 생성
thread1 = Thread.new do
puts "스레드 1: 작업 중..."
sleep(2) # 2초 동안 대기
puts "스레드 1: 작업 완료!"
end
thread2 = Thread.new do
puts "스레드 2: 작업 중..."
sleep(1) # 1초 동안 대기
puts "스레드 2: 작업 완료!"
end
# 메인 스레드에서 다른 스레드가 종료될 때까지 대기
thread1.join
thread2.join
puts "모든 작업이 완료되었습니다."
실행 결과
스레드 1: 작업 중...
스레드 2: 작업 중...
스레드 2: 작업 완료!
스레드 1: 작업 완료!
모든 작업이 완료되었습니다.
코드 한 줄씩 동작 설명
Thread.new를 사용하여 새로운 스레드를 생성하고, 각 스레드에서 실행할 작업을 블록으로 정의합니다.- 첫 번째 스레드(
thread1)는 2초 동안 대기 후 작업 완료 메시지를 출력합니다. - 두 번째 스레드(
thread2)는 1초 동안 대기 후 작업 완료 메시지를 출력합니다. join메서드를 사용하여 메인 스레드가 다른 스레드가 종료될 때까지 대기합니다.- 모든 스레드 작업이 완료된 후 최종 메시지를 출력합니다.
- 비동기 프로그래밍은 여러 작업을 동시에 처리하여 성능과 사용자 경험을 개선합니다.
Thread클래스를 사용하여 Ruby에서 비동기 프로그래밍을 쉽게 구현할 수 있습니다.- 공유 자원에 대한 동기화는 필수적입니다.
- 스레드 종료를 제대로 기다리지 않으면 작업이 중단될 수 있습니다.
- 스레드 풀(Thread Pool)과 작업 큐(Work Queue)
- 비동기 라이브러리(예: EventMachine)
- 프로미스(Promise)와 미래(Future) 패턴
4. 실전 활용 예제
예제 1: 웹 요청 처리
쇼핑몰에서 상품 정보를 가져오는 비동기 처리 예제입니다.
require 'net/http'
require 'uri'
threads = []
urls = ["http://example.com/product1", "http://example.com/product2"]
urls.each do |url|
threads << Thread.new do
uri = URI.parse(url)
response = Net::HTTP.get_response(uri)
puts "#{url}의 응답: #{response.code}"
end
end
threads.each(&:join)
예제 2: 데이터베이스 쿼리
사용자가 요청한 데이터를 동시에 처리하는 예제입니다.
require 'sqlite3'
database = SQLite3::Database.new("test.db")
threads = []
threads << Thread.new do
database.execute("SELECT FROM users WHERE id = 1")
end
threads << Thread.new do
database.execute("SELECT FROM orders WHERE user_id = 1")
end
threads.each(&:join)
5. 흔한 실수와 주의사항
실수 1: 공유 자원에 대한 동기화 문제
counter = 0
threads = []
10.times do
threads << Thread.new do
1000.times { counter += 1 } # 공유 자원에 대한 동기화가 없음
end
end
threads.each(&:join)
puts counter # 예상과 다르게 출력될 수 있음
올바른 코드:
counter = 0
mutex = Mutex.new
threads = []
10.times do
threads << Thread.new do
1000.times do
mutex.synchronize { counter += 1 } # 동기화
end
end
end
threads.each(&:join)
puts counter # 항상 10000이 출력됨
실수 2: 스레드 종료를 기다리지 않음
스레드가 실행 중에 프로그램이 종료되는 경우를 방지하기 위해 join을 사용해야 합니다.
Thread.new do
puts "작업 중..."
end
# join 없이 프로그램 종료 -> 작업이 완료되지 않을 수 있음