티스토리 뷰

반응형

오늘은 트래픽을 더 감당하기 위한 서버의 구성을 위해 가장 심플하게 자바로 생성할 수 있도록 코드로 작성해보려고 합니다. 트래픽이라고 하는 용어는 한 사람이 웹 페이지에 들어갔을 때 보여지기 위해 서버로 요청하는 횟수를 의미하고, 웹 페이지에 연결되어 있는 API의 갯수만큼 트래픽 수가 쌓이게 됩니다. 

예를 들어, 우리 블로그 페이지에서 보았을 때 이 페이지에서 본문의 내용과, 댓글, 태그나 최근에 올라온 글들을 보여주기 위해서는 트래픽 수가 아래처럼 되겠죠.

  • 본문의 내용을 보여주기 위한 API 호출 - 트래픽 1건
  • 댓글 목록을 위한 API 호출 - 트래픽 1건
  • 태그 목록을 위한 API 호출 - 트래픽 1건
  • 최근에 올라온 글 목록을 위한 API 호출 - 트래픽 1건

 

그래서 위와 같이 화면 구성을 하여도 총 4건의 트래픽이 1번의 접속으로 이루어지네요. 그러면 이런 서버 트래픽 부하 테스트를 위해서 구축을 해보고 코드를 작성해보려 합니다. 제가 진행하고자 하는 부하 테스트는 위의 4가지 그래프 중 피크 테스트입니다. 그리고 일부러 쿼리의 지연을 발생시키고자 서비스에 들어 있는 쿼리는 리소스를 다른 일반 쿼리보다 더 사용하는 내용을 의도적으로 넣은 것을 참고해주세요.

 


 

1. JUnit Test 작성하기

가장 쉽게 개발할 수 있도록 ChatGPT에게 물어보았습니다. 

MockMvc 는 우리가 컨트롤러에 구성한 URI로 연결해서 실제로 데이터가 유입될 수 있게 테스트를 할 수 있는 방법입니다. 가장 기초적으로 위에서 ChatGPT가 알려준 방법으로는 URL만 넘겨서 테스트를 하는 코드를 알려주었지만, RequestBody 이나 인증 정보들도 MockMvc에 넣을 수 있습니다. 

예를 들어, 아래와 같은 Controller가 존재한다고 가정해보겠습니다. 

@RestController
@RequestMapping(value = "/api/main", produces = "application/hal+json;charset=utf8")
@CrossOrigin("*")
@RequiredArgsConstructor
public class MainRestController {

    private UserService userService; 
    
    @GetMapping("/user-data/{userId}")
    public ResponseEntity<UserGetDTO> getUserDetails(@PathVariable Long userId) {
        return userService.getUserDetailDTOById(userId);
    }
}

위 메인 컨트롤러에서는 사용자 정보를 조회해올 수 있는 API가 표시되어 있습니다. 여기서 URI는 "http://localhost:8080/api/main/user-data/" 이고 URL은 "/api/main/user-data/{userId}" 입니다.

그러면 MockMvc의 엔드포인트는 다음과 같습니다

  • EndPoint : "/api/main/user-data/1"

 

그러면 엔드포인트에 대한 1만건의 트래픽 부하 테스트는 아래처럼 코드를 작성할 수 있습니다. 

private String createEndPoint(boolean host, Integer userId) {
	String endPoint = "/api/main/user-data/";
	if(host) {
    	return HOST + endPoint;
    }
    return endPoint;
}

@Test
public void testEndpoint() throws Exception {
    int loopCount = 10000;
    int assertThat = 0;
    for (int i = 0; i < loopCount; i++) {
//            Thread.sleep(100l);

        final ResultActions resultActions = mockMvc.perform(get(createEndPoint(true, Long.valueOf(i))))
            .andExpect(status().isOk());

        System.out.println("resultActions = " + resultActions);
        if(resultActions != null) {
            assertThat++;
        }
    }
    assertEquals(loopCount, assertThat);
}

 

2. 멀티 쓰레드 방식의 JUnit Test

하지만 이는 단일 쓰레드에 대한 부하 테스트 방법이므로 또 한번 ChatGPT에게 멀티 쓰레드인 경우에 대해서도 물어보았습니다.

 

그럼 부하 테스트에서 단일 쓰레드보다 다중 쓰레드를 선호하는 이유는 무엇일까요? 제가 생각해본 이유는 아래 2가지 입니다.

  1. 쓰레드1 = 사용자 1명이라고 생각한다면 서비스를 사용자 1명이 사용하는 경우는 드물기 때문에 여러 사용자가 사용한다는 기준으로 테스트를 하는 것이 바람직하다. 
  2. 쓰레드1, 쓰레드2가 테스트를 진행하다가 쓰레드1이 치명적인 오류가 생겨 동작할 수 없는 경우 쓰레드2에는 영향이 없기 때문에 정상적으로 테스트를 할 수 있다. (물론 assert는 실패가 나올 것입니다.)

 

위와 같이 테스트 코드를 작성하고 ChatGPT가 코드 하단에 설명해준 부분과 같이 ExecutorService 에서 쓰레드풀을 가져와 쓰레드 개수를 5로 지정하여 테스트를 진행해봤습니다. 나머지 코드는 엔드포인트를 가져오는 코드와 같기 때문에 사실 차이는 없고, foreach로 한 번 더 감싼 것만 차이가 있습니다. 

@Test
public void multiThreadTestEndPoint() throws Exception {
    int numThreads = 5;
    int numRequestsPerThread = 1000;

    ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
    for (int i = 0; i < numThreads; i++) {
        executorService.execute(() -> {
            for (int j = 0; j < numRequestsPerThread; j++) {
                try {
                    String url = createEndPoint(false, j);
                    final ResultActions resultActions = mockMvc.perform(get(url))
                        .andExpect(status().isOk());
                    final MvcResult mvcResult = resultActions.andReturn();
                    System.out.println("mvcResult = " + mvcResult.getResponse());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
    executorService.shutdown();
    executorService.awaitTermination(10, TimeUnit.SECONDS);
}

저 같은 경우는 위의 코드 처럼 하나의 쓰레드마다 천번의 요청을 전송하는 코드로 작성하여 테스트를 진행하였고, 쓰레드 개수는 5개로 조정하였습니다. 그래서 5 * 1000 번의 요청을 보내는 총 5천번의 요청을 전송하는 구조입니다. 

 

3. RestTemplate 을 활용하여 단방향 요청 전송

이제 어느정도 정상적으로 작동하는 테스트 코드 작성이 완료되었다면, 실제 운영중인 서비스에 요청을 보내보는 작업이 남았습니다. RestTemplate 은 자바에서 지원해주는 좋은 네트워크 서비스이며 라이브러리이고, 쉽게 사용가능합니다. 

@Autowired
private RestTemplate restTemplate;

@Test
public void testApiEndPoint() throws Exception {
    int loopCount = 10000;
    int assertThat = 0;
    for(int i =0; i< loopCount; i++) {
        String url = createEndPoint(true, i);
        ResponseEntity<String> exchange =
            restTemplate.exchange(url, HttpMethod.GET, null, String.class);

        if(StringUtils.isNotBlank(exchange.getBody())) {
            assertThat++;
        }
    }

    assertEquals(loopCount, assertThat);
}

 

이제 HOST 부분에 true 값을 넣어 원격 도메인 서버의 URL을 넣고 실행을 한번 해보겠습니다. 

RDS 지표에 나온 요청 쿼리별 부하 테스트 결과값

테스트 코드는 정상적으로 작동한 것처럼 보입니다. 요청 수가 순간적으로 많이 늘었고, 테스트 서버에다가 구성을 하고 실행을 한 것이기 때문에 실제 트래픽이 거의 잡히지 않다가 순간적으로 수치가 늘어난 것으로 보입니다.

 

어제 진행한 부하 테스트의 서버 상태 결과값

서버로 트래픽을 요청해서 부하 테스트를 진행해본 결과로는 위처럼 CPU 사용률이 4.66%까지 증가하고 메모리 사용량이 37%까지 증가하는 수치를 보여주고 있었습니다. 단일 쓰레드의 경우 위 처럼 발생하는 것을 보니 다중 쓰레드이면 분명 더 높게 나타날 것이고, 이는 서버의 스펙을 더 키워야 하는 'Scale Out' 또는 서버의 대수를 늘리는 'Scale Up'을 해야 한다는 말이 되지 않을까 합니다.

 

이로써 알게 된 결과는 아래와 같습니다.

 

결과

  • 테스트 코드를 작성해서 내부적으로 작동하는 것도 데이터베이스의 부하량을 확인할 수 있다. 심지어 데이터베이스에서 심하게 리소스를 잡아먹는 쿼리를 테스트 코드를 통해 파악하기 용이하다.
  • 데이터베이스에 지속적으로 부하량을 가해서 문제가 생길것을 대비해 데이터베이스의 스펙을 올리거나 다중화를 할 전략을 짤 수 있다.
  • 서버도 마찬가지로 특정 트래픽에 대한 요청 응답량을 수치로 환산할 수 있고, 로드 밸런싱 전략을 짜는데 도움이 된다.
  • 릴리즈 한 후 사용자들이 사용하기 전에 같은 스펙으로 부하량을 측정할 때 사전 진단을 하기 용이하다.

 

참고 

실전 웹앱 부하 테스트 1편 - https://blog.imqa.io/load_test1/

 

실전 Web Application 부하 테스트 1편

실제 고객사 사이트에서 얻은 부하 테스트 노하우 경험을 공유합니다.

blog.imqa.io

 

반응형
댓글
공지사항