프로젝트 동시성 문제 해결 - 데이터베이스 조건

동시성 문제란?

  • 동일한 하나의 데이터에 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();
    }
  • 또한 해당 코드는 다음과 같은 잠재적인 문제가 존재합니다.
  1. Thread A는 update()를 호출하고 validateDuplicateName() 검사를 통과합니다.
  2. Thread A가 트랜잭션을 완료하기 전에 Thread B는 동일한 스토어 이름으로 update()를 호출합니다.
  3. 스레드 A가 아직 트랜잭션을 커밋하지 않았기 때문에 스레드 B는 validateDuplicateName() 검사를 통과합니다.
  4. 이제 스레드 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 제약 조건을 설정하여 데이터의 중복을 방지하는 방법을 선택했습니다. 이 방법은 간단하지만 모든 동시성 문제에 적용할 수 있는 해결책은 아닙니다.
  • 앞으로는 데이터베이스 락이나 다른 동기화 메커니즘을 활용하여 동시성 문제를 처리하는 방법을 탐색할 예정입니다.
  • 다양한 동시성 문제 해결 방법