동시성 문제란?
- 동일한 하나의 데이터에 2 이상의 스레드에서 데이터를 동시에 제어할 때 나타는 문제로,
- 하나의 스레드가 데이터를 수정 중일때, 다른 스레드에서 수정 전의 데이터를 조회해 로직을 처리함으로써 데이터의 정합성이 깨지는 문제를 말합니다.
- 저희 프로젝트에서도 동시성 문제가 일어날 가능성에 대해 알아보기로 하였습니다.
동시성 문제 상황
- 스레드1가 가게의 좋아요수에 접근해서 그 값을 얻어옵니다.
- 스레드2가 가게의 좋아요수에 접근해서 그 값을 얻어옵니다.
- 스레드1은 해당 가게의 좋아요수에 +1을 합니다.
- 스레드2은 해당 가게의 좋아요수에 +1을 합니다.
- 이 결과 해당 가게의 좋아요수는
+1만 되었습니다.
- 이렇게 여러 스레드가 하나의 데이터에 동시에 접근해서 예상치 못한 결과를 낳는 문제를 "동시성 문제"라고 합니다
문제 코드
- 다음과 같은 코드에선 좋아요 사례와 같은 실제 데이터가 정확하지 않은 문제는 일어나지 않으나 의도치않은 업데이트는 생길 수 있습니다.
- (가령 업데이트를 했는데도 동시요청때문에 내가 한 업데이트가 반영되지않고 다른 사용자의 업데이트가 반영된 경우가 있습니다.)
@Transactional
public StoreSaveUpdateResponse update(final Long storeId, final StoreSaveUpdateRequest request) {
validateKeywordList(request.getKeywordIdList());
Store store = storeRepository.findActiveStoreById(storeId).orElseThrow(() -> new StoreException(NO_SUCH_STORE));
//TODO: biz 유효성 검사 필요
validateDuplicateName(store.getName(), request.getStoreName());
store.update(request.getStoreName(), request.getAddress(), request.getPhone(), request.getBusinessName(), request.getBusinessNumber(), request.getBusinessStartDate());
Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(()->new CategoryException(NO_SUCH_CATEGORY));
return StoreSaveUpdateResponse.builder()
.address(store.getAddress())
.storeId(store.getStoreId())
.businessName(store.getBusinessName())
.businessNumber(store.getBusinessNumber())
.categoryName(category.getName())
.keywordList(keywordService.getKeywordNameList(request.getKeywordIdList()))
.phone(store.getPhone())
.businessStartDate(store.getBusinessStartDate())
.storeName(store.getName()).build();
}
- 또한 해당 코드는 다음과 같은 잠재적인 문제가 존재합니다.
- Thread A는 update()를 호출하고 validateDuplicateName() 검사를 통과합니다.
- Thread A가 트랜잭션을 완료하기 전에 Thread B는 동일한 스토어 이름으로 update()를 호출합니다.
- 스레드 A가 아직 트랜잭션을 커밋하지 않았기 때문에 스레드 B는 validateDuplicateName() 검사를 통과합니다.
- 이제 스레드 A와 스레드 B 모두 동일한 이름으로 상점을 업데이트할 수 있으므로 비즈니스 규칙을 위반할 수 있습니다.
테스트
@Test
public void testUpdateConcurrency2() throws Exception {
// 서로 다른 두 개의 스레드를 생성합니다.
Thread threadA = new Thread(() -> {
// Thread A에서 update() 메소드를 호출합니다.
storeService.update(STORE_ID, STORE_UPDATE_REQUEST);
});
Thread threadB = new Thread(() -> {
// Thread B에서 update() 메소드를 호출합니다.
storeService.update(STORE_ID, STORE_UPDATE_REQUEST);
});
// 두 스레드를 거의 동시에 시작합니다.
threadA.start();
threadB.start();
// 두 스레드가 모두 종료될 때까지 대기합니다.
threadA.join();
assertThrows(StoreException.class, () -> threadB.join());
// 상점의 최종 상태를 검사합니다.
Store store = storeRepository.findById(STORE_ID).get();
Assertions.assertEquals(store.getName(), STORE_UPDATE_REQUEST.getStoreName());
}
- 문제 코드가 실제로 문제를 일으키는지에 대해 테스트코드를 작성하여 확인해보았습니다.
- 테스트 결과는 실패하였습니다. 예외가 발생하지않았습니다.
- 동일한 이름이 존재하면 안된다는 규칙을 위반한 상태로 데이터베이스에 저장됩니다 (문제 발생)
- 이 문제를 해결하기 위해
lock을 사용할 수 있습니다.
- 하지만, 사장님의 계정을 두 사용자가 공유하고, 동시에 업데이트할 가능성이 매우 적기도하고
- DB에 unique제약조건을 거는것으로 문제를 해결하기로 결정하였습니다.
결론
- 동시성 문제는 여러 스레드나 프로세스가 동시에 데이터에 접근하려고 할 때 발생하는 문제로, 이는 실제 서비스에서 큰 장애나 데이터의 부정확성을 초래할 수 있습니다.
- 저희 프로젝트에서 발견된 동시성 문제는 특정 상점의 정보를 업데이트하는 과정에서 두 개 이상의 요청이 거의 동시에 발생했을 때 나타났습니다.
- 이 문제에 대한 현재의 해결책으로는 DB의 unique 제약 조건을 설정하여 데이터의 중복을 방지하는 방법을 선택했습니다. 이 방법은 간단하지만 모든 동시성 문제에 적용할 수 있는 해결책은 아닙니다.
- 앞으로는 데이터베이스 락이나 다른 동기화 메커니즘을 활용하여 동시성 문제를 처리하는 방법을 탐색할 예정입니다.
- 다양한 동시성 문제 해결 방법