반응형

getParameter() 그리고 getReader() 사용

HttpServletRequest 객체의 getParameter()를 통해 요청으로부터 파라미터 값을 가져와 작업을 하는 경우 주의가 필요하다.

만약 getParameter()를 통해 요청 파라미터 값을 받게 되는 경우 후속 작업에서 getReader를 통해 요청 값을 처리하는 경우가 생길 때 문제가 될 수 있다.

그 이유는 Servlet Spec에 있는데, Servlet Spec에 명시된 get Parameter에 대한 설명은 다음과 같다.

 

Parameter를 이용할 수 있는 상황에 대해 정의한 부분이다.

  1. 요청이 HTTP, HTTPS 인 경우
  2. HTTP POST 메소드 인 경우
  3. Content-Type 이 application/x-www-form-urlencoded 인 경우
  4. Servlet이 getPrarmeter 유형의 메소드를 최초 호출한 경우

위 조건이 만족하지 않아 사용할 수 없는 경우에는 요청 객체의 input stream을 통해 post data를 이용할 수 있다.

 

다만, 위 조건이 충족한 경우에는 요청 객체에 대한 input stream을 직접 이용할 수 없다.

 

getParameter함수와 getReader 함수를 같이 써야하는 경우 주의가 필요하다.

일반적으로 함수를 사용하거나 할 때 해당 함수의 스펙이나 자세한 문서를 찾아보지 않고 그냥 쓰는 경우가 많은데 그럴 경우 문제가 될 수 있다.

이러한 문제를 극복하기 위해 Wapper 클래스를 상속하여 Reusable하게 만든 HttpServletRequest 클래스를 사용하고 있다.

하지만 was 마다 해당 부분에 대한 처리가 다를 수 있기 때문에 관련 부분을 확인해본 후 사용할 필요가 있다.

 

JEUS의 겨우 : jeus.servlet.engine.WebtoBServletRequest

Tomcat의 경우 : org.apache.catalina.connector.RequestFacade

반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

 

외부와 통신하는 기능에 대해 외부 서비스의 SSL이 2세대 인증서로 변경되면서 이에 대해 확인하는 과정을 기록한 것이다

 

어플리케이션에서 외부 서비스와의 통신을 하는 경우 RestTemplate를 통해 통신을 하는 등 java 단에서 통신을 하는 경우가 있다

이러한 경우 통신의 과정에서 SSL 인증서의 검증은 어떻게 이루어 지는지에 대해 알아보았다

 

일반적으로 웹사이트에 접속을 하게 되면 SSL 인증서에 대한 검증은 브라우저에서 수행하게 되어있는데

Java 어플리케이션 내에서 https 프로토콜로 통신 하는 경우 SSL 인증서에 대한 검증은 JVM 내에서 이루어지게 되어있다

 

Java의 SSL 인증서 검증 흐름

Java 어플리케이션 내에서 SSL 인증서의 검증 흐름은 다음과 같다

  HttpsURLConnection
         ↓
     SSLContext
         ↓
 TrustManagerFactory
         ↓
  X509TrustManager (실제 검증 수행)
         ↓
 checkServerTrusted

 

HttpsURLConnection은 네트워크 통신 레벨의 API로 SSL 핸드셰이크를 수행

SSLContext는 SSL 세션의 구성요소에 대한 초기화 및 관리르 수행하고 해당 객체에서 암호화 방식이나 TrustManager를 결정

TrustManagerFactory SSLContext에서 사용할 TrustManager 리스트를 생성하는 역할 수행한다 이 때, JVM "cacerts" 또는 사용자 keyStore를 통해 TrustManager 생성

X509TrustManager는 실제 서버 인증서 체인을 검증하는 역할을 수행하며 "checkServerTrusted" 메소드를 통해 서버가 제공한 인증서 체인을 검증

 

(RestTemplate도 내부적으로 HttpURLConnection을 사용하고 있어 인증 흐름은 동일)

 

 

Java 내 cacerts 확인 방법

cd $JAVA_HOME (JAVA가 설치된 디렉토리로 이동)

cd jre/lib/security (cacerts가 있는 위치로 이동)

# 아래 명령을 수행하면 해당 되는 alias에 대한 CA 인증서 정보를 볼 수 있다
keytool -list -v -keystore ./cacerts -storepass [password, 기본 "changeit"] -alias '[alias 명]'

 

해당 CA인증서를 확인함으로써 인증서의 세대 교체 등이 발생했을 때 신규 인증서를 어플리케이션에서 검증할 수 있는지 확인할 수 있다

 

추가

어플리케이션 실행할 때 jvm 옵션으로 아래 옵션을 추가하면 HTTPS 요청이 발생할 때 인증서, TLS 버전, 루트 인증서에 대한 정보를 로그로 확인할 수 있다

-Djavax.net.debug=ssl,handshake,certpath

 

반응형

'공부 > Java' 카테고리의 다른 글

getParameter, getReader 값 못 읽는 문제  (0) 2025.06.25
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

서비스를 개발 하거나 운영하면서 OOM(Out Of Memory)을 만나는 경우가 있다.

이런 경우 단편적 해결 방법으로는 서비스를 재기동하는 방법이 있겠다.

 

하지만 우리는 Out Of Memory가 왜 발생했는지 알고 원인을 해결헤야 하는데,

이 과정에서 heap dump를 분석할 필요가 있다.

 

heap dump란?

 

우선 heap에 대해 알아야하는데, heap은 JVM에서 관리하는 메모리 공간으로 Object들이 저장되는 공간이다.

primitive type의 경우 stack area에 저장되나 객체의 경우는 stack에는 참조를 저장하고 실제 Object는 heap에 저장되게 된다.

이러한 부분을 이해한 상태에서 대량의 객체가 생성된다면, 그 객체는 heap에 생성이 된다.

 

하지만 heap은 한정된 자원으로 계속해서 객체가 생기면 더 이상 생성할 공간이 부족해지게 되고, OOM(Out Of Memory) Error가 발생하게 된다.

이때, heap의 상태를 snapshot으로 저장된 것이 heap dump파일이다.

Heap Dump 테스트

Heapdump 테스트를 위해 Intellij의 JVM heap을 임의로 설정 후 객체를 무한정 생성해 본다

 

1. 프로젝트의 Run/Debug Configuration 에서 JVM Option으로 최소, 최대 힙사이즈를 1Gb로 부여하고 heap dump를 생성하는 옵션과 heap dump path를 지정해준다

* OOM 발생 시 heap dump 파일을 생성하기 위해서는 "-XX:+HeapDumpOnOutOfMemoryError" JVM 옵션을 꼭 지정해줘야 한다

 

2. 테스트 코드 작성

import java.util.ArrayList;
import java.util.List;

public class OomTest {

    public static void main(String[] args) {

        List<DummyDto> dummyDtoList = new ArrayList<>();
        long num = 0;

        while (true) {
            num++;
            dummyDtoList.add(new DummyDto("더미에요 " + num));
        }
    }

    static class DummyDto {
        private String text;

        public DummyDto(String text) {
            this.text = text;
        }
    }
}

 

3. 위 코드를 수행해보면 아래처럼 OOM이 발생하고 프로그램이 종료된다

그리고 힘 덤프 파일을 log 디렉토리에서 **.hprof로 얻을 수 있다

 

Heap Dump 분석

heap dump를 분석기 위해서는 여러 분석 프로그램을 이용할 수 있는데,

MAT을 사용하여 분석하는 방법을 다룬다

 

MAT (Memory Analzer)

설치 : https://eclipse.dev/mat/

 

MAT을 다운 받고 heapdump 파일을 열면 Overview를 볼 수 있다

 

Overview에서 Leak Suspects로 이동하면 대략적인 OOM의 원인을 알 수 있다

이 건은 테스트 이기 때문에 전체 용량의 대부분을 Object[] 가 차지하는 것을 알 수 있지만 실제 운영 중인 시스템에서는 그 비율 다를 수 있다

 

Leak Suspects로만 정확하게 알 수 없을 수 있다

그런 경우 다른 정보를 참고하여 문제를 일으키는 친구를 찾아내어야한다

아래는 dominator tree인데 해당 데이터를 보면 Retained Heap를 보면 많이 차지하고 있는 포인트를 유추할 수 있고 해당 포인트의 계층구조를 타고 들어가면 우리가 찾던 문제의 원인이 되는 객체를 발견할 수 있고 해당 객체의 어떤 값이 들어있는지도 확인할 수 있다

 

추가로 stacktrace를 확인하면 어떤 로직을 수행하는 도중에 문제가 발생했는지 확인할 수 있다

원인을 찾았으니 해당 객체를 발생시키는 부분의 코드를 수정해주면 된다!

 

OOM을 발생시키는 원인

  • JVM 메모리 할당 문제
  • 많은 수의 동시 요청
  • 비효율적인 데이터 조회

보통 데이터 조회 시 조건을 잘못지정하거나 페이징을 적용하지 않는 등 데이터를 과하게 가져오게 되면서 발생하는 경우가 대부분이기 때문에 개발할 때 이 부분을 신경쓰는 것이 중요하다

반응형

'공부 > Java' 카테고리의 다른 글

getParameter, getReader 값 못 읽는 문제  (0) 2025.06.25
Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

자바를 사용하면서 HashMap은 빼놓을 수 없는 컬렉션 중 하나이다.

HashMap은 Map 인터페이스를 구현한 컬렉션으로 Key, Value를 가진다.

Key와 Value가 짝을 이루어 입력되고 후에 찾을 때 get 메서드에 Key를 전달함으로서 Value 값을 얻을 수 있다.

 

아래는 String, Integer를 Key, Value로 갖는 예제이다.

public class HashMapTest {
	
	public static HashMap<String, Integer> map;
	
	public static void init() {
    
		map.put("a", 1);
		map.put("b", 2);
	}

	public static void main(String[] args) {
		
		map = new HashMap<>();
		init();
		
		String test = "a";
		
		System.out.println("##: " + map.get(test));
	}
}

결과:

##: 1

 

("a", 1)과 ("b", 2)가 들어있는 HashMap에서 "a"에 대한 Value값을 get 메서드를 통해 가져오는 예제이다.

"a"를 get에 넣으니 정상적으로 1이 나왔다.

 

 

그렇다면 Key에 우리가 만든 클래스를 사용한다면 어떤 결과가 나올까

public class HashMapTest {
	
	public static HashMap<Foo, Integer> map;
	
	public static void init() {
    
		map.put(new Foo("a"), 1);
		map.put(new Foo("b"), 2);
	}

	public static void main(String[] args) {
		
		map = new HashMap<>();
		init();
		
		Foo c = new Foo("b");
		
		System.out.println("##: " + map.get(c));
	}
	
	static class Foo{
    
		String key;
		
		public Foo(String key) {
			this.key = key;
		}
	}
}

위 경우 우리가 기대하는 결과 값은

##: 2

일 것이다.

 

하지만 결과는

##: null

이 나오게 된다.

 

이유는 간단하다.

우리가 만든 클래스에 hashCode와 equals를 Override하지 않아 발생한 문제이다.

위 String이나 Integer에서는 해당 문제가 발생하지 않는 이유도 String, Integer 등 클래스에는 이미 hashCode와 equals를 Override하고 있기 때문이다.

 

우선 HashMap의 put 메서드를 보자

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

입력받은 key에 대해 hash메서드를 호출한다.

 

HashMap내 hash메서드는 아래와 같다.

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

key에 대한 hashCode를 호출하게 된다. 즉, 직접 만든 class내에 hashCode 메서드를 Override하지 않으면 Object의 hashCode 메서드를 호출하게 된다.

 

이런 경우 다음과 같은 결과가 나온다.

public class HashCodeTest {

	public static void main(String[] args) {
		
		System.out.println("##: " + new Foo("a").hashCode());
		System.out.println("##: " + new Foo("b").hashCode());
		System.out.println("##: " + new Foo("b").hashCode());
		
	}
	
	static class Foo{
		String key;
		
		public Foo(String key) {
			this.key = key;
		}
	}
}

결과:

##: 32374789

##: 1973538135

##: 1023487453

 

즉, 기본 hashCode 메서드를 호출하게 되면 Foo 클래스 내 key가 같아도 다른 hash값이 나오게 된다.

 

그리고 HashMap의 get 메서드를 보자

public V get(Object key) {
	Node<K,V> e;
	return (e = getNode(key)) == null ? null : e.value;
}

getNode는 아래와 같다.

final Node<K,V> getNode(Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & (hash = hash(key))]) != null) {
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		if ((e = first.next) != null) {
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	return null;
}

간단하게 보면 key에 대한 hash값을 구한 뒤 hash값이 같고 equals 메서드를 통해 비교한 결과가 true인 경우 해당 값을 반환하도록 되어있다.

 

그러기 때문에 커스텀 클래스를 만든 경우 hashCode와 equals를 오버라이드해주지 않으면 HashMap에서 정상적으로 동작하지 않게 된다.

 

꼭, hashCodeequals오버라이드 하자!

 

마지막으로 hashCode와 equals를 오버라이드 한 후 HashMap을 사용한 결과이다.

public class HashMapTest {
	
	public static HashMap<Foo, Integer> map;
	
	public static void init() {
    
		map.put(new Foo("a"), 1);
		map.put(new Foo("b"), 2);
	}

	public static void main(String[] args) {
		
		map = new HashMap<>();
		init();
		
		Foo c = new Foo("c");
		
		c.key = "b";
		
		System.out.println("##: " + map.get(c));
	}
	
	static class Foo{
    
		String key;
		
		public Foo(String key) {
			this.key = key;
		}

		@Override
		public int hashCode() {
			return key.hashCode();
		}

		@Override
		public boolean equals(Object obj) {

	        if(this == obj) return true;    
	        if(obj == null || getClass() != obj.getClass()) return false;
	        
			Foo t = (Foo) obj;
			return this.key.equals(t.key);
		}
	}
}

결과:

##: 2

 

반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
Primitive, Reference Type  (0) 2021.05.07
반응형

JavaAgent란?

Java 프로그램이 시작할 때 JVM에 의해 클래스로딩이 수행되는 시점에 자바 바이트 코드를 변조를 할 수 있도록 해주는 프로그램이다

이를 통해 별도의 메인 프로그램의 코드를 변경하지 않고도 프로그램에 대한 모니터링 등을 수행 할 수 있다

프로그램의 원본 코드를 수정하지 않아도 모니터링을 할 수 있기 때문에 APM툴에서 많이 사용되고 있다

JavaAgent 실행 방법

JavaAgent는 JVM 옵션을 통해 실행할 JavaAgent를 지정하여 실행할 수 있다

-javaagnet:jarpath[=options]

**ex)**
-javaagent: /home/monitoring/monitoring.jar

JavaAgent 동작 원리

  1. Java 애플리케이션 실행을 위해 JVM이 실행 되면서 JavaAgent의 Premain 함수 수행
  2. ClassFileTransformer 구현체 로딩
  3. Class Loader에 의해 Java 애플리케이션 Class File 로딩 시 Class 파일 ClassFileTransformer에 의해 바이트 코드 변경
  4. 변경된 바이트 코드 로딩
반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
Java Virtual Machine(JVM)  (0) 2021.05.08
Primitive, Reference Type  (0) 2021.05.07
반응형

Java는 소스 코드를 컴파일 하고 컴파일의 결과로 만들어진 바이트코드를 인터프리터에 의해 해석하여 실행되는 언어이다.

JVM 구조

https://www.nowwatersblog.com/cs/JVM

 

JVM의 실행 순서

  1. JVM이 OS로부터 필요로 하는 메모리를 할당 받는다. JVM은 할당받은 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.
  2. Class Loader를 통해 자바 바이트 코드가 JVM Runtime Data Areas로 로딩 된다.
  3. 로딩된 바이트 코드가 Execution Engine에 의해 해석 된다.
  4. 해석된 바이트 코드는 JVM Runtime Data Areas에 배치되어 실행 된다.
  5. 실행 과정에서 Garbage Collection이 메모리 관리 작업을 수행한다.

 

Runtime Data Area

Heap

  • 객체 인스턴스와 배열이 저장되는 영역
  • Java에서 생성된 모든 객체는 이 힙 영역에 할당
  • 힙은 모든 스레드가 공유하며, 가비지 컬렉터에 의해 관리

Stack

  • 각 스레드마다 독립적으로 존재하는 메모리 영역
  • 메서드 호출 시 생성되는 프레임(스택 프레임) 저장
  • 각 프레임에는 메서드의 로컬 변수, 매개변수, 반환 주소 등 포함
  • 메서드가 호출될 때 프레임이 스택에 추가되고, 메서드 실행이 완료되면 프레임 제거

Method

  • 클래스에 대한 정보를 저장하는 영역
  • 클래스의 구조(클래스 이름, 메서드, 필드, 상수 풀 등)와 정적 변수(static 변수) 정보 포함
  • 모든 스레드가 공유

PC Register

  • 각 스레드마다 존재하는 작은 메모리 공간
  • 현재 실행 중인 JVM 명령 주소 저장
  • 스레드가 실행 될 때, 해당 스레드의 PC 레지스터 업데이트

Native Method

  • Java가 아닌 언어로 작성된 네이티브 메서드 호출 시 사용하는 스택
  • 네이티브 메서드의 로컬 변수와 매개변수 저장
반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Primitive, Reference Type  (0) 2021.05.07
반응형

Primitive Type

Primitive 타입은 원시 타입이라고도 아래와 같은 타입이 존재한다.

  • byte (1 byte, 정수형)
  • short (2 byte, 정수형)
  • int (4 byte, 정수형)
  • long (8 byte, 정수형)
  • float (4 byte, 부동소수점형)
  • double (8 byte, 부동소수점형)
  • char (2 byte, 문자형)
  • boolean (1 byte)

Primitive 타입은 일반적으로 Stack에 데이터가 저장 된다.

데이터를 메모리에 직접 저장하기 때문에 성능 우수

필요한 크기에 맞는 Primitive 타입을 선택하여 사용할 수 있으므로 메모리 사용에 효율적이다.

Reference Type

Reference Type은 참조 타입으로 객체를 참조하는 데이터 타입니다.

Reference Type에는 아래와 같은 타입이 존재한다.

  • 클래스 (class)
  • 배열 (array)
  • 인터페이스 (interface)
  • 열거형 (enum)

Reference Type의 경우는 데이터를 Stack이 아닌 Heap에 저장하고 그 데이터의 참조값을 Stack에 저장하여 데이터에 접근할 때는 Stack을 거쳐 Heap까지 가야하기 때문에 Premitive 타입 보다 느리다.

한가지 주의해야할 것이 Reference Type을 포인터라고 생각하는 경우가 있는데,

포인터는 주소를 직접 다루는 변수이고 Java의 Reference는 간접적으로 참조하는 변수이다.

Reference 변수로 데이터에 접근하지만 우리가 직접 메모리 주소를 조작하거나 할 수 없다.

Java의 메모리 관리는 가비지 컬렉터에 의해 자동으로 처리된다.

String의 경우는 조금 특별하다.

String a = “abc”;

형식으로 String 객체를 생성할 수 있는데, 이것을 String 리터럴이라고 하고, 데이터가 리터럴 풀에 저장되어 재사용되게 된다.

리터럴 풀은 heap의 일부이다.

String b = new String(”abc”);

이 경우는 새로운 String 객체를 리터럴 풀에 생성하는 것이 아닌 heap에 생성하게 된다.

반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08

+ Recent posts