'문자열'에 해당되는 글 1건

  1. 2009.12.14 자바 프로그래밍 일반 1

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

  • 지역 변수의 유효범위를 최소화하는 가장 좋은 방법은 쓰기 바로 전에 선언하는 것이다.
  • 대부분 지역 변수는 선언과 함께 초기화해야 한다.
  • 만약 반복변수를 반복문 안에서만 사용한다면, 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 피곤키오
,