이 글은 공부를 하면서 알게 된 내용들을 기록하는 글 입니다. 오류나 고쳐야 할 사항들이 있다면 지적 부탁드립니다!
✅ CDN - AWS CloudFront 적용
GitGet 프로젝트의 File 시스템은 운영 환경에서 사용할 Prod 버전과 개발 환경에서 사용하는 Local 버전이 있습니다.
Prod 버전에서는 AWS S3 서비스를 활용하여, S3 버킷에 이미지를 저장합니다.
AWS S3 버킷에 객체(이미지)를 저장하고 이를 조회/생성/수정/삭제하는 방식을 사용하고 있습니다.
서비스의 특성 상, 생성/수정/삭제 요청보다 조회 요청의 비율이 높기 때문에 AWS에서 제공하는 CDN 서비스인 AWS CloudFront를 적용하고, 조회 시 CloudFront를 통해 접근하도록 했습니다.
CDN 서비스는 Edge location, Cache과 같은 기술을 사용하여 이전에 S3 버킷 내의 객체를 조회한 적이 있고, 이 객체가 Cache에 존재한다면 AWS S3 버킷까지 가지 않고 Edge location을 통해 원하는 객체를 받을 수 있습니다.
이를 통해 조회에 대한 성능을 개선해보려고 합니다.
AWS S3에 AWS CloudFront를 적용하는 방법은 아래 글을 참고해주시길 바랍니다!
https://m42-orion.tistory.com/162
✅ 성능 테스트 및 비교
🔥 테스트 환경
환경
Macbook Air M2(RAM 16GB), JMeter 5.6.3, 로컬 환경(localhost)
JMeter설정
- Threads(users): 100
- Ramp-up period(seconds): 30
- Loop Count: 400
- Duration(seconds): 120
API 최대 호출 횟수 40,000번
DB 환경
- Instance 테이블에 저장된 레코드의 수 : 10,002개
테스트 대상 API
- `/api/admin/instance/{instanceId}`
- `/api/admin/instance`
- `/api/admin/topic/instances`
🔥 성능 테스트 상황 설정
GitGet 서비스의 어드민이 어드민 페이지를 통해 생성된 인스턴스들을 확인하고자 합니다.
인스턴스는 특정 주제(토픽)를 참여할 수 있는 형태의 객체로 만든 것을 이야기합니다.
어드민이 특정 토픽에서 만들어진 인스턴스를 확인하기 위해, 어드민 페이지에 진입합니다. 데이터 베이스의 Instance 테이블에는 특정 주제(토픽)를 통해 만들어진 인스턴스가 총 40,000개 존재합니다.
이 때, 어드민 페이지에서 1️⃣ 인스턴스의 목록 조회, 2️⃣ 특정 토픽에서 파생된 인스턴스 목록 조회, 3️⃣ 특정 인스턴스 단일 조회 API를 요청한다고 가정합니다. 이 때, 페이징을 통해 인스턴스 목록 조회 시, size는 10개로 설정합니다.
어드민 페이지와 관련된 API를 테스트 대상 API로 선정한 이유는, 해당 API가 인스턴스의 목록을 조회하기 위한 제일 간단한 로직을 가지고 있기 때문입니다.
다른 API에는 추천 조건을 확인하는 등의 추가적인 로직 처리가 있기 때문에 파일의 성능 개선을 확인하는데에 영향을 미칠 수 있겠다고 판단했습니다.
테스트 대상 API는 밑과 같습니다.
- 인스턴스 단건 조회 `/api/admin/instance/{instanceId}`
- 인스턴스 리스트 조회 `/api/admin/instance?size=10`
- 특정 토픽에 대한 인스턴스 리스트 조회 `/api/admin/topic/instances?size10`
🔥 변경 사항
성능 개선을 위한 변경 사항은 총 두 개 입니다.
1️⃣ AWS S3 버킷에 AWS CloudFront(CDN) 적용
2️⃣ File 응답 데이터에 Base64 인코딩 결과가 아닌, 파일(이미지)에 접근할 수 있는 URI 반환하도록 변경
AS-IS
이전에는 AWS S3를 통해 전달받은 UrlResource의 내용을 Base64로 인코딩한 문자열을 FileResponse의 `encodedFile`에 담아 전달했습니다.
프론트측에서는 응답으로 전달받은 문자열을 디코딩한 이미지를 클라이언트에게 보여줍니다.
이러한 방식은 파일에 따라(크기, 화질 등등) encodedFile의 길이가 과도하게 길어지는 등의 문제가 있었습니다.
@Service
@Transactional(readOnly = true)
public interface FileService {
/**
* Files 내에 저장된 값들을 통해 UrlResource 등으로 다운받은 후, base64로 인코딩한 결과 반환
*
* @param files 얻기 원하는 파일의 정보를 담고 있는 Files 객체
* @return base64로 encode한 결과 값(문자열) 반환
* 파일을 받아오지 못한 경우에는 빈 문자열("") 반환
*/
String getEncodedImage(Files files);
...
}
public class S3FileService implements FileService {
private final AmazonS3 amazonS3;
private final FileUtil fileUtil;
private final String bucket;
...
@Override
public String getEncodedImage(Files files) {
try {
UrlResource urlResource = new UrlResource(amazonS3.getUrl(bucket, files.getFileURI()));
byte[] encode = Base64.getEncoder().encode(urlResource.getContentAsByteArray());
return new String(encode, StandardCharsets.UTF_8);
} catch (IOException e) {
return "";
}
}
...
}
public record FileResponse(
Long fileId,
String encodedFile) {
public static FileResponse createExistFile(Long filesId, String encodedFile) {
return new FileResponse(filesId, encodedFile);
}
public static FileResponse createNotExistFile() {
return new FileResponse(0L, "");
}
}
TO-BE
개선된 방식은 Files 객체에 저장되어 있던 파일의 저장 경로(`fileURI`- ex: `/instance/a3f32a13-4d76-....jpeg/`)를 받고, CloudFront의 domain을 더해 이미지의 접근 URL을 전달합니다.
접근 URL은 `https://d35b46atvo80j1.cloudfront.net/instance/551d9d29-c7bd-4514-91fd-9a44a7ee5f8c.jpeg`와 같은 형태를 띄고 있으며, 프론트 측에서는 해당 URL을 img src 를 통해 이미지를 바로 보여줍니다.
@Service
@Transactional(readOnly = true)
public interface FileService {
/**
* Files에 저장된 파일의 접근 URI 반환
*
* @param files 얻기 원하는 파일의 정보를 담고 있는 Files 객체
* @return
*/
String getFileAccessURI(Files files);
...
}
public class S3FileService implements FileService {
private final AmazonS3 amazonS3;
private final FileUtil fileUtil;
private final String bucket;
private final String cloudFrontDomain;
...
@Override
public String getFileAccessURI(Files files) {
return cloudFrontDomain + files.getFileURI();
}
...
}
public record FileResponse(
Long fileId,
String accessURI) {
public static FileResponse createExistFile(Long filesId, String accessURI) {
return new FileResponse(filesId, accessURI);
}
public static FileResponse createNotExistFile() {
return new FileResponse(0L, "");
}
}
✅ Postman을 통한 테스트
1️⃣ 인스턴스 단건 조회: `/api/admin/instance/{instanceId}`
- Base64 Encoded 결과 반환
응답 소요 시간: 543ms
- 파일의 접근 URI 반환
응답 소요 시간: 32ms
- 개선 정도
응답 소요 시간: 543ms - 32ms = 511ms 감소
( 32 / 543 ) * 100 ≒ 약 94.11% 감소
2️⃣ 인스턴스 리스트 조회: `/api/admin/instance?page=0&size=10`
- Base64 Encoded 결과 반환
응답 소요 시간: 1828ms (약 1.8초)
- 파일의 접근 URI 반환
응답 소요 시간: 50ms
- 개선 정도
응답 소요 시간: 1828ms - 50ms = 1778ms 감소
( 50 / 1828 ) * 100 ≒ 약 97.26% 감소
3️⃣ 특정 토픽에 대한 인스턴스 목록 조회: `/api/admin/topic/instances/{topicId}`
- Base64 Encoded 결과 반환
응답 소요 시간: 2.45s
- 파일의 접근 URI 반환
응답 소요 시간: 55ms
- 개선 정도
응답 소요 시간: 2450ms - 55ms = 2395ms 감소
( 55 / 2450 ) * 100 ≒ 97.76% 감소
🎯 Postman을 통한 성능 테스트 결과
인스턴스 단건 조회 | 인스턴스 전체 목록 조회 | 특정 토픽의 인스턴스 목록 조회 | |
응답 소요 시간 | 543ms - 32ms = 511ms 감소 약 94.11% 감소 |
1828ms - 50ms = 1778ms 감소 약 97.26% 감소 |
2450ms - 55ms = 2395ms 감소 약 97.76% 감소 |
성능 개선 이후 Postman을 통한 API 요청했을 때, 세 API 모두 응답 소요 시간이 평균 96% 가량 감소한 것을 확인할 수 있습니다.
근소한 차이가 아니라 큰 차이인 것을 보아하니 확실히 성능 개선이 이루어진 것 같습니다.
이번에는 JMeter 테스트 툴을 이용해서 테스트 결과를 조금 더 자세히 살펴보도록 합시다.
✅ JMeter를 통한 성능 테스트
1️⃣ 인스턴스 단건 조회: `/api/admin/instance/{instanceId}`
- Base64 Encoded 결과 반환
실행된 요청 수 (Samples): 8900
평균 응답 시간(Average): 1190
최소 응답 시간(Min): 49
최대 응답 시간(Max): 8345
응답 시간의 표준 편차(Std. Dev.): 853.05
단위 시간 당 처리된 요청 수(Throughput): 73.2/sec
오류 비율(Error): 0%
- 파일의 접근 URI 반환
실행된 요청 수 (Samples): 40000
평균 응답 시간(Average): 159
최소 응답 시간(Min): 2
최대 응답 시간(Max): 2485
응답 시간의 표준 편차(Std. Dev.): 190.64
단위 시간 당 처리된 요청 수(Throughput): 411.7/sec
오류 비율(Error): 0%
- 개선 정도
평균 응답 시간(Average)
1190ms(Base64 Encode) - 159ms(Access URI) = 1031ms 감소
( 159 / 1190 ) * 100 ≒ 약 86.6% 감소
최소 응답 시간(Min)
49ms(Base64 Encode) - 2ms(Access URI) = 47ms 감소
( 2 / 49 ) * 100 ≒ 약 95.9% 감소
최대 응답 시간(Max)
8345ms(Base64 Encode) - 2485ms(Access URI) = 5860ms 감소
( 2485 / 8345 ) * 100 ≒ 약 70.22% 감소
응답 시간의 표준 편차(Std. Dev.)
853.05(Base64 Encode) - 190.64(Access URI) = 662.41 감소
( 190.64 / 853.05) * 100 ≒ 약 77.65% 감소
단위 시간 당 처리된 요청의 수(Throughput)
411.7 (Access URI) - 73.2(Base64 Encode) = 368.5 증가
( 73.2 / 411.7 ) * 100 ≒ 약 503.41% 증가
2️⃣ 인스턴스 리스트 조회 `/api/admin/instance?size=10`
- Base64 Encoded 결과 반환
실행된 요청 수 (Samples): 1042
평균 응답 시간(Average): 10773
최소 응답 시간(Min): 673
최대 응답 시간(Max): 43199
응답 시간의 표준 편차(Std. Dev.): 6632.10
단위 시간 당 처리된 요청 수(Throughput): 7.8/sec
오류 비율(Error): 0%
- 파일의 접근 URI 반환
실행된 요청 수 (Samples): 39982
평균 응답 시간(Average): 202
최소 응답 시간(Min): 6
최대 응답 시간(Max): 1528
응답 시간의 표준 편차(Std. Dev.): 154.19
단위 시간 당 처리된 요청 수(Throughput): 333.1/sec
오류 비율(Error): 0%
- 개선 정도
평균 응답 시간(Average)
10773ms(Base64 Encode) - 202ms(Access URI) = 10571ms 감소
( 10773 / 202 ) * 100 ≒ 약 98.12% 감소
최소 응답 시간(Min)
673ms(Base64 Encode) - 6ms(Access URI) = 670ms 감소
( 673 / 6 ) * 100 ≒ 약 99.11% 감소
최대 응답 시간(Max)
43199ms(Base64 Encode) - 1528ms(Access URI) = 41671ms 감소
( 1528 / 43199 ) * 100 ≒ 약 96.46% 감소
응답 시간의 표준 편차(Std. Dev.)
6632.10(Base64 Encode) - 154.19(Access URI) =6477.91 감소
( 154.19 / 6632.10 ) * 100 ≒ 약 97.68% 감소
단위 시간 당 처리된 요청의 수(Throughput)
333.1(Access URI) - 7.8(Base64 Encode) = 325.3 증가
( 7.8 / 333.1 ) * 100 ≒ 약 4170% 증가
3️⃣ 특정 토픽에 대한 인스턴스 목록 조회: /api/admin/topic/instances/{topicId}
- Base64 Encoded 결과 반환
실행된 요청 수 (Samples): 1047
평균 응답 시간(Average): 10959
최소 응답 시간(Min): 539
최대 응답 시간(Max): 49588
응답 시간의 표준 편차(Std. Dev.): 7091.12
단위 시간 당 처리된 요청 수(Throughput): 7.6/sec
오류 비율(Error): 0%
- 파일의 접근 URI 반환
실행된 요청 수 (Samples): 40000
평균 응답 시간(Average): 190
최소 응답 시간(Min): 6
최대 응답 시간(Max): 1766
응답 시간의 표준 편차(Std. Dev.): 147.86
단위 시간 당 처리된 요청 수(Throughput): 349.1/sec
오류 비율(Error): 0%
- 개선 정도
평균 응답 시간(Average)
10959ms(Base64 Encode) -190ms(Access URI) = 10769ms 감소
( 10959 / 190 ) * 100 ≒ 약 98.2% 감소
최소 응답 시간(Min)
539ms(Base64 Encode) - 6ms(Access URI) = 533 감소
( 539 / 6 ) * 100 ≒ 약 98.89% 감소
최대 응답 시간(Max)
49588ms(Base64 Encode) - 1766ms(Access URI) = 47822 감소
( 49588 / 1766 ) * 100 ≒ 약 96.44% 감소
응답 시간의 표준 편차(Std. Dev.)
7091.12(Base64 Encode) - 147.86(Access URI) = 6943.26 감소
( 7091.12 / 147.86 ) * 100 ≒ 약 97.91% 감소
단위 시간 당 처리된 요청의 수(Throughput)
349.1(Access URI) - 7.6(Base64 Encode) = 341.5 증가
( 7.6 / 349.1 ) * 100 ≒ 약 4493% 증가
🔥성능 테스트 결과
인스턴스 단건 조회 | 인스턴스 전체 목록 조회 | 특정 토픽의 인스턴스 목록 조회 | |
평균 응답 시간(Average) | 1190 - 159 = 1031 감소 약 86.6% 감소 |
10773 - 202 = 10571 감소 약 98.12% 감소 |
10959 -190 = 10769 감소 약 98.2% 감소 |
최대 응답 시간(Max) | 8345 - 2485 = 5860 감소 약 70.22% 감소 |
43199 - 1528 = 41671 감소 약 96.46% 감소 |
49588 - 1766 = 47822 감소 약 96.44% 감소 |
응답 시간 표준 편차 (Std. Dev.) |
853.05 - 190.64 = 662.41 감소 약 77.65% 감소 |
6632.1 - 154.19 = 6477.91 감소 약 97.68% 감소 |
7091.12 - 147.86 = 6943.26 감소 약 97.91% 감소 |
단위 시간 당 요청 처리 수 (Throughput) |
411.7 - 73.2 = 368.5 증가 약 503.41% 증가 |
333.1 - 7.8 = 325.3 증가 약 4170% 증가 |
349.1 - 7.6 = 341.5 증가 약 4493% 증가 |
JMeter를 사용하여 세 API에 대해 성능이 얼마나 개선되었는지 테스트를 진행했습니다.
평균 응답 시간(Average) 평균 94% 가량 개선,
최대 응답 시간(Max) 평균 87% 가량 개선,
응답 시간 표준 편차(Std. Dev.) 평균 91% 가량 개선,
단위 시간 당 요청 처리 수(Throughput) 평균 3055%(!!!) 가량 개선되었음을 확인했습니다.
이전의 방식은
1️⃣ File 객체에서 파일의 접근 정보 획득,
2️⃣ 파일 다운로드,
3️⃣ 파일을 Base64로 인코딩하여 반환하는 세 단계를 거쳐야 했다.
반면 변경된 방식은 위의 세 단계 중 1️⃣ 단계까지만 간다.
어떻게 생각해보면,
AWS 측에 파일을 다운로드 요청을 하는 과정(2️⃣단계)과, 받아온 파일(이미지)를 Base64로 인코딩하는 과정(3️⃣단계)이 제외되었기 때문에 당연히 API 응답 속도가 더 빨라 지는 것은 당연한 것 같습니다.
그렇다면 파일(이미지) 접근 URL을 통해 이미지를 불러오는데에 얼마나 걸리는지도 고려를 하는 것이 맞을 것 같습니다.
파일 접근 URL의 경우 AWS CloudFront를 통해 이미지를 받아오고 있는데, 이 응답 속도가 어떻게 되는지 확인해봅시다.
✅ AWS CloudFront 적용에 따른 응답 속도 변화
AWS CloudFront는 AWS에서 제공하는 CDN(Content Delivery Network)로서, AWS S3 버킷 내의 객체에 접근 시 캐싱을 이용하여 더 빠르게 접근할 수 있는 방법을 제공합니다.
AWS CloudFront는 Cache를 사용하는데, 요청한 객체가 Cache에 존재해 이를 가지고 왔는지(Cache Hit), Cached에 존재하지 않아 S3 버킷에서 불러왔는지(Cache Miss) 네트워크 탭을 통해 확인해볼 수 있습니다.
헤더의 `X-Cache` 항목을 통해 cache hit 여부를 확인할 수 있습니다.
위의 두 경우에 따라 응답 속도가 얼마나 차이가 나는지 확인해보겠습니다.
- Cache Miss
- Cache Hit
- 응답 속도 차이
Cache Miss: 59.76ms
Cache Hit: 18.98ms
CloudFront Cache Hit 상황일 때 응답 속도 40.98ms 가량 감소
68.24%가량 개선된 것을 확인할 수 있습니다.
✅ FrontEnd 측의 응답 시간 변화
실제 서비스를 사용하는 클라이언트 입장에서는 어떻게 체감되는지 서버를 직접 돌려 확인해보도록 합시다.
🔥 개선 이전 버전
파일을 다운로드 받아 Base64로 인코딩해서 전달하는 방식이기 때문에, 프론트 측에서는 전달받은 문자열을 디코딩하여 이미지로 보여주어야 합니다.
🔥 개선 이후 버전
- 첫 요청 시 응답 소요 시간(Cache hit X)
이전에 JSON을 통해 Base64로 인코딩 된 문자열을 보내주는 이전의 방식과 달리,
<img> 태그의 src 속성은 웹 페이지가 로딩되는 순간 웹 서버(AWS)에서 해당 이미지를 가져와 페이지에 삽입합니다.
CloudFront 도메인 주소를 통해 파일(이미지)에 접근하는데에 소요되는 시간은 평균 108.58ms 정도로
두 API의 소요시간을 모두 합쳐도 141.53ms(인스턴스 목록 조회) + 108.58ms(이미지 로딩) = 250.11ms 가 나옵니다.
평균적인 응답 시간은 아니지만, 위의 응답 시간으로 비교를 해보면 대략 69% 가량 개선이 되었음을 확인할 수 있습니다.
- Cache Hit 발생 시, 응답 소요 시간
첫 요청(Cache miss) 이후, 새로고침을 통해 같은 API를 재요청했습니다.
파일(이미지)의 경우에는 `디스크 캐시`에 저장되어 있던 사진을 불러와 응답 속도가 이전에 비해 굉장하게 빨라진 것을 확인할 수 있었습니다.
API 응답의 경우 추측이지만, 마찬가지로 캐시에 저장되어 있던 데이터가 전달됨으로서 이전에 비해 빠른 응답 속도가 나온 것이 아닐까 합니다.
- 결론
확실히 개선 이전의 버전과, 개선 이후의 버전에는 속도 차이가 분명하게 나는 것을 확인할 수 있었습니다.
API 응답을 받고 이미지를 받아오는 시간을 모두 더해도 최대 141ms + 116ms = 257ms로, 개선 이전에 소요되었던 시간인 807ms에 비해 상당히 개선되었음을 확인할 수 있습니다.