POJO-actor v1.0: 자바용 경량 액터 모델 라이브러리
I’m ready to translate the article for you, but I need the text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line and all formatting exactly as you specify.
개요
액터 모델은 독립적인 엔티티(액터)가 메시지 전달을 통해서만 통신하는 프로그래밍 패러다임으로, 락과 공유 상태 동시성의 복잡성을 없앱니다.
역사적으로 액터 프레임워크는 OS 스레드에 의존했으며, 이로 인해 액터 수는 CPU 코어 수로 제한되었습니다. Java 21에서 가상 스레드가 도입되면서, 보통 노트북조차도 이제 동시에 수만 개의 액터를 실행할 수 있게 되었습니다.
프로젝트 저장소:
아키텍처
POJO‑actor는 최신 Java 기능을 활용하여 단순화된 액터 모델을 구현합니다. 약 800줄의 코드만으로 실용적이고 성능 좋은 솔루션을 제공합니다.
| Component | Description |
|---|---|
| ActorSystem | 액터의 수명 주기를 관리하고 구성 가능한 워크‑스틸링 스레드 풀을 관리합니다 |
| ActorRef | 액터에 대한 참조이며 tell()(fire‑and‑forget) 및 ask()(request‑response) API를 제공합니다 |
| Virtual Threads | 각 액터는 자체 가상 스레드에서 실행되어 가벼운 메시지 처리를 수행합니다 |
| Work‑Stealing Pools | 무거운 연산은 구성 가능한 스레드 풀에 위임됩니다 |
| Zero Reflection | 표준 JDK API만을 사용하여 완전히 구축되었으며 → GraalVM Native Image에 준비되어 있습니다 |
Quick Start
Maven Dependency
<com.scivicslab>
<artifactId>POJO-actor</artifactId>
<version>1.0.0</version>
</com.scivicslab>
Note: 라이브러리는 곧 Maven Central에 배포될 예정입니다. 그때까지는 로컬에 설치하세요:
git clone https://github.com/scivicslab/POJO-actor
cd POJO-actor
./mvnw install
기본 사용법
import com.scivicslab.pojoactor.ActorSystem;
import com.scivicslab.pojoactor.ActorRef;
import java.util.concurrent.CompletableFuture;
// Define a simple POJO
class Counter {
private int count = 0;
void increment() { count++; }
int getValue() { return count; }
}
// Create an actor system (4 threads for CPU‑intensive tasks)
ActorSystem system = new ActorSystem("mySystem", 4);
ActorRef counter = system.actorOf("counter", new Counter());
// Send messages
counter.tell(c -> c.increment()); // fire‑and‑forget
CompletableFuture<Integer> result = counter.ask(c -> c.getValue()); // request‑response
int value = result.get(); // → 1
system.terminate();
Any POJO Can Become an Actor
코드베이스를 액터 모델에 맞게 재설계할 필요가 없습니다. 기존에 존재하는 모든 Java 객체—표준 라이브러리 클래스조차도—즉시 액터가 될 수 있습니다.
import com.scivicslab.pojoactor.ActorSystem;
import com.scivicslab.pojoactor.ActorRef;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
ActorSystem system = new ActorSystem("listSystem");
ActorRef<ArrayList<String>> listActor = system.actorOf("myList", new ArrayList<>());
// Mutate the list
listActor.tell(list -> list.add("Hello"));
listActor.tell(list -> list.add("World"));
listActor.tell(list -> list.add("from"));
listActor.tell(list -> list.add("POJO-actor"));
// Query the list
CompletableFuture<Integer> sizeResult = listActor.ask(list -> list.size());
System.out.println("List size: " + sizeResult.get()); // → 4
CompletableFuture<String> first = listActor.ask(list -> list.get(0));
System.out.println("First element: " + first.get()); // → Hello
CompletableFuture<String> joined = listActor.ask(list -> String.join(" ", list));
System.out.println(joined.get()); // → Hello World from POJO-actor
system.terminate();
Benefits
- 기존 코드베이스를 아키텍처 변경 없이 레트로핏 가능
- 액터 기반 스레드 안전성으로 모든 객체 보호
- 필요에 따라 객체를 액터로 변환해 점진적으로 확장 가능
- 기존 POJO를 수정 없이 재사용 가능
대규모 액터 확장성
Virtual threads는 POJO‑actor가 수천 개의 액터를 효율적으로 처리하도록 합니다. 아래 예제는 10 000 개의 Counter 액터를 생성합니다:
import com.scivicslab.pojoactor.ActorSystem;
import com.scivicslab.pojoactor.ActorRef;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
ActorSystem system = new ActorSystem("massiveSystem", 4); // 4 CPU 스레드로 연산
List<ActorRef<Counter>> actors = new ArrayList<>();
// 10 000 개의 액터 생성
for (int i = 0; i > futures = new ArrayList<>();
for (ActorRef<Counter> actor : actors) {
futures.add(actor.tell(c -> c.increment()));
}
// 모든 메시지가 처리될 때까지 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
// 각 카운터가 한 번씩 증가했는지 확인
for (ActorRef<Counter> actor : actors) {
int value = actor.ask(c -> c.getValue()).get();
assert value == 1;
}
System.out.println("Successfully processed messages for 10 000 actors!");
system.terminate();
이 예제가 보여주는 점
- 스레드를 고갈시키지 않고 수천 개의 액터를 생성
- Virtual threads 덕분에 효율적인 메시지 처리
- 어떤 POJO와도 간단하고 타입‑안전하게 상호작용
POJO‑actor로 고도로 동시성 있는 경량 애플리케이션을 즐겨 구축하세요!
Advanced Usage
Parallel Matrix Multiplication
POJO‑actor는 병렬 연산 작업에 뛰어납니다. 아래 예제는 무거운 연산을 위해 work‑stealing 풀을 사용한 분산 행렬 곱셈을 보여줍니다:
// Create large matrices for multiplication
final int matrixSize = 400;
final int blockSize = 100;
double[][] matrixA = new double[matrixSize][matrixSize];
double[][] matrixB = new double[matrixSize][matrixSize];
// Create ActorSystem with 4 CPU threads for heavy computation
ActorSystem system = new ActorSystem("matrixSystem", 4);
List<CompletableFuture<double[][]>> futures = new ArrayList<>();
// Divide matrix into blocks and assign to different actors
for (int blockRow = 0; blockRow < matrixSize; blockRow += blockSize) {
for (int blockCol = 0; blockCol < matrixSize; blockCol += blockSize) {
Calculator calculator = new Calculator(); // hypothetical block calculator
ActorRef<Calculator> actor = system.actorOf(
String.format("block_%d_%d", blockRow, blockCol), calculator);
// Light operation: Initialize actor with block coordinates (uses virtual thread)
actor.tell(calc -> calc.initBlock(matrixA, matrixB, blockRow, blockCol)).get();
// Heavy computation: Matrix multiplication (uses work‑stealing pool)
CompletableFuture<double[][]> blockSum = actor.ask(
calc -> calc.calculateBlock(), // CPU‑intensive matrix multiplication
system.getWorkStealingPool() // Delegate to work‑stealing pool
);
futures.add(blockSum);
}
}
// Wait for all parallel calculations to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
Key Performance Points
- Light operations –
initBlock()은 단순히 참조를 설정하므로 가상 스레드에 최적입니다. - Heavy computation –
calculateBlock()은 실제 곱셈을 수행하며, work‑stealing 풀이 필수적입니다. - Virtual threads – setter/getter 수준의 작업만 처리하므로 CPU 부하가 거의 없습니다.
- CPU control – 액터 수와 관계없이 무거운 작업은 4개의 CPU 스레드만 사용합니다.
- Responsiveness – 무거운 작업이 진행되는 동안에도 액터는 가벼운 메시지에 즉시 응답합니다.
- Scalability – 시스템 자원을 고갈시키지 않고 수천 개의 액터를 생성할 수 있습니다.
Custom Thread Pools
ActorSystem system = new ActorSystem("system");
// Add additional work‑stealing pools
system.addWorkStealingPool(8); // CPU‑intensive tasks
system.addWorkStealingPool(2); // I/O‑bound tasks
// Use a specific thread pool
counter.tell(c -> c.increment(), system.getWorkStealingPool(1));
Actor Hierarchies
ActorRef parent = system.actorOf("parent", new ParentActor());
ActorRef child = parent.createChild("child", new ChildActor());
GraalVM Native Image Support
많은 기존 액터 프레임워크는 메시지 라우팅, 직렬화, 동적 프록시 생성을 위해 리플렉션에 의존합니다. 이 때문에 네이티브 이미지 컴파일이 어렵습니다. POJO‑actor는 리플렉션을 전혀 사용하지 않으며 최신 JDK 기능만 활용하므로 별도 설정 없이 GraalVM 네이티브 이미지로 컴파일할 수 있습니다.
# Compile to native image
native-image -jar target/POJO-actor-1.0.0-fat.jar -o pojo-actor-native
# Run native executable
./pojo-actor-native
추가 설정 파일이나 리플렉션 힌트가 전혀 필요하지 않습니다.
Source: …
성능
- 시작 시간 – 네이티브 컴파일 덕분에 거의 즉시 시작됩니다.
- 메모리 사용량 – POJO 기반 설계 덕분에 최소한의 힙 할당만 필요합니다.
- 처리량 – 가상 스레드를 이용한 높은 메시지 처리 속도.
- 확장성 – 병렬 작업을 위한 효율적인 워크‑스틸링 스레드 풀.
성능 모범 사례
가상 스레드와 워크‑스틸링 풀을 언제 사용해야 하는지 이해하는 것이 최적의 성능을 위해 중요합니다.
가벼운 작업 — 기본 가상 스레드 사용
// 차단되지 않거나 CPU를 많이 소모하지 않는 빠른 작업
counter.tell(c -> c.increment());
counter.tell(c -> c.setName("newName"));
listActor.tell(list -> list.add("item"));
CompletableFuture<Integer> size = listActor.ask(list -> list.size());
무거운 연산 — 워크‑스틸링 풀에 위임
ActorSystem system = new ActorSystem("system", 4); // 무거운 작업을 위한 4개의 CPU 스레드
// CPU 집약적인 계산은 워크‑스틸링 풀을 사용해야 합니다
CompletableFuture<Result> result = calculator.ask(
c -> c.performMatrixMultiplication(),
system.getWorkStealingPool()
);
// I/O‑바운드 또는 차단 호출도 워크‑스틸링 풀을 사용해야 합니다
CompletableFuture<Data> data = dataProcessor.ask(
p -> p.readLargeFile(),
system.getWorkStealingPool()
);
왜 중요한가
- 가상 스레드는 가벼운 메시지 전달 및 상태 변경에 이상적입니다.
- 워크‑스틸링 풀은 가상 스레드를 차단하지 않으면서 CPU‑집약적인 작업을 처리합니다.
- 이러한 구분을 통해 무거운 연산이 액터 시스템의 응답성을 저하시키는 것을 방지합니다.
워크‑스틸링 풀의 크기를 조정하여 CPU 코어 사용량을 제어할 수 있습니다:
// 예시: 적절한 스레드 풀 사용으로 혼합 워크로드 처리
ActorSystem system = new ActorSystem("mixedSystem", 4);
ActorRef processor = system.actorOf("processor", new DataProcessor());
사용 예시
// Create an actor system
ActorSystem system = ActorSystem.create();
// Create a POJO actor
DataProcessor processor = system.actorOf(new DataProcessor());
// Light operation – uses a virtual thread
processor.tell(p -> p.updateCounter());
// Heavy operation – uses the work‑stealing pool
CompletableFuture<AnalysisResult> heavyResult = processor.ask(
p -> p.performComplexAnalysis(largeDataset),
system.getWorkStealingPool()
);
// The actor remains responsive to light messages while heavy computation runs in the background
processor.tell(p -> p.logStatus()); // This won’t be blocked by the heavy computation
요구 사항
- Java 21 이상
- Maven 3.6 이상
종속성
- 런타임: JDK 표준 라이브러리만
- 테스트: JUnit 5, Apache Commons Math (테스트 범위만)
빌드
# Compile and test
mvn clean test
# Build JAR
mvn clean package
# Generate Javadoc
mvn javadoc:javadoc
감사의 글
POJO‑actor는 Alexander Zakusylo의 actr 라이브러리에서 영감을 받았습니다. 이 라이브러리는 Java에서 POJO 기반 액터 모델 접근 방식을 최초로 시도했습니다. actr가 많은 훌륭한 개념을 도입했지만, POJO‑actor는 다음과 같은 점에서 이를 확장하고 개선했습니다:
- 메시지 순서 보장: actr와 달리 POJO‑actor는 액터에 전송된 메시지가 전송된 순서대로 처리되도록 보장합니다.
- 현대적인 Java 기능: Java 21+ 가상 스레드와 최신 동시성 패턴을 활용하여 구현되었습니다.
- 향상된 스레드 풀 관리: actr는 액터당 실제 스레드를 사용해 CPU 코어 수에 따라 확장성이 제한되고, 무거운 연산 시 성능 문제가 발생했습니다. POJO‑actor는 액터에 가상 스레드를 사용하고 무거운 연산은 구성 가능한 워크‑스틸링 풀에 위임함으로써 수천 개의 액터를 지원하면서 CPU 코어 사용을 제어합니다.
또한 POJO‑actor의 기본 아키텍처 설계—특히 ActorSystem 및 ActorRef 개념—에 영감을 준 Node.js 액터 프레임워크 Comedy.js에도 감사를 표합니다. Comedy.js는 액터당 하나의 프로세스 또는 실제 스레드를 사용하지만, POJO‑actor는 Java의 가상 스레드를 활용해 수천 개의 경량 액터를 구현합니다.