아쿠의 개발 일지

[SpringBoot] 멀티쓰레드 환경에서의 동시성 제어 (Synchronized vs Lock vs Atomic) 본문

Programming/Java

[SpringBoot] 멀티쓰레드 환경에서의 동시성 제어 (Synchronized vs Lock vs Atomic)

디아쿠 2025. 2. 10. 11:00

멀티쓰레드 환경에서 동시성 제어는 중요한 이슈입니다. Spring Boot 애플리케이션에서 여러 사용자의 요청이 동시에 처리될 때, 올바른 데이터 일관성을 유지하기 위해 적절한 동시성 제어 기법을 적용해야 합니다. 이번 글에서는 synchronized, Lock, Atomic 클래스를 활용한 동시성 제어 방법을 비교하고, 각각의 장단점을 알아보겠습니다.

 


1. 동시성 제어가 필요한 이유

멀티쓰레드 환경에서는 여러 개의 쓰레드가 동시에 공유 자원에 접근할 수 있습니다. 이때 동기화 처리를 하지 않으면 데이터 무결성이 깨지거나 레이스 컨디션(Race Condition) 문제가 발생할 수 있습니다. 예를 들어, 아래 코드를 살펴봅시다.

 

@RestController
@RequestMapping("/counter")
public class CounterController {
    private int counter = 0;

    @GetMapping("/increment")
    public int increment() {
        counter++;
        return counter;
    }
}

 

위 코드를 여러 사용자가 동시에 호출하면 counter 값이 예상과 다르게 동작할 수 있습니다. 이를 방지하기 위해 동시성 제어를 적용해야 합니다.

 


2. synchronized를 활용한 동기화 처리

2.1 synchronized 키워드 사용

sychronized 키워드는 Java에서 가장 기본적인 동기화 방법입니다. 특정 메서드나 블록을 한 번에 하나의 쓰레드만 접근할 수 있도록 제한합니다.

public synchronized int increment() {
    counter++;
    return counter;
}

또는 특정 블록만 동기화할 수도 있습니다.

public int increment() {
    synchronized (this) {
        counter++;
        return counter;
    }
}

 

2.2 synchronized의 장단점

장점

  • 구현이 간단하고 이해하기 쉬움
  • 메서드 수준 또는 블록 수준으로 유연하게 적용 가능

단점

  • 성능이 낮음 (해당 블록이 실행되는 동안 다른 쓰레드는 대기해야 함)
  • 세밀한 제어가 어렵고, 특정 쓰레드만 대기하도록 설정할 수 없음

3. Lock을 활용한 동시성 제어

3.1 ReentrantLock 사용

ReentrantLocksynchronized보다 세밀한 제어가 가능하며, 락을 해제하는 시점을 직접 설정할 수 있습니다.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@RestController
@RequestMapping("/counter")
public class CounterController {
    private int counter = 0;
    private final Lock lock = new ReentrantLock();

    @GetMapping("/increment")
    public int increment() {
        lock.lock();
        try {
            counter++;
            return counter;
        } finally {
            lock.unlock();
        }
    }
}

3.2 Lock의 장단점

장점

  • try-finally 블록을 사용하여 락 해제를 명확하게 제어할 수 있음
  • tryLock() 등의 메서드를 활용하여 락 획득 여부를 확인 가능

단점

  • lock.lock()unlock()을 호출하지 않으면 데드락(Deadlock) 발생 가능
  • synchronized보다 코드가 다소 복잡함

4. Atomic 클래스를 활용한 동기화

4.1 AtomicInteger 사용

Atomic 클래스는 내부적으로 CAS(Compare-And-Swap) 알고리즘을 사용하여 동기화 없이도 안전하게 값을 변경할 수 있습니다.

import java.util.concurrent.atomic.AtomicInteger;

@RestController
@RequestMapping("/counter")
public class CounterController {
    private final AtomicInteger counter = new AtomicInteger(0);

    @GetMapping("/increment")
    public int increment() {
        return counter.incrementAndGet();
    }
}

4.2 Atomic의 장단점

장점

  • synchronizedLock 없이도 성능이 뛰어남
  • 락을 사용하지 않기 때문에 데드락이 발생할 가능성이 없음

단점

  • 단순한 연산만 가능 (여러 변수를 동시에 변경하는 경우 AtomicReferencesynchronized 필요)
  • 복잡한 동기화 로직에는 적합하지 않음

5. 어떤 방법을 선택해야 할까?

동기화 방법 장점 단점
synchronized 사용이 간편하고 직관적 성능 저하 및 세밀한 제어 어려움
Lock 세밀한 제어 가능, 락 획득 여부 확인 가능 코드가 복잡해질 수 있고 락 해제를 명확히 해야 함
Atomic 성능이 우수하고 데드락 위험 없음 단순한 연산만 가능, 복잡한 동기화에는 부적합

💡 선택 기준

  • 성능이 중요하고 단순한 연산(카운터 증가 등)AtomicInteger
  • 간단한 동기화가 필요할 때synchronized
  • 세밀한 동기화 및 락 해제가 필요한 경우Lock

6. 결론

Spring Boot에서 멀티쓰레드 환경을 다룰 때는 상황에 맞는 동기화 전략을 선택해야 합니다.

  • 단순한 카운터 증가 같은 경우 AtomicInteger를 활용하면 성능이 좋습니다.
  • synchronized는 간단한 동기화에 적합하지만, 성능이 중요한 경우 Lock을 사용하는 것이 더 유리할 수 있습니다.
  • LocktryLock() 등을 활용하여 더 세밀한 제어가 가능합니다. 하지만 락 해제를 명확히 관리해야 합니다.

Spring Boot 애플리케이션에서 동시성 이슈를 해결할 때, 상황에 맞는 동기화 기법을 적용하는 것이 중요합니다. 필요에 따라 적절한 방법을 선택하여 성능과 안정성을 모두 잡을 수 있도록 합시다! 🚀

 

728x90