| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- Kafka
- 테스트코드
- redissearch
- 메시지브로커
- god object
- 객체지향적사고
- retry
- redis
- redisstreams
- OOP
- 배치처리
- 레디스스트림
- springboot
- 레디스
- blockingqueue
- rdb
- 임베딩
- SaaS
- aof
- jedis
- DLT
- 백엔드
- 마케팅 #퍼플카우 #새스고딘 #혁신 #독서 #이북
- 자연어캐싱
- 데이터유실방지
- 시맨틱캐싱
- 메세지브로커
- 코사인
- 장애복구
- 비동기처리
- Today
- Total
pandaterry's 개발로그
[면접파헤치기] 동시성과 병렬성을 설명해보세요. 본문
Q.동시성과 병렬성을 설명해보세요.
질문을 받으면 헷갈려서 생각이 안날 수 있다.
일단 둘은 완전 다른 개념이다.

동시성(Concurrency)
동시성, 영어로는 Concurrency이다. 정말 말그대로 함께 진행되는가? 에 대한 단어이다. 근데 이 말은 동시에 진행되는가랑은 조금 거리를 둘 필요가 있다.
실제로는 Concurrency는 함께 진행되는지 여부를 묻는 것이다. Parallelism인 병렬과 다르게 나란히 진행되지는 않는다.
Concurrency는 함께 진행되고는 있으나 매우 빠른 속도로 번갈아가며 실행되는 것을 의미한다.
아래는 동시성이 적용되는 사례이다.
단일 스레드에서 이벤트 루프를 통해 요청을 번갈아 가며 처리를 할 수 있다.
Non-blocking IO를 사용해 기존 단일스레드를 블로킹하지 않아 번갈아가며 여러 요청처리가 빠르게 진행이 가능하다.
public class ConcurrentServer {
private final EventLoop eventLoop; // 단일 스레드 이벤트 루프
public void handleRequest() {
eventLoop.on("request", event -> {
// DB 조회 시작 - non-blocking IO
readFromDb()
.onComplete(dbResult -> {
// DB 조회가 완료되면 처리
processResult(dbResult);
});
// 파일 읽기 시작 - non-blocking IO
readFile()
.onComplete(fileContent -> {
// 파일 읽기가 완료되면 처리
processFile(fileContent);
});
// HTTP 요청 시작 - non-blocking IO
callExternalApi()
.onComplete(apiResult -> {
// API 호출이 완료되면 처리
processApiResult(apiResult);
});
});
}
}
아래는 코루틴(coroutine)을 이용하여 단일스레드에서 코루틴 간 제어권을 넘겨서 빠르게 번갈아가며 처리가 가능하게 구현한 코드이다.
// 코루틴은 함수의 실행을 일시 중단했다가 재개할 수 있음
suspend fun processOrders() {
// 주문 처리 작업
while (true) {
// suspend 함수들은 다른 코루틴이 실행될 수 있게 실행을 양보할 수 있음
val order = suspendGetNextOrder() // 실행 중단점 1
val inventory = suspendCheckStock() // 실행 중단점 2
val payment = suspendProcessPayment() // 실행 중단점 3
// 각 중단점에서 다른 코루틴이 실행될 수 있음
deliverOrder(order)
}
}
아래는 CSP(Communication Sequential Processes)로 고루틴으로 채널기반 통신을 구현했다. 이것도 동일하게 채널을 통해 메시지를 고루틴간 전달하며 단일스레드에서 빠르게 번갈아가며 처리를 한다.
// Go 스타일의 채널 기반 통신
public class OrderSystem {
private Channel<Order> orderChannel = new Channel<>();
private Channel<Payment> paymentChannel = new Channel<>();
public void orderProcessor() {
while (true) {
// 채널에서 주문을 받아서 처리
Order order = orderChannel.receive();
processOrder(order);
// 결제 채널로 전달
paymentChannel.send(new Payment(order));
}
}
public void paymentProcessor() {
while (true) {
// 결제 채널에서 데이터를 받아서 처리
Payment payment = paymentChannel.receive();
processPayment(payment);
}
}
}
아래는 Actor 모델으로서 각 액터는 독립적인 실행 단위이며 메세지를 전달로 액터간 통신을 하며 단일스레드에서 여러 액터가 동시에 논리적으로 실행이 가능하다.
// Akka 스타일의 액터 시스템
public class OrderActor extends AbstractActor {
// 액터가 처리할 수 있는 메시지 정의
@Data
public static class ProcessOrder {
private final Order order;
}
@Data
public static class CancelOrder {
private final String orderId;
}
// 메시지 처리 로직
@Override
public Receive createReceive() {
return receiveBuilder()
.match(ProcessOrder.class, msg -> {
// 주문 처리 로직
processOrder(msg.getOrder());
// 다른 액터에게 메시지 전달
getContext().actorSelection("/user/inventory")
.tell(new CheckInventory(msg.getOrder()), getSelf());
})
.match(CancelOrder.class, msg -> {
// 주문 취소 로직
cancelOrder(msg.getOrderId());
})
.build();
}
}
위 코루틴, 고루틴, 액터 모두 멀티코어를 사용해서 병렬로 구현하면 병렬성이 되지만, 싱글스레드에서 non-blocking 으로 구현하면 번갈아 작업을 처리하는 구조가 되어 동시성에 해당한다.
그래서 개인적인 생각으로는 수많은 TPS를 처리하기 위해 한정된 자원에서 작업처리를 내려야하는데, 이런 경우엔 병렬성보다는 동시성에 대한 고민을 많이 하게 될 것이다.
왜냐면 싱글스레드인 한정된 상황에서도 요청을 많이 처리해야하니 말이다. 그래서 동시성과 병렬성 차이를 이해하고 동시성으로 처리가 가능하게끔 공부하는 것을 회사에서는 권장할 것이다.
병렬성(Parallelism)
사실 이건 쉽다. 정말 멀티코어, 멀티프로세스 환경에서 실제로 여러 작업이 동시에 병렬로서 실행되는 것을 말한다.
동시성은 논리적으로 동시에 실행되는 것처럼 보이게 하는 것에 초점이라면, 병렬성은 정말 동시에 실행된다.
쉽게 비유를 해보면, 동시성은 한 사람이 양손으로 뭔가를 처리하는데 두뇌에서는 빠르게 왼손으로 이걸했다가, 오른손으로 이걸 해야해! 라고 생각하는 것이라면, 병렬성은 그냥 두 사람이 독립된 일을 동시에 처리하는 것이다.
아래는 스레드 풀 기반 병렬 처리를 하는 코드이다. CompletableFuture를 사용하여 비동기로 작업을 병렬로 처리하게 된다. 스레드마다 작업을 할애하기 때문에 동시에 처리가 진행되어 병렬성에 해당한다.
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.\*;
import java.util.stream.IntStream;
public class ParallelProcessingDemo {
// 1. ExecutorService를 사용한 병렬 처리
public static void executorServiceExample() {
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
// 여러 작업 제출
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread: "
+ Thread.currentThread().getName());
return taskId;
});
}
} finally {
executor.shutdown();
}
}
// 2. CompletableFuture를 사용한 비동기 병렬 처리
public static void completableFutureExample() {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Result 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Result 2";
});
// 두 작업을 병렬로 실행하고 결과 조합
CompletableFuture<String> combinedFuture = future1
.thenCombine(future2, (result1, result2) -> result1 + " and " + result2);
System.out.println(combinedFuture.join());
}
// 3. 병렬 스트림을 사용한 데이터 처리
public static void parallelStreamExample() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 순차 처리
long sequentialTime = System.currentTimeMillis();
int sequentialSum = numbers.stream()
.map(ParallelProcessingDemo::expensiveOperation)
.reduce(0, Integer::sum);
System.out.println("Sequential Time: " + (System.currentTimeMillis() - sequentialTime));
// 병렬 처리
long parallelTime = System.currentTimeMillis();
int parallelSum = numbers.parallelStream()
.map(ParallelProcessingDemo::expensiveOperation)
.reduce(0, Integer::sum);
System.out.println("Parallel Time: " + (System.currentTimeMillis() - parallelTime));
}
// 4. ForkJoinPool을 사용한 재귀적 작업 분할
static class SumTask extends RecursiveTask<Long> {
private final int\[\] array;
private final int start;
private final int end;
private static final int THRESHOLD = 1000;
SumTask(int\[\] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array\[i\];
}
return sum;
}
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork();
long rightResult = rightTask.compute();
long leftResult = leftTask.join();
return leftResult + rightResult;
}
}
public static void forkJoinExample() {
int\[\] numbers = IntStream.rangeClosed(1, 1000000).toArray();
ForkJoinPool forkJoinPool = new ForkJoinPool();
Long result = forkJoinPool.invoke(new SumTask(numbers, 0, numbers.length));
System.out.println("Sum: " + result);
}
// 유틸리티 메소드
private static int expensiveOperation(int number) {
sleep(100);
return number \* 2;
}
private static void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 메인 메소드
public static void main(String\[\] args) {
System.out.println("1. ExecutorService Example:");
executorServiceExample();
System.out.println("\\n2. CompletableFuture Example:");
completableFutureExample();
System.out.println("\\n3. Parallel Stream Example:");
parallelStreamExample();
System.out.println("\\n4. ForkJoin Example:");
forkJoinExample();
}
} '개발 > 면접대비' 카테고리의 다른 글
| [면접파헤치기] 억단위 데이터 조회시 로딩시간 개선문제 (0) | 2025.02.23 |
|---|