새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

3 분 소요

📌 2025-04-01 TIL

1. 오늘의 학습 주제

  1. 필터와 commit, 그리고 응답 객체와 래퍼 객체

2. 학습 내용

@Slf4j
public class LogFilter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		// 초기화 작업이 있다면 구현
	}

	/**
	 * 응답 데이터를 캡처하기 위한 HttpServletResponseWrapper 구현
	 */
	public class CustomResponseWrapper extends HttpServletResponseWrapper {
		private ByteArrayOutputStream capture;
		private ServletOutputStream output;
		private PrintWriter writer;

		public CustomResponseWrapper(HttpServletResponse response) {
			super(response);
			capture = new ByteArrayOutputStream();
		}

		@Override
		public ServletOutputStream getOutputStream() throws IOException {
			if (writer != null) {
				throw new IllegalStateException("getWriter() 이미 호출됨");
			}
			if (output == null) {
				output = new ServletOutputStream() {
					@Override
					public void write(int b) throws IOException {
						capture.write(b);
					}

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

					@Override
					public void setWriteListener(WriteListener writeListener) {

					}

				};
			}
			return output;
		}

		@Override
		public PrintWriter getWriter() throws IOException {
			if (output != null) {
				throw new IllegalStateException("getOutputStream() 이미 호출됨");
			}
			if (writer == null) {
				writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()), true);
			}
			return writer;
		}

		// 캡처된 응답 데이터를 문자열로 반환
		public String getCapturedBody() throws IOException {
			// flush() 호출하여 writer나 output의 데이터를 확실하게 버퍼에 기록
			log.info("getCaptureBody = {}",capture.toString());
			log.info("writer = {}", writer);
			log.info("output = {}", output);
			if (writer != null) {
				writer.flush();
			} else if (output != null) {
				output.flush();
			}
			return new String(capture.toByteArray(), getCharacterEncoding());
		}

		// 캡처된 데이터를 실제 응답에 복사
		public void copyBodyToResponse() throws IOException {
			byte[] bytes = capture.toByteArray();
			getResponse().setContentLength(bytes.length);
			getResponse().getOutputStream().write(bytes);
		}
	}

	@Override
	public void doFilter(jakarta.servlet.ServletRequest request, jakarta.servlet.ServletResponse response,
		FilterChain chain)
		throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest)request;
		HttpServletResponse httpResponse = (HttpServletResponse)response;

		// 응답 데이터를 캡처할 수 있도록 CustomResponseWrapper로 감싼다.
		CustomResponseWrapper responseWrapper = new CustomResponseWrapper(httpResponse);

		try {
			// 체인 진행: 컨트롤러에서 응답 데이터를 작성하면 CustomResponseWrapper에 캡처됨
			chain.doFilter(request, responseWrapper);

			// 캡처된 원본 응답 데이터를 가져온다.
			String originalBody = responseWrapper.getCapturedBody();
			log.info("원본 응답: {}", originalBody);

			// 수정 작업 수행: 예제에서는 단순히 [Modified] 문자열을 추가
			String modifiedBody = originalBody + " [Modified]";
			log.info("수정된 응답: {}", modifiedBody);

			// 최종 수정된 응답을 클라이언트로 전송
			httpResponse.setContentLength(modifiedBody.getBytes(httpResponse.getCharacterEncoding()).length*2);
			// httpResponse.setContentLength(1);
			log.info("커밋이 되었나요??? [] {}", responseWrapper.isCommitted());
			PrintWriter writer = httpResponse.getWriter();
			writer.write(modifiedBody);
			for (int i = 0; i < 30; i++) {
				writer.println(i);
				log.info("커밋이 되었나요??? [{}] {}",i ,responseWrapper.isCommitted());
				Thread.sleep(100);
			}
		} catch (Exception e) {
			log.error("필터 처리 중 예외 발생", e);
			try {
				throw e;
			} catch (InterruptedException ex) {
				throw new RuntimeException(ex);
			}
		}
	}

	@Override
	public void destroy() {
		// 리소스 정리가 필요하면 구현
	}
}

전체 소스 코드입니다.

여기서 필터가 원본 객체를 수정할 수 있는 이유를 확인할 수 있습니다.

// 응답 데이터를 캡처할 수 있도록 CustomResponseWrapper로 감싼다.
CustomResponseWrapper responseWrapper = new CustomResponseWrapper(httpResponse);

try {
  // 체인 진행: 컨트롤러에서 응답 데이터를 작성하면 CustomResponseWrapper에 캡처됨
  chain.doFilter(request, responseWrapper);

  // 캡처된 원본 응답 데이터를 가져온다.
  String originalBody = responseWrapper.getCapturedBody();
  log.info("원본 응답: {}", originalBody);

  // 수정 작업 수행: 예제에서는 단순히 [Modified] 문자열을 추가
  String modifiedBody = originalBody + " [Modified]";
  log.info("수정된 응답: {}", modifiedBody);

  // 최종 수정된 응답을 클라이언트로 전송
  httpResponse.setContentLength(modifiedBody.getBytes(httpResponse.getCharacterEncoding()).length*2);
  // httpResponse.setContentLength(1);
  log.info("커밋이 되었나요??? [] {}", responseWrapper.isCommitted());
  PrintWriter writer = httpResponse.getWriter();
  writer.write(modifiedBody);
  for (int i = 0; i < 30; i++) {
    writer.println(i);
    log.info("커밋이 되었나요??? [{}] {}",i ,responseWrapper.isCommitted());
    Thread.sleep(100);
  }
} catch (Exception e) {
  log.error("필터 처리 중 예외 발생", e);
  try {
    throw e;
  } catch (InterruptedException ex) {
    throw new RuntimeException(ex);
  }
}
  1. 원본 객체는 필터에서 주입받은 객체이다.
  2. 물론 필터가 주입 받은 객체도 원본 객체가 아닐 수 있다.
  3. 그러나 원본 객체에 대해서 flush()를 호출하는 건 래퍼 객체를 주입한 필터의 책임이다.

컨트롤러에서 flush 호출해도

컨트롤러에서 호출한 flush()는 래퍼 내부 버퍼에만 영향을 주고, 실제 전송은 필터가 원본에 기록할 때 이루어진다

setContentLength 중요성

일부로 원본 객체의 contentlength를 변경했습니다.

그러니 `writer.write(modifiedBody);를 하고 플러시를 해도 클라이언트에게 전달이 되지 않습니다.

물론 클라이언트에게 보내기는 한겁니다.

그런데 웹 브라우저마다 스트림 처럼 보여줄 수 있으나 대부분 클라이언트는 setContentLength에 맞춰준다고 생각하도 데이터를 기다립니다.

여기서 response의 인터페이스가 중요한 역할을 합니다.

Jsp에서도 마찬가지로 한번 플러시된 response는 헤더와 바디를 수정할 수 없습니다. 추가는 가능합니다.

그래서 테스트를 해봤습니다.

// 최종 수정된 응답을 클라이언트로 전송
httpResponse.setContentLength(modifiedBody.getBytes(httpResponse.getCharacterEncoding()).length*2);
// httpResponse.setContentLength(1);
log.info("커밋이 되었나요??? [] {}", responseWrapper.isCommitted());
PrintWriter writer = httpResponse.getWriter();
writer.write(modifiedBody);
writer.flush()
for (int i = 0; i < 30; i++) {
  writer.println(i);
  log.info("커밋이 되었나요??? [{}] {}",i ,responseWrapper.isCommitted());
  Thread.sleep(100);
  writer.flush()
}
커밋이 되었나요??? [15] true

flush를 호출한 순간부터 커밋되었다고 합니다.

그러면 flush를 매번해도 결국 setContentLenth를 맞추지 못하면 클라이언트는 오류가 날 수 있다. 그것이 TCP/IP입니다.

커밋을 안해도 content-length 헤더는 응답 본문의 총 바이트 수를 말해줍니다.

클라이언트는 이 값을 기준으로 데이터를 읽고 , 정확하게 이 길이만큼 데이터가 도착하면 응답이 완료되었다고 생각하죠

실제로 Content-Length에 도달했을 때 컨테이너가 출력 스트림을 관리합니다.

Content-Length에 도달하면 스트림을 정리하거나 커밋할 수 있습니다.

배운 점 요약

  • 래퍼 객체는 응답을 가로채고 수정할 수 있지만, 실제 전송은 원본 객체에 의존한다.
  • setContentLength는 클라이언트와 서버 간 데이터 일관성을 유지하는 핵심이다.
  • flush()는 커밋을 유발하며, 이후 헤더 수정은 불가능하다.

댓글남기기