새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.
TIL) 필터 공부하다가 알게 되었다.
📌 2025-04-01 TIL
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);
}
}
- 원본 객체는 필터에서 주입받은 객체이다.
- 물론 필터가 주입 받은 객체도 원본 객체가 아닐 수 있다.
- 그러나 원본 객체에 대해서
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()
는 커밋을 유발하며, 이후 헤더 수정은 불가능하다.
댓글남기기