우와한 개발자

[Spring] HTTP Method Conversion과 ETag(Entity Tag)란? X-HTTP-Method-Override, ShallowEtagHeaderFilter

by 우와한개발자

[HTTP Method Conversion]

1. HTTP Method Conversion이란?

  • 일부 방화벽, 프록시, 오래된 브라우저는 GET, POST만 허용하고 PUT, PATCH, DELETE 요청이 차단되는 경우 발생하는데 이때, POST 요청으로 PUT, PATCH, DELETE를 흉내낼 수 있게 해주는 기술
  • POST 요청에 X-HTTP-Method-Override 헤더를 추가해 실제 메서드를 명시
  • HttpFilter를 이용해 구현
클라이언트
  POST /v1/users/123
  X-HTTP-Method-Override: DELETE
        ↓
   [HttpFilter에서 감지 후 변환]
        ↓
  서버는 DELETE /v1/users/123 으로 처리

 

구분 Filter HttpFilter
도입 Servlet 2.3 Servlet 4.0+
파라미터 타입 ServletRequest (타입 캐스팅 필요) HttpServletRequest (바로 사용 가능)
사용 편의성 매번 캐스팅 필요 HTTP 특화 메서드 바로 사용 가능

 

2. 구현 : HttpMethodOverrideFilter.java

@Component
public class HttpMethodOverrideFilter extends HttpFilter {

    private static final String HEADER_NAME = "X-HTTP-Method-Override";
    private static final Set<String> ALLOWED_OVERRIDE =
            Set.of("PUT", "PATCH", "DELETE");

    @Override
    protected void doFilter(HttpServletRequest request,
                            HttpServletResponse response,
                            FilterChain chain)
            throws IOException, ServletException {

        String overrideMethod = request.getHeader(HEADER_NAME);

        // POST 요청이고, 유효한 Override 헤더가 있을 때만 변환
        if ("POST".equalsIgnoreCase(request.getMethod())
                && overrideMethod != null
                && ALLOWED_OVERRIDE.contains(overrideMethod.toUpperCase())) {

            chain.doFilter(
                new MethodOverrideRequestWrapper(request, overrideMethod), response
            );
        } else {
            chain.doFilter(request, response);
        }
    }

    // HttpServletRequestWrapper로 메서드만 교체
    private static class MethodOverrideRequestWrapper extends HttpServletRequestWrapper {

        private final String overrideMethod;

        public MethodOverrideRequestWrapper(HttpServletRequest request, String overrideMethod) {
            super(request);
            this.overrideMethod = overrideMethod.toUpperCase();
        }

        @Override
        public String getMethod() {
            return overrideMethod;  // POST 대신 PUT/PATCH/DELETE 반환
        }
    }
}

 

[ETag(Entity Tag)

1. ETag(Entity Tag)란?

  • HTTP 프로토콜에서 사용되는 웹 캐싱(웹 리소스의 변경 여부 확인) 및 동시성 제어를 위한 식별자
  • 서버는 특정 리소스의 버전을 식별하기 위해 ETag 사용
  • 클라이언트는 ETag를 저장해두었다가 다음 요청 시 서버에 전달해 변경 여부 확인
  • 생성 방식 : 응답 본문 전체의 해시값 (파일 해시, 수정 시각, 버전 식별자 등)
⚠️ 단순 hashCode()나 id값은 충돌 위험 및 변경 감지 불가해 사용 비추천

 

2. ETag 동작 원리

  • 서버에서 리소스를 반환할 때, HTTP 응답 헤더에 포함
  • 보통 파일의 해시값, 마지막 수정시각, 고유한 버전 식별자 등으로 생성됨
[최초 요청]
  클라이언트 → GET /v1/users/1
  서버       ← 200 OK
               ETag: "abc123"
               body: { id:1, name:"홍길동" }

[재요청 - 변경 없음]
  클라이언트 → GET /v1/users/1
               If-None-Match: "abc123"
  서버       ← 304 Not Modified  (body 없음, 네트워크 절약)

[재요청 - 변경 있음]
  클라이언트 → GET /v1/users/1
               If-None-Match: "abc123"
  서버       ← 200 OK
               ETag: "xyz789"  (새 ETag)
               body: { id:1, name:"김철수" }  (변경된 데이터)

 

3. Spring Boot에서 ETag 구현

  • Spring Boot는 ShallowEtagHeaderFilter를 통해 ETag를 자동으로 생성 및 비교

 

1) ShallowEtagHeaderFilter (전역 적용)

  • 모든 응답에 ETag를 자동으로 생성 및 비교
  • 응답 body 전체를 MD5로 해싱해서 ETag 생성
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    Filter shallowEtagHeaderFilter() {
        return new ShallowEtagHeaderFilter();  // 전역으로 모든 응답에 ETag 적용
    }
}

 

2) ResponseEntity.eTag() (컨트롤러 개별 적용)

  • 특정 핸들러 메서드에만 ETag를 적용하고 싶을 때
  • 개발자가 직접 ETag 값을 지정

(1) Entity에 @Version 추가

@Entity
@Table(name = "users")
@Getter @Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private int age;

    @Version                // 변경될 때마다 자동으로 1씩 증가
    private Long version;
}

 

(2) Controller

@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
    Optional<UserDto> user = userService.getUserById(id);

    if (user.isPresent()) {
        //  hashCode() 대신 version 필드 사용
        String eTag = "\\"" + user.get().getVersion() + "\\"";

        return ResponseEntity
                .ok()
                .eTag(eTag)
                .body(user.get());
    } else {
        return ResponseEntity.notFound().build();  // 404 Not Found
    }
}
@Version 필드는 JPA가 UPDATE 시마다 **자동으로 1씩 증가**시켜줌
→ 리소스 변경 여부를 정확하게 감지 가능
→ hashCode()처럼 충돌 위험 없음
구분 ShallowEtagHeaderFilter ResponseEntity.eTag()
적용 범위 전역 (모든 응답) 특정 메서드만
ETag 생성 body 전체 MD5 해싱 (자동) 개발자가 직접 지정
설정 위치 Config 클래스 Controller 메서드
구현 난이도 쉬움 직접 관리 필요
세밀한 제어 불가 가능

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기