인자의 유효성을 검사하라.

유효성 검사를 통해 인자가 유효하지 않을 때 적절한 예외를 발생시키면. 빠르고 깔끔하게 메소드를 종료할 수 있다.

Public 메소드의 경우 javadoc의 @throws 태그를 써서 인자값이 제약 조건을 어겼을 때 던지는 예외를 문서화 해야한다.

public class Big {
	/**
	* (this mod m)의 값을 가지는 BigInteger를 리턴한다.
	* 이 메소드는 항상 음수가 아닌 BigInteger를 리턴한다는 점이
	* remainder 메소드와 다르다
	* 
	* @param m 나누는 수, 반드시 양수이다.
	* @return this BigInteger를 m으로 나눈 몫. 항상 양수이다.
	* @throws ArithmeticException m <=0 일 때 발생한다.
	*/
	public BigInteger mod(BigInteger m) {
		if(m.signum() <= 0)
			throw new ArithmeticException("Modulus not positive");
		//...실제 계산 코드
		return null;
	}
}
 

필요한 경우 방어복사하라.

클래스의 모든 클라이언트는 항상 불변규칙을 깨뜨리기 위해 최선을 다한다고 가정하고, 프로그래밍할 때 항상 방어하는 자세를 가져야 한다.

다음 클래스는 두 날짜 사이의 기간을 표시한다.

//가짜 불변 클래스
public final class Period {
	private final Date start;
	private final Date end;
    
	/**
	* @param start        시작일.
	* @param end  종료일. 반드시 시작일 이후이다.
	* @throws IllegalArgumentException 시작일이 종료일보다 늦을 때 발생한다.
	* @throws NullPorinterException 시작일이나 종료일이 null일때 발생한다.
	*/
	public Period(Date start, Date end) {
		if(start.compareTo(end) > 0)
			throw new IllegalArgumentException(start + "after" + end);
               
		this.start = start;
		this.end = end;
	}

	public Date start() {
		return start;
	}

	public Date end() {
		return end;
	}

	//...이하 생략
	
}

Date 클래스가 가변 클래스이기 때문에 불변규칙은 쉽게 깨진다.

//Period 인스턴스의 내부를 공격하라.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);       //p를 수정한다.

이런 종류의 공격으로부터 Period 인스턴스 내부를 보호하려면, 생성자에 전달되는 변경가능 인자들을 방어복사(defensive copy)해야한다.

//인자를 방어복사한다.
public Period(Date start, Date end) {
	this.start = new Date(start.getTime());
	this.end = new Date(end.getTime());
               
	if(start.compareTo(end) > 0)
			throw new IllegalArgumentException(start + "after" + end);
               
	}

생성자를 이렇게 하면 앞에 나왔던 공격 패턴은 더 이상 먹히지 않는다. 인자의 유효성 검사를 하지 전에 먼저 복사하고 나서 원본이 아닌 복사본의 유효성을 검사한다는 것에 주목할 필요가 있다.

개선한 생성자가 모든 공격을 막아낼 수 있을 것 같지만, 아직도 Period 인스턴스를 공격할 수 있는 방법이 있다.

//Period 인스턴스에 대한 다른 공격
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);   //p를 수정한다.

접근자 메소드에서 가변 객체에 대한 참조를 방어복사하여 리턴하도록 고쳐야 이런 종류의 공격을 막아 낼 수 있다.

public Date start() {
	return (Date) start.clone();
}

public Date end() {
	return (Date) end.clone();
}

Period의 내부 Date 객체는 java.util.Date 클래스의 인스턴스라는 것이 확실하기 때문에 clone 메소드를 써도 아무 문제가 없다.

 

메소드를 중복정의할 때는 신중하라.

다음 예제는 컬렉션을 종류에 따라 분류할 목적으로 만든 것이다.

//틀린 구현 - 메소드 중복정의를 잘못 쓰고 있다.
public class CollectionClassifier {
	public static String classify(Set s) {
		return "Set";
	}
    
	public static String classify(List l) {
		return "List";
	}
      
	public static String classify(Collection c) {
		return "Uknow Collection";
	}
        
	public static void main(String[] args) {
		Collection[] tests = new Collection[] {
			new HashSet(),         //Set
			new ArrayList(),       //List
			new HashMap().values() //Set도 List도 아닌 것
		};
               
		for(int i=0; i<tests.length; i++)
			System.out.println(classify(tests[i]));
	}
}

아마 다음과 같은 출력을 예상했을 것이다.

Set
List
Unknown Collection

하지만, 실제로는 “Unknown Collection” 만 세 번 출력한다.

이 문제를 해결하려면 세개의 classify 메소드를 하나로 합치고 명시적으로 instanceof를 써서 타입을 검사해야 한다.

public static String classify(Collection c) {
	return (c instanceof Set ? "Set" : (c instanceof List ? "List" : "Unknow Collecion"));
}
 

널( null)이 아닌 길이가 0인 (zero-length)배열을 리턴하라.

다음과 같이 메소드를 만드는 경우를 많이 볼 수 있다.

private List cheeseInStack = …;
 
/**
 * 상점에 남아 있는 모든 치즈의 배열을 리턴하거나,
 * 팔 수 있는 치즈가 없으면 null을 리턴한다.
 */
public Cheese[] getCheeses() {
	if(cheeseInStack.size() == 0)
		return null;
 
	//...코드 생략
 
}

팔 수 있는 치즈가 남아 않으면 다음과 같이 처리 할 수 있다.

Cheese[] cheese = shop.getCheeses();

public void in() {
	if(Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON))
		System.out.println("Jolly good, just the thing.");
}

길이가 0인 배열을 리턴할 수 있는 곳에서는 null을 처리하는 코드가 반복되어야 한다. 배열을 리턴하는 메소드에서 null을 리턴할 이유가 전혀 없다. Null을 리턴할 상황이라면 길이가 0인 배열을 리턴하면 된다.

다음 코드는 null대신에 길이가 0인 배열을 리턴한다.

Main.java
class Cheese {
	String name;
        
	Cheese(String name) {
		this.name = name;
	}
        
	public static final Cheese STILTON = new Cheese("Stilton");
	public static final Cheese CHEDDAR = new Cheese("Cheddar");  
        
}
 
class CheeseShop {
	private static Cheese[] ECA = new Cheese[0];
	private List cheesesInStock = Collections.singletonList(Cheese.STILTON);
        
	private final static Cheese[] NULL_CHEESE_ARRAY = new Cheese[0];
        
	/**
	* @return Cheese[] 팔 수 있는 모든 치즈의 배열을 리턴한다.
	*/
	public Cheese[] getCheeses() {
		return (Cheese[]) cheesesInStock.toArray(NULL_CHEESE_ARRAY);
	}
}
 
public class Main {
	static CheeseShop shop = new CheeseShop();
        
	public static void main(String[] args) {
		Cheese[] cheeses = shop.getCheeses();
		if (Arrays.asList(cheeses).contains(Cheese.STILTON))
			System.out.println("Jolly good, just the thing.");
		if (Arrays.asList(cheeses).contains(Cheese.CHEDDAR))
			System.out.println("Oops, too bad.");        
	}
}

'개발자 센터 > Java' 카테고리의 다른 글

자바 프로그래밍 일반  (1) 2009.12.14
자바 클래스를 싱글 패턴으로 구현하기  (0) 2009.12.14
Posted by 피곤키오
,

지역 변수의 유효범위를 최소화하라

  • 지역 변수의 유효범위를 최소화하는 가장 좋은 방법은 쓰기 바로 전에 선언하는 것이다.
  • 대부분 지역 변수는 선언과 함께 초기화해야 한다.
  • 만약 반복변수를 반복문 안에서만 사용한다면, while 반복문 보다는 for 반복문을 쓰는 것이 좋다.

다음은 컬렉션 전체를 순회할 때 주로 사용하는 구현패턴이다.

for( Iterator i = c.iterator(); i.hasNext(); ) {
        doSomething(i.next());
}

일부로 버그를 넣은 코드를 살펴보면서 왜 for 반복문이 while 반복문보다 나은지 알아보자.

Iterator i = c.iterator();
while ( i.hasNext() ) {
        doSomething(i.next());
}

//...코드 생략


Iterator i2 = c2.iterator();
while ( i.hasNext() ) {		//버그 발생!
        doSomething(i2.next());
}

두 번째 반복문에서 복사 붙여넣기 때문에 발생한 오류가 있다. 이 코드는 문제없이 컴파일 되고, 아무런 예외도 발생하지 않고 실행 될것이다. 하지만 분명히 오류가 있다. 이런 종류의 오류는 찾아내기 어렵다.

다음과 같이 for문을 사용했을 때에는 같은 오류를 범했다하더라도 컴파일에서 에러가 난다.

for(Iterator i = c.iterator(); i.hasNext(); ) {
        doSomething(i.next());
}

//컴파일 시점에서 오류 - the symbol i cannot be resolved
for(Iterator i2 = c2.iterator(); i.hasNext(); ) {
        doSomething(i2.next());
}

지역 변수의 유효범위를 최소화하면서 리스트를 순회하는 반복문의 또 다른 구현패턴을 살펴보자

//임의 접근(random access) 리스트를 순회하는 구현패턴, 성능 좋다.
for(int i = 0, n = list.size(); i < n; i++) {
        doSomething(list.get(i));
}

이 구현 패턴은 ArrayList, Vector 클래스와 같은 임의 접근 리스트를 순회할 때, 앞에서 설명한 구현패턴보다 더 빠르다. 이 구현 패턴은 반드시 임의 접근 리스트에만 사용해야 한다. 다른 종류의 리스트에 사용하면 성능은 저하된다.

이 구현 패턴은 사용하지 말자.

for(int i = 0; i < list.size(); i++) {
		//size()를 매번 호출한다.
        doSomething(list.get(i));
}

마지막으로 지역변수의 유효범위를 최소화하려면, 한 메소드는 한가지 작업만 처리하도록 가능하면 작게 만들어야 한다.

 

라이브러리를 배우고 익혀서 써라

많은 프로그래머들이 0이상의 int 타입 난수를 생성하기 위해 다음과 같은 메소드를 사용할 것이다.

static Random rnd = new Random();

//흔히 쓰는 방법이지만 문제가 있다.
static int random(int n) {
        return Math.abs(rnd.nextInt()) % n;
}

이 메소드는 세가지 결함이 있다.

  • 첫째, n이 2의 제곱수 중 작은 수라면, 이 메소드가 만들어 내는 난수열은 꽤 짧은 주기로 반복된다.
  • 둘째, n이 2의 제곱수가 아니라면, 어떤 수가 다른 것보다 더 자주 나타날 가능성이 크다. n이 클수록 이 결함은 더 커진다.
  • 셋째, 지정한 범위를 벗어나는 난수가 발생할 수도 있다.

하지만, 이 결함을 해결할 Random.nextInt(int)라는 메소드가 1.2 배포판부터 java.util 패키지에 들어있으니 걱정할 필요가 없다.

다음은 안전하게 0이상의 int 타입 난수를 만드는 예제이다.

Main.java

import java.util.Random;

public class Main {    

        static Random rnd = new Random();

        public static void main(String[] args) {
               int n = 2 * (Integer.MAX_VALUE/3);
               int low = 0;
               for (int i=0; i<1000000; i++)
                       if(rnd.nextInt(n) < n/2)
                              low++;

               System.out.println(low);
        }
}

 

정확한 계산에 fload이나 double 타입을 쓰지 마라

특히, 금전을 계산할 때 float이나 double 타입을 절대로 쓰지 말아야 한다. 이 타입들은 0.1, 0.01과 같은 10의 음의 지수값들을 정확하게 표현하지 못한다.

//틀린 구현 - 화폐 계산에 부동 소수점 타입을 사용
public static void main(String[] args) {
	   double funds = 1.00;
	   int itemsBought = 0;

	   for(double price = .10; funds >= price; price += .10) {
			   funds -= price;
			   itemsBought++;
	   }

	   System.out.println(itemsBought + " items bougnt.");
	   System.out.println("Change: $" + funds);
}
결과값:
3 items bougnt.
Change: $0.3999999999999999

사탕은 세개 밖에 사지 못했고 잔돈은 0.3999999999999999 달러 밖에 남지 않는다.

다음과 같이 BigDecimal, int, long과 같은 타입을 써야 정확한 소수점 계산을 할 수 있다.

public static void main(String[] args) {
	   final BigDecimal TEN_CENTS = new BigDecimal(".10");	   

	   BigDecimal funds = new BigDecimal("1.00");
	   int itemsBought = 0;

	   for(BigDecimal price = TEN_CENTS; 
			   funds.compareTo(price) >= 0 ; 
			   price = price.add(TEN_CENTS)) {
			   funds = funds.subtract(price);
			   itemsBought++;
	   }

	   System.out.println(itemsBought + " items bougnt.");
	   System.out.println("Money left over: $" + funds);
}
결과값:
4 items bougnt.
Money left over: $0.00

이제서야 사탕 네개를 살 수 있고, 잔돈도 남지 않는다. 하지만 BigDecimal은 기본타입을 쓰는 것보다 불편하고 느리다.

소수점 이하 자릿수를 직업 관리한다면 BigDecimal을 쓰지 않고 int나 long을 쓸수 있다. 예를 들어 달러 대신에 센트를 기본단위로 계산하면 위의 코드가 다음과 같이 바뀐다.

public static void main(String[] args) {  

	   int funds = 100;
	   int itemsBought = 0;

	   for(int price = 10; funds >= price; price += 10 ) {
			   funds -= price;
			   itemsBought++;
	   }

	   System.out.println(itemsBought + " items bougnt.");
	   System.out.println("Money left over: $" + funds);
}

다루어야 하는 숫자의 자릿수가 9자리를 넘지 않으면 int를 쓰고, 18자리를 넘지 않으면 long을 써라. 이것보다 더 커지면 BigDicimal을 써라.

 

적절한 타입 대신 문자열을 쓰지 마라

  • 문자열로 다른 값타입(value type)을 대신하지 마라.
  • 문자열로 열거타입을 대신하지 마라, 타입안전 열거를 쓰는 것이 가장 좋다.
  • 문자열로 집합타입(aggregate type)을 대신하지 마라.
  • 문자열로 어떤 기능(capability)을 대신하지 마라.
//틀린 구현 – 문자열로 기능을 대신하고 있다.
public class ThreadLocal {
        private ThreadLocal() {       }       //인스턴스를 만들수 없다.        

        //특정 이름을 가진 키를 써서 현재 스래드의 값을 저장한다.
        public static void set(String key, Object value) {

               //...코드 생략

        }        

        //특정 이름을 저장된 현재 스레드의 값을 리턴한다.
        public static Object get(String key) {

               //...코드 생략

               return null;
        }
}

이 방식의 문제점은 키로 쓴 문자열을 모든 스레드가 공유할 수 있다는 것이다. 즉, 보안상 문제가 생길 수 있다.

위조 불가능한 키를 써서 다음과 같이 수정할 수 있다.

public class ThreadLocal {
        private ThreadLocal() {       }       //인스턴스를 만들수 없다.        

        public static class Key {
               Key() { }
        }        

        //위조할 수 없는 유일한 키를 생성한다.
        public static Key getKey() {
               return new Key();
        }        

        public static void get(Key key, Object value) {

               //...코드 생략

        }

        public static Object get(Key key) {

               //...코드 생략

               return null;
        }      
}

 

성능을 떨어뜨리는 문자열 연결을 조심하라.

//잘못된 문자열 연결로 엄청 느리다

public String statement() {
	   String s = "";

	   for(int i=0; i < numItems(); i++)
			   s += lineForItem(i);   //문자열 연결

	   return s;
}

이 메소드는 항목이 많아지면 성능은 엄청 느려진다. String 클래스 대신에 StringBuffer 클래스를 써야 원하는 성능을 낼 수 있다.

public String statement() {
	   StringBuffer s = new StringBuffer(numItems() * LINE_WIDTH);

	   for(int i=0; i < numItems(); i++)
			   s.append(lineForItem(i));     //문자열 연결

	   return s.toString();
}

위의 두 메소드의 성능은 90배 정도 차이난다. 새로 만든 메소드에서 StringBuffer를 주문서를 다 담을 수 있을 만큼 넉넉하게 메모리를 할당했다.

 

인터페이스 타입으로 객체를 참조하라.

  • 객체를 참조할 때에는 클래스 타입보다는 인터페이스 타입을 쓰는 것이 더 좋다.
  • 적절한 인터페이스 타입이 있다면 인자, 리턴값, 변수, 필드는 반드시 인터페이스 타입으로 선언해야 한다.
//Good - 인터페이스 타입을 쓴다.
List subscribers1 = new Vector();
List subscribers2 = new ArrayList();

이렇게 하면 안 된다.

//Bad - 클래스 타입을 쓴다.
Vector subscribers1 = new Vector();
ArrayList subscribers2 = new ArrayList();

만약, 적절한 인터페이스 타입이 있다면, 객체를 반드시 인터페이스 타입으로 참조해야 프로그램이 유연해진다. 그러나 인터페이스 타입이 없다면, 필요한 기능을 제공해 주는 최상위 클래스 타입을 사용하는 것이 좋다.

 

리플렉션보다 인터페이스를 써라

Java.lang.reflect 패키지는 이미 로딩한 클래스에 접근할 수 있는 리플렉션(reflection)기능을 제공한다. reflect 패키지가 제공하는 Constructor, Method, Field 인스턴스를 쓰면, 이것들의 근원이 되는 실제 생성자, 메소드, 필드를 직접 조작할 수 있다. 하지만, 여기에는 대가가 따른다.

  • 예외에 대한 검사를 포함하여, 모든 컴파일 시점의 타입 검사를 포기해야 한다.
  • 리플렉션을 쓰면 코드가 매우 번거로워지고 양도 많아진다.
  • 성능이 떨어진다.

응용프로그램 설계시점(design time)에만 리플렉션을 쓴다. 일반 응용프로그램에서 실행시점에 리플렉션을 써서 객체에 접근하면 안된다.

물론, 리플랙션을 꼭 써야 하는 복잡한 응용 프로그램이 있기는 하다. 클래스 브라우저(class brower), 객체 조사기(object inspector), 코드 분석기, 인터프리터 기반의 내장 시스템과 같은 응용프로그램에서는 리플렉션을 쓸 수 밖에 없다.

아주 제한된 형태로 리플렉션을 장점만을 활용할 수가 있다. 리플랙션으로 존재하진 않는 클래스의 인스턴스를 생성하고, 인터페이스나 상위클래스로 이 인스턴스에 접근하는 방식을 쓸 수 있다.

//리플렉션을 써서 인스턴스를 생성하고 
//인터페이스를 통해 이 인스턴스에 접근한다.
public static void main(String[] args) {
	   //클래스 이름을 클래스 객체로 바꾼다.
	   Class cl = null;
	   try {
			   cl = Class.forName(args[0]);
	   } catch(ClassNotFoundException e) {
			   System.err.println("Class not found");
			   System.exit(1);
	   }               

	   //인스턴스를 생성한다.
	   Set s = null;
	   try {
			   s = (Set) cl.newInstance();
	   } catch(IllegalAccessException e) {
			   System.err.println("Class not accessible.");
			   System.exit(1);
	   } catch(InstantiationException e) {
			   System.err.println("Class not instantiable.");
			   System.exit(1);
	   }             

	   //집합의 동작을 검사한다.
	   s.addAll(Arrays.asList(args).subList(1, args.length));
	   System.out.println(s);
}

 

EFFECTIVE JAVA
카테고리 컴퓨터/IT
지은이 Joshua Bloch (대웅, 2003년)
상세보기
Posted by 피곤키오
,

생성자 대신 스태틱 팩토리 메소드를 고려하라

스태틱 팩토리 메소드는 단순히 자신이 정의된 클래스의 인스턴스를 리턴하는 메소드로 public static으로 정의한다. 기본타입 boolean의 래퍼클래스인 boolean에 1.4 배포판부터 추가된 Boolean.valueOf(boolean b) 메소드는 스태틱 메소드의 좋은 예이다.

  • 스태틱 팩토리 메소드는 생성자와 달리 알맞은 이름을 줄 수 있다.
  • 스태틱 팩토리 메소드는 생성자와 달리 호출될 때마다 새로은 객체를 생성하지 않아도 된다.
  • 생성자는 자신이 정의된 클래스의 인스턴스만 리턴할 수 있지만, 스태틱 팩토리 메소드는 자신이 선언된 것과 같은 타입의 인스턴스는 모두 리턴할 수 있다.
  • 스태틱 팩토리 메소드의 가장 큰 단점은 이 메소드를 정의한 클래스가 public 이나 protected 생성자를 제공하지 않으면, 다른 클래스가 이 클래스를 상속 받을 수 없다는 것이다.
  • 스태틱 팩토리 메소드의 두번째 단점은 다른 스태틱 메소드와의 차이를 명시 할 수 없다는 것이다.

Foo.java 서비스 제공자 프레임워크 예제
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
 

//서비스 제공자 프레임워크 예제
public abstract class Foo {
        //문자열 키와 구현 클래스의 class 객체를 결합시킨 맵
        private static Map implementations = null;
        private static ResourceBundle classNames;
        
        //처음 호출되었을때 맵을 초기화 한다.
        private static synchronized void initMapIfNecessary() {
               if( implementations == null ) {
                       implementations = new HashMap();
               }
               
               //속성 파일에서 키와 구현 클래스 이름을 가져온다.
               //Class.forName을 써서 클래스 이름으로부터 Class 객체를
               //생성하고 Map에 저장한다.
               
               String firstFoo = getValue("firstFoo");
               String secondFoo = getValue("secondFoo");
               String thirdFoo = getValue("thirdFoo");
               
               try {
                       Class obj1 = Class.forName(firstFoo);
                       Class obj2 = Class.forName(secondFoo);
                       Class obj3 = Class.forName(thirdFoo);
                       implementations.put("firstFoo", obj1);
                       implementations.put("secondFoo", obj2);
                       implementations.put("thirdFoo", obj3);
                       
               } catch (ClassNotFoundException e) {
                       System.out.println("Class not found");
               }
        }
        
        private static String getValue(String s) {
               String value = classNames.getString(s);
               return value;
        }
        
        static {
               try {
                       classNames = ResourceBundle.getBundle("TableText");                  
               } catch (java.util.MissingResourceException e) {
                       System.out.println("Resource File not Found");
                       System.exit(1);
               }
        }
        
        public static Foo getInstance(String key) {
               initMapIfNecessary();
               Class c = (Class) implementations.get(key);
               System.out.println(c + " " + "returned");
               if (c == null)
                       return new DefalutFoo();
               try {
                       return (Foo)c.newInstance();
               } catch (Exception e) {
                       return new DefalutFoo();
               }
        }
}

 

Private 생성자를 써서 싱글톤을 유지하라

싱글톤(singleton)이란 정확히 하나의 인스턴스만 만들어지는 클래스로 원래부터 유일할 수밖에 없는 비디오 출력이나 파일 시스템과 같은 시스템 구성요소들을 주로 표현한다.

싱글톤은 두가지 방법으로 구현 할수 있다. 두 방법 모두 생성자를 private으로 정의하고 클라이언트가 이 클래스의 유일한 인스턴스에 접근 할 수 있는 public static 멤버를 제공한다.

public static final 필드로 구현한 싱글톤
//public static final 필드로 구현한 싱글톤
public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        
        private Elvis() {
               //... 코드 생략
        }
        
        //... 이하 생략
        
}

Elvis 클래스의 private 생서자는 public static final 멤버 필드인 Elvis.INSTANCE가 초기화 될 때 딱 한번만 호출된다. 싱글톤을 유지 하려면 public이나 protected 생성자를 두지 말아야 한다.

스태틱 팩토리 메소드로 구현한 싱글톤
//스태틱 팩토리 메소드로 구현한 싱글톤
public class Elvis {
        private static final Elvis INSTANCE = new Elvis();
        
        private Elvis() {
               //... 코드 생략
        }
        
        public static Elvis getInstance() {
               return INSTANCE;
        }
        
        //... 이하 생략
        
}

스태틱 팩토리 메소드인 Elvis.getInstance는 호출될 때마다 새로운 Elvis 인스턴스를 생성하지 않고 계속 같은 객체 참조를 리턴한다.

 

Priavet 생성자로 인스턴스를 만들지 못하게 하라

가끔 static 메소드와 static 필드로만 이루어진 클래스를 만들어야 할때가 있다. 이때에는 private 생성자 하나만 만들어 주면 된다.

//인스턴스를 만들 필요가 없는 유틸리티 클래스
public class UtilityClass {
        //private 생성자를 하나 만들어 컴파일러가 자동으로 기본 생성자를
        //추가 하지 못하게 한다.
        private UtilityClass() {
               //이 생성자는 호출되지 않는다.
        }
        
        //...이하 생략
        
}

Utility 클래스의 생성자는 이 클래스 밖에서 접근할 수 없다. 따라서 utility 클래스 내부에서 생성자를 호출하지 않는 다면 , 절대 인스턴스가 생성되는 일은 없다.

 

쓸데없는 객체를 중복 생성하지 마라

동등한 기능을 하는 객체는 필요할때마다 생성하는 것보다 한 객체를 재사용하는 것이 더 낫다. 다음과 같은 코드는 만들면 안된다.

String s = new String("바보"); //이렇게 만들지 말것!

다음은 가변 객체를 잘못 사용하고 있는 예제이다.

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
 
public class Person {
        private final Date birthDate;
        //다른 필드는 생략했다.
        
        public Person(Date birthDate) {
               this.birthDate = birthDate;
        }
        
        //절대 이렇게 하지 말 것!
        public boolean isBabyBoomer() {
               Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
               gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
               Date boomStart = gmtCal.getTime();
               gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
               Date boomEnd = gmtCal.getTime();
               return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
        }
}

isBabyBoomer 메소드를 호출할때마다 Calendar 인스턴스하나, TimeZone 인스턴스하나, Date 인스턴스 두개가 쓸데 없이 계속 생성된다. isBabyBoomer 메소드는 내용이 변하지 않기 때문에 static 초기화를 사용하여 다음과 같이 해결한다.

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
 
public class Person {
        private final Date birthDate;
        //다른 필드는 생략했다.
        
        public Person(Date birthDate) {
               this.birthDate = birthDate;
        }
        
        /**
         * 베이비 붐의 시작 및 종료 일자
         */
        private static final Date BOOM_START;
        private static final Date BOOM_END;
        
        static {
               Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
               gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
               BOOM_START = gmtCal.getTime();
               gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
               BOOM_END = gmtCal.getTime();  
        }
        
        public boolean isBabyBoomer() {
               return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
        }
}

 

쓸모 없는 객체 참조는 제거하라

다음 예제를 살펴보자.

import java.util.EmptyStackException; 

//어디선가 메모리가 새고(memory leak) 있다.
public class Stack {
        private Object[] elements;
        private int size = 0;
        
        public Stack(int initialCapacity) {
               this.elements = new Object[initialCapacity];
        }
        
        public void push(Object e) {
               ensureCapacity();
               elements[size++] = e;
        }
        
        public Object pop() {
               if(size ==0)
                       throw new EmptyStackException();
               return elements[--size];
        }
        
        /**
         * 최소한 하나의 구성요소를 담을 수 있는 여유를 두어야 하며,
         * 여유가 없을 때 구성요소를 담는 배열의 크기를 두배로 늘린다.
         */
        public void ensureCapacity() {
               if(elements.length == size) {
                       Object[] oldElements = elements;
                       elements = new Object[2*elements.length+1];
                       System.arraycopy(oldElements, 0, elements, 0, size);
               }
        }
}

elements 배열의 “유효부분(active portion)”을 벗어난 배열 구성요소가 저장하고 있는 참조들은 쓸모 없는 참조들이다. 팝(pop)된 객체들은 다른 프로그램에서 더 이상 참조하지 않더라도 이 객체들은 가비지 컬렉션 대상이 되지 않는다. 가비지 컬렉터가 있는 언어에서 메모리 누수현상은 매우 위험한 것이다. 이런문제는 쓸모없는 참조에 null을 대입해 버리면 된다. 따라서 Stack 클래스의 pop메소드는 다음과 같이 수정되어야 한다.

public Object pop() {
       if(size ==0)
	       throw new EmptyStackException();
       Object result =  elements[--size];
       elements[size] = null; //쓸모없는 참조를 없앤다.
       return result;
}
* 가비지 컬렉션

자바 플랫폼의 모든 배열과 객체들은 힘(heap)이라는 메모리 공간에 저장된다. New 키워드를 쓸때마다 힙에 새로운 메모리가 객체마다 할당된다. 이때 할당된 메모리를 가비지 컬렉터가 알아서 반환해준다. 보통 프로그래머들은 메모리를 반환할 때, 대상 객체 참조에 null을 대입하거나 System.gc()를 호출하는 방법을 사용한다. 물른, System.gc()를 호출했다고 해서 가비지 컬렉션이 바로 실행된다는 보장은 없다.

* 종료자

Object 클래스에는 protected 메소드인 finalize라는 메소드가 있다. 자바의 모든 클래스는 이메소드를 재정의 할 수 있다. 특정 객체에 실제로 호출되는 finalize 메소드를 이 객체의 종료자(finalizer)라고 한다. 시스템은 가비지 컬렉터가 객체를 파괴하기 전에 반드시 종료자를 호출한다. 하지만, 가비지 컬렉터가 객체를 언제 파괴할지 알수 없기 때문에 종료자도 언제 호출될지 알수 없고, 아예 호출되지 않을수도 있다.

//종료가 가디언 구현패턴
public class Foo {
        //이 객체는 오직 자신을 포함하는 Foo객체의 종료처리를 수행한다.
        private final Object finalizerGuradin = new Object() {
               protected void finalize() throws Throwable {
                       System.out.println("Outer Foo object finalized");
               }
        };
        
        public static void main(String args[]) {
               Foo foo = new Foo();
               //강제 종료자 수행
               foo = null;
               System.gc();
        }
}

 

EFFECTIVE JAVA
카테고리 컴퓨터/IT
지은이 Joshua Bloch (대웅, 2003년)
상세보기

'개발자 센터 > Java' 카테고리의 다른 글

자바 메소드 작성시 알아야 할 것  (0) 2009.12.16
자바 프로그래밍 일반  (1) 2009.12.14
Posted by 피곤키오
,