프로젝트

최종 프로젝트 트러블슈팅 + 기술적 의사 결정

개발공명 2024. 11. 14. 23:55

트러블 슈팅

  • CDN Server 같은 이미지 동시 요청 시 DB 및 storage service 여러 번 가는 문제
  • 대용량 이미지 업로드 시 CDN URL 생성 오래 걸리는 문제
  • 이미지 정보 FeignClient 전송 시 문제

 

기술적 의사 결정

  • CDN Server 도입으로 조회 및 다운로드 성능 향상
  • CDN Server 캐싱 동작 구현 방법

 

CDN Server 같은 이미지 동시 요청 시 DB 및 storage service 여러 번 가는 문제

개요

현재 CDN Server의 구현은 아래와 같이 구현되어 있다. 

 

  1. 브라우저에서 CDN Server에게 CDN URL로 이미지 조회 및 다운로드를 요청한다. 
  2. CDN Server는 Redis에 해당 CDN URL을 key로 가지고 있는 value가 있는지 확인한다. 
  3. 첫번째 요청일 경우 이미지가 CDN Server에 저장되어 있지 않기 때문에 Image Fetch Server로 이미지 요청한다. 
  4. Image Fetch Server는 DB로 이미지 정보를 요청한다. 
  5. Image Fetch Server는 받은 이미지 정보로 storage service로부터 이미지를 받아와 CDN Server로 반환한다. 
  6. 두번째 요청일 경우 Redis에 CDN URL을 key로 가진 value가 있으니 = 이미지가 CDN Server에 저장되어 있으니 바로 이미지를 반환한다. 

따라서 같은 이미지를 여러 번 요청하게 되면 DB 및 storage service에 요청을 1번만 하는 것이 맞는 상황이다. 

 

문제 상황

이것을 확인하기 위해 JMeter로 부하 테스트를 진행했다. 

 

Thread 100개로 10번 요청 = 즉 1000번 요청을 보냈는데 DB 쿼리를 찍어보니 DB 쿼리가 32번이나 나가고 있었다. 

 

아래 이미지에서 같은 쿼리가 또 나가고 있는 것을 볼 수 있다. 

 

왜 이런 일이 발생했는가 생각해보니 이유는 아래와 같았다. 

 

첫번째 요청이 와서 위의 프로세스를 진행하고 Redis에 값을 넣는 와중에도 동시에 요청이 들어오기 때문에 생긴 문제였다. 

 

Redis에 값은 넣는데 걸리는 시간이 있으니 그 시간동안 들어온 요청들은 Redis에 값이 없으니 전부 첫번째 요청처럼 진행되어 DB 및 storage service로 요청이 가는 것이다. 

 

이때 처리량을 측정해보니 아래와 같았다. 

 

해결 과정

이 문제는 Redis를 처음 조회할 때 발생하는 동시성 문제라고 판단했다. 

 

따라서 Redis를 처음 조회할 때 = 즉 Redis에 값이 없으면 락을 걸게 했다. 

 

Redis를 처음 조회한 요청은 Lock을 얻고 이미지를 CDN Server에 저장을 한다. 

 

그리고 그 뒤에 온 요청들은 Lock이 없으니 Redis 조회를 기다리는 것이다. 

 

Redis를 처음 조회한 요청이 Redis에 값을 넣고 CDN Server에 이미지를 저장 후 Lock을 해제하면 뒤에 온 요청들이 Redis를 조회한다. 

 

이때는 Redis에 값이 들어가 있기 때문에 뒤에 온 요청들은 DB 및 storage service로 요청 보내지 않고 CDN Server에서 바로 이미지를 조회해 가는 것이다. 

 

이렇게 Lock을 설정하니 DB에 쿼리 한번만 날아가는 것을 확인했다. 

 

그 후 JMeter로 똑같은 상황으로 테스트 하니 결과는 아래와 같았다. 

 

처리량이 약 3배 이상 늘어난 것을 볼 수 있다. 

 

이렇게 Lock을 설정해 DB 및 storage service로 가는 요청 횟수를 줄여 성능을 개선하는데 성공하였다. 

 

또 원래 CDN Server가 동작해야하는 것처럼 동작하게 만들었다. 

 

 

대용량 이미지 업로드 시 CDN URL 생성 오래 걸리는 문제

개요

이미지 크기가 몇 십 MB만 되어도 굉장히 큰 이미지라고 한다. 

 

그래서 굳이 몇 십, 몇 백 MB 이미지는 테스트 안 해 봐도 된다고 튜터님의 말했다.

 

하지만 대용량 이미지도 처리할 수 있는 Image Module을 만들고 싶어 1GB까지 업로드 할 수 있게 하려고 했다. 

 

그래서 약 400MB 이미지 업로드를 시도했다. 

 

업로드는 잘 되었고 그 후 이미지 조회 및 업로드를 위해 CDN URL를 요청했는데 계속 오류가 났다.

 

문제 상황

왜 오류가 났는가 보니 CDN URL 조회를 했을 때 DB에 값이 들어가 있지 않았기 때문이다. 

 

그리고 기다려보니 시간이 꽤 지난 후에 DB에 CDN URL이 들어왔다. 

 

CDN URL 생성이 약 400MB 이미지 업로드 시 120초(!!) 정도 걸렸다. 

 

그래서 어디에서 이렇게 오래 걸리나 문제를 찾기 위해 주요 메서드의 시간을 측정해 봤다. 

 

업로드가 오래 걸릴 것 같았지만 의외로 시간이 오래 걸린 곳은 Image Convert Server였다. 

 

Image Convert Server에서는 메타데이터 삭제, 메타데이터 삭제 이미지 업로드, webp 변환, CDN URL 생성 등의 일을 하고 있다. 

 

각 일들의 시간을 찍어보니 아래와 같았다. 

 

의외로 메타데이터 삭제가 35s, webp 변환이 96s로 여기서 다 시간을 잡아먹고 있었다. 

 

Minio에서 다운로드하고 업로드하는 것은 굉장히 빨리 진행되었다. 

 

CDN URL을 왜 Image Convert Server에서 생성하는가 궁금할 수 있다. 

 

왜 그런가 하면 원본 이미지의 메타데이터에는 아래와 같이 개인 정보 (카메라 정보, GPS 정보 등)들이 들어가 있다. 

 

따라서 원본 이미지를 조회 및 다운로드 했을 때 개인 정보가 보이지 않도록 원본 이미지의 메타데이터를 삭제한 후 CDN URL을 생성하는 것이다.

 

해결 과정

이 문제를 해결하기 위해 다양한 webp 변환 알고리즘을 찾아보려고 했다. 

 

하지만 webp 변환 알고리즘이 다 예전 것이거나 안되는 것이 많았다. 

 

다른 것을 시도해도 똑같이 느렸다. 

 

그래서 다른 방법을 생각해 보았다.

 

먼저 Image Convert Server의 코드를 보니 하나의 메서드에서 위의 많은 일들이 전부 일어나고 있었다. 

 

위의 시간을 찍어본 사진에서 나온 순서대로 하나의 메서드에서 진행되고 있었다. 

 

CDN URL은 원본의 메타데이터가 삭제되고 Minio에 업로드된 동시에 생성되어도 문제가 없다. 

 

하지만 webp 변환까지 진행된 후 CDN URL을 생성하고 있었다. 

 

따라서 우선은 CDN URL 생성하는 코드를 메타데이터가 삭제되고 Minio에 업로드하는 코드 다음으로 옮겼다. 

 

이렇게 했더니 CDN URL 생성 속도가 120초 → 30초로 감소했다. 

 

추후에 메서드를 분리하고 더 빠르게 할 수 있는 방법은 없을지 고민해 볼 예정이다. 

 

 

이미지 정보 FeignClient 전송 시 문제

개요

CDN Server는 이미지를 항상 가지고 있는 것이 아니라 캐싱하고 있는다.

 

따라서 CDN Server는 현재 이미지가 없다면 Image Fetch Server에서 이미지를 받아와야 한다. 

 

이렇게 Image Fetch Server에서 CDN Server로 이미지를 보내는 과정에서 발생한 문제들을 해결하는 과정이다. 

 

문제 상황

이 문제를 해결하는 과정은 3단계로 나눠진다. 

 

1단계 : FeignClient에 DTO 담아 이미지 + 이미지 정보 보내기

MSA 환경에서 Image Fetch Server에서 CDN Server로 이미지를 FeignClient를 사용해서 보내려고 시도했다. 

 

FeignClient로 받으려고 했던 DTO는 아래와 같다. 

public class ImageDto{
	private byte[] imageByte;
	private String fileName;
}

 

이렇게 DTO에 byte[ ]을 필드로 가지고 이미지를 보내려고 했다. 

 

CDN Server에서 DTO를 받아 이미지를 꺼내려고 하니 계속 null이 반환이 되었다. 

 

문제 원인

왜 계속 null이 반환되는지 확인해보니 FeignClient의 직렬화 때문이였다. 

 

FeignClient는 기본적으로 byte[ ]을 Base64 문자열로 직렬화하려 시도한다고 한다. 

 

이때 byte[ ]의 데이터가 이미지와 같이 바이너리 데이터라면 null로 직렬화 된다고 한다. 

 

해결 방법

해결 방법은 2가지로 생각해 볼 수 있다. 

  • FeignClient의 ObjectMapper를 byte[ ]을 사용할 수 있게 customize하는 방법
  • 응답 Header에 Content-Type : application/octet-stream 넣는 방법

 

두 방법 중 두번째 방법이 더 수월해 보이기 때문에 두 번째 방법을 선택했다. 

 

 

2단계 : FeignClient에 Content-Type 추가해 이미지 + 이미지 정보 보내기

FeignClient의 Header에 이제 Content-Type : application/octet-stream을 넣어서 이미지를 보내려고 했다.

 

그런데 DTO를 이렇게 보내면 문제가 발생한다.

 

문제 상황

위처럼 하게 되면 FeignClient는 DTO 전체를 단순 바이너리 스트림으로 취급한다. 

 

따라서 DTO의 String 필드는 JSON으로 직렬화되지 않고 이상하게 나오게 된다. 

 

해결 방법

이 문제를 해결하기 위해 2번 요청을 보내는 것을 생각해 보았다. 

 

보내는 요청은 아래와 같다.

  1. Header에 Content-Type : application/octet-stream 넣고 Body에 byte[ ]이 넘어오는 요청
  2. 그냥 DTO (byte[ ]이 제외된 이미지 정보가 담긴)가 JSON으로 직렬화 되어 넘어오는 요청

 

그런데 이렇게 하면 Data Server에 같은 요청이 2번 가게 되어 낭비가 발생한다. 

 

심지어 Data Server에서 받아오는 응답은 2번의 요청 모두 같기 때문에 굉장히 낭비이다. 

 

따라서 Redis를 사용하는 방법을 고민해 봤다. 

  1. Redis를 사용해 첫번째 요청 때 가져온 정보를 저장
  2. 두번째 요청 때 Redis에서 정보 가져와 CDN Server로 반환

그런데 이렇게 하면 Redis를 또 추가해서 관리 포인트도 많아진다. 

 

그리고 코드 변경도 굉장히 많아져 번거로워진다는 생각을 하였다. 

 

이렇게 고민하다가 응답의 Header에 원래 DTO로 보내던 정보들을 넣으면 어떨까라는 생각을 하게 되었다. 

 

 

3단계 : FeignClient에 Content-Type 추가해 이미지 보내고 Header에 이미지 정보 추가하기

이 방식을 채택해 문제를 해결하게 되었다. 

 

해결 방법은 해결 과정에 적어보겠다. 

 

해결 과정

byte[ ]이 null로 넘어오던 문제는 Content-Type에 application/octet-stream을 넣어 해결하였다. 

 

이미지 이름 같은 이미지 정보를 받아오기 위해 또 요청을 보내야 하는 문제는 Header에 이미지 정보를 추가해 해결하였다. 

 

Http 응답이 아래와 같은 형태인 것이다. 

 

이렇게 해서 Body에서는 byte[ ]을 꺼내 이미지를 받아오고, Header에서 이미지 정보를 꺼내와 사용했다. 

 

이때 Header에 이미지 이름을 넣을 때 한글일 경우 또 문제가 생겨서 인코딩해서 넣고, 디코딩해서 꺼내와야 했다. 

 

 

CDN Server 도입으로 조회 및 다운로드 성능 향상

개요

이미지 처리 모듈에서 이미지 조회와 다운로드는 필수 기능이자 매우 중요한 기능이다. 

 

이전 프로젝트에서는 이미지 조회 및 다운로드를 할 때 AWS S3와 같은 storage service에서 직접 했었다

 

AWS S3에서 반환하는 url을 사용자에게 직접 제공해 이미지에 접근할 수 있게 하였다. 

 

하지만 이렇게 storage service에서 이미지를 직접 접근할 수 있게 하는 것은 문제점이 있다고 한다.  

 

문제 상황

storage service에서 이미지를 직접 접근하게 하면 아래와 같은 문제점들이 있다. 

  • 조회 및 다운로드 시간이 오래 걸림
  • 보안 관련 문제 발생 가능
  • storage service에 요청 수와 데이터 전송량이 증가하여 비용이 상승할 수 있음

 

이 외에도 여러 문제점들이 있을 수 있다고 한다. 

 

따라서 storage service에서 직접 이미지 조회하고 다운로드하는 것보다 다른 방법을 생각해 봤다. 

 

해결 과정

이런 문제에 대해 고민하던 와중 CDN이라는 것을 알게 되었다

 

CDN이 무엇인지는 추후에 정리를 해서 글을 작성해 보겠다.

 

이런 CDN을 도입하면 이미지 처리 모듈에서 위의 문제점들을 해결 할 수 있을 것이라 생각했다. 

 

따라서 조회 및 다운로드는 CDN에서 처리하도록 하기로 결정하였다. 

 

위의 문제들을 아래의 방법으로 해결하였다. 

  • 조회 및 다운로드 시간이 오래 걸림
    • →  CDN에 캐싱 기능을 구현해 성능 개선
    • → 늘 직접 stroage service에서 이미지 조회 및 다운로드하지 않고 CDN에만 접근해 속도 문제 개선
  • 보안 관련 문제 발생 가능
  • storage service에 요청 수와 데이터 전송량이 증가하여 비용이 상승할 수 있음
    • Backend Client에게 CDN URL을 반환
    • storage service의 URL은 숨겨 보안 및 비용 관련 문제도 해결

 

그래서 local에서 테스트한 결과 11.31MB의 이미지의 경우 534ms → 212ms로 이미지 조회 속도를 개선하였다. 

 

CDN Server 캐싱 동작 구현 방법

개요

썸네일이나 아이콘 같이 거의 바뀌지 않고 용량이 작은 것들은 CDN Server에 계속 빼지 않고 저장하기도 한다고 들었다. 

 

이런 부분은 추후에 ngnix를 활용해 static CDN Server를 만들어 볼 생각이다. 

 

하지만 용량이 크거나 자주 바뀌는 이미지는 CDN Server에 계속 가지고 있을 수 없다. 

 

따라서 CDN Server에 캐싱을 적용하기로 하였다. 

 

CDN Server에서 얼마동안 이미지를 캐싱해 가지고 있는다.

 

그리고 조회 및 다운로드 요청 왔을 때 storage service 가지 않고 CDN Server에서 이미지를 반환할 수 있게 하는 것이다. 

 

이렇게 캐싱을 구현하려고 하는데 고민 사항이 생겼다.

 

문제 상황

이미지를 캐싱하는 것이기 때문에 CDN Server에 우선 이미지를 저장해야 한다. 

 

그리고 캐싱하는 시간이 지나면 이 이미지를 삭제를 해야 한다. 

 

어떤 방식으로 이 CDN의 캐싱 방식을 구현할지 고민이 되었다. 

 

여러 방법을 생각해보던 와중 Redis를 생각하게 되었다.

 

캐싱으로 자주 사용하기도 해서 Redis를 떠올렸다. 

 

그런데 Redis에 이미지 같이 용량이 큰 것들은 넣지 않는다고 했다. 

 

해결 과정

이 문제를 해결하기 위해 Redis의 다른 기능들을 사용하기로 하였다. 

 

Redis의 TTL과 EventListener를 통해 CDN의 캐싱 기능을 구현하기로 하였다. 

 

인메모리 DB 중 TTL과 EventListener 기능을 모두 가진 것은 Redis뿐이였다. 

 

또한 Redis는 수평적으로 확장 가능해 추후 지역 기반 CDN으로 확장 시 유용하다고 판단하였다. 

 

해결한 과정은 아래와 같다. 

  1. 이미지 CDN Server에 저장 시 Redis에 key = CDN URL, value = CDN Server내 이미지 저장 경로로 Redis에 저장
  2. 저장과 동시에 Redis에 TTL을 캐싱 시간으로 설정
  3. Redis에서 TTL 만료 시 event 발생
  4. Spring에 Redis 만료 이벤트 수신하는 EventListener 등록해 놔서 event 수신
  5. EventListener에서 event 수신 시 이미지 삭제 로직 실행

 

이제 EventListener에서 이미지 삭제 로직을 구현하려고 했는데 EventListener에서 받아오는 것은 event가 발생한 key만 받아왔다. 

 

value 값은 TTL이 만료된 후 바로 삭제되어 알 수가 없었다. 

 

따라서 이미지 저장 시 backup:CDN URL 이런 key에다가 이미지 저장 경로를 한번 더 저장해 뒀다. 

key value
CDN URL 이미지 저장 경로
backup:CDN URL 이미지 저장 경로

 

그리고 EventListener에서 이 backup key를 찾아 이미지 저장 경로를 받아온 후 해당 이미지를 삭제했다. 

 

삭제 후 이 backup key까지 같이 삭제하도록 구현해 CDN Server의 캐싱을 구현하였다.