Java Run-Time 오류를 줄이기 위한 안전한 상수의 사용#1

[이미지 출처 - eHow.com]
이 번 포스트에서는 런타임시에 내가 작성한 상수 관련 코드가 문제아가 되지 않도록 하는 "고전적인 방법"에 대해 얘기해볼까 합니다. 몇 가지 소주제는 다음과 같습니다.

  • 상수이해하기
  • 안전한 상수
  • Immutable 객체
  • 읽기전용 인터페이스
  • 복제하기(cloning)
  • 복제가능한 읽기전용 객체

상수이해하기


예를 들어, "어항의 최대 물고기 수"를 표현하는 상수를 정의해 봅시다.
final int MAX_NUMBER_OF_FISH = 10;
여기서는 상수 10을 코드 곳곳에 그대로 사용하기 보다 이름(naming)을 붙여주어 그 이름을 사용하도록 했습니다. 이렇게 상수에 이름을 (잘~~)짓는 프로그래밍 기법은 쉬우면서도 강력한 효과를 가집니다.

  1. 코드 가독성 증가
    • "maigc number" 10을 그대로 사용한다면, 코드에서 상수 10이 무슨 의미인지 알아내는데 시간을 걸릴겁니다.
  2. 코드 관리/유지의 편의성
    • 만약 코드에서 MAX_NUMBER_OF_FISH 이름을 70번 사용했다면, 맨 처음 초기화 문장을 변경했을 때 70 군데의 코드는 모두 변경사항을 적용받습니다. 만약 상수 10을 이름없이 그대로 사용했다면 70군데를 모두 수정해야합니다.
외부클래스에서 사용할 수 있는 클래스필드 형태의 상수를 정의한다면 다음과 같은 모양이 될 수 있습니다.
public class FishTank {

    public static final int MAX_NUMBER_OF_FISH = 10;

}
외부클래스는 클래스필드를 다음과 같이 사용합니다.
int max = FishTank.MAX_NUMBER_OF_FISH;

문제점


이러한 형태의 상수 정의는 C/C++로부터 유래된 것으로 다양한 Java API에서 상수를 정의하는데 사용되고 있습니다. 한 예로 java.awt.Font 클래스를 살펴보면,
public class Font implements java.io.Serializable {
    ...
    public static final int PLAIN = 0;

    public static final int BOLD = 1;

    public static final int ITALIC = 2;
    ...

    public Font(String name, int style, int size) {
         ...
    }
}
과 같이 폰트 스타일을 각각 정수형(primitive type) 상수로 구분하고 있습니다. 상수에 이름이 붙어있어 어떤 스타일인지 알기 쉽다는 점에서 가독성은 있으나, 다음과 같은 코드에서 문제점이 드러납니다.
Font fontA = new Font("Helvetica", Font.BOLD, 40);

Font fontB = new Font("Helvetica", 40, Font.BOLD); 
fontA 객체의 생성자 호출에서 폰트의 이름, 스타일(Bold), 크기(40)이 각각 제대로 주어졌습니다. 반면 fontB 객체의 생서자 호출은 스타일과 크기 인자의 순서가 바뀌었습니다. 이러한 잘못된 인자 전달은 의도적이었든 실수였든 흔히 일어날 수 있는 일이지만, 컴파일러 입장에서는 두 인자의 타입에 맞는 정수 두 개가 전달되었으므로 아무런 불평없이 컴파일을 수행합니다. 아래와 같은 코드도 컴파일러 입장에선 아주 행복한 케이스입니다.
Font fontC = new Font("Helvetica", FishTank.MAX_NUMBER_OF_FISH, 15);
런타임시에는 어떤일이 발생할까요? 결과는 인자를 사용하는 각 메서드가 인자의 범위나 적절성을 검사하여 어떻게 처리하는지에 달려있습니다. 예를 들어, 특정 메서드에서 각 인자에 대한 적절성을 검사하여 예외처리를 하거나, 아무런 검사 없이 그대로 이 후 코드에 인자 값을 사용하여 프로그램의 비정상 종료를 불러올 수 도 있습니다. 우선 아래 코드처럼 적절성 검사를 통해 예외처리를 한다고 가정해 봅시다.(java.awt.Font 클래스의 실제 구현에서는 생성자에서 비트연산자를 이용하여 범위검사)
if(style != BOLD && style != PLAIN && style != ITALIC)
    // THROW an Exception!!
style 인자 값을 사용하는 모든 코드 부분에 적절성을 검사하는 것은 그리 부담스러운 작업은 아닌것 같습니다. 또 그렇게 해야할 것 같기도 합니다. 하지만 스타일 상수가 하나 추가된다면 어떻게 할까요? 모든 적절성 검사 코드에 추가된 사항을 반영해야 합니다. 수정의 번거러움을 해결하는 직관적인 방법은 위 검사코드를 메서드로 만들어 메서드 호출 코드로 수정하는 것입니다. 그럼 상수가 추가되더라도 적절성을 검사하는 메서드만 수정하면 되니까요. 조금더 개선해 보도록 합시다. 메서드 본문까지도 수정하지 않고자 한다면 어떻게 할까요?
public class ClassWithConstants {
    public static final int MIN_BOUND = 0;
    public static final int CONST1 = 1;
    public static final int CONST2 = 2;
    public static final int MAX_BOUND = 3;

    //private helper method
    private void validateConstant(int constant)
        throws IllegalConstantException //defined elsewhere!!
    {
        if(constant < MIN_BOUND || constant > MAX_BOUND)
            throw new IllegalConstantException();
    }
}
validateConstant 메서드는 상수의 범위를 검사하여 적절히 예외를 발생시킵니다. 상수가 추가된다 하더라도 validateConstant 메서드는 수정할 필요가 없습니다. 단 상수를 추가할 때 MAX_BOUND를 수정해야 하는 번거로움은 있습니다.


고전적인 상수 정의 방법에 따른 문제점을 해결하기 위해 몇 가지 시도를 했습니다. 그러나 런타임에 IllegalConstantException이 발생할 가능성은 아직 남아있습니다. 이러한 문제의 근원은 컴파일러가 인자의 적절성을 제대로 검사하지 못하도록 상수를 정의했다는데 있습니다.
Get your compiler to check that constants passed as parameters are valid.

컴파일러의 타입체크 기능을 충분히 활용하기 위해서는 정수와 같이 원시 데이터 타입이 아닌 클래스 타입의 상수객체를 만들필요가 있습니다. "안전한 상수" 만들기는 다음 글로 넘기도록 하겠습니다.

[참고문헌]
1. JDK 1.6.0 update 25 source code
2. Design styles and Idioms for Effective Java, Nigel Warren, Philip Bishop, Addison-Wesley.

0 댓글