Java 성능을 높이는 프로그래밍

돈까스전사·2022년 6월 16일
0
post-thumbnail

Java 성능개선을 위한 Programming 기법 👼

JDK1.3버전 이후로 지원되는 HotSpot VM은 기본적으로 Hip에 동적으로 할당된 Object는 거의 회수할 수 있다고 한다.
하지만 이 기능으로 인해서 VM은 엄청난 OverHead를 가지게 된다.
무리한 Object의 생성은 생성 당시에도 많은 OverHead를 초래하지만, 생성된 Object를 회수하기 위해서는 더 많은 작업이 요구된다.
이를 해결하기 위한 몇 가지 Tip이 있는데, Avoiding Garbage Collection, Object 재사용, static instance variable의 사용에 의한 단일 클래스 인스턴스 구현 방법 등이 그것이다.
핵심은 가능한 Object 생성을 피하자는 것이다.

1) Avoiding Garbage Collection (static method사용)

예제1)
String string="55";
int theInt=new Integer(string).intValue();

예제2)
String string="55";
int theInt=Integer.parseInt(string);

예제1)에서는 Integer 클래스를 생성한 다음 string에서 정수값을 추출해 냈다.
Object를 생성하고 초기화하는 과정은 상당한 cpu 집약적인 작업이고, Hip 영역에 Object가 생성되고 수집되기 때문에 가비지 컬렉터 작업을 수반한다.
가능한 Object의 Instance가 필요 없는 static 메소드를 사용한다.
예제2) 참조.


2) Avoiding Garbage Collection (임시 Object 생성 금지)

가장 흔한 예로 String Object의 append를 위해서 (+) 연산을 사용하는 것을 들 수 있다.
(+) 연산자를 사용해서 String Object를 append할 경우 우리가 생각하는 것 보다 훨씬 더 많은 임시 Object가 생성되고, 가비지 컬렉터에 의해 다시 수집된다.
String Object의 append연산을 위해서는 StringBuffer 클래스를 사용한다.
예제1)과 예제2) 참조

예제1)
String a="Hello";
a=a+"World";
System.out.println(a);

예제2)
StringBuffer a=new StringBuffer();
a.append("Hello");
a.append("World");
System.out.println(a.toString());

어떤 메소드는 Object의 복사본을 반환하는 경우가 있다.
대표적인 예로 스트링 클래스의 trim() 메소드를 들 수 있다.
trim()메소드가 수행되면 기존의 Object는 수집되고, 기존 Object의 복사본이 사용되게 된다.
임시 Object 생성과 복사본을 반환하는 메소드가 루프 안에서 사용될 경우 무수히 많은 Object가 생성되고 수집되기 때문에 심각한 문제를 야기하게 된다.
루프 내부에서 Object를 생성하는 것은 가급적 피해야 한다.

예제3)
for(i=0;i<1000;i++){
  Date a=new Date();
  :
}

예제4)
Date a;
for(i=0;i<1000;i++){
  a=new Date();
  :
  a=null;
}

예제3)과 예제4)는 현재 날짜와 시간을 얻어오기 위해 루프 안에서 Object를 생성했다.
보통 set으로 시작하는 메소드는 Object의 Instance 값을 재정의 한다. API를 충분히 참조한 다음 지원 메소드가 없을 경우, 클래스를 상속받아 요구에 적합한 메소드를 만드는 방법도 고려할 필요가 있다.
기존 API를 이용한 Object 초기화 방법은 아래 Object 재사용 (메소드를 사용한 Object 초기화) 부분 참조.


3) Avoiding Garbage Collection (primitive data type 사용)

Date 클래스나 String 클래스의 값들중 int나 long 형으로 표현하여 사용할 수 있는 경우가 있다. 예를 들어

String a="1492";
String b="1997";

과 같을 경우 a와 b는 굳이 String 형으로 표현하지 않아도 된다.
하지만 여기서 주의할 점은 Object의 값을 기본 데이타형으로 Casting 하는 작업이 오히려 시간이 더 많이 걸릴 수도 있는 것이다.
클래스의 인스턴스 변수로 기본 데이터형을 사용하면 Object의 크기도 줄어들고, Object 생성 시간도 줄일 수 있다.


4) Object 재사용(Pool Management)

Object 재사용 기법으로 흔히 Pool Management 기법을 사용한다.
이는 임의 갯수의 Object를 미리 생성해 두고 이를 Vector 클래스를 사용해서 관리하는 방법이다.
해당 Object를 사용하기 위해서 Pool의 Vector에 있는 Object를 하나 가져오고, 다 사용하고 나면 다시 Pool에 반납한다.
이는 기존에 공개되어 있는 Hans Bergsten의 Connection-Pool의 PoolManager클래스에서 사용되고 있다.
Object Pool을 사용할 경우, 반환되는 Object가 초기화되어 반환되지 않을 경우 다음에 Pool에서 Object를 가져와서 사용하게 되면 문제를 야기할 수 있기 때문에 초기 클래스 Design시 꼼꼼하게 따져 봐야 한다.


5) Object 재사용(메소드를 사용한 Object 초기화)

예제1)
StringBuffer sb=new StringBuffer();
sb.append("Hello");
out.println(sb.toString());
sb=null;
sb=new StringBuffer();
sb.append("World");
out.println(sb.toString());

예제1)과 같이 사용할 경우 하나의 인스턴스 변수를 사용하기는 하지만, 두 번의 초기화 과정을 거치게 된다.

예제2)
StringBuffer sb=new StringBuffer();
sb.append("Hello");
out.println(sb.toString());
sb.setLength(0);
sb.append("World");
out.println(sb.toString());

위와 같이 각 클래스에서 지원해 주는 메소드를 사용하여 Object를 재사용 할 수 있다.


6) static instance variable의 사용에 의한 단일 클래스 인스턴스 구현

다음은 Hans Bergsten의 PoolManager 클래스 코드 중 일부다.

public class PoolManager{
  static private PoolManager instance;
  :
  private PoolManager(){
    init();
  }
  static synchronized public PoolManager getInstance(){
    if (instance == null){
      instance = new PoolManager();
    }
    :
    return instance;
  }
  private void init(){
  :
  }

PoolManager형의 인스턴스가 static으로 선언되어 있다.
getInstance() 메소드는 현재 생성되어 있는 PoolManager의 Object를 조사하고 만약 Object가 있으면 Object를 반환하고 없으면 생성자를 호출해서 PoolManager의 Object를 생성한 후 반환한다.
결국 JVM 내부에는 하나의 PoolManager Object가 존재하게 된다.
단일 클래스 인스턴스 기법을 사용할 경우 하나의 인스턴스를 사용하기 때문에 해당 인스턴스의 무결성 부분이 문제가 된다.
이를 위해 다양한 Synchronization 기법을 사용하게 된다.
(아래 I/O 퍼포먼스 개선 부분 참조.)


7) clone() 메소드 사용으로 Object 생성에 따른 OverHead를 피함

private static int[] data = new int[2][2];
  int[] someMethod(){
  int[] a = (int[])this.data.clone();
  return a;
}

대부분의 클래스들에는 clone() 메소드가 존재한다.
clone() 메소드가 호출되면 Object의 복사본을 반환하는데, 대신 클래스의 생성자를 호출하지 않기 때문에 생성자 호출에 의한 OverHead를 피할 수 있다.
clone() 메소드를 사용할 때의 trade-off 문제는 다음에 예제를 참조! 🎈

static int[] Ref_array1={1,2,3,4,5,6,7,8,9};
static int[][] Ref_array2={{1,2},{3,4},{5,6},{7,8}};

int[] array1={1,2,3,4,5,6,7,8,9}; //faster than cloning
int[] array1=(int[])Ref_array1.clone(); //slower than initializing

int[][] array2={{1,2},{3,4},{5,6},{7,8}}; //slower than cloning
int[][] array2=(int[][])Ref_array2.clone(); //faster than initializing

8) Method Inline에 의한 method호출 감소

예제1)
public class InlineMe{
  int counter=0;
  public void method1(){
    for(int i=0;i<1000;i++){
      addCount();
      System.out.println("counter="+counter);
  }
  public int addCount(){
    counter=counter+1;
    return counter;
  }
  public static void main(String args[]){
    InlineMe im=new InlineMe();
    im.method1();
  }
}

예제1)에서 addCount() 메소드를 다음과 같이 수정하면

public void addCount(){
  counter=counter+1;
}

위와 같이 수정할 경우 addCount() 메소드는 컴파일시 Inline 되어서 실제 메소드를 호출하지 않고 같은 결과를 반환한다.
즉 method1()이 실제 수행될 때는 다음과 같이 수행.

public void method1(){
  for(int i=0;i<1000;i++){
  counter=counter+1;
  System.out.println("counter="+counter);
}

9) 생성자 설계

예제1,2,3) 모두 같은 역할을 하는 생성자들로 구성되어 있다.
하지만 퍼포먼스 측면에서 보면 예제3)이 가장 효율적이다.
하지만 클래스를 설계 할 때 예제1)과 같이 해야 할 때도 있다.
클래스가 요구하는 조건에 따라 생성자의 설계방법이 다르겠지만, Object를 생성할 때 가능한 생성자를 적게 호출하는 방법을 사용하는 것이 퍼포먼스 면에서 좋다는 것은 당연한 일이다.

예제1)
예제2)
예제3)
class SlowFlow{
  private int someX, someY;
    SlowFlow(){
    this(777);
  }

  SlowFlow(int x){
    this(x,778);
  }

  SlowFlow(int x, int y)
    someX=x;
    someY=y;
  }
}

class SlowFlow{
  private int someX, someY;
    SlowFlow(){
      this(777,778);
    }
    SlowFlow(int x){
      this(x,778);
    }
    SlowFlow(int x, int y)
      someX=x;
      someY=y;
    }
}

class SlowFlow{
  private int someX, someY;
    SlowFlow(){
      someX=777;
      someY=778;
    }
    SlowFlow(int x){
      someX=x;
      someY=778;
    }
    SlowFlow(int x, int y)
      someX=x;
      someY=y;
    }
}

10) extends" VS "implements"

예제1) extends
import java.awt.event.*;
import java.awt.*;
public class MyWindowAdapter extends WindowAdapter{
  public void windowClosing(WindowEvent we){
    Container source=(Container)we.getSource();
    source.setVisible(false);
  }
}
예제2) implements
import java.awt.event.*;
import java.awt.*;
public class MyWindowListener implements WindowListener{
  public void windowClosing(WindowEvent we){
    Container sourve=(Container)we.getSource();
    source.setVisible(false);
  }
  public void windowClosed(WindowEvent we){}
  pubic void windowActivated(WindowEvent we){}
  public void windowDeactivated(WindowEvent we){}
  public void windowIconified(WindowEvent we){}
  public void windowDeiconified(WindowEvent we){}
  public void windowOpened(WindowEvent we){}
}

"implements"의 경우에는 특정 메소드를 구현하고 인터페이스에 정의된 모든 메소드를 코딩해야 하기 때문에 코드의 낭비를 초래하는 반면, “extends"의 경우에는 슈퍼 클래스에 정의된 메소드들 중 필요한 메소드만 overriding 하면 된다.
(설계 시 추상클래스를 사용할 것인지 인터페이스를 사용할 것인지 고려)


11) 클래스 집약

예제1)
public class Person{
  private Name name;
  private Address address;
}
class Name{
  private String firstName;
  private String lastName;
  private String[] otherNames;
}
class Address{
  private int houseNumber;
  private String houseName;
  private String streetName;
  private String town;
  private String area;
  private String greaterArea;
  private String country;
  private String postCode;
}

예제1)에서 정의된 Person 클래스는 Name형의 name과 Address형의 address, 두 개의 인스턴스 변수를 가진다. 클래스를 설계할 때 가능한 클래스의 수를 줄여서 수행 시 동적으로 생성되는 Object의 수를 줄일 수도 있다. 예제1)에서 정의된 세 개의 클래스는 하나의 클래스로 집약될 수 있다.

예제2)
public class Person{
  private String firstName;
  private String lastName;
  private String[] otherNames;
  private int houseNumber;
  private String houseName;
  private String streetName;
  private String town;
  private String area;
  private String greaterArea;
  private String country;
  private String postCode;
}

12) I/O 퍼포먼스 개선

자바에서는 자료를 읽거나 쓰기 위해 stream을 사용한다.
자바는 두가지 형태의 stream을 지원한다.
Readers and Writers와 Input and Output stream이 그것이다.
Reader and Writers는 high-level의 I/O(예. String)를 지원하고 Input and Output stream은 low-level의 I/O(byte)를 지원한다.
속도 향상을 위해서는 Buffered stream을 사용한다.
Buffered stream을 사용할 경우 버퍼의 기본값은 2K이다.
이 값은 조정될 수 있으나, 자료의 용량이 클 경우 메모리가 많이 필요하기 때문에 Buffered stream을 사용할 경우 여러 가지 사항을 고려해야 한다.

예제1) Simple file copy
public static void copy(String from, String to) throws IOException{
  InputStream in=null;
  OutputStream out=null;
  try{
    in=new FileInputStream(from);
    out=new FileOutputStream(to);
    while(true){
      int data=in.read();
      if(data==-1)
      break;
      out.write(data);
    }
    in.close();
    out.close();
  }finally{
    if(in!=null){in.close();}
    if(out!=null){out.close();}
  }
}

Buffered stream을 사용하지 않고 I/O를 했을 경우의 예제이다.
370K의 JPEG 파일을 복사하는데 10800ms.

예제2) Faster file copy
public static void copy(String from, String to) throws IOException{
  InputStream in=null;
  OutputStream out=null
  try{
    in=new BufferedInputStream(new FileInputStream(from));
    out=new BufferedOutputStream(new FileOutputStream(to));
    while(true){
      int data=in.read();
      if(data==-1)
      break;
      out.write(data);
    }
  }finally{
      if(in!=null){in.close();}
      if(out!=null){out.close();}
  }
}

Bufferd stream을 사용해서 퍼포먼스를 개선한 예제이다.
예제1)과 같은 파일을 복사하는데 130ms.

예제3) Custom buffered copy
public static void copy(String from, String to) throws IOException{
  InputStream in=null;
  OutputStream out=null;
  try{
    in=new FileInputStream(from);
    out=new FileOutputStream(to);
    int length=in.available();
    byte[] bytes=new byte[length];
    in.read(bytes);
    out.write(bytes);
  }finally{
    if(in!=null){in.close();}
    if(out!=null){out.close();}
  }
}

while루프를 사용하지 않고 배열을 사용함으로서 퍼포먼스를 개선한 예제이다. 예제1)과 같은 파일을 복사하는데 33ms.
하지만 예제3)은 byte배열로 선언되는 메모리 버퍼의 크기가 실제 파일의 크기와 동일해야 한다.
이에 따라 두 가지의 문제점이 발생할 수 있다.
☝ 첫 번째는 파일의 용량이 클 경우 상당한 메모리 낭비를 초래한다는 점이다.
✌ 두 번째 문제점은 copy()메소드가 수행될 때마다 new byte[]에 의해 버퍼가 새로 만들어진다는 점이다.
만일 파일의 용량이 클 경우 버퍼가 만들어지고 Garbage Collector에 의해 수집될 때 상당한 OverHead를 초래할 수 있다.

예제4) Improved custom buffered copy
static final int BUFF_SIZE=100000;
static fianl byte[] buffer=new byte[BUFF_SIZE];
public static void copy(String from, String to) throws IOException{
  InputStream in=null;
  OutputStream out=null;
  try{
    in=new FileInputStream(from);
    out=new FileOutputStream(to);
    while(true){
      synchronized(buffer){
      int amountRead=in.read(buffer);
      if(amountRead==-1)
      break;
      out.write(buffer,0,amountRead);
    }
  }finally{
    if(in!=null){in.close();}
    if(out!=null){out.close();}
  }
}

크기가 100K인 byte 배열을 임시버퍼로 지정하고, 이를 static으로 선언함으로서 퍼포먼스를 개선했다. 예제1)과 같은 파일을 복사하는데 22ms.
static buffer의 사용으로 I/O작업을 수행할 경우 발생할 수 있는 문제점을 해결하기 위해 synchronized block을 사용했다.
비록 synchronization을 사용함에 따라 성능 저하를 초래하지만, 실제 while 루프에 머무는 시간이 극히 짧기 때문에 퍼포먼스에 문제는 없다.
테스트에 의하면 synchronized 버전과 unsynchronized 버전 모두 같은 시간에 수행을 완료했다.


13) 웹 환경에서 Caching을 이용한 자바 퍼포먼스 개선

웹 환경에서 Object caching 기법은 주로 DB나 파일에서 동일한 내용을 가져오는 루틴에서 사용된다.
DBMS에 sql문을 던지고 결과를 받아오는 부분의 내용이 거의 변동이 없을 경우 요청 시마다 매번 sql문을 실행시켜서 결과를 받아오는 것이 아니라 최초 실행된 값을 그대로 반환하는 기법이다.
그리고 시간 Check 기법을 이용해서 특정 시간 경과 후 요청이 들어오면 이전 요청에 의해 수행되어진 값을 갱신해서 반환한다.
결과에 의한 반환값이 메모리에 부담이 되지 않을 정도로 크지 않은 경우, 실시간으로 변경된 정보를 반환값으로 사용하는 루틴이 아닐 경우 유용하게 사용될 수 있다.


Performance CheckList

. 임시로 사용하기 위해 Object를 생성하는 것을 피하라. 특히 Loop에서..
. 빈번하게 호출되는 메소드에서 Object를 생성하는 것을 피하라.
. 가능한 Object를 재사용 하라.
. 임시 Object의 생성을 줄이기 위해 데이타 타입 컨버전 메소드를 재정의 하는 방법을 고 려하라.
. 메소드 설계 시 데이타를 유지하고 있는 Object를 반환하는 메소드보다 데이타로 채워진 재사용 가능한 Object에 접근하는 메소드를 정의하라.
. string이나 Object를 integer 로 대치하라. Object 비교를 위해 equal() 메소드를 호출하지 말고 기본 데이타 타입의 == 연산자를 사용하라.
. 인스턴스 변수로 기본 데이타 타입을 사용하라.
. 단지 메소드 호출을 위해 Object를 생성하는 것을 피하라.
. String 연속 연산자 (+)를 사용하는 것보다 StringBuffer 클래스를 사용하라.
. 복사본을 생성하는 메소드 보다 Object를 직접 수정하는 메소드를 사용하라.
. 생성자는 간단하게... 상속 계층은 얕게...
. 인스턴스 변수를 초기화 하는 것은 한 번 이상 하지 말 것.
. 생성자 호출을 피하기 위해 clone() 메소들 사용할 것.
. 간단한 배열일 경우에는 초기화를.. 복잡한 배열일 경우에는 clone() 메소드 호출.
. 프로그램 내부에서 Object 생성 시기를 조절해서 Object 생성에 따른 bottlenecks를 없앤다.
. 어플리케이션 내부에서 여분의 시간이 허용된다면 가능한 Object를 빨리 생성하라. 생성 된 Object를 내부적으로 유지하고 있다가 요청이 발생하면 할당하라.
. 사용 가능성이 희박하거나, 분산처리에 의해 Object를 생성할 경우 Object는 생성 시기를 늦춰라.

profile
네이버 지식인 ZIZON

0개의 댓글