Framework/Spring

[Spring Boot]압축 전송하기

  • -
반응형

최근 팀프로젝트로 회원가입을 통해 몇 개의 이미지들을 저장 받고, 조회가 가능한 기능을 구현하고 있습니다.

농실농실과 같이 최소한의 비용으로 서버를 구동시킬 계획으로 Cafe24 호스팅을 염두해두고 있는데요. 

아무래도 파일 저장소를 따로 두기에는 비용 가늠을 할 수 없어 이미지 파일을 Base64로 변환해 DB로 저장하기로 했습니다. Cafe24의 호스팅 서비스는 서버 용량내 DB 용량이 무제한이기 때문에 주기적으로 데이터를 정리하며 DB를 활용할 예정입니다. 

 

본론으로 돌아와 이미지를 Base64로 변환하다보니 트래픽 용량을 생각해야 했습니다. 

최대 등록 가능한 이미지는 20개로 기획했고, 테스트를 통해 확인했을 때 총 20개의 이미지가 담긴 응답 크기는 455KB( 227ms)로 최적화가 필요한 수준이었습니다.

 

Spring Boot에서는 Gzip 적용을 통해 응답 크기를 최적화할 수 있습니다.

Gzip 적용은 간단합니다.

properties 파일에 아래 항목을 추가합니다.

server.compression.enabled=true
server.compression.mime-types=text/html,text/xml,text/plain,application/json,application/javascript,image/png
server.compression.min-response-size=1024

 

  • server.compression.enabled : 압축 사용 여부
  • server.compression.mime-types : 압축 대상 mime-type
  • server.compression.min-response-size : 압축을 수행할 최소 Byte 사이즈

이후 다시 API를 호출해보면 응답 크기가 20KB(31ms)약 22배나 개선된 것을 확인할 수 있습니다.

 

다만 Gzip 압축 기능을 사용하게 되면 압축 과정에서 CPU 자원을 사용하기 때문에 성능 하락의 위험성이 존재합니다. 아직 서비스가 완성되지 않아서 운영 레벨로 넘어가게 되면 자세한 확인이 필요하겠지만 CPU 성능도 제한적일 것을 대비해 특정 API에 한해서만 압축이 적용되도록 추가적인 작업을 진행했습니다.

작업에는 여기에 작성된 코드를 참고했습니다.

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

public class GzipHttpServletResponseWrapper extends HttpServletResponseWrapper {

    private final GZIPOutputStream gzipOutputStream;
    private ServletOutputStream outputStream;
    private PrintWriter writer;

    public GzipHttpServletResponseWrapper(HttpServletResponse response) throws IOException {
        super(response);
        this.gzipOutputStream = new GZIPOutputStream(response.getOutputStream());
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (this.outputStream == null) {
            this.outputStream = new GzipServletOutputStream(this.gzipOutputStream);
        }
        return this.outputStream;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (this.writer == null) {
            this.writer = new PrintWriter(
                new OutputStreamWriter(this.gzipOutputStream, getCharacterEncoding()));
        }
        return this.writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (this.writer != null) {
            this.writer.flush();
        }
        if (this.outputStream != null) {
            this.outputStream.flush();
        }
        this.gzipOutputStream.flush();
    }

    public void close() throws IOException {
        this.gzipOutputStream.close();
    }
}

 

GzipHttpServletResponseWrapper : HttpServletResponse를 감싸 응답 데이터를 GZIP으로 압축하도록 확장한 클래스입니다.

  • getOutputStream() : 원래의 응답 추력 스트림을 GzipServletOutputStream으로 감싸고 데이터를 GZIP으로 압축합니다.
  • getWriter() : 응답 데이터를 문자 기반 스트림으로 출력하도록 처리하고, GZIP 압축을 수행합니다.

 

import java.io.IOException;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;

public class GzipServletOutputStream extends ServletOutputStream {

    private final GZIPOutputStream gzipOutputStream;

    public GzipServletOutputStream(GZIPOutputStream gzipOutputStream) {
        this.gzipOutputStream = gzipOutputStream;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {
    }

    @Override
    public void write(int b) throws IOException {
        this.gzipOutputStream.write(b);
    }

}

 

GzipServletOutputStream : ServletOutputStream을 확장해 데이터를 GZIP으로 압축할 수 있도록 구현한 클래스입니다.

  • write(int b) : 응답 데이터를 GZIPOutputStream에 기록합니다.

 

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final List<String> EXCLUDE_GZIP_URLS = Collections.singletonList("gallery");

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            // JWT 토큰 인증 로직...
            
            
            String requestUri = request.getRequestURI().split("/")[1];

            // 갤러리 조회시에만 압축 진행
            if (EXCLUDE_GZIP_URLS.contains(requestUri)) {
                GzipHttpServletResponseWrapper gzipResponse = new GzipHttpServletResponseWrapper(
                    response);
                gzipResponse.setHeader("Content-Encoding", "gzip");
                filterChain.doFilter(request, gzipResponse);
                gzipResponse.close();
            } else {
                filterChain.doFilter(request, response);
            }
        } catch (AuthException e) {
            handleException(response, e);
        }
    }
}

 

요청당 한 번만 실행 + 특정 API에서만 압축이 될 수 있게 OncePerRequestFilter를 확장하였습니다. 요청 URL의 첫번째 경로가 EXCLUDE_GZIP_URLS에 포함되어 있으면 GZIP 응답을 수행합니다.

 

결과를 확인해 보겠습니다.

/gallery/4 호출시에는 압축된 압축 크기를 확인할 수 있으며, /etc/gallery/4 호출시에는 압축되지 않은 응답 크기가 확인됩니다.

 

 

참고문서
 

Conditional Gzip Compression in Spring Boot - BootcampToProd

In this article, we will explore three ways to achieve conditional Gzip compression in Spring Boot:

bootcamptoprod.com

반응형
Contents

포스팅 주소를 복사했습니다.

이 글이 도움이 되었다면 공감 부탁드립니다.