Antman 프로젝트 개발기

들어가며

최근 사용자 미디어를 위한 클라우드 서비스가 상당히 인기를 끌고 있습니다. Google Photos나 NAVER nCloud 같은 서비스를 예로 들 수 있는데요. LINE도 LINE 앨범이라는 서비스를 통해서 사용자의 사진을 서버에 영구 저장해서 언제든 찾아볼 수 있는 서비스를 제공합니다. LINE 앨범은 2013년 9월에 서비스를 시작해서 올해로 만 6년이 넘은, LINE의 대표 서비스 중 하나입니다. 많은 사용자가 활발하게 사용하고 있는 만큼 서버에 쌓여가는 데이터 양도 엄청날 수밖에 없습니다.

사진과 동영상 같은 LINE의 모든 미디어 데이터는 LINE 미디어 플랫폼에서 운영하는 OBS(Object Storage)라는 미디어 스토리지에서 관리합니다. OBS는 LINE과 LINE의 모든 패밀리 서비스에서 사용하는 미디어 데이터를 관리하며 총 100 PB(petabyte) 정도의 스토리지를 사용합니다. 그리고 그중 30 PB 정도를 차지하는 것이 바로 LINE 앨범 서비스입니다. 모든 데이터가 서버에 저장되어야 하다 보니, 순수하게 스토리지 서버 비용만 따져봐도 무시할 수 없는 수준의 금액이 지출되고 있습니다.

돈도 돈이지만 가장 큰 문제는 데이터 센터의 서버 상면(서버 공간을 임차하는 것)입니다. 서비스가 활성화되는 것은 분명 기뻐할 일입니다. 하지만 영구저장 정책이 적용된 서비스는 서비스 초기부터 쌓여가는 모든 데이터를 서버에 저장해야 하고, 분명히 어느 시점에는 그 한계에 봉착할 수밖에 없습니다. 비유를 하자면, 인구가 점점 늘어나 결국 집이 부족해지는 상황이라고 할까요?

 

최초의 스토리지 효율화 전략

사실 한참 전부터 앨범 서비스에서 급증하는 데이터에 대해 고민했었고, 이미 한 번 개선 작업을 진행했습니다. OBS는 사용자들의 앨범 데이터 소비 패턴을 분석해서 Long-tail 형태를 나타낸다는 결론을 도출했습니다.

사용자는 새로 업로드된 사진들은 자주 보지만, 오래된 사진들은 좀처럼 보지 않는다는 것을 알 수 있었습니다. 결론적으로 오래된 사진들은 잘 조회되지도 않은 채 스토리지 공간을 차지하고 있었습니다. 하지만 그렇다고 임의로 사용자의 데이터를 삭제할 수는 없습니다.

그래서 OBS는 스토리지를 효율적으로 사용하기 위해 업로드 시기에 따라 저장 공간을 구분하기 시작했습니다. 비교적 요청이 많은 신규 데이터들은 고성능의 SSD가 장착된 서버에 저장하고, 업로드 후 일정 기한이 지나면 저성능, 고용량의 SATA가 장착된 서버인 고(高)집적 스토리지로 이전하는 전략을 도입했습니다. 우리는 이 스토리지 구분 전략을 ‘스토리지 레이어링’이라고 정의합니다. OBS에서 사용하는 대부분의 스토리지가 안정성을 위해서 3개의 복사본을 유지하지만 고집적 스토리지는 2개의 복사본만 유지합니다. 이 스토리지 레이어링 전략을 적용하여 데이터 센터의 공간이 부족한 문제를 어느 정도 해결할 수 있었습니다.

하지만, 스토리지 레이어링도 데이터를 좀 더 효율적으로 저장할 수 있을 뿐이었습니다. 시간이 지나면서 앨범 서비스는 다시 한 번 데이터 센터 상면의 한계를 위협하는 상황을 만들었습니다. 결국 우리는 좀 더 효과적인 방법을 찾아야 했고, ‘Antman’을 개발하게 되었습니다.

 

HEIF를 도입하자

Antman의 역할은 아주 간단합니다. 흔히 ‘콜드(cold) 스토리지’라고 부르는, 좀처럼 조회되지 않는 고집적 스토리지의 JPEG 파일을 HEIF(High Efficiency Image File Format) 파일로 교체하는 것입니다. HEIF는 JPEG보다 압축 효율이 2배 정도 높은 이미지 포맷인데요. 이해를 돕기 위해 먼저 JPEG과 HEIF에 대해 간략하게 소개하겠습니다. 

JPEG(Joint Picture Expert Group)은 1994년에 처음 소개되어 지금까지도 가장 범용적으로 사용되는 이미지 포맷입니다. 우리가 사용하는 대부분의 비디오 코덱도 JPEG의 알고리즘을 조금씩 개선하는 방식인 만큼, 비디오 압축의 ‘시조새’격이라고 볼 수 있습니다. 또한 범용적으로 널리 사용되다 보니 대부분의 모바일 프로세서가 하드웨어 JPEG 코덱을 포함하고 있고, 대부분의 소프트웨어 코덱도 최적화가 매우 잘 되어 있습니다. 하지만 세월이 야속하게도 벌써 출시 후 25년이나 지난 만큼, JPEG보다 성능이 월등히 좋은 이미지 포맷들이 계속해서 등장하고 있는데요. 그중 하나가 바로 HEIF(High-Efficiency Image File Format)입니다.

HEIF는 2013년 처음 공개된, HEVC(a.k.a H.265)를 기반으로 한 이미지 포맷입니다. HEVC는 최근 가장 주목받는 비디오 코덱 중 하나입니다. 영상 특성에 따라 다르지만, 전작인 H.264보다 압축 효율이 2배 정도 높다고 알려져 있습니다. HEIF는 영상 데이터를 감싸는 컨테이너 역할을 하며, 실제 영상 데이터는 HEVC로 압축되어 있습니다. 스틸 이미지는 HEVC의 Key-frame 한 장으로 구성할 수 있으며, 애니메이션 이미지는 HEVC 동영상 압축과 동일하게 처리할 수 있습니다. 투명도 조절을 위한 알파 채널까지 지원하기 때문에 현재 통용되는 대부분의 이미지 포맷을 대신할 수 있습니다. 게다가 HEIF는 기존 JPEG에서 지원하는 부가 정보를 그대로 담을 수 있습니다. JPEG에서 가장 널리 사용되는 부가 정보인 Exif와 XMP도 HEIF에서 공식적으로 지원하고 있습니다. 결론적으로, HEIF는 앨범 서비스에 쌓여있는 JPEG 이미지들을 좀 더 효율적으로 저장하기 위한 최적의 이미지 포맷이라고 볼 수 있습니다.

 

GPU를 적용하자

HEVC는 뛰어난 효율만큼 계산량도 상당히 클 수밖에 없습니다. 구현에 따라 다르겠지만, JPEG 코덱의 수백 배 이상의 계산량이 필요합니다. Antman 개발을 시작할 당시, 앨범 스토리지에는 이미 20PB 이상의 JPEG 파일이 있었고, 이 파일들을 단기간에 모두 HEIF로 변환하려면 수많은 서버가 필요할 수밖에 없었습니다. 이에 대한 해결책은 생각보다 상당히 간단하게 찾을 수 있었습니다. ‘도메인에 맞는 장비를 도입하자’. 우리는 GPU가 장착된 서버를 검토하기 시작했습니다. 최근 딥러닝 기술이 서비스에 활발하게 적용되면서, GPU 서버가 딥러닝 기술만을 위한 전유물처럼 생각되는 면이 있습니다. 하지만 GPU에는 딥러닝 기술을 가속화하는 하드웨어뿐 아니라 여러 가지 이미지, 비디오 하드웨어 코덱도 내장되어 있습니다. 다행히 우리가 원하는 JPEG과 HEVC 코덱도 GPU에서 지원하고 있어서 우리는 주저 없이 GPU 서버를 선택했습니다.

GPU 코덱 인터페이스는 모두 C 혹은 C++ 언어로 제공됩니다. 그래서 저희는 JPEG ↔ HEIF 간 변환을 담당하는 C 라이브러리를 개발했고, ‘핌(Pym)’이라는 이름을 붙였습니다. 같은 사진이지만 데이터 크기를 우리가 원하는 대로 확대하거나 축소할 수 있다는 의미에서 프로젝트 이름을 동명의 마블 슈퍼히어로의 이름과 같은 ‘Antman’이라고 명명한 것처럼, JPEG ↔ HEIF 간 변환을 담당하는 핵심 라이브러리의 이름은 원작에서 Antman 능력의 원천인 핌 입자(Pym Particle)에서 따왔습니다. Antman은 JAVA Spring 기반의 웹서버로 개발되었으며, JPEG ↔ HEIF 간 변환 기능을 API로 제공합니다. 그리고 이 API들을 OBS에 제공합니다. Pym의 기능을 Antman에서 사용하기 위해서, Pym의 모든 인터페이스는 JNA(JAVA Native Access)를 통해서 연동됩니다. Antman은 새로 유입되는 JPEG의 처리는 물론, 이미 만 6년 동안 차곡차곡 쌓인 과거의 JPEG도 처리하고 있습니다. GPU를 도입하면서 향상된 처리 속도를 고집적 스토리지가 감당하기 어려운 문제도 있었고, 그 부하를 조금이라도 낮추기 위해서 연도별로 분산해서 처리하고 있습니다.

지금은 성공적으로 Antman을 서비스에 연동한 상태지만, Antman 프로젝트를 진행하면서 여러 가지 문제를 겪었습니다. 그중 몇 가지 중요한 문제와 해결 과정을 소개하려 합니다.

 

HEIF가 지원되지 않는 단말은 어떻게 할 것인가?

아무래도 HEIF가 최신의 이미지 포맷이다 보니, 지원하지 않는 단말이 많을 수밖에 없습니다. iOS의 경우에는 11버전부터 HEIF(iOS에서 사용하는 HEIC는 HEIF와 동일한 포맷이며, 분산처리를 위해서 조금 개량된 형태)를 지원하며, Android는 9.0부터 공식적으로 지원합니다. 게다가 대부분의 윈도 PC에서도 HEIF를 지원하지 않습니다. 그래서 Antman은 HEIF 미지원 단말에서 다운로드를 요청하면, 변환된 HEIF를 JPEG으로 실시간 변환하여 전송합니다. 여기서 한 가지 문제가 발생합니다.

‘JPEG으로 얼마만큼 압축할 것인가’

복원되는 JPEG의 화질을 아주 낮게 설정하면, 작아지는 파일 크기 덕분에 트래픽 비용을 줄일 수는 있지만 그만큼 화질이 열화되어 사용자 경험에 안 좋은 영향을 줄 수밖에 없습니다. 반대로 화질을 너무 높게 설정하면, 화질을 보존할 수는 있지만 커진 파일 크기만큼 ‘불필요한 트래픽 비용’이 발생합니다.

JPEG과 HEIF는 모두 손실 압축 방법의 한 가지로 변환을 할 때마다 화질이 점점 열화되는 현상이 생깁니다. 인터넷에 떠도는 일명 ‘화질구지‘ 이미지들도 반복적으로 공유, 캡처하는 과정에서 화질이 열화된 것입니다. 한 번 구겨진 종이는 아무리 잘 펴도 접힌 부분이 보이는 것처럼, 손실 압축 방식에서 한 번 압축된 이미지는 아무리 높은 화질로 다시 압축한다고 해도 이전보다 좋아지지 않습니다. 이런 이유로 ‘불필요한 트래픽 비용’이라고 표현했습니다.

 

원본 JPEG DQT 백업

그렇다면 HEIF를 JPEG으로 변환할 때 사용할 적절한 JPEG 압축률은 어떻게 결정할 수 있을까요? 우리는 상당히 직관적이고 간단한 가정을 세웠습니다. 원본 JPEG의 압축 설정을 저장해서 추후 HEIF을 JPEG으로 실시간 변환할 때 사용하면 파일 크기와 화질 사이에서 최적의 균형점을 찾을 수 있을 거라고 예상했습니다. JPEG에서 압축률을 결정하는 단 한 가지 정보는 바로 De-Quantization 행렬입니다. De-Quantization 행렬은 8×8 coefficient block에 그대로 매칭되는 64개의 8비트 또는 16비트 값으로 이뤄집니다. Luminance와 Chrominance 성분을 처리하기 위해 각각 행렬을 사용하기도 하고, 하나의 행렬만 사용하기도 합니다. JPEG에서는 ‘DQT’라는 헤더(header)에 해당 행렬이 어떤 성분을 위해서 사용되는지 등의 정보와 함께 저장합니다. 우리는 변환된 HEIF의 어딘가에 이 DQT라는 정보를 그대로 백업하기로 결정했고, 추가로 HEIF에서 JPEG으로 변환할 때 유용하게 사용할 수 있는 정보를 함께 저장하면 좋겠다는 생각을 했습니다. 그래서 이 정보들을 모아서 Pym 헤더라는 자체 포맷을 개발했습니다.

Pym 헤더는 3바이트의 ‘PYM’이라는 식별자로 시작하며, 다음과 같은 정보를 포함합니다.

  • SAR(Sample Aspect Ratio)
    • JFIF(JPEG File Interchange Format)라는 JPEG 파일의 확장 포맷에서 추가로 저장하는 정보입니다. 픽셀을 1:1의 정방형으로 간주하지 않고, 임의의 비율의 사각형으로 표현할 수 있습니다. 예를 들어, 이미지의 해상도를 1080×1080이라고 가정하겠습니다. 이 이미지는 화면상에 보일 때 정방형의 이미지로 보여야 합니다. 하지만 각 픽셀의 모양을 정의하는 SAR이 16:9라면, 정방형의 이미지가 16:9의 비율을 갖는 이미지로 표현됩니다.
  • Original Image Resolution
    • 해상도는 이미 HEVC에서 정의하기 때문에, 별도로 저장할 필요가 없을 수 있습니다. 하지만 JPEG은 1 픽셀 단위로 해상도 지정이 가능한 것에 비해, HEVC는 2 픽셀 단위까지만 해상도 지정이 가능합니다. 따라서 홀수 해상도를 갖는 JPEG을 HEIF로 변환한 후 다시 JPEG으로 변경했을 때, 원래 해상도를 유지하지 못하는 문제가 있습니다. 이 문제를 해결하기 위해서 원본 JPEG의 해상도를 추가로 저장해 둡니다. 예를 들어, 원본 JPEG의 해상도가 1920×1079라고 하면, 변환된 HEIF의 해상도는 1920×1080이 되고, 이 HEIF를 다시 JPEG으로 변환하면 1920×1080이 나옵니다. 따라서 원본과 동일한 JPEG을 제공하기 위해서는 원본의 해상도인 1920×1079를 저장하고 있어야 합니다.
  • DQT
    • Luminance 또는 JPEG 전반에 사용되는 DQT를 저장합니다. 해당 내용은 JPEG의 DQT 마커(Marker)와 DQT 헤더 길이(Header Legnth)를 제외한 DQT 정보를 그대로 사용합니다. 가장 상위의 4비트는 DeQuantization 테이블의 정밀도를 결정하는 역할을 하며, 0과 1은 각각 8비트 혹은 16비트 정밀도를 표현합니다. 다음 4비트는 DQT의 고유 번호로, 각각의 이미지 컴포넌트가 사용할 DQT를 지정할 때 참조됩니다. 그리고 실제 DeQuantization 테이블이 추가됩니다. 총 64개의 계수를 포함하기 때문에 8비트 정밀도인 경우에는 64바이트, 16비트 정밀도인 경우에는 128바이트의 데이터가 포함됩니다.
  • DQT for chroma(옵션)
    • JPEG은 어떠한 색상 포맷이라도 컴포넌트의 개수가 4개 이하라면 사용할 수 있습니다. 하지만 Antman에서는 원본의 색상 포맷이 YUV인 경우만 처리하고 있습니다. JPEG에서는 Y, U 그리고 V의 모든 컴포넌트가 하나의 DQT를 사용하기도 하지만, 경우에 따라서 색차 신호인 U,V에 대해서는 별도의 DQT를 사용하는 경우도 있습니다. 원본에 U,V를 위한 별도 DQT가 있는 경우에 해당 정보를 추가로 저장합니다.

이렇게 생성된 Pym 헤더를 변환된 HEIF에 포함시킵니다. HEIF는 동영상에서 많이 사용하는 MP4 컨테이너를 개량해서 만들었습니다. MP4는 기본적으로 BOX 구조를 갖고 있으며, HEIF는 기존 MP4에 이미지 저장에 필요한 정보를 담는 몇 가지 BOX를 추가한 형태로 볼 수 있습니다. MP4의 여러 가지 BOX 타입 중 BOX와 BOX 사이의 패딩(padding) 역할을 하는 ‘FREE’ BOX가 있습니다.  ‘FREE’ BOX은 MP4 파일을 생성하는 과정에서 유동적으로 변할 수 있는 BOX의 공간을 미리 확보하기 위한 역할을 하는, 요컨대 이미지 자체에 어떤 영향도 미치지 않는 BOX입니다. 우리는 이 ‘FREE’ BOX에 Pym 헤더를 추가해서 HEIF 파일의 끝에 추가했습니다. 이렇게 저장된 Pym 헤더는 HEIF를 JPEG으로 변환하는 과정에서 추출, JPEG을 압축하는 설정으로 사용됩니다.

 

가정의 증명

앞서 우리는 원본 JPEG의 DQT를 HEIF을 JPEG으로 변환할 때 그대로 사용하면 파일 크기와 화질 사이에서 최적의 균형점을 찾을 수 있다고 가정했습니다. 이 가정을 증명하기 위해서 우리는 일부 샘플을 대상으로 테스트를 진행했습니다. 테스트 방식은 다음과 같습니다.

  • 비교 군 1 : HEIF → JPEG 변환 시 백업된 DQT를 그대로 사용
  • 비교 군 2 : HEIF → JPEG 변환 시 백업된 DQT/2를 사용
    • DQT는 Quantization 스텝 크기이기 때문에 작을수록 높은 화질의 이미지 획득 가능

아래 그래프에서 파란색 선은 각 비교 군 1과 2의 파일 크기 차이를 나타내며, 비교 군 1이 비교 군 2보다 평균 43% 정도 파일 크기가 작습니다. 빨간색 바 그래프는 원본과 비교 군 1, 2 각각의 화질 차이의 차분입니다. 비교군 간 화질 비교용 IQA(Image Quality Assessment) 알고리즘은 SSIM(Structural Similarity)을 사용했으며, 샘플 대부분의 화질이 비슷한 수준이라는 것을 확인할 수 있습니다. 몇몇 샘플에서 0.01 정도의 SSIM 차이가 발생하지만, 최대값 1을 기준으로 생각하면 상당히 낮은 값입니다. 결론적으로 HEIF를 JPEG으로 변환하는 과정에서 JPEG의 압축 강도를 원본의 압축 강도 보다 낮춘다고 해도 파일 크기만 늘어날 뿐 이미지의 품질이 좋아지지는 않는다는 것을 알 수 있습니다.

요컨대 Antman은 HEIF 미지원 단말을 위해서 실시간 JPEG 변환 기능을 제공하며, 파일 크기와 화질 사이에서 최적의 균형점을 찾기 위해 HEIF를 JPEG으로 변환할 때 원본 JPEG의 DQT를 사용합니다.

 

JPEG에서 HEIF로 제대로 변환된 건가?

위에서 HEIF를 지원하지 않는 단말을 위한 실시간 JPEG 변환 아이디어를 설명했습니다. 이제 JPEG을 HEIF로 변환하기만 하면 됩니다. 하지만 JPEG을 HEIF로 변환하는 걸로 끝나는 게 아닙니다. 변환 후에 원본 JPEG을 삭제해야 비로소 스토리지 감축 효과를 얻을 수 있습니다. 하지만 원본 JPEG을 삭제하면 HEIF에 문제가 발생해도 되돌릴 수 있는 방법이 전혀 없습니다. 그래서 우리는 변환된 HEIF가 원본 JPEG의 품질을 그대로 유지하는가에 대한 확인이 필요합니다. 여기서 품질에 대한 확인은, ‘이 정도면 봐줄 만한데?’의 수준이 아닌, ‘원본 JPEG이 가진 영상 열화까지 그대로 표현하는가?’하는 수준의 확인입니다. 수량이 많지 않다면 직접 눈으로 확인할 수도 있겠지만 LINE 앨범은 최대 초당 1,000개 정도의 JPEG이 업로드되는 서비스입니다. 이 정도로 방대한 수량의 변환을 모두 눈으로 확인한다는 것은 사실상 불가능하며, 사용자의 데이터이기 때문에 볼 수도 없습니다. 그래서 우리는 ‘변환 결과가 원본의 화질을 그대로 유지하는가’를 확인하는 기술을 개발해야 했습니다.

변환 결과에 확신을 갖기 위해 우리는 원본 JPEG과 변환된 HEIF를 비교하기로 했습니다. 일반적인 IQA(Image Quality Assessment) 방식과 같이, 저희도 원본 JPEG과 변환된 HEIF를 각각 디코딩한 RAW 이미지를 비교하기로 했습니다. 하지만 Antman에서 사용하는 GPU의 하드웨어 JPEG/HEVC 코덱의 안정성을 100% 확신할 수 없습니다. 다시 말하면, Antman의 하드웨어 HEVC 인코더로 압축한 이미지가 Antman의 하드웨어 HEVC 디코더에서 정상 인식된다고 해서, 모든 HEVC 디코더에서 정상 동작한다고 확신할 수는 없다는 것입니다. 그래서 우리는 GPU 덕분에(?) 남는 CPU 리소스를 레퍼런스 코덱을 구동하는 데 사용하기로 했습니다. 일반적으로 많이 사용되는 소프트웨어 JPEG, HEVC 디코더를 CPU에서 구동하고 그 결과 RAW 이미지도 비교 군에 포함합니다. 그리고 변환된 HEIF로부터 다시 복원된 JPEG을 하드웨어와 소프트웨어 코덱으로 각각 디코딩한 RAW 이미지 2개도 비교 군에 포함합니다. 결론적으로 우리는 총 6가지의 RAW 이미지 비교 군을 두고, 그 사이 가장 낮은 IQA 값을 기준으로 사용하기로 했습니다.

비교 군 간에 어떤 IQA를 사용할 것인가를 두고, 여러 가지 방식을 시도해봤습니다. 변환 후 원본 JPEG을 삭제하면 다시 되돌릴 수 없기 때문에 상당히 오랜 시간 동안 다양한 방식을 시도하고, 변경했습니다.

 

PSNR & SSIM

처음 시도한 방식은 가장 기초적인 IQA인 PSNR(Peak Signal to Noise Ratio)과 SSIM(Structural Similarity)입니다. PSNR은 단순하게 각 픽셀 간의 차이 값에 기초한 전통적인 IQA 방식입니다. 그와 달리 SSIM은 좀 더 HVS(Human Visual System)를 고려한 알고리즘으로 볼 수 있는데요. PSNR과 같이 단순한 픽셀 값의 차이가 아닌 이미지 속 작은 블록 안에서 신호 변화에 대한 상관관계가 기반인 IQA 알고리즘입니다. 두 가지 알고리즘을 검토한 결과 각각에서 장단점을 발견했는데요. Antman의 목적을 고려하면 PSNR을 선택하는 것이 좀 더 합리적이라는 판단이 들었습니다. Antman의 목적은 사용자들이 변화를 느끼지 못할 만큼 원본 JPEG의 화질을 그대로 HEIF로 변환하는 데 있습니다. 결국 원본 JPEG이 갖는 영상 열화까지 그대로 HEIF에 나타내야 합니다. 따라서 HVS를 고려한 고도화된 IQA 방식은 오히려 Antman의 목적과 맞지 않는다는 결론을 내렸습니다. 그런데 PSNR을 선택해서 상당히 많은 샘플을 이용해 테스트를 진행하다가 생각지 못했던 문제에 당면했습니다. 바로 PSNR은 이미지 전체에 대한 평균값이라는 문제였습니다.

아래와 같이 배경이 전반적으로 단순하면서 아주 작은 영역에 복잡한 패턴이 있는 이미지를 예로 들어보겠습니다. 

위의 두 이미지 간 PSNR 차이는 50dB 정도로, 일반적인 기준에서 두 이미지의 화질 차이는 거의 없다고 볼 수 있습니다. 비트율(bitrate)이 낮아져도, 단순한 배경에서는 복잡한 신호가 없기 때문에 화질의 열화 현상이 좀처럼 발생하지 않습니다. 하지만 빨간색 박스 구간엔 비교적 복잡한 형태의 신호가 포함되어 있으며, 눈에 띄게 열화 현상이 발생한 것을 알 수 있습니다. 결국 PSNR은 평균의 함정 때문에 국부적으로 발생하는 화질 열화를 검출하기 어렵다는 결론을 내렸습니다.

 

Grid PSNR

국부적으로 발생하는 화질 열화를 쉽게 찾아내기 위해서, 우리는 Grid 방식의 PSNR을 적용해 보았습니다.

그림과 같이 이미지를 특정 크기의 타일(tile)로 나눠서 각각의 Tile에서의 PSNR(N,M)을 측정한 후, 가장 낮은 값을 대푯값으로 사용하는 방식입니다. Grid PSNR 방식으로 어느 정도 국부적인 열화 현상을 찾을 수 있었지만, 여러 가지 새로운 고민이 뒤따랐습니다. 우선 타일의 크기에 따라서 대표 PSNR 값의 변동이 컸습니다. 또 하나의 문제점은 아래와 같이 타일과 타일의 경계면에 열화가 집중된 경우, 열화가 두 타일로 분산되어 검출하기 어렵다는 점입니다. 결국 모든 이미지에 절대적으로 맞는 타일 구조를 찾는 것은 상당히 어렵다고 판단했습니다.

 

Sliding-window PSNR

Grid PSNR의 두 가지 문제를 바탕으로 우리는 다음과 같은 결론을 도출했습니다.

  • 타일의 크기가 작을수록 국부적인 열화를 찾아내기 쉽다.
  • 타일 경계면의 국부적인 열화를 찾아내기 위해 타일은 서로 촘촘하게 겹쳐지도록 구성해야 한다.

결론적으로 아래와 같이 Sliding-window 방식의 PSNR 추출 방식을 시도했습니다. Sliding-window PSNR은 국부적인 열화 구간을 찾아내는 데 상당히 뛰어난 성능을 보여주었습니다. 커피 거름망처럼 촘촘한 PSNR 측정을 통해 모든 열화 구간을 찾아낼 수 있었습니다.

 

하지만 우리는 또 다른 문제에 봉착했습니다. 작은 타일로 하나의 이미지를 픽셀 단위로 옮겨가며 PSNR을 추출하다 보니, 하나의 이미지를 검수하는 데 엄청나게 많은 PSNR 계산이 필요하게 되었습니다. 예를 들어, 8×8 타일로 1024×1024 픽셀 이미지에 대해 Sliding-window PSNR을 수행하면 총 1,034,289번의 PSNR 계산이 필요합니다. 이동하는 픽셀의 단위를 좀 더 크게 잡으면 횟수는 줄어들지만 PSNR을 여러 번 수행해야 하는 것은 마찬가지입니다. 게다가 Pym에서 PSNR 계산은 GPU의 CUDA(Compute Unified Device Architecture) 함수를 통해 처리하는데요. CUDA 기반의 함수는 로딩과 릴리스에 많은 시간이 필요합니다. 그러다 보니 실제 PSNR을 계산하는 데에도 시간이 많이 소요되고, PSNR 계산 함수의 로딩과 릴리스도 각각 1,034,289번 필요해서 불필요하게 많은 시간이 소요될 수밖에 없었습니다.

 

Sliding-window MSE

우리는 Sliding-window PSNR을 조금 변경하여 GPU 상에서 최적화된 Sliding-window MSE(Mean Squared Error) 방식을 개발했습니다. 이 방식은 현재 Antman에 적용되어 있는 최종 형태의 IQA 알고리즘입니다.

우선 PSNR 공식을 보면 PSNR은 결국 MSE의 로그 스케일(log scale) 변환일 뿐입니다. 결론적으로 PSNR에 대한 임곗값(threshold)이 있다면 이를 MSE에 대한 임곗값으로 변환하는 것이 가능합니다. 저희는 수많은 테스트를 통해서 PSNR의 임곗값을 설정했기 때문에 해당 값을 기반으로 MSE에 대한 임곗값을 설정할 수 있었습니다. 그리고 Sliding-window를 통해서 이미지 전 구간에 걸쳐서 MSE를 구하는 방식은 아주 간단하게 구현할 수 있었습니다.

우선 각 비교 군 이미지 전반에 대해 절대차(absolute difference) 를 추출합니다. 그리고 추출된 값 간의 내적(dot product)인 를 계산하면 MSE의 공식에서 ∑ 내부 항의 결과와 동일한 결과를 만들 수 있습니다. 마지막으로 에 대해서 K x K의 커널을 이용해서 2-D Mean filter를 적용하면 Sliding-window MSE를 산출할 수 있습니다.

 

압축률 최적화

이렇게 화질 열화 구간을 찾아내는 알고리즘을 개발한 뒤 저희는 좀 더 공격적으로 압축률 향상을 꾀할 수 있었습니다. 방식은 아주 간단합니다. HEVC 인코더의 Quality Factor(값이 클수록 화질이 낮아짐)를 변경해가며, HEIF로 변환 및 IQA 진행 과정을 반복 수행합니다. 그리고 열화가 발생하지 않는 선에서 가장 높은 Quality Factor로 변환된 HEIF를 최종 결과로 선택합니다.

이렇게 결정된 최종 Quality Factor는 꾸준히 Antman에서 통계화하여 다음 이미지 처리의 초기 Quality Factor로 사용하여 시도 횟수를 최소화합니다. 초기 Quality Factor부터 시작해서 일정한 스텝 크기로 변경해가며 최적의 Quality Factor를 찾아내고 있습니다. 이와 같은 접근을 통해서 Antman은 최초 50%가 목표였던 스토리지 절감 비율을 60%까지 올릴 수 있었습니다.

 

Antman의 인프라 비용 = 0

HEVC의 높은 계산량 때문에 GPU를 도입했습니다. 장비의 사양에 따라 다르겠지만, 서버에서 사용하는 GPU 카드의 가격은 웬만한 웹서버 가격과 비슷한 수준으로 매우 고가입니다. 스토리지 비용을 줄이더라도 빠른 처리를 위해서 GPU 장비를 대거 투입한다면, 총 인프라 비용의 절감 효과가 떨어질 수밖에 없습니다. 그래서 우리는 학교 도서관에서 자주 보이는 이른바 ‘메뚜기 전략’을 쓰기로 했습니다. 잠시 비어있는 리소스를 활용하는 전략입니다. 이미 OBS에서는 해당 플랫폼의 비디오 트랜스코더(일명 Licoder)에 GPU를 적용한 바 있습니다. GPU를 적용하면서 OBS는 트랜스코딩을 위해 사용되는 막대한 인프라 비용을 절감할 수 있었습니다(관련 내용은 추후 다른 엔지니어링 블로그 글로 소개될 예정입니다).

LINE 서비스에는 새해가 시작되는 시점에 평소의 3배 정도 되는 트래픽이 유입됩니다. 연말에 안정적으로 서비스하기 위해서는 급증하는 트래픽에 걸맞은 장비를 준비해야 하는데요. 이에 따라 Licoder도 평소에는 사용하지 않아도 되는 GPU 서버를 계속 보유하고 있어야 하는 상황입니다. 물론 가끔 예상치 못한 이벤트나 사회적 이슈로 트래픽이 급증할 때 유용하게 사용할 수는 있지만, 여전히 고가의 GPU 장비를 ‘여유 장비’로 보유하는 것은 인프라 비용의 낭비일 수밖에 없습니다. 그래서 우리는 남는 GPU 장비를 Antman 장비로 사용하기로 했습니다. GPU 장비에는 Licoder 인스턴스와 Antman 인스턴스가 모두 올라갑니다. 그리고 Licoder의 요청량을 지속해서 모니터링하면서 Licoder 인스턴스에 여유가 있다고 판단되면 Licoder 인스턴스는 요청 처리를 멈추고 Antman 인스턴스가 활성화됩니다. 

결국 Licoder의 요청량이 적은 시간엔 유휴 상태로 남아 있던 GPU 장비를 Antman 장비로 사용하면서 낭비되는 인프라 비용을 줄일 수 있었고, Antman도 별도의 인프라 비용이 필요치 않게 되었습니다. 모든 GPU 장비가 항상 바쁘게 돌아가는 모습을 보는 흐뭇함은 보너스라고 할 수 있습니다.

 

드디어 LINE 앨범 서비스에 적용!

2019년 상반기를 기준으로 LINE 앨범의 스토리지 사용량은 매월 0.8 PB 정도의 증가량을 보였습니다. 증가량을 고려했을 때 Antman이 없었다면 2019년 연말까지 9 PB가 늘어난 34 PB 정도의 스토리지 사용량을 예상할 수 있었습니다. Antman은 2019년 5월 말에 LINE 앨범 서비스에 적용되었으며, 7월 말부터는 본격적으로 원본 JPEG을 삭제하기 시작했습니다. 원본을 삭제하기 시작하며 스토리지 사용량 그래프는 하향 곡선을 나타내기 시작했고, 2019년 12월 연말의 사용량은 24 PB 정도로 예상하고 있습니다. 2019년 12월 현재까지 대략 10 PB 정도의 스토리지를 절감했고, 지금도 Antman은 지난 6년간 쌓인 데이터를 처리하고 있는 중입니다. 투입 후 1년이 지난 시점인 2020년 중순 정도에 저장된 모든 파일의 처리가 끝날 것으로 예상하고 있습니다.

 

마치며

동료들과 술자리에서 이런저런 얘기를 하다 갑자기 생각난 아이디어가 지금의 Antman이 되었습니다. 사실 처음 계획을 잡을 때는 줄어들 스토리지 비용만 생각하며 호기롭게 시작했습니다. 하지만 아무래도 사용자 데이터를 영구적으로 변경하는 프로젝트이다 보니 신경 쓰이는 부분이 한두 가지가 아니었고, 서비스에 처음 적용했을 때는 뭔가 놓친 부분이 있지 않을까 하는 마음에 몇 달간 신경이 곤두서 있었고 밤잠을 설치기도 했습니다. 그래도 큰 문제 없이 매일 줄어드는 스토리지 사용량을 보면서 성취감을 느낄 수 있었고, 좋은 아이디어가 나오지 않을 때는 ‘술 한잔해야 하나’하는 농담을 주고받는 여유도 생겼습니다.

우리는 Antman을 시작으로 LINE 서비스 내에 통용되는 미디어 포맷의 고도화를 계속해서 진행할 예정입니다. LINE이 다양한 국가, 다양한 단말에서 제공되는 서비스이다 보니 모든 플랫폼이 빠르게 동기화되어 진행하기 어려운 부분이 많은 건 사실입니다. 하지만 Antman을 통한 HEIF 도입이 앞으로의 발전에 좋은 밑거름이 되어 주었으면 합니다.

마지막으로 함께 Antman을 개발하며 같은 부담감을 느꼈을 백승훈 님과 Antman의 OBS 연동을 위해서 고생해 준 신우철 님에게도 심심한 감사의 말을 전합니다.