[ 🌱 synchronized ]
자바의 synchronized는 동시성 상황에서 정합성을 지키기 위해 사용됩니다.
하지만 트랜잭션 환경에서 같이 쓰인다면 정합성을 보장할 수 없습니다.
먼저 동시성 문제의 간단한 예시를 알아보겠습니다.
[ 🌱 Count 도메인 ]
수를 저장하는 Count 도메인 입니다.
현재 자신의 수에서 1을 더하는 도메인 로직이 있습니다.
@Getter
@Entity
public class Count {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private int count;
protected Count() {
}
public Count(int count) {
this.count = count;
}
public void countUp() {
this.count += 1;
}
}
[ 🌱 Count 비지니스 로직 ]
Count 도메인을 찾아와 변경 감지를 통해 자신의 수를 + 1 하는 로직입니다.
변경 감지를 사용하기 위해 트랜잭션이 걸려 있습니다.
@Transactional
@RequiredArgsConstructor
@Service
public class CountService {
private final CountRepository countRepository;
public void countUp(long countId) {
Count count = countRepository.findById(countId).orElseThrow();
count.countUp();
}
}
[ 🌱 테스트 ]
하나의 스레드로 100번 카운트를 증가 시키면 총 카운트 수는 100이 됩니다.
@SpringBootTest
class CountServiceTest {
@Autowired
private CountService countService;
@Autowired
private CountRepository countRepository;
@DisplayName("카운트를 100번 증가 시키면 총 카운트 수는 100이다.")
@Test
void countUp() throws InterruptedException {
int taskCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(1);
CountDownLatch countDownLatch = new CountDownLatch(taskCount);
Count initCount = countRepository.save(new Count(0));
for (int i = 0; i < taskCount; i++) {
executorService.submit(() -> {
try {
countService.countUp(initCount.getId());
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
Count findCount = countRepository.findById(initCount.getId()).orElseThrow();
assertThat(findCount.getCount()).isEqualTo(taskCount);
}
}

현재 상황에서 스레드 개수를 10개로 늘리게 된다면 어떻게 될까요?

동시성 문제로 인해 카운트가 재대로 증가 하지 않습니다.
자바는 이런 동시성 문제에 대한 해결책으로 synchronized를 사용할 수 있습니다.
[ 🌱 서비스 Synchronized ]
서비스 로직에 synchronized 적용 하였습니다.
하지만 테스트 시 동시성 문제는 해결 되지 않습니다.
이유는 트랜잭션과 synchronized 정합성 보장 구간이 다르기 때문입니다.
@Transactional
@RequiredArgsConstructor
@Service
public class CountService {
private final CountRepository countRepository;
public synchronized void countUp(long countId) {
Count count = countRepository.findById(countId).orElseThrow();
count.countUp();
}
}

[ 🌱 원인 ]
트랜잭션의 순서를 그림으로 나타낸 것입니다.
자바 Synchronized는 객체 내부의 락을 사용 합니다.
비니지스 로직 시작할 때 획득 후 끝나고 락을 반납 합니다. 여기서 문제는 락을 해제하는 타이밍 입니다.
Count 업데이트는 JPA 변경감지를 통해서 일어나는데 커밋 시점에는 이미 락이 해제 되어있습니다.
그레서 업데이트를 하기 전에 이미 다른 트랜잭션이 실행 될 수 있기 때문에 문제가 발생합니다.

[ 🌱 컨트롤러 Synchronized ]
그러면 Synchronized를 Controller에 사용하면 어떻게 될까요?
트랜잭션이 시작하기 전 락 획득 후 끝나고 반납하기 때문에 정합성이 보장 됩니다.
하지만 멀티 스레드 환경에서 단일 스레드로 사용 하는것이기 때문에 성능이 좋지 않습니다.
@RequiredArgsConstructor
@Controller
public class CountController {
private final CountService countService;
public synchronized void countUp(@RequestParam("countId") long countId){
countService.countUp(countId);
}
}

'Spring' 카테고리의 다른 글
| @Async 메일 전송 개선하기 (0) | 2025.09.06 |
|---|---|
| [ Spring ] Swagger 415 Unsupported Media Type (0) | 2025.08.04 |
| [ Spring ] Enum Type 검증 (0) | 2025.02.25 |
| [Spring] @ExceptionHandler API 에러 핸들링 (3) | 2024.09.06 |
| [Spring] 필터, 인터셉터 (Interceptor) (0) | 2024.09.05 |