현재 토이프로젝트로 개발중인 데브맨토 에 '멘토신청' 이라고 하는 기능이 있습니다.
말 그대로 멘토를 받고 싶은 유저가 멘토가 되어주었으면 하는 유저에게 멘토가 되어 달라고 하는 기능입니다.
이때, 인기있는 유저의 경우 여러명의 멘토 요청이 올 수 있기 때문에 멘토 입장에서는 일부의 유저만 신청을 원할 수 있다고 판단했습니다. 그래서 유저는 멘토가 제한해 놓은 인원 수 까지만 멘토 신청을 할 수 있습니다. 가령, 100 명의 인원만 받겠다고 설정하면, 최대 100 명의 인원만 신청할 수 있는거죠. 1000명의 인원이 동시에 신청하면 100명을 제외한 900명의 인원은 신청 시 "제한된 인원을 초과"했다고 하는 메시지를 받게 되고, 더 이상 신청할 수 없습니다.
이때 현재 신청 인원 수 나 최대 멘토 인원 수 등은 테이블로 관리하고 있습니다.
@Transactional
public void request(AuthenticatedUser authUser, MentorRequestDto mentorRequestDto) {
MentorInfo mentorInfo = findMentorInfo(mentorRequestDto);
MentorRequest mentorRequest = MentorRequest.create(authUser, mentorRequestDto);
saveMentorRequest(mentorRequest);
mentorInfo.increaseMentee();
}
@Table(name = "MENTOR_INFO")
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MentorInfo extends BaseEntity {
// .. 생략
@Column(name = "max_mentees")
private long maxMentees;
@Column(name = "current_mentees")
private long currentMentees;
// .. 생략
public void increaseMentee() {
if (currentMentees >= maxMentees) {
throw new OverflowMentorRequestException("멘토 요청이 마감되었습니다.");
}
this.currentMentees = this.currentMentees + 1;
}
}
📝 내용 진행 상 흐름에 방해될 것 같아 increateMentee() 에서 사용하지 않는 내부 필드 및 다른 메서드 들은 따로 작성하지 않았습니다. 추가적인 필드와 메서드를 보고 싶다면 github 리포지토리를 참고하시기 바랍니다.
간단하게 서비스 로직을 설명하자면 아래와 같습니다.
findMentorInfo(mentorRequestDto);saveMentorRequest(mentorRequest) mentorInfo.increateMentee()* (참고) 1번부터 4번은 하나의 트랜잭션으로 실행됩니다.
테스트는 JMeter를 이용하여 테스트를 해보았습니다.
먼저 JMeter 의 ThreadGroup 의 Thread 수는 120 으로 설정하였습니다.


실제로 111명의 유저가 성공 응답을 받고 나머지 9명의 유저는 실패응답을 받았습니다.
멘토는 100명의 유저를 받고 싶었지만, 111명의 유저를 허용하고 말았습니다.


이미 100명의 인원 모두가 신청을 했음에도 불구하고 7명을 더 신청 받을 수 있는 형태입니다. 이는 의도하지 않은 결과입니다.
위의 로직과 조금은 다르지만, 결국 현재 멘티 인원수가 최대 멘티 인원 수를 초과 할 경우 에외를 던지도록 했습니다.
정말 위의 로직대로 잘 돌아가는 지 테스트 코드를 작성해보았습니다. mvcMock 을 활용하여 endpoint 를 직접 100번 호출했을 경우 멘티 인원수가 그에 맞게 100번 증가 했는 지 그리고 멘토 요청이 100개가 DB에 쌓여있는 지를 확인하는 테스트 코드 입니다.
결과는 어떻게 되었을까요? 테스트에 통과한 것을 알 수 있습니다. 
기본적으로 Spring의 MockMvc는 기본적으로 테스트 스레드에서 동작합니다. 즉, 다른 스레드에서 동작하도록 명시적으로 설정하지 않는 한 싱글 스레드에서 작동하게 됩니다. 스레드가 할당 된 일을 끝내고 난 후 그 다음 있는 요청을 진행하는 식입니다.
하지만, JMeter는 실제 HTTP 요청을 보내는 것처럼 다중 스레드로 요청을 보내고 병렬로 처리합니다. 따라서 여러 스레드가 동시에 서버에 요청을 보내게 됩니다.
즉, mockMvc 를 이용한 테스트는 100건을 동시에 요청하는 상황이 아닌, 100건의 요청을 연 이어 하는 테스트 상황이였던 것입니다.
JMeter 와 같이 실제 유저가 동시에 요청하는 것처럼 여러개의 스레드가 멘토요청을 할 수 있도록 테스트 코드를 수정하였습니다.
ExecutorService 를 이용하여 스레드 풀을 만들어 여러개의 스레드가 각 멘토 요청을 할 수 있도록 하였습니다.
또한 이번에는 문제가 되는 부분을 집중적으로 테스트 하기위해 이번엔 mockMvc 를 사용한 end-to-end 테스트가 아닌 memtorRequestService 의 request 메서드를 직접 호출 하도록 변경했습니다.
ExecutorService란? 병렬 작업 시 여러 개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA 라이브러리이다.

과연 테스트 결과는??

JMeter 테스트 결과와 마찬가지로 현재 멘티수가 제대로 반영되지 않은 것을 알 수 있습니다.
어떤 일이 일어났는 지 확인하기 위해 현재 멘티 수를 증가시키는 코드 내부에 어떤 스레드가 요청을 처리하고 있으며, 그 때의 현재 멘티 수를 출력하도록 로그를 남겨보았습니다. (만약 로그 관련 아무런 설정을 하지 않았다면, 기본적으로 현재 실행되고 있는 스레드가 출력되기 때문에 굳이 적지 않아도 상관은 없습니다.)

결과는 ??

pool-2-thread-9 가 읽은 현재 멘티 수는 0 입니다. 하지만 그 외 다른 스레드가 읽은 멘티 수 역시 0인 것을 알 수 있습니다.
위의 결과에서 보면, 하나의 스레드가 요청을 처리하여 값을 반영하기도 전에(즉, 현재 멘티 수를 가져와 값을 하나 증가시키는 생위), 또 다른 스레드가 현재 멘티 수 값에 접근하는 것을 알 수 있습니다.
여러 스레드가 공동의 자원을 접근하여 수정할 때 문제가 생기는 것이므로, 여러 스레드가 동시에 접근할 수 없도록 하면 됩니다. 즉, 100개의 스레드가 동시에 increateMentee() 에 접근하려고 하려는 것을 막고, 하나의 스레드만 접근할 수 있도록 해주면 됩니다.
위 와 같이 두 개 이상의 스레드가 하나의 공유된 자원에 접근하여 데이터를 조작할 때 문제가 되는 상황을 경쟁 조건(Race Condition) 이라고 부릅니다. 즉, Race Condition 이 일어날 수 있는 상황에 대해 우리는 대비를 해야합니다.
하지만 공유되는 자원에 여러개의 스레드가 동시에 접근할 수 없도록 하기위한 방법은 여러 개가 존재합니다. 크게 애플리케이션에서 해결하는 것과 데이터베이스에서 해결하는 것으로 접근해보았습니다.
~ 2편에서 계속