반응형

정적 리소스 배포 시 브라우저의 캐시로 인해 변경된 내용이 반영되지 않는 이슈를 해결하기 위해
정적 리소스의 Versioning 방법을 정리하였습니다.

🤔 기존의 문제점

js, css 등 정적 리소스에 오늘 일자를 쿼리 스트링으로 추가하여 매일 새로운 파일을 다운로드 받도록 하고 있었습니다.

<script src="/r/v2/js/assets/common/common.js?ver=${useDate}"></script>

이 방식의 문제점은 일별로 쿼리 스트링이 변경 되므로 정적 리소스가 변경되지 않아도 매일 새로운 파일을 다운로드 받게 되며,
새로운 정적 리소스를 배포할 경우 이미 동일 일자에 접속한 이력이 있으면 변경된 정적 리소스를 다운로드 받지 못 하게 된다는 점입니다.

🔎 해결 방법

1) 파일명에 해시 값 추가

정적 리소스 파일명의 파일의 해시 값을 추가하여, 파일이 변경 되었을 때 새로운 해시 값이 부여되어 새로 다운로드 받을 수 있습니다.

예) <link href="/css/spring.css"/> ➡️ <link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

2) 버전 고정 값 사용

파일명의 교체 대신에 고정된 버전 값을 설정하고 이 값으로 가상의 디렉토리를 생성하여 위치하도록 resolve 하고
버전 값을 변경하면 디렉토리 명이 바뀌어 새로 다운로드 받을 수 있도록 합니다.

예) "/js/lib/mymodule.js" ➡️ "/v12/js/lib/mymodule.js"

spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/js/lib/
spring.resources.chain.strategy.fixed.version=v12

위의 방법은 Thymeleaf와 FreeMarker에서는 Spring Boot의 기본 ResourceUrlEncodingFilter가 자동으로 처리해 주지만,
일반적인 JSP 사용 시에는 별도의 ResourceUrlProvider를 사용해야 합니다.

🛠️ 개선된 해결책

JSP를 기본 템플릿으로 사용하는 환경에서는 별도의 ResourceUrlProvider 의 선언 및 추가 설정이 필요합니다.

properties 설정은 하지 않아도 됩니다.

1. ResourceHandler 추가

WebMvcConfigurer에 Version 전략 Resource handler를 추가합니다.

1) Hash 값

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/r/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
}

2) 고정 버전 관리

sacle.resources.chain.strategy.fixed.version=202212_01
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${sacle.resources.chain.strategy.fixed.version}")
    String resourcesChainStrategyFixedVersion;
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/r/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addFixedVersionStrategy(resourcesChainStrategyFixedVersion, "/**"));
    }
}

 

2. JSP 접근 허용

JSP에 정적 리소스에 접근할 수 있는 @ModelAttribute 를 추가합니다.

@ControllerAdvice
public class ResourceUrlAdvice {

    private final ResourceUrlProvider resourceUrlProvider;

    public ResourceUrlAdvice(ResourceUrlProvider resourceUrlProvider) {
        this.resourceUrlProvider = resourceUrlProvider;
    }

    @ModelAttribute("urls")
    public ResourceUrlProvider urls() {
        return this.resourceUrlProvider;
    }
}

3. JSP 적용

실제 적용할 정적 리소스의 URL 부분을 위에서 설정한 @ModelAttribute 를 이용하여 수정합니다.

기존

<script src="/r/v2/js/assets/common/common.js?ver=${useDate}"></script>
<script src="/r/v2/js/assets/common/commonApi.js?ver=${useDate}"></script>

변경

<script src="${urls.getForLookupPath('/r/v2/js/assets/common/common.js')}"></script>
<script src="${urls.getForLookupPath('/r/v2/js/assets/common/commonApi.js')}"></script>

※ 바꾸기 정규표현식
원본: "([A-Za-z0-9/.-]+)?ver=${useDate}"
변환: "${urls.getForLookupPath('$1')}"

🔬 반영 결과

개발자 도구로 정적 리소스 버저닝 확인

🔗  참고 링크

반응형
반응형

※ 본 게시물은 네이버 테크 블로그와 2015년에 출간된 〖네이버를 만든 기술, 읽으면서 배운다 - 자바 편〗 중 05. JVM 이해하기 챕터를 정리한 내용입니다.

 

JVM의 특징

  • 스택 기반의 가상 머신 : x86이나 ARM 아키텍처가 레지스터 기반인데 비해 JVM은 스택 기반으로 작동한다.
  • 심벌릭 레퍼런스 : Primitive data type을 제외한 모든 타입(클래스와 인터페이스)을 메모리 주소 기반이 아닌, 심벌릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션 : 인스턴스는 코드에 의해 명시적으로 생성되고 GC에 의해 자동 파괴된다.
  • 기본 자료형을 명확하게 정의해 플랫폼 독립성 보장 : 전통적인 언어(C/C++)는 플랫폼에 따라 기본 자료형의 크기가 달라지나, JVM은 기본 자료형을 명확하게 정의해 호환성을 유지하고 플랫폼 독립성을 보장한다.
  • 네트워크 바이트 순서(network byte order) : x86의 리틀 엔디안이나 RISC 아키텍처의 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 순서를 유지해야 하므로 네트워크로 데이터를 전송할 때 사용하는 네트워크 바이트 순서를 사용한다. 네트워크 바이트 순서는 빅 엔디안이다.

 

자바 바이트코드

  • 자바 컴파일러는 고수준 언어를 JVM이 이해하는 자바 바이트코드로 번역한다.
  • 자바 바이트코드는 플랫폼 의존적인 코드가 없기 때문에 JVM이 설치된 장비라면 CPU나 운영체제에 상관 없이 실행할 수 있다.
  • 컴파일 결과물의 크기가 소스 코드와 크게 다르지 않기 때문에 네트워크로 전송하여 실행하기 쉽다.
  • 클래스 파일 자체는 바이너리 파일이어서, javap라는 역어셈블러(diassembler)를 제공한다. (결과물을 자바 어셈블리라고 부른다.) 
  • 어셈블리 형식 
    • index : 각 메서드 기준으로 바이트 오프셋
    • opcode : 명령어 OpCode. 1바이트의 바이트 번호로 표현된다. (aload_0=0x2a, getfield=0xb4, invokevirtual=0xb6, ...) 최대 256개
    • operandN : 피연산자 (0개 이상). getfield, invokevirtual 등은 2바이트의 피연산자가 필요하다.
    • comment : 주석 (라인 마지막에 위치)
<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]

 

  • 자바 바이트코드의 명령어는 1바이트의 OpCode(최대 256개)와 2바이트 피연산자(Operand)로 분리할 수 있다.
  • 메서드를 호출하는 명령어 OpCode는 다음의 4가지가 있다.
    • invokeinterface : 인터페이스 메서드 호출
    • invokespecial : 생성자, private 메서드, 슈퍼 클래스의 메서드 호출
    • invokestatic : static 메서드 호출
    • invokevirtual : 인스턴스 메서드 호출

 

클래스 파일

ClassFile {
 u4 magic;
 u2 minor_version;
 u2 major_version;
 u2 constant_pool_count;
 cp_info constant_pool[constant_pool_count-1];
 u2 access_flags;
 u2 this_class;
 u2 super_class;
 u2 interfaces_count;
 u2 interfaces[interfaces_count];
 u2 fields_count;
 field_info fields[fields_count];
 u2 methods_count;
 method_info methods[methods_count];
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

 

  • 클래스 파일 포맷
    • magic : 자바 클래스 파일 구별을 위한 매직 넘버 (0xCAFEBABE)
    • minor_version, major_version : 클래스 파일의 버전 지정
    • constant_pool_count : constant_pool의 개수 + 1
    • constant_pool[] : 런타임 상수 풀 영역에 들어갈 정보(문자열 상수, 클래스명, 인터페이스명, 필드명, 기타 상수)
    • access_flags : 클래스의 변경자 정보(public, final, abstract, enum 등) 또는 인터페이스 여부를 나타내는 플래그
    • this_class, super_class : 각각 this, super 클래스에 대한 constant_pool 내 인덱스
    • interfaces_count, interfaces[] : 클래스가 구현한 인터페이스의 개수와 인터페이스에 대한 constant_pool 내 인덱스
    • fields_count, fields[] : 클래스의 필드 개수와 필드 정보(필드 이름, 타입 정보, modifier, constant_pool 내 인덱스 등)
    • methods_count, methods[] : 클래스의 메서드 개수와 메서드 정보(메서드 이름, 파라미터 타입과 개수, 반환 타입, modifier, constant_pool 내 인덱스, 메서드 자체 실행 코드, 예외 정보 등)
    • attributes_count, attributes[] : attribute의 개수와 attribute_info 구조체
  • javap -verbose 명령으로 클래스 파일 포맷을 사용자가 읽을 수 있는 형태로 간략하게 보여준다.

 

JVM 구조

  • 자바로 작성한 코드는 클래스 로더가 컴파일된 자바 바이트코드를 런타임 데이터 영역에 로드하고, 실행엔진이 자바 바이트코드를 실행한다.

클래스 로더

  • 자바는 동적 로드, 즉 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크한다. 이 동적 로드를 담당하는 부분이 클래스 로더이다.
  • 클래스 로더의 특징
    • 계층 구조 : 클래스 로더끼리 부모-자식 관계를 이룬다. 최상위 클래스 로더는 부트스트랩 클래스 로더이다.
    • 위임 모델 : 클래스 로더끼리 로드를 위임하는 구조로 작동한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인해 상위에 있으면 해당 클래스를 사용하고 없다면 요청 받은 클래스 로더가 클래스를 로드한다.
    • 가시성 제한 : 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만 상위는 하위의 클래스를 찾을 수 없다.
    • 언로드 불가 : 클래스 로더는 클래스를 로드할 수는 있지만 언로드 할 수는 없다. 대신 현재 클래스 로더를 삭제하고 새로운 클래스 로더를 생성할 수 있다.
  • 클래스 로더가 클래스 로드를 요청받으면, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고 없으면 상위로 거슬러 올라가며 확인한다. 부트스트랩 클래스 로더까지 확인해도 없으면 요청 받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.
    • 부트스트랩 클래스 로더(bootstrap class loader) : JVM 가동할 때 생성되며 Object 클래스를 비롯해 자바 API를 로드한다. 자바가 아닌 네이티브 코드로 구현돼 있다.
    • 확장 클래스 로더(extension class loader) : 기본 자바 API를 제외한 확장 클래스를 로드한다. (다양한 보안 확장 기능)
    • 시스템 클래스 로더(system class loader) : 애플리케이션의 클래스를 로드한다. 사용자가 지정한 CLASSPATH 내 클래스를 로드한다.
    • 사용자 정의 클래스 로더(user-defined class loader) : 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.
  • WAS와 같은 컨테이너는 웹 애플리케이션과 엔터프라이즈 애플리케이션이 서로 독립적으로 작동하도록 사용자 정의 클래스 로더를 사용하여, 애플리케이션의 독립성을 보장한다.
  • 클래스 로드 단계
    • 로드(loading) : 클래스 파일을 JVM 메모리에 로드한다.
    • 검증(verifying) : 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성됐는지 검사한다.
    • 준비(preparing) : 클래스가 필요로 하는 메모리를 할당하고 클래스에 정의된 필드, 메서드, 인터페이스를 나타내는 데이터 구조를 준비한다.
    • 분석(resolving) : 클래스의 상수 풀 내 모든 심벌릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
    • 초기화(initializing) : 클래스 변수를 적절한 값으로 초기화한다. static 필드를 설정된 값으로 초기화 한다.

 

런타임 데이터 영역

  • 런타임 데이터 영역은 JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다.

  • PC(Program Counter) 레지스터 : 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 현재 실행 중인 JVM 명령의 주소가 저장된다.
  • JVM 스택 : 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임이라는 구조체를 저장하는 스택으로, JVM은 JVM에 스택 프레임을 추가하고 꺼내는 작업만 실행한다. 예외 발생 시 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.
    • 스택 프레임 : 메서드가 실행될 때마다 하나의 스택 프레임이 생성돼 해당 스레드의 JVM 스택에 push되고 메서드가 종료되면 스택 프레임이 pop 된다. 각 스택 프레임은 지역 변수 배열, 피연산자 스택, 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다.
    • 지역 변수 배열 : 인덱스 0은 메소드가 속한 클래스 인스턴스의 this 레퍼런스이고, 인덱스 1부터는 메서드에 전달된 파라미터, 메서드 파라미터 이후에는 메서드의 지역 변수가 저장된다.
    • 피연산자 스택 : 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 push 하거나 pop 한다.
  • 네이티브 메서드 스택 : JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 실행하기 위한 스택이다.
  • 메서드 영역 : 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. 각 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 제조사마다 다양한 형태로 구현할 수 있으며, 오라클 핫스폿 VM에서는 Permanent Generation(Perm Gen)이라고 한다.
  • 런타임 상수 풀 : 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. JVM 작동에서 가장 핵심적인 역할을 실행하는 곳이다. 각 클래스와 인터페이스의 상수 뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메로리 상의 주소를 찾아서 참조한다.
  • : 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 이슈에서 가장 많이 언급되는 공간이다.

 

실행 엔진

  • JVM 내 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행된다. 실행 엔진은 바이트코드를 명령어 단위로 읽어 실행한다.
  • 실행 엔진은 자바 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경한다.
    • 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 해석은 빠른 대신 결과의 실행은 느리다. 바이트코드는 기본적으로 인터프리터 방식으로 작동한다.
    • JIT (Just-In-Time) 컴파일러 : 인터프리터 방식으로 실행하다 적절한 시점에 바이트코드를 컴파일해 네이티브 코드로 변경하고 이후 네이티브 코드로 직접 실행하는 방식이다. 인터프리팅 방식보다 빠르고 네이티브 코드를 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 실행된다.

 

JIT 컴파일러

  • JIT 컴파일러는 바이트코드를 중간 단계의 표현인 IR (Intermediate representation)로 변환해 최적화하고, 그 다음에 네이티브 코드를 생성한다.

  • 오라클 핫스폿 VM은 HotSpot 컴파일러라 불리는 JIT 컴파일러를 사용한다. 내부적으로 프로파일링을 통해 가장 컴파일이 필요한 부분 HotSpot을 찾아낸 다음, 이 HotSpot을 네이티브 컴파일한다. HotSpot VM은 컴파일된 바이트코드라도 자주 호출되지 않으면 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 작동한다.
  • IBM JVM은 JIT 컴파일러 뿐만 아니라 AOT(Ahead-On-Time) 컴파일러를 도입했다. 이미 컴파일된 네이티브 코드를 여러 JVM이 공유 캐시를 통해 공유한다. AOT 컴파일러를 통해 컴파일된 코드는 다른 JVM에서 컴파일하지 않고 사용할 수 있다. 또한 AOT 컴파일러를 통해 JXE(Java Executable) 파일 포맷으로 프리컴파일된 코드를 작성해 빠르게 실행하는 방법도 제공한다.
  • 자바 성능 개선의 많은 부분은 이 실행 엔진을 개선해 이뤄지고 있다.

 

출처

  • 강경태, 강운덕, 구태진, 김민수, 김택수, 박세훈, 송기선, 이상민, 정상혁, 최동순, 〖네이버를 만든 기술, 읽으면서 배운다 - 자바 편〗, 위키북스(2015) p.123 - 150
  • 네이버 D2 테크 블로그, JVM Internal https://d2.naver.com/helloworld/1230

 

 

반응형
반응형

※ 여기 있는 내용은 구글링을 통해 수집한 정보를 바탕으로 기존의 소스들을 재구성한 내용입니다.

참고한 소스의 URL은 글 하단에 표기하였습니다.

 

기존의 운영되던 프로젝트를 확인하다 레거시 쪽에 API를 호출하는 부분을 살펴 보게 되었습니다.

한 화면에 여러 결과를 한꺼번에 모아 보여주다 보니 페이지 접속 시 API 호출이 5번 발생하게 되더군요.

Apache의 Http Component 4.5.X를 사용하고 있는데,

좀 더 자세히 살펴 보니 API를 호출할 때 마다 Apache HttpClient를 생성하고 자원을 반환하고 있었습니다.

 

표 1) 기존 소스

CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;

try {
    URI uri = new URIBuilder()
            .setScheme("http")
            .setHost("HOST_정보")
            .setPath("URI_경로")
            .build();
    HttpPost httpPost = new HttpPost(uri);
    httpClient = HttpClients.createMinimal();
    response = httpClient.execute(httpPost);
    ...
} catch (URISyntaxException | IOException exception) {
    throw new ApiCallException("API 조회에 실패하였습니다.", exception);
} finally {
    if (response != null) {
        try {
            response.close();
        } catch (IOException ignore) {
            LOGGER.debug("HttpResponse Close 에러", ignore);
        }
    }
    if (httpClient != null) {
        try {
            httpClient.close();
        } catch (IOException ignore) {
            LOGGER.debug("HttpClient Close 에러", ignore);
        }
    }
}
 
 

여기서는 HttpClients.createMinimal()를 사용하고 있는데, createMinimal()의 소스를 보면

HttpClient를 객체를 새로 생성하고 PoolingHttpClientConnectionManager를 등록하고 있습니다.

 

표 2) HttpClients 소스 일부

public static CloseableHttpClient createMinimal() {
    return new MinimalHttpClient(new PoolingHttpClientConnectionManager());}
}
 

결국 API 호출 후 HttpClient 자원을 반환하면서 Connection Pool 도 같이 반환하게 되어

결과적으로 Connection Pool을 전혀 사용하지 못 하는 상황이 되었습니다.

 

우선 PoolingHttpClientConnectionManager를 등록해서 사용할 수 있도록 Customizing 된 HttpClient가 필요했습니다.

 

표 3) HttpClient 빈 등록

@Configuration
public class HttpClientConfig {

    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .build();
    }

}
 

먼저 HttpClient 객체를 재사용할 수 있도록 Bean으로 등록하였습니다.

 

이어서 해당 HttpClient에서 사용할 PoolingHttpClientConnectionMananger 등록이 필요했습니다.

 

표 4) PoolingHttpClietConnectionManager 추가

@Configuration
public class HttpClientConfig {

    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(poolingHttpClientConnectionManager())
                .build();
    }

    private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        return connectionManager;
    }

}
 

HttpClient에 Pooling Connection Manager까지 추가했습니다.

그런데 PoolingHttpClientConnectionManager는 기본값으로 Max Connection은 20, Route 당 Connection 수는 2였습니다.

실제 운영환경에서는 접속자가 많아질 경우 Connection 할당이 되지 않는 경우가 생길 거 같네요.

적당한 값으로 조정했습니다.

 

표 5) Max Connection 수 및 Route 당 Connection 수 할당

@Configuration
public class HttpClientConfig {

    private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
    private static final int MAX_CONNECTIONS_TOTAL = 100;

    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(poolingHttpClientConnectionManager())
                .build();
    }

    private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
        connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL);
        return connectionManager;
    }

}
 

이렇게 해서 Pooling Connection Mananger가 적용된 HttpClient Bean이 만들어졌습니다.

이제 호출해서 잘 쓰기만 하면 되겠네요.

 

저희 서비스는 Spring 환경에서 구축되었기 때문에 HttpClient Bean을 Injection 하여 사용하였습니다.

 

표 6) Customizing된 HttpClient Bean을 이용한 API 호출

private CloseableHttpClient myHttpClient;

@Autowired
public void setMyHttpClient(CloseableHttpClient myHttpClient) {
    this.myHttpClient = myHttpClient;
}

...

// 실제 비즈니스 코드 부분
CloseableHttpResponse response = null;

try {
    URI uri = new URIBuilder()
            .setScheme("http")
            .setHost("HOST_정보")
            .setPath("URI_경로")
            .build();
    HttpPost httpPost = new HttpPost(uri);
    response = myHttpClient.execute(httpPost);
    ...
} catch (URISyntaxException | IOException exception) {
    throw new ApiCallException("API 조회에 실패하였습니다.", exception);
} finally {
    if (response != null) {
        try {
            response.close();
        } catch (IOException ignore) {
            LOGGER.debug("HttpResponse Close 에러", ignore);
        }
    }
}
 

이제 Connection Pool이 적용된 HttpClient를 사용하여 API를 호출하게 되었습니다.

이제 매번 Socket Open을 하지 않아도 되게 되었습니다.

 

여기까지만 해도 코드는 잘 작동합니다.

그런데 관련 내용을 좀 더 찾다 보니 Idle Connection에 대한 자원 반환 이슈가 있더군요.

Socket 통신을 할 경우 정상적으로 종료되지 않을 경우 TIME_WAIT에 걸리며 자원이 제대로 반환되지 않는 현상이 발생하게 되고

TIME_WAIT이 점점 늘어나다 보면 나중에는 할당할 수 있는 자원이 남지 않게 되지요.

 

만료되거나 비정상적으로 종료된 Connection에 대한 반환 작업이 필요했습니다.

 

주기적으로 사용하지 않는 Connection을 반환하기 위해서는

스프링의 스케줄링 기능을 이용하여 자원 반환을 지속 수행하도록 해야 했습니다.

 

물론 앞서 만들었던 Connection Manager도 Bean으로 등록하여

자원 반환 Thread에서 Injection 하여 사용하는 것이 필요했습니다.

 

표 7) 사용하지 않는 Connection 반환 스케줄러 추가

@Configuration
@EnableScheduling
public class HttpClientConfig {

    private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
    private static final int MAX_CONNECTIONS_TOTAL = 100;

    private static final int IDLE_TIMEOUT = 30 * 1000;

    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(poolingHttpClientConnectionManager())
                .build();
    }

    @Bean
    public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
        connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL);
        return connectionManager;
    }

    @Bean
    public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
        return new Runnable() {
            @Override
            @Scheduled(fixedDelay = 30 * 1000)
            public void run() {
                try {
                    if (connectionManager != null) {
                        LOGGER.info("{} : 만료 또는 Idle 커넥션 종료.", Thread.currentThread().getName());
                        connectionManager.closeExpiredConnections();
                        connectionManager.closeIdleConnections(IDLE_TIMEOUT, TimeUnit.MILLISECONDS);
                    } else {
                        LOGGER.info("{} : ConnectionManager가 없습니다.", Thread.currentThread().getName());
                    }
                } catch (Exception e) {
                    LOGGER.error(Thread.currentThread().getName() + " : 만료 또는 Idle 커넥션 종료 중 예외 발생.", e);
                }
            }
        };
    }
}
 
 

ConnectionManager를 검사하여 이미 만료되었거나 Idle 상태인 Connection들을 종료해 주는 Bean을 만들어 30초(fixedDelay) 주기로 수행되도록 하였습니다.

IDLE_TIMEOUT은 30초로 설정하여 30초 동안 Connection을 재사용하지 않으면 Idle Connection으로 판단하여 종료되도록 하였습니다.

 

이제 Spring의 Scheduling을 이용할 Bean도 만들었으니 추가로 Scheduling을 관리하는 Configuration도 필요하겠죠.

 

표 8) Spring Scheduing Configuration

@Configuration
public class SchedulingConfig implements SchedulingConfigurer {

    private static final int POOL_SIZE = 10;

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(POOL_SIZE);
        taskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        taskScheduler.initialize();

        scheduledTaskRegistrar.setTaskScheduler(taskScheduler);
    }
}
 

이렇게  HttpClientConfig와 SchedulingConfig를 @Configuration으로 등록하고

비즈니스 로직에서 HttpClient Bean을 사용하여 Connection Pool을 사용하여 HttpClient를 사용하는 방법을 정리해 봤습니다.

 

구글링을 통해 수집한 정보를 제 나름대로 해석하여

필요에 맞게 가공하여 정리했기에 완벽하지 않습니다.

 

완전하지는 않지만 작업한 내용을 정리하기 위해 우선 기록해 둡니다.

수정되는 내용이 발생하거나 의견을 주시면 계속해서 수정, 반영해 나가도록 하겠습니다.

 

감사합니다.

 

참고

Tutorial - Connection management http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html

HttpClient Examples - Threaded request execution https://hc.apache.org/httpcomponents-client-4.5.x/httpclient/examples/org/apache/http/examples/client/ClientMultiThreadedExecution.java

Apapche HttpComponent 제대로 사용하기 https://inyl.github.io/programming/2017/09/14/http_component.html

Spring RestTemplate + HttpClient configuration example https://howtodoinjava.com/spring-boot2/resttemplate/resttemplate-httpclient-java-config/

How to Schedule Tasks with Spring Boot https://www.callicoder.com/spring-boot-task-scheduling-with-scheduled-annotation/

반응형
반응형

Tomcat 7.0.32 + JDK 1.6.0 u37 + Spring 3.1.3 운영 중

특별한 장애 없이

Servlet.service() for servlet [xxx] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.RecoverableDataAccessException:
### Error querying database.  Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 48,092,122 milliseconds ago.  The last packet sent successfully to the server was 48,092,123 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.

위와 같은 tomcat 에러 로그가 발생했습니다.

구글링을 해보니
특정 시간동안 DB 접근이 없어서 DB 커넥션이 끊어진 상태에서
다시 DB 접근하려고 할 때 validation 체크를 할 수 있는 쿼리가 없을 때 발생하는 거 같더군요.

그래서 applicationContext.xml 중 dataSource 부분에
validationQuery 를 추가했습니다.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName">
        <value>#{properties['jdbc.driverClassName']}</value>
    </property>
    <property name="url">
        <value>#{properties['jdbc.url']}</value>
    </property>
    <property name="username">
        <value>#{properties['jdbc.username']}</value>
    </property>
    <property name="password">
        <value>#{properties['jdbc.password']}</value>
    </property>
    <property name="maxActive">
        <value>100</value>
    </property>
    <property name="validationQuery">
        <value>SELECT 1</value>
    </property>
    <property name="testWhileIdle">
        <value>true</value>
    </property>
</bean>

validationQuery 중요하게 보지 않았는데..
이런 이슈가 있더군요..

조금이나마 도움 되시길...

참고 링크

http://commons.apache.org/dbcp/configuration.html

http://fbwotjq.tistory.com/entry/IBATIS-%EC%BB%A4%EB%84%A5%EC%85%98-%EC%97%90%EB%9F%AC

 

반응형

+ Recent posts