1️⃣ 들어가며
"한 명에게만 줘야 하는 쿠폰인데, 어떻게 두 명이 받았지?"
이런 상황, 개발을 하며 정말 자주 접하게 됩니다. 특히 선착순 쿠폰, 이벤트 응모, 재고 1개 남은 상품 구매와 같은 케이스에서는 "빠르게 두 번 클릭했더니 쿠폰이 두 장 발급됐다(따닥 이슈)", "100명 이벤트인데 101명이 응모 완료했다" 같은 문제가 벌어지곤 하죠.
이번 글에서는 이 문제를 실험으로 명확하게 확인하고, Java에서 제공하는 ReentrantLock을 통해 동시성 문제를 어떻게 해결할 수 있는지 단계적으로 살펴보려 합니다.
이전에 작성한 글 🔗ReentrantLock 정리와 🔗HashMap vs ConcurrentHashMap을 먼저 읽어보면 해당 글을 이해하는데 도움이 됩니다!!
🧩 실습 목표
실험을 통한 가설 검증
- ✅ 가설 1: 락 없이 동시에 쿠폰 발급 요청 시 중복 발급이 발생한다
- ✅ 가설 2: 전역 락을 사용하면 단일 발급이 보장된다
- ✅ 가설 3: 한명의 사용자가 빠르게 클릭을 했을때(따닥 이슈), 쿠폰이 하나만 발급된다.
- ✅ 가설 4: 서로 다른 쿠폰 코드를 동시에 발급 요청하면, 서로 다른 쿠폰은 독립적으로 발급 처리 되어야한다.
- ✅ 추가적으로.... : 락 타임아웃을 설정하면, 일정 시간 대기 후 락을 획득하지 못한 요청은 실패해야 한다
"그냥 synchronized 걸면 되지 않나요?" 같은 접근으로는 동시성 처리에 부족한 경우가 많습니다.
이 글을 통해 락을 왜 써야 하는지, 어떤 방식으로 써야 서비스 병목 없이 설계할 수 있을지를 직접 코드로 작성하고 테스트로 검증합니다.
2️⃣ 기본 구조와 코드 소개
📌 1. CouponController
HTTP 요청을 받아 쿠폰 발급 서비스
를 호출하는 컨트롤러입니다.
두 가지 API가 존재합니다.
/coupon/without-lock
: 락 없이 쿠폰을 발급/coupon/with-lock
:ReentrantLock
으로 보호된 쿠폰 발급
@RestController
@RequestMapping("/coupon")
@RequiredArgsConstructor
public class CouponController {
private final CouponService couponService;
@PostMapping("/without-lock")
public void issueWithoutLock(@RequestBody CouponRequest request) {
couponService.issuedCouponWithoutLock(request);
}
@PostMapping("/with-lock")
public void issueWithLock(@RequestBody CouponRequest request) {
couponService.issuedCouponWithLock(request);
}
}
📌 2. CouponRequest
쿠폰 발급 요청을 나타내는 DTO입니다.
이 요청 객체를 기반으로 Coupon 엔티티를 조립할 수 있도록 toEntity(Long id)
메서드를 제공합니다.
public record CouponRequest(
String code,
Long ownerId
) {
public Coupon toEntity(Long id) {
return Coupon.builder()
.id(id)
.code(code)
.ownerId(ownerId)
.issued(false)
.build();
}
}
📌 3. Coupon
쿠폰 도메인 객체입니다. 이미 발급된 상태인지, 누구에게 발급되었는지를 나타냅니다. 또한 issueTo(Long ownerId)
메서드를 통해 발급 상태의 복사 객체를 만들 수 있습니다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coupon {
private Long id;
private String code;
private Long ownerId;
private boolean issued;
public Coupon issueTo(Long ownerId) {
return Coupon.builder()
.id(this.id)
.code(this.code)
.ownerId(ownerId)
.issued(true)
.build();
}
}
📌 4. CouponRepository
쿠폰을 저장하고 조회하는 역할을 담당하는 인메모리 저장소
입니다.
@Repository
public class CouponRepository {
private final Map<Long, Coupon> store = new ConcurrentHashMap<>();
private final AtomicLong sequence = new AtomicLong(0);
public Coupon save(Coupon coupon) {
Long id = (coupon.getId() == null) ? sequence.incrementAndGet() : coupon.getId();
Coupon saved = Coupon.builder()
.id(id)
.code(coupon.getCode())
.ownerId(coupon.getOwnerId())
.issued(coupon.isIssued())
.build();
store.put(id, saved);
return saved;
}
public Optional<Coupon> findByCode(String code) {
return store.values().stream()
.filter(coupon -> coupon.getCode().equals(code))
.findFirst();
}
public List<Coupon> findAll() {
return new ArrayList<>(store.values());
}
}
📌 5. CouponService
비즈니스 로직을 수행하는 핵심 클래스입니다.
여기서 실제 쿠폰 발급 시 중복을 막기 위한 동기화 처리를 수행합니다.
@Service
@RequiredArgsConstructor
public class CouponService {
private final CouponRepository couponRepository;
private final ReentrantLock lock = new ReentrantLock();
// 락 없이 쿠폰 발급
public void issuedCouponWithoutLock(CouponRequest request) {
Coupon coupon = couponRepository.findByCode(request.code())
.orElseThrow(() -> new IllegalArgumentException("해당 코드의 쿠폰이 존재하지 않습니다."));
if (coupon.isIssued()) {
throw new IllegalStateException("이미 발급된 쿠폰입니다.");
}
Coupon issuedCoupon = coupon.issueTo(request.ownerId());
couponRepository.save(issuedCoupon);
}
// 전체 락 걸고 발급
public void issuedCouponWithLock(CouponRequest request) {
lock.lock();
try {
Coupon coupon = couponRepository.findByCode(request.code())
.orElseThrow(() -> new IllegalArgumentException("해당 코드의 쿠폰이 존재하지 않습니다."));
if (coupon.isIssued()) {
throw new IllegalStateException("이미 발급된 쿠폰입니다.");
}
Coupon issuedCoupon = coupon.issueTo(request.ownerId());
couponRepository.save(issuedCoupon);
} finally {
lock.unlock();
}
}
// 코드별 락 적용 (쿠폰이 각각 다를때, 극 각 쿠폰마다 락을 건다)
private final ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>();
public void issuedPerCouponWithLock(CouponRequest request) {
ReentrantLock perLock = locks.computeIfAbsent(request.code(), k -> new ReentrantLock());
perLock.lock();
try {
Coupon coupon = couponRepository.findByCode(request.code())
.orElseThrow(() -> new IllegalArgumentException("해당 코드의 쿠폰이 존재하지 않습니다."));
if (coupon.isIssued()) {
throw new IllegalStateException("이미 발급된 쿠폰입니다.");
}
Coupon issuedCoupon = coupon.issueTo(request.ownerId());
couponRepository.save(issuedCoupon);
} finally {
perLock.unlock();
}
}
}
이 구조를 바탕으로 이제 동시성 실험과 ReentrantLock
의 효과를 직접 확인할 준비가 끝났습니다.
3️⃣ 테스트코드를 통한 가설 검증
초기 세팅
@BeforeEach
void setUp() {
couponRepository = new CouponRepository();
couponService = new CouponService(couponRepository);
Coupon initial = Coupon.builder()
.id(1L)
.code("TEST_CODE")
.ownerId(null)
.issued(false)
.build();
couponRepository.save(initial);
}
✅ 가설 1 : 락 없이 동시에 발급 요청하면 중복 발급이 일어날 수 있다.
해당 가설은 issuedCouponWithoutLock()
메서드를 대상으로 진행된다. 락을 적용하지 않은 상태에서 다수의 스레드가 동시에 쿠폰을 발급 요청할 때 어떤 현상이 발생하는지 확인한다. 즉, 락 없이 1000명이 발급을 요청할 때 몇 개의 쿠폰이 발급되는지를 검증한다.
🔍 실험 조건
- 총 1000개의 스레드가 동시에
POST /coupon/without-lock
요청 수행 - 쿠폰 코드는 단 하나만 존재 (
TEST_CODE
) - 모든 스레드가 같은 쿠폰을 발급받으려 시도
- 각 스레드는 고유한
ownerId
를 가지고 요청
🧪 테스트 코드
@Test
void 락없이_동시에_쿠폰을_발급_요청하면_쿠폰이_다수가_발행된다_withoutLock() throws InterruptedException {
int threadCount = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch ready = new CountDownLatch(threadCount);
CountDownLatch start = new CountDownLatch(1);
AtomicInteger successCount = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
ready.countDown();
try {
start.await();
couponService.issuedCouponWithoutLock(
new CouponRequest("TEST_CODE", Thread.currentThread().getId()));
successCount.incrementAndGet();
System.out.println("접근 Thread - : " + Thread.currentThread().getId() + " 쿠폰 발급 성공");
} catch (Exception e) {
System.out.println("접근 Thread - : " + Thread.currentThread().getId() + " 쿠폰 발급 실패: " + e.getMessage());
}
});
}
ready.await();
start.countDown();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("총 쿠폰 발급 개수: " + successCount.get());
assertTrue(successCount.get() > 1, "락 없이 동시 접근 시 1개 초과 발급되어야 합니다.");
}
✅ 테스트 결과
접근 Thread - : 25 쿠폰 발급 성공
접근 Thread - : 130 쿠폰 발급 성공
접근 Thread - : 86 쿠폰 발급 성공
...(생략)
접근 Thread - : 615 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
접근 Thread - : 807 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
접근 Thread - : 758 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
접근 Thread - : 666 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
총 쿠폰 발급 개수: 691
예상과 다르게 발급 성공 개수가 1보다 크다는 것은 동시에 여러 스레드가 발급 로직을 통과했다는 의미이다.
즉, 발급 체크 조건을 여러 스레드가 동시에 통과해 중복 발급이 발생한 것이다.
❗ 왜 이런 일이 발생했을까?
issued
필드 확인 후true
로 바꾸는 작업이 원자적이지 않다- 두 스레드가 거의 동시에 아래 조건문을 통과할 수 있다:
if (coupon.isIssued()) {
throw new IllegalStateException("이미 발급된 쿠폰입니다.");
}
- 첫 번째 스레드가 아직
issued=true
를 저장하지 않은 상태에서 두 번째 스레드가 같은 조건을 통과하면 Race Condition이 발생하게 된다.
✅ 가설 2: 전역 락을 사용하면 단일 발급이 보장된다
이전 테스트에서는 락을 사용하지 않을 경우, 동시에 여러 스레드가 발급 조건을 통과해 중복 발급이 발생하는 현상을 확인했다. 이번에는 ReentrantLock
을 이용해 발급 로직 전체에 락을 적용함으로써 단일 발급이 보장되는지를 검증한다.
🔍 실험 조건
- 총 1000개의 스레드가 동시에
/coupon/with-lock
요청을 수행 - 쿠폰 코드는
"TEST_CODE"
하나만 존재 - 모든 스레드는 서로 다른 ownerId를 가지고 발급 요청
🧪 테스트 코드
@Test
void 락_사용_시_단일_쿠폰만_발급_되어야_한다_withLock() throws InterruptedException {
int threadCount = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch ready = new CountDownLatch(threadCount);
CountDownLatch start = new CountDownLatch(1);
AtomicInteger successCount = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
ready.countDown();
try {
start.await();
couponService.issuedCouponWithLock(new CouponRequest("TEST_CODE", Thread.currentThread().getId()));
successCount.incrementAndGet();
System.out.println("접근 Thread : " + Thread.currentThread().getId() + " 쿠폰 발급 성공");
} catch (Exception e) {
System.out.println("접근 Thread : " + Thread.currentThread().getId() + " 쿠폰 발급 실패: " + e.getMessage());
}
});
}
ready.await();
start.countDown();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("총 발급된 쿠폰 개수: " + successCount.get());
assertEquals(1, successCount.get(), "락 사용 시 단일 발급만 되어야 합니다.");
}
✅ 테스트 결과
접근 Thread : 22 쿠폰 발급 성공
접근 Thread : 580 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
접근 Thread : 263 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
...(생략)
접근 Thread : 903 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
접근 Thread : 615 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
접근 Thread : 127 쿠폰 발급 실패: 이미 발급된 쿠폰입니다.
총 발급된 쿠폰 개수: 1
📌 결론
ReentrantLock
을 통해 발급 로직 전체를 감싸면, 동시에 여러 스레드가 접근해도 단 하나의 발급만 허용된다.- 다른 모든 스레드는 쿠폰이 이미 발급된 상태이기 때문에 예외 처리되어 실패하게 된다.
- 락이 동작하고 있다는 증거이며, 전역 락은 단일 자원에 대해 철저한 동시성 제어를 가능하게 해준다.
✅ 가설 3: 한 명의 사용자가 빠르게 두 번 클릭해도 쿠폰은 한 번만 발급된다
이번 가설은 실 서비스에서 자주 발생할 수 있는 "빠른 중복 클릭 이슈"를 검증하기 위한 것이다. 실제로 사용자가 버튼을 빠르게 두 번 클릭하면 같은 요청이 짧은 시간 내에 연달아 서버로 전송되는데, 이 상황에서도 동일한 쿠폰이 두 번 발급되지 않아야 한다는 것이 이번 테스트의 핵심이다.
🔍 실험 조건
- 단 2개의 스레드만 사용한다.
- 두 스레드는 동시에
issuedCouponWithLock()
메서드를 호출한다. - 같은 쿠폰 코드 (
TEST_CODE
)를 가지고 요청하며, 이 코드는 테스트 시작 전 미리 한 개 등록해둔 상태다. - 각 스레드는 고유한
ownerId
를 가지고 있지만, 실험 목적상 동시에 발급되는지를 보기 위함이다.
🧪 테스트 코드
@Test
void 빠른_중복_클릭_테스트_withLock() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
AtomicInteger successCount = new AtomicInteger();
for (int i = 0; i < 2; i++) {
executor.submit(() -> {
ready.countDown();
try {
start.await();
couponService.issuedCouponWithLock(new CouponRequest("TEST_CODE", Thread.currentThread().getId()));
successCount.incrementAndGet();
System.out.println("접근 Thread : " + Thread.currentThread().getId() + " 쿠폰발급 성공");
} catch (Exception e) {
System.out.println("접근 Thread : " + Thread.currentThread().getId() + " 쿠폰발급 실패 " + e.getMessage());
}
});
}
ready.await();
start.countDown();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("쿠폰 발급 개수: " + successCount.get());
assertEquals(1, successCount.get(), "빠른 중복 클릭 시에도 한 번만 발급되어야 합니다.");
}
✅ 테스트 결과
접근 Thread : 22 쿠폰발급 성공
접근 Thread : 23 쿠폰발급 실패 이미 발급된 쿠폰입니다.
쿠폰 발급 개수: 1
🧠 분석
ReentrantLock
을 통해 해당 코드 블록이 동시에 하나의 스레드만 진입할 수 있도록 제어되었기 때문에, 한 스레드가 발급을 완료한 순간issued = true
가 되어 두 번째 스레드는 조건문을 통과하지 못하고 예외를 던진다.- 따라서 실제로 쿠폰이 하나만 발급되는 현상이 발생했고, 이는 우리가 예상한 Race Condition 방지 효과와 정확히 일치한다.
✅ 결론
- 단시간 내 빠르게 연속 클릭이 발생해도,
ReentrantLock
으로 임계영역을 안전하게 보호하고 있다면 단일 쿠폰만 발급된다. - 실서비스에서 "따닥 이슈"를 방지하기 위해 락 처리는 매우 유효한 방어책이 될 수 있음을 확인했다.
✅ 가설 4: 서로 다른 쿠폰 코드를 동시에 발급 요청하면, 서로 다른 쿠폰은 독립적으로 발급 처리 되어야 한다.
이번 실험은 서로 다른 코드의 쿠폰이 동시에 요청될 때, 각 코드에 대해 하나씩만 발급이 되는지를 확인한다. issuedPerCouponWithLock()
메서드는 쿠폰 코드별로 ReentrantLock을 분리하여 관리하기 때문에, 각 코드별로 동시성을 독립적으로 제어할 수 있어야 한다.
🔍 실험 조건
- 총 1000개의 스레드가 동시에 실행됨
- "CODE_A", "CODE_B" 두 종류의 쿠폰만 존재
- 스레드가 번갈아 가며 각 쿠폰 코드를 요청
- 각 코드마다 오직 하나의 쿠폰만 발급되어야 한다
🧪 테스트 코드
@Test
void 서로_다른_코드_동시_발급_테스트() throws InterruptedException {
int threads = 1000;
ExecutorService exec = Executors.newFixedThreadPool(threads);
CountDownLatch ready = new CountDownLatch(threads);
CountDownLatch go = new CountDownLatch(1);
AtomicInteger successA = new AtomicInteger();
AtomicInteger successB = new AtomicInteger();
for (int i = 0; i < threads; i++) {
String code = (i % 2 == 0) ? "CODE_A" : "CODE_B";
exec.submit(() -> {
ready.countDown();
try {
go.await();
couponService.issuedPerCouponWithLock(new CouponRequest(code, Thread.currentThread().getId()));
if (code.equals("CODE_A")) successA.incrementAndGet();
else successB.incrementAndGet();
} catch (Exception ignored) {}
});
}
ready.await();
go.countDown();
exec.shutdown();
exec.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("CODE_A 쿠폰 발급개수: " + successA.get());
System.out.println("CODE_B 쿠폰 발급개수: " + successB.get());
assertEquals(1, successA.get(), "CODE_A는 하나만 발급");
assertEquals(1, successB.get(), "CODE_B는 하나만 발급");
}
✅ 테스트 결과
CODE_B succeeded by 437
CODE_B failed: 이미 발급된 쿠폰입니다.
CODE_A succeeded by 438
CODE_A failed: 이미 발급된 쿠폰입니다.
...(생략)
CODE_A 쿠폰 발급개수: 1
CODE_B 쿠폰 발급개수: 1
🧠 분석
- 서로 다른 쿠폰 코드에 대해 별도의 락을 부여함으로써 동시 요청 시에도 각 쿠폰은 정확히 한 번씩만 발급되었다.
- 전역 락을 사용하는 방식보다 병목을 줄이면서 높은 동시성 처리가 가능하다는 점에서 실용적인 방식이다.
📌 결론
쿠폰 코드별로 락을 분리하면, 서로 다른 쿠폰을 동시에 요청해도 충돌 없이 안전하게 처리 가능하다. 이는 고성능 시스템이나 실시간 서비스에 특히 유용한 방식이다.
✅ 가설 5: 락 타임아웃을 설정하면, 일정 시간 대기 후 락을 획득하지 못한 요청은 실패해야 한다
🧩 실험 목적
스레드 간 락 경쟁이 치열한 상황에서 tryLock(timeout)
을 사용하면, 일정 시간 안에 락을 획득하지 못한 스레드는 더 이상 기다리지 않고 적절히 종료될 수 있는지 확인한다.
💡 기대 결과
- 첫 번째 스레드는 락을 획득하고 발급 로직을 수행 (2초 대기 포함)
- 두 번째 스레드는 1초 동안 락을 기다리다가 실패하고 예외를 반환
- 테스트는 예외 발생 여부 및 예외 메시지를 통해 타임아웃 동작을 확인
🔐 쿠폰 서비스 코드 (with timeout)
public void issuedCouponWithTimeout(CouponRequest request) {
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
throw new RuntimeException("쿠폰 발급 대기 시간 초과");
}
// 락 획득 후 인위적 대기
Thread.sleep(2000);
// 기존 발급 로직
Coupon coupon = couponRepository.findByCode(request.code())
.orElseThrow(() -> new IllegalArgumentException("쿠폰 없음"));
if (coupon.isIssued()) throw new IllegalStateException("이미 발급됨");
couponRepository.save(coupon.issueTo(request.ownerId()));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("스레드 인터럽트", ie);
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
🧪 테스트 코드
@Test
void 락_타임아웃_검증() throws InterruptedException, ExecutionException {
ExecutorService exec = Executors.newFixedThreadPool(2);
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch go = new CountDownLatch(1);
Future<?> first = exec.submit(() -> {
ready.countDown();
try {
go.await();
service.issuedCouponWithTimeout(new CouponRequest("TIMEOUT", 1L));
} catch (Exception e) {
fail("첫 스레드 예외: " + e.getMessage());
}
});
Future<Exception> second = exec.submit(() -> {
ready.countDown();
try {
go.await();
service.issuedCouponWithTimeout(new CouponRequest("TIMEOUT", 2L));
return null;
} catch (Exception e) {
System.out.println("두 번째 타임아웃 예외: " + e.getMessage());
return e;
}
});
ready.await();
go.countDown();
exec.shutdown();
exec.awaitTermination(5, TimeUnit.SECONDS);
Exception ex = second.get();
assertNotNull(ex, "두 번째 스레드는 예외가 발생해야 합니다.");
assertTrue(ex.getMessage().contains("대기 시간 초과"), "타임아웃 예외 메시지 확인");
}
✅ 테스트 결과
두 번째 타임아웃 예외: 쿠폰 발급 대기 시간 초과
🧾 결론
ReentrantLock.tryLock(timeout)
을 이용하면, 무한 대기를 방지하고 일정 시간 안에 처리되지 못하는 요청을 적절히 실패시킬 수 있다.- 락 경쟁이 심한 환경에서 타임아웃 설정은 서비스 응답성과 안정성을 유지하는 데 큰 도움이 된다.
4️⃣ 마무리 및 정리
이번 실습을 통해 동시성 환경에서 안전한 쿠폰 발급 로직을 구현하기 위한 다양한 방법들을 실험하고, 각 방식의 안정성과 위험성을 비교해보았다.
✅ 실험을 통해 검증한 가설 요약
가설 번호 | 내용 | 결과 |
---|---|---|
가설 1 | 락 없이 동시에 발급 요청 시 중복 발급 가능성 존재 | ✅ 중복 발급 발생함 |
가설 2 | 전역 락 사용 시 단일 발급 보장 | ✅ 성공적으로 단일 발급 유지됨 |
가설 3 | 빠른 중복 클릭(따닥 클릭) 시에도 락으로 중복 방지 가능 | ✅ 중복 방지 성공 |
가설 4 | 서로 다른 코드 동시 발급 시 각각 독립적으로 처리되어야 한다 | ✅ 각 코드당 1개만 발급됨 |
가설 5 | 락 획득 실패 시 타임아웃 처리 필요 | ✅ 타임아웃으로 예외 처리됨 |
💡 실험을 통해 얻은 교훈
- 멀티스레드 환경에서 동시성 제어는 필수적이다.
- 단순한 조건 체크 (
if (coupon.isIssued())
) 만으로는 중복 발급을 막을 수 없다. - 락을 사용하지 않으면 수십, 수백 개의 쿠폰이 동시에 발급되는 일이 실제로 발생한다.
- 단순한 조건 체크 (
- ReentrantLock을 이용한 락 제어는 효과적인 동시성 제어 방법이다.
- 전역 락은 간단하지만 경쟁이 치열할수록 병목이 될 수 있고,
code
별로 락을 분리하는 방식은 병렬성을 높이면서도 안전성을 유지할 수 있는 절충안이 된다.
- 락은 무조건 좋은 것만은 아니다.
- 잘못된 사용은 데드락의 원인이 되고,
- 락을 무기한 기다리는 구조는 사용자 경험을 망칠 수 있다.
- 이를 방지하기 위해
tryLock(timeout)
과 같은 타임아웃 기법이 중요하다.
📌 정리 포인트
ConcurrentHashMap
을 활용해 락 객체를 쿠폰 코드 별로 분리하여 병렬 처리와 안정성의 균형을 맞출 수 있다.- 락을 사용할 땐
tryLock()
을 적절히 활용해 시스템 전체가 멈추는 상황을 방지해야 한다. - 테스트 코드를 통해 실전 환경에서 발생할 수 있는 race condition, 빠른 중복 클릭, 데드락 등을 미리 검증해보는 과정이 매우 중요하다.
🧠 결론
동시성 문제는 코드의 양보다 훨씬 복잡한 버그를 유발할 수 있는 영역이다.
이번 실험을 통해 동시성 제어의 필요성과 락의 사용 방법, 그리고 락을 안전하게 사용하는 실전 기법까지 함께 다뤘다.
단순히 "락을 쓰자"가 아닌, 어떻게, 어디서, 얼마나 정교하게 쓰는지가 시스템의 안정성과 직결된다.
'Java & Spring' 카테고리의 다른 글
[Java] ReentrantLock 정리 (feat. synchronized) (0) | 2025.06.27 |
---|---|
[Java] Map의 computeIfAbsent()과 computeIfPresent() 메서드 (0) | 2025.06.22 |
[Java & Spring] HashMap vs ConcurrentHashMap, 실습으로 알아보는 차이 (2) | 2025.06.13 |
[Java] Java의 ConcurrentHashMap 정리 (1) | 2025.06.12 |