0. 들어가며...
현재 구인 구직 서비스의 서버를 만들며, 다음과 같이 구현한 상태이다. 일 (job)
과 회원 (member)
는 매칭(matching)
이란 교차 테이블을 두고, 서로 관계를 맺고 있기에, 연관이 있는 회원은 일과 하나의 상태를 가진다.
위의 의뢰인은 일의 주인이다. 따라서 상태는 OWNER
만 가진다. 반면 해결사는 자신의 현재 단계에 따라 다른 상태를 가진다. 신청만 했거나 의뢰인에게 선 의뢰를 받은 상태라면, ATTENDER
나 REQUEST
를 가지고, 의뢰인에게 해결 요청을 승낙 혹은 거절 받거나 혹은 해결사가 선의뢰를 승낙 혹은 거절한 상태이면, YES
혹은 NO
라는 상태를 가진다.START
는 일을 진짜 시작해야만 가지는 상태이며, SLEEP
은 해결사가 일 해결에서 노쇼한 경우 의뢰인이 그것을 인지하고 서버에 알린 상태이다. 만약 10분 내로 해당 해결사에게서 일 재개 요청이 오지 않으면 자동으로 CANCEL
단계로 넘어간다. 이러면 일 해결 자체가 취소된 상태이다.FINISH
는 해결사가 일을 끝냈다고 의뢰인에게 알린 상태이다. 만약 의뢰인이 그 미션 성공을 받아들이면 CONFIRM
상태로들어가고 일 처리가 마음에 들지 않아서 거절하고 싶다면 다시 REJECT
로 넘어간다.
이렇게 현재 일에 대한 사용자의 상태를 상세히 설명한 이유는 독자가 앞으로 설명할 테스트 시나리오 작성에서의 어려움과 그 극복을 더 공감하시길 원해서 이다. 필자는 2가지 측면에서 어려움을 느꼈다.
- 일이 등록되고 해결되는 전체 생애주기에서 두 명의 사용자가 움직인다. 한 명의 Virtual User를 대상으로만 진행하면 되었던 API 개별 테스트의 경우 이런 문제가 없었으나, 한 번도 두 명의 유저가 동시에 움직이는 시나리오를 짜보지 않았던 필자로서는 크나큰 도전이었다.
- 일 해결이 실패하는 상황 일 해결이 성공하는 상황이 나뉜다.
이것 또 하나의 시나리오에서 다 해결하고 싶은데 참 힘들었다.
1. 시나리오 테스트 작성
먼저 시나리오는 다음과 같다. 시나리오 자체를 짜는 것이 처음이라 조건 분기는 아직 따로 하지 않았다.
- (1) 의뢰인의 일 등록 (해당 의뢰인의 아이디를 3번이라 하자.)
- (2) 다른 아이디의 회원이 해당 일을 신청한다. (이 회원의 아이디를 7번이라 하자.)
- (3) 의뢰인이 해당 일을 승낙한다.
- (4) 7번이 일을 시작한다.
- (5) 7번이 일을 끝냈음을 알린다.
- (6) 의뢰인이 해당 일 해결을 확정 시키고, 하나의 시나리오 주기가 끝난다.
시나리오를 작성하고 보니 각 단계별로 다음과 같은 일이 필요했다.
- 단계 (1) 전에 JWT 토큰을 받아서 각 요청 Header에 주입해야 한다. 그러기 위해서는 테스트 시작 전에 JWT 토큰을 의뢰인용 하나, 해결사용 하나를 미리 가지고 있어야 한다.
- 단계 (1)에서 일을 등록하기 위한 Request의 BODY 데이터가 매번 달라야 한다. 왜냐면 각 일마다 일을 해야하는 위치인 장소를 위도, 경도 형태로 저장하는데, 이게 같아버리면, 데이터의 분포가 편중되게 되어 다른 API 요청을 할 때,
- 단계 (1)에서 등록한 일 아이디를 추출해서 전역 변수로 가지고 있어야 한다.
- 의뢰인이 해결사의 상태를 바꾸는 요청에서 해결사의 회원 아이디를 가지고 있어야 한다. 따라서, 이 또한 주입 받아서 가지고 있어야 한다.
2. JWT 토큰 기 주입
이를 위해서 jmeter의 csv data set config
를 활용 했다. 대량의 유저 동접을 위한 JWT 토큰 추출 및 수집기에서 얻은 JWT 토큰을 각각 임의로 client_token과 worker_token으로 나누어서 다음과 같이 CSV 파일을 만들었다.
client_token | worker_token |
---|---|
eyJOe~.... | eyJOe... |
이렇게 해서 저장해둔 뒤, csv data set config
를 다음과 같이 설정 했다.
저기서 변수의 이름들에 적힌 변수들이 쉼표를 기준으로 구분되어 CSV 파일에서의 하나의 열을 참조하게 된다. 내 설정의 경우 client_token이 1열, worker_token이 2열을 담당한다. csv 파일의 첫 행이 실제 내용이 아닌, 토큰 구분자임으로 첫 행 무시를 true
로 두었다. 이러면 이제 각 Client 요청 Header에는 client_token이, Worker의 요청 Header에는 worker_token이 저장되게 된다. 근데 진짜 client_token
이나 worker_token
으로 Header에 하드 코딩 해두는 것은, 수정이 있을 때, 변경점을 일일히 확인해야 해서 유지보수에 안 좋을 수 있다. 그래서 나는 JSR223 사전 처리기를 도입해 groovy code를 다음과 같이 설정했다.
String samplerName = sampler.getName()
if (samplerName.contains("Client")) {
vars.put("Authorization", "Bearer " + vars.get("client_token"))
} else if (samplerName.contains("Worker")) {
vars.put("Authorization", "Bearer" + vars.get("worker_token"))
}
Http 요청 이름에 client가 들어가면 Header의 Authorization
에 클라이언트의 토큰을 주입한다. 반대의 경우에는 해결사의 토큰을 주입한다.
2. 일 등록 Request Body에 동적인 json 주입
이 부분이 개인적으로 가장 힘든 작업이었다. 왜냐하면, 만약 일 등록 자체가 application/json 이었다면, JS223 PreProcessor
를 활용해서, json 값을 동적으로 주면 되었지만, 현 서비스는 일에 대한 설명 사진은 multipart data로 줘야 해서, json 요청과 image는 application/octet-stream
으로 보내야만 했다. 따라서 둘 다 하나의 파일 형태로 파일들을 업로드
칸에 넣어야 해서, 동적인 json 주입이 불가 했다.
무식하긴 하지만, 이렇게 해결했다.
- (1) python을 활용해 json 파일을 안에 값이 전부 다르게 3000개를 만든다.필자의 API의 경우 위도 경도만 다르면 되기 때문에 이와 같이 설정했다. 이것을
python job_maker.py
로 돌리면, 해당 job_maker 파일이 있는 폴더에generated_jobs
라는 폴더가 생기고 다음과 같이 3000개의 json이 생성되게 된다. import json import random import os # 판교 위도/경도 base_lat = 37.4021 base_lng = 127.1086 def random_offset(): return (random.random() - 0.5) * 0.054 # 3km 이내 os.makedirs("generated_jobs", exist_ok=True) for i in range(3000): data = { "title": "음쓰 버려주실 분~", "content": "아파트 현관문 비번: 6541", "money": 10000, "point": 500, "lat": round(base_lat + random_offset(), 7), "lng": round(base_lng + random_offset(), 7) } with open(f"generated_jobs/job_{i:04}.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)
- 다음은 JWT 토큰 주입과 방식이 같다. 앞에서 만든 job_0000 부터 job_2999라는 아이디 이름을 가진 csv 파일을 하나 만든다. 나는 이를
job_payloads
라고 불렀다.
이제 이걸 똑같이 csv data set config를 만들어 해당 이름을 동적 주입한다.
그리고 다음과 같이 파일 경로에 동적인 변수 값을 지정해주면, 파일 경로가 동적으로 만들어지게 된다. 이를 이용해 값을 넣었다.
3. (1) 단계에서 만든 일의 아이디를 추출하여 전역 변수로 설정
앞에서 일 등록을 마치면 다음과 같이 응답 데이터서 job_id를 반환한다.
해당 일을 추출하기 위해 다음과 같이 설정 했다. Threads Group
➜ 사후 처리기
➜ JSON 추출기
를 선택한다.
이것을 일 등록 API HTTP 요청 후로 drap해서 가져간 뒤, 다음과 같이 설정한다.
생성된 변수들의 이름들
: 쉼표 단위로 구분되며, 추출하여 전역변수로 설정할 때의 변수 이름이 된다.JOSN PATH 표현식들
: 여기는 추출할 데이터의 위치를 적으면 된다. 나의 경우 jobId가 root
➜data
➜jobId
에 있어서 이렇게 설정 했다. 이렇게 하면 data
속 jobId
가 추출되어 jobId
라는 이름으로 전역 변수로 설정된다.
4. 해결사의 아이디 추출
해결사의 아이디 추출의 경우 기 주입된 JWT 토큰에서 그것을 가져와야 했다. 이것은 JS223 사전처리기
를 활용해 다음과 같이 code를 짰다.
import java.util.Base64
import groovy.json.JsonSlurper
String jwt = vars.get("worker_token") // JWT 토큰 가져오기
String[] parts = jwt.split("\\.")
if (parts.length >= 2) {
String payloadBase64 = parts[1]
// padding 보정 (Base64 디코딩용)
int padding = 4 - (payloadBase64.length() % 4)
if (padding > 0 && padding < 4) {
payloadBase64 += "=" * padding
}
byte[] decodedBytes = Base64.decoder.decode(payloadBase64)
String payloadJson = new String(decodedBytes, "UTF-8")
def json = new JsonSlurper().parseText(payloadJson)
def attenderId = json.sub // 우리가 원하는 값: "2"
vars.put("attenderId", attenderId.toString())
} else {
log.error("JWT 토큰 형식 이상함: " + jwt)
}
worker의 토큰을 가져와서 base64로 디코딩하고 sub에 담긴 memberId를 뽑아냈다.
5. 총 결론
그래서 현재는 다음과 같이 짰다. 보면 스포이드
형태가 바로 HTTP 요청이고, 그에 따른 그래프 확인이 가능하도록 해놨다. 여기까지 짜니 밤이 되어서 분기 컨트롤러를 활용한 실패 시나리오 접목은 아직 하지 못했지만, 그것을 넣어서 해보겠다. 결과 트리는 다음과 같이 순차적으로 잘 찍힌다.