/map/public
Response body 예시
{
"page_number" : 0,
"size" : 10,
"total_pages" : 1,
"total_elements" : 1,
"first" : true,
"last" : true,
"content" : [ {
"id" : 1,
"map_name" : "changed map",
"map_emoji" : "U+1F600",
"host_id" : 1,
"host_nickname" : "nickname",
"host_profile_image" : "image",
"places_count" : 2
} ]
}
![](https://velog.velcdn.com/images/cmsskkk/post/65a03e3a-b4bf-410a-ba21-5916ac7cbadf/image.png)
import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager
/**
* A simple example using the HTTP plugin that shows the retrieval of a single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author admin
*/
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static Map<String, String> headers = [:]
public static Map<String, Object> params = [:]
public static List<Cookie> cookies = []
public static Random random = new Random()
@BeforeProcess
public static void beforeProcess() {
HTTPRequestControl.setConnectionTimeout(300000)
test = new GTest(1, "127.0.0.1")
request = new HTTPRequest()
headers.put("Content-Type", "application/json")
grinder.logger.info("before process.")
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true
grinder.logger.info("before thread.")
}
@Before
public void before() {
request.setHeaders(headers)
CookieManager.addCookies(cookies)
grinder.logger.info("before. init headers and cookies")
}
@Test
public void test() {
String path = "http://127.0.0.1:8080/map/public?page=%d&size=%d"
int[] arr = getRandomPageAndSize(getRandomSize())
int pageNumber = arr[1]
int size = arr[0]
String uri = String.format(path, pageNumber, size);
HTTPResponse response = request.GET(uri)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertEquals(response.statusCode, is(200))
}
}
public int getRandomSize() {
int limit = 29
return random.nextInt(limit) + 1;
}
public int[] getRandomPageAndSize(int size) {
int[] arr = new int[2]
arr[0] = size
int count = 500
int pageLimit = count / size
int page = random.nextInt(pageLimit)
arr[1] = page
return arr
}
}
TPS : (Tests Per Seconds) 7.5
Mean Test Time : 13.324.42ms (약 13.3초)
TPS 그래프 또한 안정적인 그래프를 지향해야하는데, 아주 들쭉날쭉하다.
실제 서비스라고 생각한다면…. 아찔하다.
포스트맨으로 간단히 API 호출을 했을때, API 응답속도는 704ms
name 파라미터가 들어오면 name에 해당하는 지도를 검색해서 반환한다.
부하테스트에서는 검색조건이 없는 전체 리스트를 조회했다.
이름 검색 또한 들어가면… %name%
형태의 like 쿼리인데 index가 작동하지않아, 풀스캔을 하기 때문에 더 많은 성능 저하가 예상된다. TODO 포인트
map.getPlacesCount()
(this.places.size())를 호출하면 모든 Place를 select 쿼리를 통해서 로딩한다. 데이터가 Map당 1000개의 Place를 가지고있으니 1000번의 가까운 추가쿼리가 나갈뿐만 아니라(나의 경우 batch_fetch_size로 인해서 1000번의 쿼리가 발생하지는 않는다.), 단순히 갯수만이 필요한데 place의 모든 데이터를 로딩하는 엄청난 비효율이 발생하는 것이다.Batch-size 설정을 증가시킴에 따른 성능 변화를 테스트해보고자 했다.
default_batch_fetch_size
100 → 1000
N+1문제를 해결하기위한 방법 중 하나로 1000으로 증가 시킨 후에 다시 테스트 해보았지만 TPS에서 유의미한 차이는 발생하지 않았다.
Place의 갯수만 필요한데 place의 모든 필드를 다 반환하는 비효율의 문제가 크다. 그리고 Batch_fetch_size 를 증가시켜도 데이터양이 워낙 많아, 유의미한 결과가 나오지 않은 것 같다.
지도 내의 장소의 갯수를 조회하는 로직을 쿼리로 채우도록 해보았다.
placeRepository.countPlacesByMap()
jpql 네이밍 쿼리를 활용했다.
Redis 설정에 대해서는 따로 정리하고 링크 남길 예정
캐싱할 서비스 로직은 이렇게 수정이 되었다.
Spring에서 적용하는 Cache 추상화를 통해서 @Cachealble 과 같은 어노테이션을 적용하면 CacheManager
를 활용해서 @Transactinal 과 같이 AOP로 기존 코드에는 영향을 주지않고 캐싱이 가능하다.
해당 어노테이션안의 정보를 통해서 key를 만들고 메서드의 반환값을 Value로 저장한다.
"pagingPublicMaps::[3,21,Optional.empty]”
와 같은 Key가 만들어져서 저장된다.
/map/public?page=3&size=21
와 같이 API를 호출하면 Redis에서 파라미터를 통해서 Key를 만들어서 Key가 존재하는지 확인하고, 존재한다면 메서드의 로직들이 호출되지 않고, 저장되어있는 Value를 반환한다.
Key가 존재하지 않는다면, 로직을 실행하고 반환할 때, Key와 Value를 저장한다.
첫 테스트의 결과가 아주 익사이팅한 것과 같이 캐싱의 결과도 아주 익사이팅했다.
Cache Hit를 통해서 오래걸리는 쿼리 로직의 실행을 막기 위해서는 메서드의 pageNumber, size, name 파라미터가 같아야한다. 테스트에서는 name 검색 조건은 없지만 랜덤으로 pageNumber와 size를 요청하기 때문에 Cache Hit가 과연 잘 될까라는 생각을 했었다.
테스트의 결과
데이터를 많이 넣지않고 테스트 및 클라이언트들에게 API를 제공했기에 로직의 문제를 파악하지 못했었다.
또한, 개발 프로세스와 일정들이 명확하지 않았기에, 아주 기본적인 것도 놓치고 있었다는 생각이 든다.
org.ngrinder.common.exception.NGrinderRuntimeException: Can not check available ports because given local IP address
여담으로 우리집 와이파이 Issue로 ngrinder-controller가 실행되지 않아서, .ngrinder/system.conf, etc/hosts 파일을 설정해보려는 등등 원인을 찾지못하는 에러로 시간을 꽤나 낭비하면서 테스트를 시작도 못할 뻔 했다.
Caused by: org.ngrinder.common.exception.NGrinderRuntimeException: Can not check available ports because given local IP address '218.38.137.27' is unreachable. Please check the /etc/hosts file or manually specify the local IP address in ${NGRINDER_HOME}/system.conf.
단순 네트워크 문제다. 다른 네트워크환경에서는 정상적으로 작동했다.
Spirng Cache와 관련해서 캐시를 수정, 삭제하는 과정에서 캐시의 변화,그리고 기본적인 TTL과 같은 설정과 Clustering을 비롯한 Redis에 대한 이해가 더 필요하다고 느껴진다. (Serializer만 해도 고려해볼 요소가 많다.)
또 다른 API들을 테스트 해보면서 리팩토링을 지속하고 싶다.
캐싱을 통해서 그룹 멤버 검증에 대한 로직 리팩토링하고, 다시 포스팅 해볼 예정이다.
TODO : 배포환경을 구축하고 PinPoint까지도 찍먹해보고 싶다.
잘봤습니다.. 도움이 많이 되었네요 ㅎㅎ