개발 낙서장

[TIL] 내일배움캠프 59일차 - 낙관적 락 동시성 테스트 본문

Java/Sparta

[TIL] 내일배움캠프 59일차 - 낙관적 락 동시성 테스트

권승준 2024. 3. 21. 23:46

 

 

오늘의 학습 키워드📚

낙관적 락

낙관적 락이란 간단히 말해서 데이터가 수정 중일 때 해당 데이터로의 다른 수정 접근을 막는 동시성 제어 방식이다.

동시성 제어는 서비스에서 반드시 필요한데 콘서트 티켓팅을 한다고 가정해보자. 총 100자리가 있고 순간 동시 접속자가 1000명일 경우 같은 자리에 예매하는 경우가 반드시 생길 것이다.
이 때 동시성 제어를 하지 않게 될 경우 같은 자리에 여러 사람이 예매가 되는 대혼란 사태가 발생할 것이다.

이 외에도 정말 다양한 곳에서 동시성 제어를 필요로 하는데 가장 많이 사용되는 부분이 위의 예매 시스템이나 재고 관리 같은 Count 시스템 등에 활용된다.

동시성 제어 테스트

먼저 스프링 부트 환경에서 낙관적 락을 적용하는 방법은 매우 쉽다.

    @Version
    private int version;

Entity 클래스에 version 필드를 생성하면 된다.
version은 int, Integer, long, Long, short, Short, java.sql.TimeStamp 중 하나여야 한다.

@Version 어노테이션을 통해 추가된 version은 자동으로 버전 관리가 되어 수정 요청이 들어갔을 때 버전이 증가된다.
따라서 동시에 요청이 들어올 경우 version 값을 체크해 요청을 막는 방식이다.

이후 테스트 코드를 구성해야 하는데 이 부분에서 정말정말정말 많이 헤맸다.
현재 팀 프로젝트를 진행하는데 Column 테이블에서 컬럼의 순서 변경 동시성 제어 테스트를 진행했다.

    @Test
    @DisplayName("컬럼 수정 낙관적 락 동시성 테스트")
    void 컬럼_수정_낙관적_동시성_테스트() {
        // given
        AtomicInteger optimisticLockFailures = new AtomicInteger(0);

        // when
        IntStream.range(0, 100).parallel().forEach(i -> {
            try {
                ModifyColumnSequenceRequest request = ModifyColumnSequenceRequest.builder()
                    .prevSequence(new Random().nextLong(5000L))
                    .nextSequence(new Random().nextLong(5001L, 10000L)).build();

                columnService.modifyColumnSequence(100L, 4L, request);
            } catch (ObjectOptimisticLockingFailureException e) {
                optimisticLockFailures.incrementAndGet();
            }
        });

        // then
        System.out.println(columns.getSequence());
        System.out.println("낙관적 락 실패 횟수 : " + optimisticLockFailures.get());
    }

Mock 객체를 통해 테스트를 진행하고자 했고 가짜 객체를 만들어 실행을 했는데 실행 오류는 발생하지 않았지만 낙관적 락 실패 횟수가 0이었다.
사실상 락이 걸리지 않았다는 것이다.

이후 여러 방법을 생각하다가 통합 테스트 환경으로 바꾸어 테스트했다.
기존에는 테스트 클래스에 @ExtendWith(MockitoExtention.class) 어노테이션을 달아 Mock 테스트를 했었는데 @SpringBootTest 어노테이션으로 변경해 실제 Repository에서 테스트를 진행했다.

당연히 실제 데이터로 테스트했기에 락에 걸려 실패한 횟수가 84, 86 이렇게 나오고 version도 잘 올라갔지만 아무리 더미데이터라도 실제 데이터로 테스트하는 것은 옳지 않다고 생각했다.

그래서 H2 임베디드 DB를 활용해 테스트 DB 환경을 구성하여 테스트를 진행했다.

# applictaion-test.properties
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=test
spring.datasource.password=1234
spring.h2.console.enabled=true

jwt.secret.key=???

테스트용 properties 파일을 만들어 test/resources 디렉토리에 넣어주었다.

이후 테스트 클래스에 어노테이션을 넣어줘야 하는데
테스트 properties를 설정해주는 @TestPropertySource 와 H2 DB 환경을 설정해주는 @AutoConfigureTestDabase 어노테이션을 추가했다.

@SpringBootTest
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY)
@TestPropertySource("classpath:application-test.properties") //test용 properties 파일 설정
public class ColumnOptimisticLockTest {

    @Autowired
    UserRepository userRepository;
    @Autowired
    BoardRepository boardRepository;
    @Autowired
    ColumnRepository columnRepository;
    @Autowired
    BoardUserJpaRepository boardUserJpaRepository;

    @Autowired
    ColumnService columnService;

    @MockBean
    private BoardUserValidateAspect boardUserValidateAspect;

    private User user;
    private Board board;
    private Columns columns;

    @BeforeEach
    void setUp() {
        user = User.builder().email("abc123@naver.com").username("abc123").password("abc!2345")
            .profile("").build();
        user = userRepository.save(user);

        board = Board.builder().boardName("보드").color(BoardColorEnum.BLACK).description("")
            .user(user).build();
        board = boardRepository.save(board);

        BoardUser boardUser = new BoardUser(board, user, BoardRoleEnum.OWNER);
        boardUserJpaRepository.save(boardUser);

        columns = Columns.builder().columnName("컬럼").sequence(1000L).board(board).build();
        columns = columnRepository.save(columns);

        doNothing().when(boardUserValidateAspect).validateBoardUser(any(JoinPoint.class));
    }

    @Test
    @DisplayName("컬럼 수정 낙관적 락 동시성 테스트")
    void 컬럼_수정_낙관적_동시성_테스트() {
        // given
        AtomicInteger optimisticLockFailures = new AtomicInteger(0);

        // when
        IntStream.range(0, 100).parallel().forEach(i -> {
            try {
                ModifyColumnSequenceRequest request = ModifyColumnSequenceRequest.builder()
                    .prevSequence(new Random().nextLong(5000L))
                    .nextSequence(new Random().nextLong(5001L, 10000L)).build();

                columnService.modifyColumnSequence(100L, 1L, request);
            } catch (ObjectOptimisticLockingFailureException e) {
                optimisticLockFailures.incrementAndGet();
            }
        });

        // then
        System.out.println("낙관적 락 실패 횟수 : " + optimisticLockFailures.get());
    }
}

테스트용 데이터를 추가해 100번 병렬 처리를 통해 동시성 제어가 잘 걸리는지 테스트했다.

100번의 시도 중 66번의 수정 시도가 동시성 제어에 걸려 실패하게 됐다.


오늘의 회고💬

동시성 제어 부분에서 어떻게 진행해야 될지 갈피를 못잡아 정말 많이 헤맸던 것 같다.
그래도 결국 해결을 해서 다행이다.

 

내일의 계획📜

동시성 제어까지 얼추 완료 됐으니 테스트 코드랑 쿼리 최적화를 마무리하고 Redis를 이용한 분산락에 대해 좀 더 알아봐야겠다.

Comments