본문 바로가기
Project/SpringBoot

Java의 동시성 문제를 해결가능한 3가지 방법

by 꽃요미 2025. 2. 12.

✅ 1. synchronized (Java 기본 동기화)

📌 원리

자바의 기본 동기화 방식으로, 한 번에 하나의 스레드만 enterRoom() 메소드 실행 가능.

메소드 자체에 synchronized 키워드를 붙이거나 특정 객체에 대해 동기화 블록을 사용할 수 있음.

@GetMapping("/room/{roomId}")
public synchronized ModelAndView enterRoom(@PathVariable Long roomId, @LoginUser LoginUserRequest loginUserRequest) throws IllegalAccessException {
    // ... codes
}

 

장점

간단하고 구현이 쉬움 (synchronized 키워드만 추가).

자바 기본 제공 기능이므로 별도의 라이브러리 불필요.

JVM 수준에서 관리되므로 안정적.

 

단점

블로킹 방식이므로, 하나의 스레드만 enterRoom()을 실행 가능 → 성능 저하.

전체 메소드가 동기화됨 → 불필요한 요청까지 대기하게 됨.

분산 서버 환경에서는 적용 불가능 (싱글 인스턴스에서만 유효).

 


 

✅ 2. ReentrantLock (Java 동기화, 더 세밀한 제어)

📌 원리

synchronized와 유사하지만, 더 정교한 동기화 제어 가능.

공정 락 (fair lock) 설정 가능먼저 요청한 스레드가 우선 처리됨.

시도하다 실패하면 다른 작업을 수행할 수 있음 (tryLock 지원).

import java.util.concurrent.locks.ReentrantLock;

@Component
public class RoomLockManager {
    private final Map<Long, ReentrantLock> roomLocks = new ConcurrentHashMap<>();

    public ReentrantLock getLock(Long roomId) {
        return roomLocks.computeIfAbsent(roomId, id -> new ReentrantLock());
    }
}

 

@GetMapping("/room/{roomId}")
public ModelAndView enterRoom(@PathVariable Long roomId, @LoginUser LoginUserRequest loginUserRequest) throws IllegalAccessException {
    ReentrantLock lock = roomLockManager.getLock(roomId);
    
    lock.lock();
    try {
        // codes...
    } finally {
        lock.unlock();
    }
}

 

장점

동기화 범위를 세밀하게 조정 가능 (lock() / unlock() 명시적 제어).

tryLock()을 사용하면 대기하지 않고 실패 시 다른 작업 수행 가능.

fair 모드를 사용하면 먼저 요청한 스레드가 우선 처리됨 (기아 문제 방지).

 

단점

synchronized보다 코드가 길어지고 복잡.

락을 잘못 해제하면 데드락 발생 가능.

싱글 서버에서만 동작, 분산 환경에서는 사용 불가.


 

✅ 3. Redis Lock (분산 환경에서 강력한 동기화)

📌 원리

Redis를 이용해 분산 락을 생성여러 서버에서도 동시에 실행되지 않도록 제어 가능.

SET NX(Not Exists) 옵션을 사용하여 이미 락이 존재하면 요청을 무시.

TTL(Time To Live) 설정 가능 → 일정 시간이 지나면 자동 해제.

 

import org.redisson.api.RedissonClient;
import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;

@Service
public class RoomService {
    private final RedissonClient redissonClient;

    public RoomService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    public ModelAndView enterRoom(Long roomId, LoginUserRequest loginUserRequest) throws IllegalAccessException {
        RLock lock = redissonClient.getLock("room:" + roomId);
        
        try {
            if (lock.tryLock(10, 5, TimeUnit.SECONDS)) { // 10초 대기, 5초 유지
                // codes...
            } else {
                throw new RuntimeException("다른 사용자가 방 입장 중입니다.");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("입장 중 오류 발생");
        } finally {
            lock.unlock();
        }
    }
}

 

장점

분산 환경에서도 적용 가능 → 여러 서버에서 동일한 데이터 동기화 가능.

TTL 설정 가능 → 만약 서버가 다운되면 자동으로 락 해제됨.

과도한 대기 방지 가능tryLock()을 사용해 특정 시간 동안만 대기.

 

단점

Redis 서버가 필요함 (설치 및 설정 필요).

lock()을 놓치면 다른 요청이 무한 대기할 가능성 있음.

일반적인 synchronized나 ReentrantLock보다 속도가 느림 (네트워크 오버헤드).

 


 

💡 어떤 방법이 가장 효율적일까?

방법 특징 장점 단점 추천 상황
synchronized JVM 기본 락 간단함, 빠름 블로킹, 단일 서버 전용 동시 요청이 적고 단일 서버
ReentrantLock 더 세밀한 동기화 락 세부 제어 가능, fair lock 코드 복잡, 데드락 위함 멀티스레드 환경에서 부분 동기화 필요
Redis Lock 분산 환경 동기화 여러 서버 지원, TTL 가능 Redis 필요, 네트워크 오버헤드 다중 서버에서 경쟁이 많은 경우

 

 

- 결론 : 로컬에서 1번과 2번을 적용해서 테스트 하고, 3번을 적용해서 배포할 예정이다.