자바 성능을 결정짓는 코딩 습관과 튜닝 이야기 - 이상민

uijin kim·2023년 2월 21일
0

정리된 책의 내용은 [작가의 주관적인 의견] 이며, 개인적으로는 여러 책과 실무 경험을 통해 효율적인 방법과, 답을 찾아가고자 함
모든 내용을 요약하지 않으며 해당 단락의 핵심만을 요약하고자 함
(다만 요약 내용이 책의 전부는 아님)

1. 디자인 패턴 꼭 써야 한다.

디자인 패턴은 중복 로직의 최적화 등을 통해 직, 간접적으로 성능 향상의 이점을 가져다 준다.
자바 기반의 시스템을 분석, 설계하고 개발하면서 패턴을 모른다면 반쪽 분석 설계자나 개발자라고 할 수 있다.

2. 도대체 GC는 언제 발생할까?

유닉스든 윈도우 기반의 서버든 Full GC를 수행하는 시점에는 해당 WAS의 컨테이너에서 서비스가 처리되지 않는 다는 단점이 있다. GC를 많이 하면 할수록 응답 시간에 많은 영향을 끼친다.

GC란?

자바에서는 메모리를 GC라는 알고리즘을 통하여 관리하며, 개발자가 메모리를 처리하기 위한 로직을 만들 필요가 없고, 절대로 만들어서는 안 된다.
Garbage Collection은 쓰레기 객체를 정리하는 작업이며, 하나의 객체는 메모리를 점유하고, 필요하지 않으면 메모리에서 해제되어야 한다.

다음은 메모리 점유의 쉬운 예이다.

String a = new Stirng();

GC 작업을 하는 가비지 콜렉터(Garbage Collector)의 역할

  • 메모리 할당
  • 사용 중인 메모리 인식
  • 사용하지 않는 메모리 인식

JVM의 메모리는 클래스, 스택, 힙, 네이티브 메서드 4가지 영역으로 나뉘어진다.
메모리 영역은 힙 영역에 대해서만 생각한다.

힙 영역은 크게 Young, Old 영역으로 나뉜다.
Young 영역은 다시 Eden 과 두개의 Survivor 영역으로 나뉜다.
우리가 고려해야할 자바의 메모리 영역은 총 4개영역으로 나뉜다고 볼 수 있다.

일단 메모리에 객체가 생성되면, Young 영역의 Eden에 객체가 지정된다.
Eden 영역에 데이터가 어느 정도 쌓이면, 이 영역에 있던 객체가 어디론가 옮겨지거나 삭제된다. 이 때 옮겨가는 위치가 Survivor 영역이다. Survivor 영역에 우선순위가 있는 것은 아니나 두 개의 영역 중 한 영역은 반드시 비어 있어야 한다. 그 비어 있는 영역에 Eden영역에 있던 객체가 할당 된다.

Survivor영역이 차면, GC가 되며 Eden 영역에 있는 객체와 꽉 찬 Survivor 영역에 있는 객체가 비어 있는 Survivor 영역으로 이동한다. 더이상 Young 영역에 공간이 남지 않으면 객체들은 Old 영역으로 이동하게 된다.

GC의 종류

Minor GC - Young 영역에서 발생하는 GC
Major GC - Old 영역에서 발생하는 GC

JDK5.0 이하 버전의 레거시 GC에 대한 내용으로 생략함.

3. 내가 만든 프로그램의 속도를 알고 싶다.

애플리케이션의 튜닝 및 성능 모니터링을 위해서는 프로파일링 툴 혹은 APM을 사용한다.

프로파일링 툴은 소스 레벨의 분석을 위한 툴로써 메모리 사용량을 객체나 클래스, 소스의 라인 단위까지 분석할 수 있다.
APM툴은 애플리케이션의 장애 상황에 대한 모니터링 및 문제점 진단이 주 목적이다.
또한 실시간 모니터링을 위한 운영툴이다.

System 클래스를 이용하여 자신의 코드 속도를 확인할 수 있다.
자바에서 System.nanoTime() 을 제공하며 CurrentTimeMills()와 달리 시간 측정용으로 만들어졌기에 사용을 권장한다.

4. 왜 자꾸 String을 쓰지 말라는거야?

문자열을 만드는 클래스는 String, StringBuffer, StringBuilder가 가장 많이 사용된다.
StringBuffer는 스레드에 안전하게(ThreadSafe) 설계되어 있으므로, 여러 개의 스레드에서 하나의 StringBuffer 객체를 처리해도 전혀 문제가 되지 않는다. 다만 StringBuilder는 단일 스레드에서의 안전성만을 보장한다.

대략적인 테스트로 String, StringBuffer, StringBuilder의 속도 차이는
String보다 StringBuffer가 약 367배 빠르며, StringBuilder가 약 512배 빠르다.이 뿐 아니라 String은 나머지 두 클래스보다 메모리를 약 3390배 더 사용한다.

이러한 이유에 대해서는 GC와 관련이 있다.
String을 사용할 경우 'abcde' 값을 가진 객체 생성후 'abcde'라는 문자열을 다시 더하고자 하였을때 기존 'abcde'객체를 지우고(쓰레기처리) 새로운 객체이 두 문자열을 합쳐서 생성한다. 이후 더이상 필요 없어진 기존 객체는 GC의 대상이 된다.

반면 StringBuffer, StringBuilder의 경우 String과 다르게 새로운 객체를 생성시키지 않고, 기존에 있는 객체의 크기를 증가시키면서 값을 더한다.

문자열의 계산이 많이 필요하다면 String 보다는 StringBuffer혹은 StringBuilder를 사용하는 것을 권장한다.

5. 어디에 담아야 하는지...

일반적으로 배열을 제외하면 데이터를 담기 가장 좋은 객체는 Collection 및 Map인터페이스를 상속한 객체이다.

각 데이터 객체들에는 속도적인 차이가 있으며 효율적인 데이터 구조를 선택하여 데이터를 담아야 한다.

6. 지금까지 사용하던 for 루프를 더 빠르게 할 수 있다고?

자바의 조건문은 다음과 같이 나뉘어 진다.

  • if - else if - else
  • switch
    일반적으로 if문에서 분기를 많이 하면 시간이 많이 소용된다 생각하지만 잘못된 생각이다. if문의 비교 구문에서 속도를 잡아먹지 않는 한, if 문장 자체에서는 많은 시간이 필요하지 않다.

예를 들어 단순 비교연산을 하는 if문 10개를 10번 반복 실행할 경우
0.0000021초가 소요된다.
if문은 느리지 않다는 뜻

자바에서 사용하는 반복 구문은 다음과 같이 나뉘어 진다.

  • for
  • do-while
  • while

일반적으로 for문을 사용할 때 다음과 같이 사용하는 습관은 좋지 않다.

for(int loop=0;loop<v.size();loop++)

매번 반복하며 size() 메소드를 호출하기 때문이다.
이 코드는 다음과 같이 수정할 수 있다.

int vSize=v.size();
for(int loop=0;loop<vSize;loop++)

실제 위 코드의 속도를 측정하면 for 내부 구문을 무시했을때, 개선된 코드는 약 4배 정도의 속도가 더 빠름을 확인할 수 있다.

반복 구문에서 필요 없는 반복을 줄이는 것이 가장 중요하며, 작은 반복 구문이 큰 성능 저하를 가져올 수 있다는 것을 명심해야한다.

7. static 제대로 한번 써 보자

자바에서 성능을 향상시키는 가장 좋은 방법은 static을 사용하는 것이다.
자주 사용하고 절대 변하지 않는 변수는 final static으로 선언하자, 간단한 데이터들도 static으로 선언할 수 있지만, 템플릿 성격의 객체를 static으로 선언하는 것도 성능 향상에 많은 도움이 된다.

주의점
클래스 멤버 변수를 static으로 선언할 경우 해당 클래스 객체가 모두 공유하는 자원 상태가 된다.
static으로 선언한 부분은 GC가 되지 않는다.
예를 들어 어떤 ArrayList에 데이터를 담을때 Collection 객체를 static으로 선언하고 지속적으로 데이터를 샇는다면 더이상 GC가 되지 않으면서 시스템은 OutOfMemoryError(OOM)을 발생시킨다. (더이상 사용 가능한 메모리가 없지는 현상을 가르켜 메모리 릭[Memory Leak]이라고 한다)

8. 클래스 정보, 어떻게 알아낼 수 있나?

자바 API에는 reflection이라는 패키지가 있다. 해당 패키지의 클래스를 이용하여 JVM에 로딩되어 있는 클래스와 메소드 정보를 읽어 올 수 있다.
Class 클래스
Method 클래스
Field 클래스

9. Synchronized는 제대로 알고 써야 한다.

우리가 개발하는 WAS는 여러 개의 스레드가 동작하도록 되어 있다. 그래서 자주 사용하는 것이 Synchronized이다. 다만 Synchronized를 쓴다하여 무조건 안정적인 것은 아니다.

프로세스와 스레드
클래스를 하나 수행시키거나 WAS를 기동하면 서버에 자바 프로세스가 하나 생성된다. 하나의 프로세스에는 여러 개의 스레드가 생성된다. 단일 스레드가 될수도, 여러 개의 스레드가 될 수도 있다.

자바에서 스레드의 구현은 Thread 클래스를 상속받는 법과 Runnable 인터페이스를 구현하는 방법 두 가지가 있다. 기본적으로 Thread 클래스는 Runnable 인터페이스를 구현한 것이기 때문에 어느것을 사용해도 거의 차이는 없다. 다만 Runable 인터페이스를 구현하면 원하는 기능을 추가할 수 있다.

Runnable 을 구현한 로직

public class RunableImpl implements Runnable{
	public void run() {
    	System.out.println("This is RunnableImpl.");
    }
}

Thread 클래스를 확장한 경우

public class ThreadExtends extends Thread{
	public void run() {
    	System.out.println("This is ThreadExtends.");
    }
}

Thread 클래스를 상속받은 경우에는 start() 메소드를 실행하면된다. Runnable 인터페이스를 구현한 경우에는 Runnable 인터페이스를 매개변수로 받는 생성자를 사용해서 Thread 클래스르 만든후 start() 메소드를 호출해야 한다.

Synchronized
Synchronized는 사전적 의미로 '동시에 이러나다. 동시에 진행하다' 라는 의미를 가지고 있다.
Synchronized는 하나의 객체에 여러 객체가 동시에 접근하여 처리하는 상황이 발생 할 때 사용한다. 하나의 객체에 여러 요청이 동시에 달려들면 원하는 처리를 하지 못하고 이상한 결과가 나올 수 있다. 그래서 Synchronized를 사용해서 동기화 하게 된다. 이 식별자를 사용하면 "천천히 한명 씩 들어와!" 라고 해당 메소드나 블록에서 제어 한다. 다만 생성자의 식별자로는 절대 사용할 수 없다.

그럼 언제 동기화 해야할까?
1. 하나의 객체를 여러 스레드에서 동시에 사용할 경우
2. static으로 선언한 객체를 여러 스레드에서 동시에 사용할 경우

거꾸로 이야기하면, 위의 경우가 아니면 동기화를 할 필요가 별로 없다.

profile
느리더라도, 꾸준하게

0개의 댓글