멀티태스킹 vs 멀티프로세싱 이해해보기
개발을 하다 보면 '멀티태스킹'과 '멀티프로세싱'이라는 용어를 꽤나 자주 접하게 됩니다.
두 개념 모두 여러 작업을 처리하는 방식이지만 실무에서 마주하는 성능 문제나 동시성 이슈를 해결하려면 이 둘의 차이를 명확히 알아야 합니다.
이번 글은 멀티태스킹/멀티 프로세싱의 좀 더 명확한 차이와 동시성까지의 이해도를 높이고자 작성했습니다.
✅멀티태스킹과 멀티프로세싱의 핵심 차이
멀티태스킹이란 무엇일까?

멀티태스킹은 하나의 CPU가 여러 작업을 번갈아가며 처리하는 방식입니다. 실제로는 한 번에 하나씩 처리하지만, 작업을 매우 빠르게 교체하면서 실행하기 때문에 우리 눈에는 동시에 실행되는 것처럼 보입니다. (시분할 서비스)
일상에서 예를 들면 음악을 들으면서 문서를 작성하거나, 웹서핑을 하면서 파일을 다운로드하는 것이 멀티태스킹입니다. 한 사람이 공부하다가 전화를 받고 다시 공부로 돌아오는 것과 비슷하다고 생각하면 됩니다.
자바에서는 주로 멀티스레드를 통해 멀티태스킹을 구현합니다. 하나의 JVM 프로세스 안에서 여러 스레드가 CPU 시간을 나눠 쓰면서 작업을 처리하는 방식입니다.
멀티프로세싱이란 무엇일까?

멀티프로세싱은 여러 CPU 코어가 여러 작업을 진짜 동시에 병렬로 처리하는 방식입니다. 여러 프로세스를 동시에 실행하여 CPU 자원을 적극적으로 활용합니다.
예를 들어 영상 렌더링, 머신러닝 학습, 웹서버에서 여러 개의 worker를 운영하는 경우가 멀티프로세싱에 해당합니다. 여러 사람이 일을 나눠서 동시에 처리하는 것과 같습니다.
자바에서는 여러 개의 JVM 인스턴스를 띄우거나, ProcessBuilder를 사용해 별도의 프로세스를 생성하는 방식으로 멀티프로세싱을 구현할 수 있습니다.
실무에서 마주했던 질문들
CPU 개수와 실무와는 관련이 어떻게 될 수 있을까?
CPU 갯수를 늘리는 것. 즉,멀티프로세싱은 CPU 코어 수와 직접적인 관계가 있습니다. 프로세스를 여러 개 띄워도 CPU 코어가 부족하면 동시에 실행되지 못합니다. CPU가 충분해야 병렬 처리가 의미가 있습니다. 즉, 프로세스를 늘린다고 무조건 성능이 빨라지는 것이 아닙니다. 4코어 CPU에서 10개의 프로세스를 실행하면 결국 컨텍스트 스위칭 오버헤드만 증가할 수 있습니다.
실무에서 성능 문제나 장애가 발생하면 항상 이런 질문으로 이어집니다. 서버가 느린 이유가 무엇일까요? CPU를 늘려야 할까요? Worker 프로세스를 늘려야 할까요? 병목이 CPU인가요, 아니면 데이터베이스인가요?
이러한 질문과 판단은 결국, 애플리케이션단에서 해결해야 할 문제와, 인프라적으로 해결 할 수 있는 문제로 나눠서 생각해볼 수 있을 것 같습니다.
멀티태스킹과 멀티프로세싱은 단순한 개념이 아니라 성능과 비용을 결정하는 설계 요소입니다. 클라우드 환경에서 인스턴스 타입을 선택하거나, 애플리케이션의 스레드 풀 크기를 결정할 때 이런 개념을 정확히 이해하고 있어야 합니다.
병목이 생기면 어떻게 판단해야 할까?
상황에 따라 접근 방법이 다릅니다.
CPU 사용률이 100%에 가깝다면 CPU-bound 병목입니다. 이 경우 프로세스를 늘려도 소용이 없습니다. 오히려 컨텍스트 스위칭만 증가해서 성능이 더 나빠질 수 있습니다. 이럴 때는 CPU 코어를 증설하거나 서버 자체를 scale-out 할 문제로 꼽힙니다.
반대로 CPU 사용률은 낮은데 서버가 느리다면 CPU 문제가 아닙니다. 대부분 데이터베이스 쿼리 지연, 디스크 I/O 병목, 네트워크 대기 같은 원인입니다. 이럴 때는 데이터베이스 튜닝, 캐시 적용, 비동기 처리 같은 방법으로 해결해야 할 문제로 꼽힙니다.
동시성과의 관계
멀티태스킹, 멀티프로세싱이 나오게 되면 반드시 자연스럽게 따라오는 개념이 동시성 / 병렬성 개념입니다.
동시성과 병렬성은 어떻게 다를까?
동시성(Concurrency)은 여러 작업이 겹쳐서 진행되는 상태를 말합니다. CPU가 1개여도 가능합니다. 작업들이 번갈아 실행되더라도 동시에 처리되는 것처럼 보입니다.
병렬성(Parallelism)은 여러 작업이 실제로 동시에 실행되는 상태를 말합니다. 여러 CPU 코어가 있어야 가능합니다.
멀티태스킹은 동시성을 구현하는 방식이고, 멀티프로세싱은 병렬성까지 확장 가능한 방식입니다.
자바의 ExecutorService를 사용할 때 스레드 풀 크기를 CPU 코어 수에 맞춰 설정하는 이유도 이 때문이라고 볼 수 있습니다.
✅동시성 문제는 어디서 발생하는가?
멀티태스킹과 멀티프로세싱 둘 다 동시성 문제를 만들지만, 발생 위치가 다릅니다.
1. 멀티태스킹에서의 동시성 문제
멀티태스킹은 보통 멀티스레드나 비동기 작업처럼 하나의 프로세스 내부에서 동작합니다. 즉, 공유 메모리 때문에 문제가 발생합니다.
Race Condition 문제
두 작업이 동시에 같은 변수를 수정하면 결과가 뒤바뀝니다. 자바에서 흔히 볼 수 있는 예시를 보겠습니다.
// AS-IS: 동시성 문제가 있는 코드
public class PointService {
private int totalPoints = 0;
public void addPoints(int points) {
// 여러 스레드가 동시에 실행하면 값이 꼬임
totalPoints += points;
}
}
// TO-BE: synchronized로 해결
public class PointService {
private int totalPoints = 0;
public synchronized void addPoints(int points) {
// 한 번에 하나의 스레드만 실행 가능
totalPoints += points;
}
}
위 코드에서 totalPoints += points는 실제로는 세 단계의 연산입니다. 현재 값을 읽고(read), 더하고(modify), 다시 저장합니다(write). 두 스레드가 동시에 실행되면 둘 다 같은 값을 읽어서 한 번의 증가만 반영될 수 있습니다.
Deadlock 문제
스레드끼리 락을 잡고 서로 기다리면서 멈춰버리는 상황입니다.
// AS-IS: Deadlock 가능성이 있는 코드
public class TransferService {
public void transfer(Account from, Account to, int amount) {
synchronized(from) {
synchronized(to) {
from.debit(amount);
to.credit(amount);
}
}
}
}
// 스레드 1: transfer(A, B, 100) - A 락 획득 후 B 락 대기
// 스레드 2: transfer(B, A, 200) - B 락 획득 후 A 락 대기
// → Deadlock 발생
// TO-BE: 락 순서를 일관되게 유지
public class TransferService {
public void transfer(Account from, Account to, int amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized(first) {
synchronized(second) {
from.debit(amount);
to.credit(amount);
}
}
}
}
락을 획득하는 순서를 일관되게 유지하면 Deadlock을 방지할 수 있습니다.
멀티태스킹에서의 동시성 문제는 프로세스 내부에서 발생하며, 공유 메모리 접근이 원인입니다. 자바에서는 synchronized, Lock, Semaphore 같은 동기화 메커니즘으로 해결합니다.
2. 멀티프로세싱에서의 동시성 문제
멀티프로세싱은 프로세스가 분리되어 있어서 메모리는 공유하지 않습니다. 하지만 외부 자원인 데이터베이스, 파일, Redis를 공유할 때 문제가 발생합니다.
데이터베이스 재고 문제
프로세스 여러 개가 동시에 주문 처리를 하면 재고가 음수가 되거나 중복 결제가 발생할 수 있습니다.
// AS-IS: 동시성 문제가 있는 재고 감소 로직
@Service
public class OrderService {
@Transactional
public void order(Long productId, int quantity) {
Product product = productRepository.findById(productId);
// 여러 프로세스/스레드가 동시에 같은 재고를 읽음
int currentStock = product.getStock();
if (currentStock >= quantity) {
// 둘 다 통과할 수 있음
product.setStock(currentStock - quantity);
// 재고가 꼬임
}
}
}
// TO-BE: 비관적 락으로 해결
@Service
public class OrderService {
@Transactional
public void order(Long productId, int quantity) {
// FOR UPDATE로 row lock 획득
Product product = productRepository
.findByIdWithPessimisticLock(productId);
int currentStock = product.getStock();
if (currentStock >= quantity) {
product.setStock(currentStock - quantity);
}
}
}
// Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findByIdWithPessimisticLock(@Param("id") Long id);
}
비관적 락을 사용하면 하나의 트랜잭션이 데이터를 읽을 때 다른 트랜잭션은 대기하게 됩니다.
파일 쓰기 충돌
여러 프로세스가 동시에 로그 파일을 쓰면 내용이 섞입니다.
// AS-IS: 동시성 문제가 있는 파일 쓰기
public class LogWriter {
public void writeLog(String message) {
try (FileWriter fw = new FileWriter("app.log", true)) {
fw.write(message + "\n");
// 여러 프로세스가 동시에 쓰면 내용이 섞임
}
}
}
// TO-BE: 파일 락을 사용하거나 전용 로깅 라이브러리 사용
public class LogWriter {
public void writeLog(String message) {
try (RandomAccessFile raf = new RandomAccessFile("app.log", "rw");
FileChannel channel = raf.getChannel()) {
FileLock lock = channel.lock();
try {
channel.position(channel.size());
channel.write(ByteBuffer.wrap(
(message + "\n").getBytes()
));
} finally {
lock.release();
}
}
}
}
실무에서는 Logback이나 Log4j2 같은 라이브러리를 사용하는 것이 더 안전합니다.
캐시 불일치 문제
여러대의 톰캣 프로세스로 구성되어있는 경우 로컬 캐시가 달라 데이터가 일관되지 않을 수 있습니다.
// AS-IS: 각 프로세스마다 다른 캐시를 가짐
@Service
public class ProductService {
private Map<Long, Product> localCache = new ConcurrentHashMap<>();
public Product getProduct(Long id) {
return localCache.computeIfAbsent(id,
key -> productRepository.findById(key));
// 프로세스 A와 프로세스 B의 캐시가 다를 수 있음
}
}
// TO-BE: Redis 같은 중앙 캐시 사용
@Service
public class ProductService {
private final RedisTemplate<String, Product> redisTemplate;
public Product getProduct(Long id) {
String key = "product:" + id;
Product cached = redisTemplate.opsForValue().get(key);
if (cached == null) {
cached = productRepository.findById(id);
redisTemplate.opsForValue().set(key, cached,
Duration.ofMinutes(10));
}
return cached;
}
}
중앙 캐시를 사용하면 모든 프로세스가 동일한 데이터를 참조합니다.
멀티프로세싱에서의 동시성 문제는 외부 공유 자원에서 발생하며, 트랜잭션, 분산 락, 메시지 큐 설계 같은 방법으로 해결합니다.
Spring Boot 환경에서의 동시성
단일 스레드면 동시성 문제가 없지 않을까?
멀티스레딩에 대한 개념이 낮을 때 스스로 착각했던 개념으로, "아무 설정을 하지 않으면 단일 스레드로 실행되니까 동시성 문제가 없겠지?"라고 생각했습니다. 하지만 이는 꽤나 위험한 착각이었습니다.
대부분의 프로그램은 기본적으로 프로세스 1개와 메인 스레드 1개로 시작합니다. 간단한 자바 콘솔 프로그램이나 스크립트는 처음에는 단일 스레드가 맞습니다.
그렇다면 spring에서는 왜 동시성 문제가 발생할까?
하지만 Spring Boot 웹 애플리케이션은 다릅니다. 내 코드는 단일 스레드로 작성했더라도 서비스는 동시에 여러 요청을 받습니다.
Spring Boot에 내장된 Tomcat은 요청마다 스레드를 할당합니다. 요청 10개가 동시에 들어오면 스레드 10개가 동시에 실행됩니다. 내가 설정하지 않아도 웹서버는 기본적으로 멀티스레드입니다.
게다가 외부 공유 자원인 데이터베이스는 항상 동시 접근됩니다. 재고 감소, 잔액 차감, 포인트 증가 같은 로직은 동시에 실행되면 꼬일 수 있습니다.
Spring에서 충돌이 발생하는 구간
요청이 10개 들어오면 스레드 10개가 동시에 실행되고, 공유 자원을 건드리는 순간 충돌이 발생합니다.
// 1) 전역 Map을 사용할 때
@RestController
public class CounterController {
// 모든 요청이 이 Map을 공유함
private Map<String, Integer> counter = new HashMap<>();
@GetMapping("/count")
public void count() {
Integer value = counter.getOrDefault("x", 0);
counter.put("x", value + 1);
// HashMap은 thread-safe하지 않아서 값이 꼬임
}
}
// 2) Singleton Bean에 상태가 있을 때
@Service
public class OrderService {
private int count = 0; // 모든 요청이 공유
public void order() {
count++; // 동시에 증가하면 값이 깨짐
}
}
Spring Bean은 기본적으로 Singleton이므로 모든 요청이 같은 객체를 공유합니다. 상태를 가진 필드가 있으면 동시성 문제가 발생합니다.
반대로 요청마다 값이 분리되어 있다면 안전합니다.
// 안전한 코드: 지역 변수는 스레드마다 따로 생성됨
@Service
public class SafeService {
public void process() {
int local = 0; // 각 스레드의 스택에 독립적으로 생성
local++;
// 동시성 문제 없음
}
}
정리
멀티태스킹은 작업을 번갈아 실행하는 방식이고, 멀티프로세싱은 작업을 병렬로 실행하는 방식입니다. 멀티태스킹은 CPU 1개로도 가능하지만, 멀티프로세싱은 여러 코어가 필요합니다.
동시성 문제의 발생 위치도 다릅니다. 멀티태스킹은 프로세스 내부 메모리에서 Race Condition이나 Deadlock이 발생하며, synchronized나 Lock으로 해결합니다. 멀티프로세싱은 데이터베이스, 파일 같은 외부 자원에서 재고 꼬임이나 캐시 불일치가 발생하며, 트랜잭션이나 분산 락으로 해결합니다.
가장 중요한 차이점은 멀티태스킹은 코드 내부에서 문제가 터지고, 멀티프로세싱은 시스템 전체에서 문제가 터진다는 점입니다. 또한 단일 스레드로 시작하더라도 실무 환경에서는 동시성이 필연입니다. Spring Boot 같은 웹 프레임워크는 기본적으로 멀티스레드이고, 공유 자원을 건드리는 순간 문제가 터집니다.
멀티태스킹과 멀티프로세싱은 단순한 기술 용어가 아니라 서버 성능 튜닝, 장애 대응, 동시성 버그 예방, 비용 효율적인 인프라 운영 모두와 직결되는 실무 핵심 개념입니다. 이 개념들을 정확히 이해하고 있어야 실제 서비스에서 발생하는 문제를 올바르게 진단하고 해결할 수 있습니다.