Tomcat은 네트워크 통신에 대해 3개의 옵션을 제공한다. BIO, NIO, APR 3가지이다.
BIO vs NIO vs APR
디폴트 값은 BIO이다(HTTP/1.1).
acceptCount=”10”
request Queue의 길이를 정의한다. 클라이언트가 HTTP Request를 요청했을 때 Idle Thread가 존재하지 않을 때, Idle Thread가 생길 때 까지의 대기 길이이다.
보통 큐에 메시지가 쌓여있다는 의미는 톰캣 인스턴스가 처리할 수 있는 쓰레드가 없다는 상황이며, 쓰레드를 사용해도 요청을 처리하지 못한다는 것은 이미 장애 상태일 가능성이 높다.
디폴트 값은 100이다.
enableLookups=”false”
HttpServletRequest.getRemoteHost()는 DNS Lookup을 통해 클라이언트의 도메인을 반환하며, HttpServletRequest.getRemoteAddr()는 클라이언트의 IP 정보를 반환한다. enableLookups을 false로 지정할 경우, getRemoteHost()는 getRemoteAddr()과 같이 IP 정보를 반환한다. 즉, DNS Lookup 과정을 생략한다.
디폴트 값은 false이다.
compression=”off”
네트워크 대역폭을 절약하기 위해, HTTP Body를 GZIP 형태로 압축하여 response한다. 만약 HTTP Response Body의 크기가 클 경우, 해당 기능을 사용하는 것이 좋다.
해당 기능은 compressionMinSize(일정 사이즈 이상부터 압축), compressibleMimeType(압축하고자하는 마임 타입 지정)와 같은 부가 기능과 함께 사용한다.
해당 기능을 사용하여 Response할 때 Response Header는 Content-Encoding: gzip이라 표기한다.
디폴트 값은 off이다.
maxConnections
서버가 허용할 수 있는 최대 커넥션 수이다. 최대 커넥션 수가 도달하면 해당 메시지는 큐잉한다. 이때 큐 사이즈는 acceptCount가 결정한다. 해당 큐에서 처리를 대기한다.
디폴트-NIO: 10000, APR/BIO: 8192
maxKeepAliveRequests=”1” or maxKeepAliveRequests=$maxConnections_val
HTTP 프로토콜(1.1)의 Keep Alive Connection을 사용할 때 최대 유지할 Connection의 수를 결정한다. Stateless REST API 서버의 경우, 1을 설정한다. Stateful한 웹페이지 서버의 경우, maxKeepAliveRequests는 maxConnection의 수치와 동일하게 설정한다.
디폴트는 100이다. 1은 비활성화, -1은 무제한이다.
HTTP 프로토콜은 Stateless(Connection less) 프로토콜이다. 즉, 모든 요청은 1회성 요청이며, 커넥션은 재활용되지 않는다.
Pros and cons
Pros
핸드 셰이킹 생략으로 인한 네트워크 레이턴시 감소
커넥션을 새로 만들지 않기 때문에 cpu 사용량 감소
HTTP 파이프 라이닝 사용
Cons
리소스 낭비 우려
(HTTP/1.1의 Default Connection 방식은 Keep-Alive이다. 그러나, 서버 측에서 해당 기능을 비활성화면 HTTP 프로토콜의 Keep Alive 기능을 사용하지 못한다.)
(해당 기능을 비활성화 하면, HTTP Response Header에서 Connection:close 표기된다.)
(TCP의 KeepAlive와 HTTP KeepAlive는 다르다.)
maxThread=”500~750”
톰캣 내의 쓰레드 수를 결정하는 옵션이다. 쓰레드의 수는 실제 Active User 수를 의미한다. 즉 순간 처리 가능한 트랜잭션의 수이다. 그러나, 너무 많은 수치는 쓰레드 문맥 교환으로 인해 되려 느려질 우려가 있다.
해당 설정은 성능 테스트를 통해 서버 환경에 절적할게 조절하는 것이 괜찮다.
디폴트는 200이다.
tcpNoDelay=”true”
톰켓은 패킷들을 모아서 버퍼 사이즈에 맞춰 보내는 로직을 사용한다. 보내고자 하는 패킷이 버퍼 사이즈에 못 미치면 일정시간 또는 버퍼가 찬 후 보낸다. 해당 설정 true를 통해 바로바로 보낼 수 있도록 한다.
디폴트는 true이다.
DB 커넥션 풀
DataSource 리소스 설정
보통 Tomcat을 사용할 때 Tomcat 설정 파일에 DataSoure 리소스를 설정한다.
Tomcat은 JVM위에서 돌아가고 JVM은 OS위에서 돌아간다. Tomcat의 퍼포먼스 향상은 JVM과 관계 있고, OS설정에 관계가 있다. 그 이상의 충분한 속도와 최적화는 JVM의 이해와 OS의 이해가 요구된다. 해당 게시물은 JVM/OS 설정 튜닝을 다루지 못했다. 해당 게시물을 작성하면서 한계점을 많이 느꼈다. 진짜 어플리케이션의 성능 튜닝은 OS의 이해까지 내려가야 한다는 점에 정말 한계를 느꼈다. 시간은 부족한데 배워야할 것은 너무 많다.
근데, 궁금한 것이 많다. TrustManager 인터페이스는 무엇이며, HostNameVerifier는 무엇일까? 그리고, 사설인증서는 어떻게 왜 제대로 통신이 되지 않는지 삽질을 해보자.
사용 인증서 항목
1개의RootCA
1개의 intermediateCA
6개 인증서 = 2개 서버 * 3개의 Certificate( 와일드 카드 인증서, SAN 도메인 설정 인증서, SAN 미지정 인증서)
오류 만들기 및 오류 원인
1) java.security.cert.CertificateException: No subject alternative names present
원인: 인증서에 SAN 값에 IP 또는 도메인을 지정하지 않았음.
private static void matchIP(String var0, X509Certificate var1) throws CertificateException {
Collection var2 = var1.getSubjectAlternativeNames();
if (var2 == null) {
throw new CertificateException("No subject alternative names present");
2) javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching ka.glaso.net found
원인: SAN에 도메인 값을 지정하였는데, 다른 도메인을 통해 접근함.
아래와 같이 SAN의 도메인 값을 ka.glaso.net으로 하였으나, k.glaso.net으로 접근하였음
private void matchDNS(String var1, X509Certificate var2) throws CertificateException {
try {
new SNIHostName(var1);
} catch (IllegalArgumentException var9) {
throw new CertificateException("Illegal given domain name: " + var1, var9);
}
Collection var3 = var2.getSubjectAlternativeNames();
if (var3 != null) {
boolean var4 = false;
Iterator var5 = var3.iterator();
while (var5.hasNext()) {
List var6 = (List) var5.next();
if ((Integer) var6.get(0) == 2) {
var4 = true;
String var7 = (String) var6.get(1);
if (this.isMatched(var1, var7)) {
return;
}
}
}
if (var4) {
throw new CertificateException("No subject alternative DNS name matching " + var1 + " found.");
}
}
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching k.glaso.net found.
at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:204)
at sun.security.util.HostnameChecker.match(HostnameChecker.java:95)
at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:347)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:203)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1428)
... 38 more
3) sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
원인: 오류의 말마따나, 접근하고자 하는 타깃의 인증서를 이용하여, 신뢰할 수 있는 유효한 인증서 체인을 찾을 수 없음.
내것은 신뢰관계가 설정되지 않은 사설인증서이다.
보통 Java에서 HTTPS 개발하는 사람들은 이 오류 메시지를 많이 확인할 수 있다.
인증서 검증 과정 삽질
제대로 분석을 하지 못했다. 왠지 모르겠지만, 디버깅이 너무 힘들어서 도중에 던졌다.
실질적으로 X509TrustManagerImpl클래스의 checkTrusted메소드가 서버 인증서의 검증을 실시하는 메소드이다. X509TrustManagerImpl은 JSSE 버전의 단순 유효성 검증 알고리즘과 PKIX 검증을 지원한다.
위와 같이 Stack Trace를 보면 ClientHandshaker.class 클래스에서 서버 인증서 검증을 시작한다. 위의 빨간 라인에서 checkServerTrusted 메소드를 호출하는데 해당 메소드는 X509TrustManager 인터페이스의 구현체이다. X509TrustManager인터페이스는 TrustManager를 확장하였다. 즉, 우리가 정의하는 TrustManager이다(TrustManager를 사용자가 구현하여 TLS 핸드셰이킹 할 수 있음.). 그리고 최종 구현체는 X509TrustManagerImpl이다.
X509TrustManagerImpl 구현체의 checkServerTrusted메소드는 checkTrusted 메소드를 호출한다. 해당 메소드에서 본격적으로 서버 인증서의 검증을 실시한다.
checkIdentity를 통해 SAN의 IP 또는 DNS를 검증한다. checkIdentity 안의 실질적으로 위의 match 메소드가IP/DNS를 체크하여 검증을 실시한다. 만약, 인증서에 SAN(Subject Alternative Name)이 존재하지 않으면 예외가 발생한다(HostNameVerifier true를 설정하면, checkIdentity 메소드를 건너뛴다.).
PKIX 검증을 하기 전, 이것저것 로직이 있는데, 잘 모르겠다.
오류가 나는 PKIX 검증 부분을 봐 보자.
sun.security.validator.PKIXValidator를 통해 PKIX(public key infrastructures based on X.509)검증을 실시한다. PKIXValidator의 engineValidate메소드에서 이미 Trust Chain을 갖고 있다. 즉, 접근하고자 하는 엔티티의 종단 인증서, 그것을 발급한 중간 CA, 그것을 발급한 RootCA 총 3개의 인증서를 갖고 있다.
해당 인증서를 갖고 시스템이 갖고 있는 trustCerts를 통해 engineValidate를 실시한다. trustedCerts의 값들과 내가 갖고 있는데 인증서 체인을 DN 검증을 실시하는 것 같다(추측).
또한 위와 같이trustedCerts는 92개의 인증서를 갖고 있는데 아마 이것들은 JDK의 cacerts 목록 같은데… 한번 확인해보자.
맞다. 아래와 같이 인증서 항목을 짤막하게 봤고, 총 개수도92개이다. 아마 여기에 생성한 루트 인증서를 적재하면 될 것 같다.
여기가 끝은 아니지만 도저히 디버깅이 잘 안되는 관계로 패스한다.
결론:1
결론적으로 HTTPS 통신을 할 때 다음과 같은 경우의 수를 통해 진행할 수 있다.
1. 공인 인증서 사용 - HttpURLConnection 그냥 구성해도 괜찮음.
2. 사설 인증서 사용
2-1. cacerts에 인증서를 보관한다(trust certs에 추가하는 방안).
2-2. 2-1을 하지 않고 SSLContext 구성하여, HTTPS 통신을 한다.
2-2-1. SAN 값에 도메인 또는 IP가 지정되어 있음.
2-2-1-1. 단일 인증서 사용 - 단일 인증서만 키스토어 생성
2-2-1-2. 다중 인증서 사용 - 중간 CA 또는 Root CA 키스토어 생성
2-2-2. SAN 값이 존재하지 않음
2-2-2-1. HTTPS HostNameVerifier를 disable한다.
2-2-1-1-1. 단일 인증서 사용 - 단일 인증서만 키스토어 생성
2-2-1-2-2. 다중 인증서 사용 - 중간 CA 또는 Root CA 키스토어 생성
주의사항
사설 인증서를 허용하기 위해 TrustManager의 구현체에 대해 모든 검증에 대해 아무 로직 없이 return true를 절대 하지 말아야 한다.를 사용하는 이유가 사라진다.
javax.naming.NamingException: Cannot create PoolableConnectionFactory (FATAL: could not open relation mapping file "global/pg_filenode.map": Permission denied)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:464)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1119)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1014)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:504)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1120)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1044)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:942)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:813)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
... 109 more
아마도 소유자를 강제로 바꿔서 문제가 된 듯... 해당 예외를 구글링 해도 잘 모르겠음..
스프링에서 빈을 생성할 때 빈의 Scope(생명주기)은 싱글톤이다. 해당 빈 생성은 디폴트이다.
Prototype, request, session 등 다양하게 존재한다.
여기서 잠깐 싱글톤이 뭔지 지고 넘어가자.
싱글톤 패턴은 생성자가 여러 차례 호출되더라도 결국 실제로 생성되는 객체는 하나이고, 최초 생성한 객체를 계속 리턴한다. 보통 Java 에서의 getInstace() 메소드는 “보통” 싱글톤을 의미한다(다 그렇지는 않다).
즉, Spring에서 @Components, @Service, @Controller, @Repository 등의 모든 빈들은 WAS가 가동할 때 싱글톤으로 객체를 인스턴스화 후, Spring Container에 보관한다(IoC 행위). 그 다음은 우리는 Inject를 통해 사용한다(DI 행위).
여기서 질문은 다음과 같다.
아니, Controller나 Service 객체가 1개인데 멀티 쓰레드 환경에서 Thread-Safe하게 어플리케이션을 어떻게 운용하냐? 그리고 Spring Bean은 Thread-Safe 하냐??
일단 답은 No이다. Spring 빈은 Thread-Safe하지 않다.
Thread-Safe하게 사용하면 Yes이고, 그렇지 않으면 No이다. 다음과 같이 맴버 변수를 사용하면 Thread-Safe하지 않다.
그럼 어떻게 사용해야하는가?
빈을 불변하도록 한다(Immutable).
1. Constructor Inject 사용한다.
2. Builder 패턴을 활용한다.
3. 빈에 대해 Setter를 허용하지 않는다.
빈은 무상태여야 한다(Stateless).
빈의 특정 상태를 나타내는 변수를 계속 힙메모리에 상주시키면 안된다(위의 Calculator와 같은 행동).
<if test="search.startDate != null and search.startDate != ''">
<![CDATA[ and date >= #{search.startDate}]]>
</if>
<if test="search.endDate != null and search.endDate != ''">
<![CDATA[ and date <= #{search.endDate}]]>
</if>
search.startDate != '' , search.endDate != ''
위의 코드가 문제 였다....;
해당 사항을 제거하면 정상적인 비교를 시전한다.
결론
1. Date/TimeStamp 타입은 무조건 해당 타입과 비교를 시전한다.
2. 쿼리문 왼쪽에 to_char(date, ~~) 이런짓을 절대 하지말아라(index가 죽어버린다)