Vegan-Life / VeganLife-Backend

채식주의자를 위한 식단 및 영양관리 앱 BE
2 stars 0 forks source link

레시피 좋아요/좋아요 취소 API 구현 #250

Closed soun997 closed 10 months ago

soun997 commented 10 months ago

이슈 번호 (#247)

요약

동시성 이슈

현재는 좋아요를 추가하기 전에 좋아요를 눌렀는지 먼저 확인하는 로직을 거침으로써 연속적으로 좋아요 요청이 발생하더라도 이를 처리할 수 있도록 하였습니다.

그러나 트랜잭션 격리 수준을 공부하면서 해당 로직이 제대로 동작하지 않을 것이라는 생각이 들었습니다.

image

위의 사진과 같이 간발의 차이로 TA가 좋아요를 save 하기 전에 TB가 조회 로직을 수행한다면 TB 또한 좋아요가 되지 않은 것으로 간주하고 add 로직을 수행하게 될 것 입니다.

테스트

        Runnable userA =
                () -> {
                    recipeLikeService.add(recipe.getId(), member.getId());
                };
        ...
        assertThatThrownBy(
                        () -> {
                            try {
                                Future<?> futureA = executorService.submit(threadA);
                                Future<?> futureB = executorService.submit(threadB);
                                futureA.get(); // 스레드의 작업이 끝날 때까지 메인 스레드는 대기, 스레드의 작업이 끝나면 결과 반환
                                futureB.get();
                            } catch (Exception e) {
                                e.printStackTrace();
                                throw e.getCause();
                            }
                        })
                .as("좋아요 요청을 동시에 처리하는 경우, 동시성 문제가 발생한다. (Unique index or primary key violation)")
                .isInstanceOf(DataIntegrityViolationException.class);

위와 같이 add(좋아요 여부 확인 + 좋아요 save)를 수행하는 스레드를 두 개 만들어 동시에 실행함으로써 테스트를 진행했습니다.

결과

image

동시성 이슈가 발생하여 테스트가 통과한 것을 확인할 수 있었습니다.

사실 격리 수준 문제가 아니다!

처음에는 격리 수준에 대한 문제로 보고 @Transactional(isolation = Isolation.SERIALIZABLE)과 같이 최고 격리 수준을 적용하면 트랜잭션이 순차적으로 실행되지 않을까? 라고 생각했습니다. (이해가 부족했던...^^)

결과적으로는 똑같이 동시성 문제가 발생했습니다. SERIALIZABLE 격리 수준 또한 읽기 잠금을 사용하는 것이기 때문에 TB가 TA가 add를 완료하기 전 레코드를 읽어들이는 것을 막을 수는 없었습니다.

아무튼 (좋아요를 눌렀는지 확인하는 로직 + 가능하다면 좋아요를 save하는 로직)을 무조건 한 덩어리로 실행되도록 조치해야 했습니다.

synchronized 사용

add 메서드 자체에 synchronized 키워드를 적용하여 여러 스레드가 동시에 접근하는 것을 막는 방법입니다.

해당 키워드를 사용하면 원하는 목적을 달성할 수는 있었습니다. 그러나 해당 메서드에 한해서는 싱글 스레드로 동작하는 것이기 때문에 매우 저조한 퍼포먼스를 보여주었습니다.

10000개의 스레드 요청을 처리할 때의 퍼포먼스를 비교해보았습니다.

synchronized 키워드 X

image

synchronized 키워드 O

image

약 2배 이상의 성능 차이가 나는 것을 확인할 수 있었습니다.

낙관적 락? 비관적 락?

synchronized 키워드를 사용하여 원하는 목적을 달성할 수 있었지만, 거즘 싱글 스레드로 작동한다는 단점이 있었습니다. 이러한 현상을 해결하기 위해서 Locking 기법을 사용할 수 있다고 하는데, 다음에는 이에 대해 조사하여 공유하도록 하겠습니다!