Chapter 12 지네릭스, 열거형, 애너테이션

Jun-hee Cho·2023년 4월 3일
0

Java

목록 보기
12/13

1. 지네릭스(Generics)

1.1 지네릭스란?

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

예를 들어, ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 더 많다. 그런데도 꺼낼 때 마다 타입체크를 하고 형변환을 하는 것은 아무래도 불편할 수밖에 없다. 게다가 원하지 않는 종류의 객체가 포함되는 것을 막을 방법이 없다는 것도 문제다. 이러한 문제들을 지네릭스가 해결해 준다.

간단히 얘기하면 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 얘기다.

1.2 지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 지네릭 타입에 대해서 알아보자. 예를 들어 클래스 Box가 다음과 같이 정의되어 있다고 가정하자.

이 클래스를 지네릭 클래스로 변경하면 다음과 같이 클래스 옆에 <T>를 붙이면 된다. 그리고 'Object'를 모두 'T'로 바꾼다.

T를 '타입 변수'라고 하며, 'Type'의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList <E>의 경우, 타입 변수 E는 'Element(요소)'의 첫 글자를 따서 사용했다. 타입 변수가 여러 개인 경우에는 Map<K, Y>와 같이 콤마','를 구분자로 나열하면 된다. K는 Key를 의미하고, V는 Value를 의미한다. 무조건 'T'를 사용하기보다 가능하면, 이처럼 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.

이들은 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다. 마치 수학식 'f(x, y) = x + y'가 'f(k, v) = k + v'와 다르지 않은 것처럼 말이다.

기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.

이제 지네릭 클래스가 된 Box클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T대신에 사용될 실제 타입을 지정해주어야 한다.

위의 코드에서 타입 T대신에 String타입을 지정해줬으므로, 지네릭 클래스 Box<T>는 다음과 같이 정의된 것과 같다.

class Box<String> { // 지네릭 타입을 String으로 지정
	String item;
    
    void setItem(String item) { this.item = item; }
    String getItem() { return item; }
}

만일 Box클래스에 String만 담을 거라면, 타입 변수를 선언하지 않고 위와 같이 직접 타입을 적어주는 것도 가능하다. 단, Box<String>클래스는 String타입만 담을 수 있다. 반면에 Box<T>클래스는 어떤 타입이든 한 가지 타입을 정해서 담을 수 있다.

지네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용된다. 다만 지네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.

Box b = new Box(); // OK. T는 Object로 간주된다.
b.setItem("ABC"); // 경고. unchecked or unsafe operation
b.setItem(new Object()); // 경고. unchecked or unsafe operation

아래와 같이 타입 변수 T에 Object타입을 지정하면, 타입을 지정하지 않은 것이 아니라 알고 적은 것이므로 경고는 발생하지 않는다.

Box<Object> b = new Box<Object>();
b.setItem("ABC"); // 경고발생 안함
b.setItem(new Object()); // 경고발생 안함

지네릭스가 도입되기 이전의 코드와 호환성을 유지하기 위해서 지네릭스를 사용하지 않은 코드를 허용하는 것일 뿐, 앞으로 지네릭 클래스를 사용할 때는 반드시 타입을 지정해서 지네릭스와 관련된 경고가 나오지 않도록 하자.

지네릭스의 용어
다음과 같이 지네릭 클래스 Box가 선언되어 있을 때,


타입 문자 T는 지네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라고 하는데, 메서드의 매개변수와 유사한 면이 있기 때문이다. 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '지네릭 타입 호출'이라고 하고, 지정된 타입 'String'을 '매개변수화된 타입(parameterized type)'이라고 한다. 매개변수화된 타입이라는 용어가 좀 길어서, 앞으로 이 용어 대신 '대입된 타입'이라는 용어를 사용할 것이다.

예를 들어, Box<String>과 Box<Integer>는 지네릭 클래스 Box<T>에 서로 다른 타입을 대입하여 호출한 것일 뿐, 이 둘이 별개의 클래스를 의미하는 것은 아니다. 이는 마치 매개변수의 값이 다른 메서드 호출, 즉 add(3, 5)와 add(2, 4)가 서로 다른 메서드를 호출하는 것이 아닌 것과 같다.

컴파일 후에 Box<String>과 Box<Integer>는 모두 이들의 '원시 타입'인 Box로 바뀐다. 즉, 지네릭 타입이 제거된다. 이에 대해서는 '1.8 지네릭 타입의 제거'에서 자세히 설명한다.

지네릭스의 제한
지네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 지네릭스는 이처럼 인스턴스별로 다르게 동작하도록 하려고 만든 기능이니까.

Box<Apple> appleBox = new Box<Apple>(); // OK. Apple객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); // OK. Grape객체만 저장가능

그러나 모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문이다. 이미 알고 있는 것처럼 static멤버는 인스턴스변수를 참조할 수 없다.

static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다. 즉, 'Box<Apple.item>'과 'Box<Grape.item>'이 다른 것이어서는 안된다는 뜻이다.

그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안 된다는 뜻이다.

지네릭 배열을 생성할 수 없는 것은 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 위의 코드에 정의된 Box<T>클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof연산자도 new연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

꼭 지네릭 배열을 생성해야할 필요가 있을 때는, new연산자대신 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object배열을 생성해서 복사한 다음에 'T[]'로 형변환하는 방법 등을 사용한다.

1.3 지네릭 클래스의 객체 생성과 사용

지네릭 클래스 Box<T>가 다음과 같이 정의되어 있다고 가정하자. 이 Box<T>의 객체에는 한 가지 종류, 즉 T타입의 객체만 저장할 수 있다. 전과 달리 ArrayList를 이용해서 여러 객체를 저장할 수 있도록 하였다.

Box<T>의 객체를 생성할 때는 다음과 같이 한다. 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다. 일치하지 않으면 에러가 발생한다.

두 타입이 상속관계에 있어도 마찬가지이다. Apple이 Fruit의 자손이라고 가정하자.

단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.
FruitBox는 Box의 자손이라고 가정하자.

JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다. 참조변수의 타입으로부터 Box가 Apple타입의 객체만 저장한다는 것을 알 수 있기 때문에, 생성자에 반복해서 타입을 지정해주지 않아도 되는 것이다. 따라서 아래의 두 문장은 동일하다.

생성된 Box<T>의 객체에 'void add(T item)'으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

그러나 타입 T가 'fruit'인 경우, 'void add(Fruit item)'가 되므로 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다. Apple이 Fruit의 자손이라고 가정하였다.

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // OK.
fruitBox.add(new Apple()); // OK. void add(Fruit item)

1.4 제한된 지네릭 클래스

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다. 그렇다면, 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없을까?

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy()); // OK. 과일상자에 장난감을 담을 수 있다.

다음과 같이 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.

FruitBox<Apple> appleBox = new FruitBox<Apple>(); // OK
fruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아님

게다가 add()의 매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로, 아래와 같이 여러 과일을 담을 수 있는 상자가 가능하게 된다.

다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능한 것이다. 타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.

만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends'를 사용한다. 'implements'를 사용하지 않는 다는 점에 주의하자.

클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 '&'기호로 연결한다.

1.5 와일드 카드

매개변수에 과일박스를 대입하면 주스를 만들어서 반환하는 Juicer라는 클래스가 있고, 이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice()라는 static메서드가 다음과 같이 정의되어 있다고 가정하자.

class Juicer {
	static Juice makeJuice(FruitBox<Fruit> box) { // <Fruit>으로 지정
    	String tmp = "";
        for (Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
     }
 }

Juice클래스는 지네릭 클래스가 아닌데다, 지네릭 클래스라고 해도 static메서드에는 타입 매개변수 T를 매개변수에 사용할 수 없으므로 아예 지네릭스를 적용하지 않던가, 위와 같이 타입 매개변수 대신, 특정 타입을 지정해줘야 한다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
		...
System.out.println(Juicer.makeJuice(fruitBox)); // OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox)); // 에러. FruitBox<Apple>

이렇게 지네릭 타입을 'FruitBox<Fruit>'로 고정해 놓으면, 위의 코드에서 알 수 있듯이 'FruitBox<Apple>'타입의 객체는 makeJuice()의 매개변수가 될 수 없으므로, 다음과 같이 여러 가지 타입의 매개변수를 갖는 makeJuice()를 만들 수밖에 없다.

static Juice makeJuice(FruitBox<Fruit> box) {
	String tmp = "";
    for (Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

static Juice makeJuice(FruitBox<Apple> box) {
	String tmp = "";
    for (Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

그러나 위와 같이 오버로딩하면, 컴파일 에러가 발생한다. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다. 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 그래서 위의 두 메서드는 오버로딩이 아니라 '메서드 중복 정의'이다.

이럴 때 사용하기 위해 고안된 것이 바로 '와일드 카드'이다. 와일드 카드는 기호 '?'로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다. '?'만으로는 Object타입과 다를 게 없으므로, 다음과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

와일드 카드를 사용해서 makeJuice()의 매개변수 타입을 FruitBox<Fruit>에서 FruitBox<? extends Fruit>으로 바꾸면 다음과 같이 된다.

이제 이 메서드의 매개변수로 FruitBox<Fruit>뿐만 아니라, FruitBox<Apple>와 Fruit Box<Grape>도 가능하게 된다.

만일 매개변수의 타입을 FruitBox<? extends Object>로 하면, 모든 종류의 FruitBox가 이 메서드의 매개변수로 가능해 진다. 대신, 전과 달리 box의 요소가 Fruit의 자손이라는 보장이 없으므로 아래의 for문에서 box에 저장된 요소를 Fruit타입의 참조변수로 받을 수 없다.

static Juice makeJuice(FruitBox<? extends Object> box) {
	String tmp = "";
    
    for(Fruit f : box.getList()) tmp += " "; // 에러. Fruit이 아닐 수 있음
    return new Juice(tmp);
}

그러나 실제로 테스트 해보면 문제없이 컴파일되는데 그 이유는 바로 지네릭 클래스 FruitBox를 제한했기 때문이다.

class FruitBox<T extends Fruit> extends Box<T> {}

컴파일러는 위 문장으로부터 모든 FruitBox의 요소들이 Fruit의 자손이라는 것을 알고 있으므로 문제 삼지 않는 것이다.

1.6 지네릭 메서드

메서드의 선언부 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. 앞서 살펴본 것처럼, Collections.sort()가 바로 지네릭 메서드이며, 지네릭 타입의 선언 위치는 반환타입 바로 앞이다.

지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자 T를 사용한다 해도 같은 것이 아니라는 것에 주의해야 한다.

위의 코드에서 지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와 지네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다. 그리고 sort()가 static메서드라는 것에 주목하자. 앞서 설명한 것처럼, static멤버에는 타입 매개변수를 사용할 수 없지만, 이처럼 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다.

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉬운데, 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없다.

1.7 지네릭 타입의 형변환


위에서 알 수 있듯이, 지네릭 타입과 넌지네릭(non-generic) 타입간의 형변환은 항상 가능하다. 다만 경고가 발생할 뿐이다. 그러면, 대입된 타입이 다른 지네릭 타입 간에는 형변환이 가능할까?

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>)strBox; // 에러. Box<String> -> Box<Object>
strBox = (Box<String>)objBox; // 에러. Box<Object> -> Box<String>

불가능하다. 대입된 타입이 Object일지라도 말이다.

와일드 카드가 사용된 지네릭 타입으로는 형변환 가능하다.

를 줄여서 로 쓸 수 있다.

1.8 지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.

이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서이다. JDK1.5부터 지네릭스가 도입되었지만, 아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다. 그러나 앞으로 가능하면 원시 타입을 사용하지 않도록 하자.

1. 지네릭 타입의 경계(bound)를 제거한다.
지네릭 타입이 <T extends Fruit>라면 T는 fruit로 치환된다. <T>인 경우는 T는 Object로 치환된다. 그리고 클래스 옆의 선언은 제거된다.

2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
List의 get()은 Object타입을 반환하므로 형변환이 필요하다.

와일드 카드가 포함되어 있는 경우에는 다음과 같이 적절한 타입으로의 형변환이 추가된다.

2. 열거형(enums)

2.1 열거형이란?

이전까지 자바는 C언어와 달리 열거형이라는 것이 존재하지 않았으나 JDK1.5부터 새로 추가되었다. 자바의 열거형은 C언어의 열거형보다 더 향상된 것으로 열거형이 갖는 값뿐만 아니라 타입까지 관리하기 때문에 보다 논리적인 오류를 줄일 수 있다.

기존의 많은 언어들, 예를 들어 C언어에서는 타입이 달라도 값이 같으면 조건식결과가 참(true)이였으나, 자바의 '타입에 안전한 열거형'에서는 실제 값이 같아도 타입이 다르면 조건식의 결과가 false가 된다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 하는 것이다.

if(Card.CLOVER==Card.TWO) // true지만 false이어야 의미상 맞음.
if(Card.Kind.CLOVER==Card.Value.TWO) // false. 값은 같지만 타입이 다름

그리고 더 중요한 것은 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일해야 한다는 것이다. 하지만 열거형 상수를 사용하면, 기존의 소스를 다시 컴파일하지 않아도 된다.

2.2 열거형의 정의와 사용

열거형을 정의하는 방법은 가능하다. 다음과 같이 괄호{}안에 상수의 이름을 나열하기만 하면 된다.

예를 들어 동서남북 4방향을 상수로 정의하는 열거형 Direction은 다음과 같다.

이 열겨형에 정의된 상수를 사용하는 방법은 '열거형이름.상수명'이다. 클래스의 static변수를 참조하는 것과 동일하다.

열거형 상수간의 크기에는 '=='를 사용할 수 있다. equals()가 아닌 '=='로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 얘기다. 그러나 '<', '>'와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용가능하다. 앞서 배운 것과 같이 compareTo()는 두 비교대상이 같으면 0, 왼쪽이 크면 양수, 오른쪽이 크면 음수를 반환한다.

다음과 같이 switch문의 조건식에도 열거형을 사용할 수 있다.

void move() {
	switch(dir) {
    	case EAST: x++; // Direction.EAST라고 쓰면 안된다.
        	break;
        case WEST: x--;
        	break;
        case SOUTH: y++;
        	break;
        case NORTH: y--;
        	break;
    }
}

이 때 주의할 점은 case문에 열거형의 이름은 적지 않고 상수의 이름만 적어야 한다는 제약이 있다.

모든 열거형의 조상 - java.lang.Enum
열거형 Direction에 정의된 모든 상수를 출력하려면, 다음과 같이 한다.

Direction[] dArr = Direction.value();

for(Direction d : dArr) // for (Direction d : Direction.value())
	System.out.printf("%s = %d%n", d.name(), d.ordinal());

vlaue()는 열거형의 모든 상수를 배열에 담아 반환한다. 이 메서드는 모든 열거형이 가지고 있는 것으로 컴파일러가 자동으로 추가해 준다. 그리고 ordinal()은 모든 열거형의 조상인 java.lang.Enum클래스에 정의된 것으로, 열거형 상수가 정의된 순서(0부터 시작)를 정수로 반환한다.

Enum클래스에는 그 밖에도 다음과 같은 메서드가 정의되어 있다.

이외에도 values()처럼 컴파일러가 자동적으로 추가해주는 메서드가 하나 더 있다.

이 메서드는 열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다.

Direction d = Direction.valueOf("WEST");

System.out.println(d); // WEST
System.out.println(Direction.WEST == Direction.valueOf("WEST")); // true

2.3 열거형에 멤버 추가하기

Enum클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋다. 이 값은 내부적인 용도로만 사용되기 위한 것이기 때문이다.

열거형 상수의 값이 불규칙적인 경우에는 이때는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.

그리고 지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해 주어야 한다. 이 때 주의할 점은, 먼저 열거형 상수를 모두 정의한 다음에 다른 멤버들을 추가해야한다는 것이다. 그리고 열거형 상수의 마지막에 ';'도 잊지 말아야 한다.

열거형의 인스턴스 변수는 반드시 final이어야 한다는 제약은 없지만, value는 열거형 상수의 값을 저장하기 위한 것이므로 final을 붙였다. 그리고 외부에서 이 값을 얻을 수 있게 getValue()도 추가하였다.

열거형 Direction에 새로운 생성자가 추가되었지만, 위와 같이 열거형의 객체를 생성할 수 없다. 열거형의 생성자는 제어자가 묵시적으로 private이기 때문이다.

enum Direction {
		...
    Direction(int value) { // private Direction(int value)와 동일
    	...
}

필요하다면, 다음과 같이 하나의 열거형 상수에 여러 값을 지정할 수도 있다. 다만 그에 맞게 인스턴스 변수와 생성자 등을 새로 추가해주어야 한다.

enum Direction {
	EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), NORTH(4, "^");
    
    private final int value;
    private final String symbol;
    
    Direction(int value, String symbol) { // 접근 제어자 private이 생략됨
    	this.value = value;
        this.symbol = symbol;
    }
    
    public int getValue() { return value; }
    public String getSymbol() { return symbol; }
}

열거형에 추상 메서드 추가하기
열거형 Transportation은 운송 수단의 종류 별로 상수를 정의하고 있으며, 각 운송 수단에는 기본요금(BASIC_FARE)이 책정되어 있다.

enum Transportation {
	BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);
    
    private final int BASIC_FARE;
    
    private Transportation(int basicFare) {
    	BASIC_FARE = basicFare;
    }
    
    int fare() { // 운송 요금을 반환
    	return BASIC_FARE;
    }
}

그러나 이것만으로는 부족하다. 거리에 따라 요금을 계산하는 방식이 각 운송 수단마다 다를 것이기 때문이다. 이럴 때, 열거형에 추상 메서드'fare(int distance)'를 선언하면 각 열거형 상수가 이 추상 메서드를 반드시 구현해야 한다.

enum Transportation {
	BUS(100)	{
    	int fare(int distance) { return distance * BASIC_FARE;)
    },
    TRAIN(150) { int fare(int distance) { return distance * BASIC_FARE; } },
    SHIP(100) { int fare(int distance) { return distance * BASIC_FARE; } },
    AIRPLANE(300) { int fare(int distance) { return distance * BASIC_FARE; } };
    
    abstract int fare(int distance); // 거리에 따른 요금을 계산하는 추상 메서드
    
    protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근가능
    
    Transportation(int basicFare) {
    	BASIC_FARE = basicFare;
    }
    
    public int getBasicFare() { return BASIC_FARE; }
}

위의 코드는 열거형에 정의된 추상 메서드를 각 상수가 어떻게 구현하는지 보여준다. 마치 익명 클래스를 작성한 것처럼 보일 정도로 유사하다.

2.4 열거형의 이해

만일 열거형 Direction이 다음과 같이 정의되어 있을 때,

사실은 열거형 상수 하나하나가 Direction객체이다. 위의 문장을 클래스로 정의한다면 다음과 같을 것이다.

3. 애너테이션(annotation)

3.1 애너테이션이란?

자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각했다. 그래서 소스코드의 주석에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML문서를 생성해내는 프로그램(javadoc.exe)을 만들어서 사용했다. 다음은 모든 애너테이션의 조상인 Annotation인터페이스의 소스코드의 일부이다.

주석 안에 소스코드에 대한 설명들이 있고, 그 안에 '@'이 붙은 태그 들이 눈에 띌 것이다. 미리 정의된 태그들을 이용해서 주석 안에 정보를 저장하고, javadoc.exe라는 프로그램이 이 정보를 읽어서 문서를 작성하는데 사용한다.

이 기능을 응용하여, 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 애너테이션이다. 애너테이션은 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.

예를 들어, 자신이 작성한 소스코드 중에서 특성 메서드만 테스트하기를 원한다면, 다으모가 같이 '@Test'라는 애너테이션을 메서드 앞에 붙인다. '@Test'는 '이 메서드를 테스트해야 한다'는 것을 테스트 프로그램에게 알리는 역할을 할 뿐, 메서드가 포함된 프로그램 자체에는 아무런 영향을 미치지 않는다. 주석처럼 존재하지 않는 것이나 다름없다.

애너테이션은 JDK에서 기본적으로 제공하는 것과 다른 프로그램에서 제공하는 것들이 있는데, 어느 것이든 그저 약속된 형식으로 정보를 제공하기만 하면 될 뿐이다.

JDK에서 제공하는 표준 애너테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 그리고 새로운 애너테이션을 정의할 때 사용하는 메타 애너테이션을 제공한다.

3.2 표준 애너테이션

자바에서 기본적으로 제공하는 애너테이션들은 몇 개 없다. 그나마 이들의 일부는 '메타 애너테이션(meta annotation)'으로 애너테이션을 정의하는데 사용되는 애너테이션의 애너테이션이다.

@Override
메서드 앞에만 붙일 수 있는 애너테이션으로, 조상의 메서드를 오버라이딩하는 것이라는 걸 컴파일러에게 알려주는 역할을 한다. 아래의 코드에서와 같이 오버라이딩할 때 조상 메서드의 이름을 잘못 써도 컴파일러는 이것이 잘못된 것인지 알지 못한다.

오버라이딩할 때는 이처럼 메서드의 이름을 잘못 적는 경우가 많은데, 컴파일러는 그저 새로운 이름의 메서드가 추가된 것으로 인식할 뿐이다. 게다가 실행 시에도 오류가 발생하지 않고 조상의 메서드가 호출되므로 어디서 잘못되었는지 알아내기 어렵다.

그러나 위의 오른쪽 코드와 같이 메서드 앞에'@Overrid'라고 애너테이션을 붙여주면, 컴파일러가 같은 이름의 메서드가 조상에 있는지 확인하고 없으면, 에러메시지를 출력한다.

오버라이딩할 때 메서드 앞에 '@Override'를 붙이는 것이 필수는 아니지만, 알아내기 어려운 실수를 미연에 방지해주므로 반드시 붙이도록 하자.

@Deprecated
새로운 버젼의 JDK가 소개될 때, 새로운 기능이 추가될 뿐만 아니라 기존의 부족했던 기능들을 개선하기도 한다. 이 과정에서 기존의 기능을 대체할 것들이 추가되어도, 이미 여러 곳에서 사용되고 있을지 모르는 기존의 것들을 함부로 삭제할 수 없다.

그래서 생각해낸 방법이 더 이상 사용되지 않는 필드나 메서드에 '@Deprecated'를 붙이는 것이다. 이 애너테이션이 붙은 대상은 다른 것으로 대체되었으니 더 이상 사용하지 않을 것을 권한다는 의미이다. 예를 들어 java.util.Date클래스의 대부분의 메서드에는 '@Deprecated'가 붙어있는데, Java API에서 Date클래스의 getDate()를 보면 아래와 같이 적혀있다.

이 메서드 대신에 JDK1.1부터 추가된 Calendar클래스의 get()을 사용하라는 얘기다. 기존의 것 대신 새로 추가된 개선된 기능을 사용하도록 유도하는 것이다.

만일 '@Deprecated'가 붙은 대상을 사용하는 코드를 작성하면, 컴파일할 때 아래와 같은 메시지가 나타난다.

해당 소스파일이 'deprecated'된 대상을 사용하고 있으며, '-Xlint:deprecation'옵션을 부텨어서 다시 컴파일하면 자세한 내용을 알 수 있다는 뜻이다.

@FunctionalInterface
'함수형 인터페이스(functional interface)'를 선언할 때, 이 애너테이션을 붙이면 컴파일러가 '함수형 인터페이스'를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다. 필수는 아니지만, 붙이면 실수를 방지할 수 있으므로 '함수형 인터페이스'를 선언할 때는 이 애너테이션을 반드시 붙이도록 하자.

@SuppressWarnings
컴파일러가 보여주는 경고메시지가 나타나지 않게 억제해준다. 이전 예제에서처럼 컴파일러의 경고메시지는 무시하고 넘어갈 수도 있지만, 모두 확인하고 해결해서 컴파일 후에 어떠한 메시지도 나타나지 않게 해야 한다.

그러나 경우에 따라서는 경고가 발생할 것을 알면서도 묵인해야 할 때가 있는데, 이 경고를 그대로 놔두면 컴파일할 때마다 메시지가 나타난다. 이전 예제에서 확인한 것과 같이 '-Xlint'옵션을 붙이지 않으면 컴파일러는 경고의 자세한 내용은 보여주지 않으므로 다른 경고들을 놓치기 쉽다.

이럴 때 묵인해야 하는 경고가 발생하는 대상에 반드시 '@SuppressWarnings'를 붙여서 컴파일 후에 어떤 경고 메시지도 나타나지 않게 해야 한다.

'@SuppressWarnings'로 억제할 수 있는 경고 메시지의 종류는 여러 가지가 있는데, JDK의 버젼이 올라가면서 앞으로도 계속 추가될 것이다. 이 중에서 주로 사용되는 것은 "deprecation", "unchecked", "rawtypes", "varargs" 정도이다.

"deprecation"은 앞서 살펴본것과 같이 '@Deprecated'가 붙은 대상을 사용해서 발생하는 경고를, "unchecked"는 지네릭스로 타입을 지정하지 않았을 때 발생하는 경고를 "rawtypes"는 지네릭스를 사용하지 않아서 발생하는 경고를, 그리고 "varargs"는 가변인자의 타입이 지네릭 타입일 때 발생하는 경고를 억제할 때 사용한다.

억제하고자 하는 경고 메시지를 애너테이션의 뒤에 괄호()안에 문자열로 지정해주면 된다.

만일 둘 이상의 경고를 동시에 억제하려면 다음과 같이 한다. 배열에서처럼 괄호{}를 추가로 사용해야한다는 것에 주의하자.

'@SuppressWarnings'로 억제할 수 있는 경고 메시지의 종류는 JDK의 버젼이 올라가면서 계속 추가될 것이기 때문에, 이전 버젼에서는 발생하지 않던 경고가 새로운 버젼에서는 발생할 수 있다. 새로 추가된 경고 메시지를 억제하려면, 경고 메시지의 종류를 알아야하는데, -Xlint옵션으로 컴파일해서 나타나는 경고의 내용 중에서 대괄호[]안에 있는 것이 바로 메시지의 종류이다.

위의 경고 메시지를 보면 대괄호[] 안에 "rawtypes"라고 적혀있으므로, 이 경고 메시지를 억제하려면 다음과 같이 하면 된다.

@SuppressWarnings("rawtypes")
public static void sort(List list)
			...
}

@SafeVarargs
메서드에 선언된 가변인자의 타입이 non-reifiable타입일 경우, 해당 메서드를 선언하는 부분과 호출하는 부분에서 "uncheked"경고가 발생한다. 해당 코드에 문제가 없다면 이 경고를 억제하기 위해 '@SafeVarags'를 사용해야 한다.

이 애너테이션은 생성자와 static이나 final이 붙은 메서드에만 붙일 수 있다. 즉, 오버라이드 될 수 있는 메서드에는 사용할 수 없다는 뜻이다.

지네릭스에서 살펴본 것과 같이 어떤 타입들은 컴파일 이후에 제거된다. 컴파일 후에도 제거되지 않는 타입을 reifiable타입이라고 하고, 제거되는 타입을 non-reifiable타입이라고 한다. 지네릭 타입들은 대부분 컴파일 시에 제거되므로 non-reifiable타입이다.

예를 들어, java.util.Arrays의 asList()는 다음과 같이 정의되어 있으며, 이 메서드는 매개변수로 넘겨받은 값들로 배열을 만들어서 새로운 ArrayList객체를 만들어서 반환하는데 이 과정에서 경고가 발생한다.

public static <T> List<T> asList(T... a) {
	return new ArrayList<T>(a); // ArrayList (E[] array)를 호출. 경고발생
}

asList()의 매개변수가 가변인자인 동시에 지네릭 타입이다. 메서드에 선언된 타입 T는 컴파일 과정에서 Object로 바뀐다. 즉, Object[]가 되는 것이다. Object[]에는 모든 타입의 객체가 들어있을 수 있으므로, 이 배열로 ArrayList<T>를 생성하는 것은 위험하다고 경고하는 것이다. 그러나 asList()가 호출되는 부분을 컴파일러가 체크해서 타입 T가 아닌 다른 타입이 들어가지 못하게 할 것이므로 위의 코드는 아무런 문제가 없다.

이럴 때는 메서드 앞에 '@SafeVarargs'를 붙여서 '이 메서드의 가변인자는 타입 안정성이 있다.'고 컴파일러에게 알려서 경고가 발생하지 않도록 해야 한다.

메서드를 선언할 때 @SafeVarargs를 붙이면, 이 메서드를 호출하는 곳에서 발생하는 경고도 억제된다. 반면에 @SafeVarargs대신, @SuppressWarning("unchecked")로 경고를 억제하려면, 메서드 선언뿐만 아니라 메서드가 호출되는 곳에도 애너테이션을 붙여야한다.

그리고 @SafeVarargs로 'unchecked'경고는 억제할 수 있지만, 'varargs'경고는 억제할 수 없기 때문에 습관적으로 @SafeVarargs와 @SuppressWarnings("varargs")를 같이 붙인다.

@SuppressWarnings("varargs")를 붙이지 않아도 경고 없이 컴파일 된다. 그러나 -Xlint옵션을 붙여서 컴파일 해보면, 'varargs'경고가 발생한 것을 확인할 수 있다. 그래서 가능하면 이 두 애너테이션을 항상 같이 사용하는 것이 좋다.

3.3 메타 애너테이션

앞서 설명한 것과 같이 메타 애너테이션은 '애너테이션을 위한 애너테이션', 즉 애너테이션에 붙이는 애너테이션으로 애너테이션을 정의할 때 애너테이션의 적용대상(target)이나 유지기간(retention)등을 지정하는데 사용된다.

@Target
애너테이션이 적용가능한 대상을 지정하는데 사용된다. 아래는 '@SuppressWarnings'를 정의한 것인데, 이 애너테이션에 적용할 수 있는 대상을 '@Target'으로 지정하였다. 앞서 언급한 것과 같이 여러 개의 값을 지정할 때는 배열에서처럼 괄호{}를 사용해야한다.

'@Target'으로 지정할 수 있는 애너테이션 적용대상의 종류는 아래와 같다.

'TYPE'은 타입을 선언할 때, 애너테이션을 붙일 수 있다는 뜻이고 'TYPE_USE'는 해당 타입의 변수를 선언할 때 붙일 수 있다는 뜻이다.

@Retention
애너테이션이 유지되는 기간을 지정하는데 사용된다. 애너테이션의 유지 정책의 종류는 다음과 같다.

'@Override'나 '@SuppressWarnings'처럼 컴파일러에 의해 사용되는 애너테이션은 유지정책이 'SOURCE'이다. 컴파일러를 직접 작성할 것이 아니라면, 이 유지정책은 사용할 일이 없다.

유지 정책을 'RUNTIME'으로 하면, 실행 시에 '리플렉션(reflection)'을 통해 클래스 파일에 저장된 애너테이션의 정보를 읽어서 처리할 수 있다. '@FunctionalInterface'는 '@Override'처럼 컴파일러가 체크해주는 애너테이션이지만, 실행 시에도 사용되므로 유지 정책이 'RUNTIME'으로 되어 있다.

유지 정책 'CLASS'는 컴파일러가 애너테이션의 정보를 클래스 파일에 저장할 수 있게는 하지만, 클래스 파일이 JVM에 로딩될 때는 애너테이션의 정보가 무시되어 실행 시에 애너테이션에 대한 정보를 얻을 수 없다. 이것이 'CLASS'가 유지정책의 기본값임에도 불구하고 잘 사용되지 않는 이유이다.

@Documented
애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다. 자바에서 제공하는 기본 애너테이션 중에 '@Override'와 '@SuppressWarnings'를 제외하고는 모두 이 메타 애니테이션이 붙어 있다.

@Inherited
애너테이션이 자손 클래스에 상속되도록 한다. '@Inherited'가 붙은 애너테이션을 조상 클래스에 붙이면, 자손 클래스도 이 애너테이션이 붙은 것과 같이 인식된다.

위의 코드에서 Child클래스에는 애너테이션이 붙지 않았지만, 조상인 Parent클래스에 붙은 '@SuperAnno'가 상속되어 마치 Child클래스에도 '@SuperAnno'가 붙은 것처럼 인식된다.

@Repeatable
보통은 하나의 대상에 한 종류의 애너테이션을 붙이느데, '@Repeatable'이 붙은 애너테이션은 여러 번 붙일 수 있다.

예를 들어 '@ToDo'라는 애너테이션이 위와 같이 정의되어 있을 때, 다음과 같이 MyClass클래스에 '@ToDo'를 여러 번 붙이는 것이 가능하다.

@Native
네이티브 메서드(native method)에 의해 참조되는 '상수 필드(constant field)'에 붙이는 애너테이션이다. 아래는 java.lang.Long클래스에 정의된 상수이다.

네이티브 메서드는 JVM이 설치된 OS의 메서드를 말한다. 네이티브 메서드는 보통 C언어로 작성되어 있는데, 자바에서는 메서드의 선언부만 정의하고 구현은 하지 않는다. 그래서 추상 메서드처럼 선언부만 있고 몸통이 없다.

이처럼 모든 클래스의 조상인 Object클래스의 메서드들은 대부분 네이티브 메서드이다. 네이티브 메서드는 자바로 정의되어 있기 때문에 호출하는 방법은 자바의 일반 메서드와 다르지 않지만 실제로 호출되는 것은 OS의 메서드이다.

그냥 아무런 내용도 없는 네이티브 메서드를 선언해 놓고 호출한다고 되는 것은 아니고, 자바에 정의된 네이티브 메서드와 OS의 메서드를 연결해주는 작업이 추가로 필요하다. 이 역할은 JNI(Java Native Interface)가 하는데, JNI는 이 책의 범위를 벗어나므로 자세한 설명은 생략한다.

3.4 애너테이션 타입 정의하기

지금까지 애너테이션을 사용하는 방법에 대해서 살펴봤는데, 이제 직접 애너테이션을 만들어서 사용해볼 것이다. 새로운 애너테이션을 정의하는 방법은 아래와 같다. '@'기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일하다.

엄밀히 말해서 '@Override'는 애너테이션이고 'Override'는 '애너테이션의 타입'이다.

애너테이션의 요소
애너테이션 내에 선언된 메서드를 '애너테이션의 요소(element)'라고 하며, 아래에 선언된 TestInfo애너테이션은 다섯 개의 요소를 갖는다.

@interface TestInfo {
	int count();
    String testedBy();
    String[] testTools();
    TestType testType(); // enum TestType { FIRST, FINAL }
    DateTime testDate(); // 자신이 아닌 다른 애너테이션 (@DateTime)을 포함할 수 있다.
}

@interface DateTime {
	String yymmdd();
    String hhmmss();
}

애너테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다. 다만, 애너테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다. 요소의 이름도 같이 적어주므로 순서는 상관없다.

@TestInfo(
	count = 3,
    testedBy = "kim",
    testTools = {"JUnit", "AutoTester"},
    testType = TestType.FIRST,
    testDate = @DateTime(yymmdd = "160101", hhmmss = "235959")
)
public class NewClass { ... }

애너테이션의 각 요소는 기본값을 가질 수 있으며, 기본값이 있는 요소는 애너테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다.

애너테이션 요소가 오직 하나뿐이고 이름이 value인 경우, 애너테이션을 적용할 때 요소의 이름을 생략하고 값만 적어도 된다.

@interface TestInfo {
	String value();
}

@TestInfo("passed") // @TestInfo(value = "passed")와 동일
class NewClass { ... }

요소의 타입이 배열인 경우, 괄호{}를 사용해서 여러 개의 값을 지정할 수 있다.

@interface TestInfo {
	String[] testTools();
}

@Test(testTools = {"JUnit", "AutoTester"}) // 값이 여러 개인 경우
@Test(testTools = "JUnit") // 값이 하나일 때는 괄호{}생략가능
@Test(testTools = {}) // 값이 없을 때는 괄호{}가 반드시 필요

기본값을 지정할 때도 마찬가지로 괄호{}를 사용할 수 있다.

java.lang.annotation.Annotation
모든 애너테이션의 조상은 Annotation인다. 그러나 애너테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 조상으로 지정할 수 없다.

게다가 아래의 소스에서 볼 수 있듯이 Annotation은 애너테이션이 아니라 일반적인 인터페이스로 정의되어 있다.

마커 애너테이션 Marker Annotation
값을 지정할 필요가 없는 경우, 애너테이션의 요소를 하나도 정의하지 않을 수 있다. Serializable이나 Cloneable인터페이스처럼, 요소가 하나도 정의되지 않은 애너테이션을 마커 애너테이션이라고 한다.

애너테이션 요소의 규칙

  • 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용된다.
  • ()안에 매개변수를 선언할 수 없다.
  • 예외를 선언할 수 없다.
  • 요소를 타입 매개변수로 정의할 수 없다.

아래의 코드에서 오른쪽 주석을 가리고 무엇이 잘못되었는지 잠시 생각해보자.

profile
최고가 되기 위해 최선을...

0개의 댓글