티스토리 뷰

자바에서 기본적인 코딩과 클래스, 상속까지를 마치고 나면 그 다음 고개가 제너릭이다. 제너릭은 상속과 인터페이스를 이해하고 나서 그 다음에 다루어져야 하는 주제여서 기초 단계에서는 그것이 왜 필요한지를 이해하기가 어렵다. 여기서는 상속과 인터페이스를 충분히 공부한 것으로 가정하고 제너릭이 왜 필요한가를 설명해 보겠다.

자바 컴파일러(또는 편집기의 툴팁)는 다음 코드에 대해 아래와 같은 경고(warning) 메시지를 보여준다. 

ArrayList list = new ArrayList();
ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized

이 의미는 ArrayList를 쓰려면 ArrayList<String> 처럼 타입을 파라매터로 주어야 한다는 뜻이다. 아무 것도 넣지 않고 경고를 무시한 채 그대로 빌드하면 그것은 ArrayList<Object>로 취급되어 컴파일된다. 즉 raw type ArrayList는 Object 타입의 요소를 가진다. 그럼 그것이 무슨 문제인가?

ArrayList<Object> list = new ArrayList<>();
list.add("first");
list.add(2);
list.add(3.5);
String name = (String) list.get(0);
int n = (Integer) list.get(1);
double d = (Double) list.get(2);

우선 첫 번째 문제는 list에서 요소를 꺼내서 쓸 때 계속 다운캐스팅을 해야 한다는 점이다. 다운캐스팅에 어떤 문제가 있는지는 앞의 업캐스팅과 다운캐스팅 절에서 살펴보았다. 다운캐스팅은 성능의 측면에서도 많은 부담이 된다. 

list에는 어떤 객체든 들어갈 수 있으므로 add()할 때 아무런 문제없이 추가가 된다. 언뜻 생각하면 어떤 객체든 들어가는 list면 좋은 것 아닌가 라고 생각할 수도 있지만, 문제는 꺼내서 쓰려면 그 타입으로 바꾸어야만 되고 그러려면 뭐가 들어갔었는지 알아야 된다는 점이다. 근데 코드 상으로는 구별이 전혀 안 되고 실행할 때 어떤 순서로 어떤 것이 들어갔는지 프로그래머의 기억(또는 프로그램의 로직)에 의존해야 한다. 

게다가 컴파일러는 리스트에서 꺼낸 요소가 Object 타입이라는 것만 알고 있으므로 잘못된 타입으로 변환하려고 할 때 컴파일 오류를 주지 못한다. 실제 어떤 타입인지는 동적으로(실행할 때) 알게 되므로 실행시킬 때 되서야 잘못된 타입 캐스팅인지 아닌지를 알 수 있다. 그러므로 컴파일 오류 없이 빌드된 시스템이 까딱 잘못하면 예외를 일으키고 깨질 수 있다(죽기도 하고 안 죽기도 하므로 테스트에서도 안 걸릴 가능성이 높다). 이러한 오류 가능성을 안고 있으므로 시스템의 안정성이 크게 위협받는다. 이러한 문제를 해결하기 위해 제너릭이 등장하게 된다.

ArrayList<Integer> nList = new ArrayList<>();

이 리스트는 숫자만 가진다. 이 리스트에는 Integer 타입의 객체만 들어가고 나올 때도 Integer 타입의 객체만 나온다. 

우리가 사용하는 컴퓨터 환경에서는 타입이 같은 데이터를 다루는 일이 더 많다. 콜렉션 클래스들은 많은 경우 동일한 타입의 객체를 모아서 가지는 경우가 많다. 자바스크립트나 파이썬 같은 언어는 그런 제약이 훨씬 덜하지만 자바는 그래도 성능도 고려하고 대용량의 데이터도 잘 다룰 수 있어야 되고 속도도 나와야 되는 경우에 많이 쓰이므로 여러 가지 타입의 객체를 마구 섞어서 사용하는 방식으로 프로그램을 작성하지는 않는다.

이러한 상황에서 라이브러리 클래스를 만드는 사람들은 가능하면 재사용성을 높여 범용적인(generic한) 코드로 작성하고 싶다. 그러나 너무 많은 다운캐스팅이 일이나는 것은 막고 싶다. 타입을 정해버리면 그 타입에 대해서만(또는 그것을 상속한 타입에 대해서만) 쓸 수 있고 그렇다고 너무 일반화되면(예를 들어 Object) 타입의 다운캐스팅이 자주 필요하게 된다. 제너릭은 이 두 가지 필요를 모두 만족해서 범용적인 코드를 작성할 때 타입을 정하지 않고 만들되 실제 그것을 사용하는 시점에 구체적인 타입을 지정하게 해주는 언어의 기능이다. 

제너릭 코드의 사용은 비교적 쉽다. 위의 ArrayLiat<Integer> 예처럼 실제 사용하는 시점에 해당하는 타입을 지정해 주면 된다. 실제 사용하는 시점은 new를 통해서 객체를 만들거나 implements로 제너릭 인터페이스를 구현할 때다. 

많이 사용되는 제너릭 인터페이스 타입의 예로 Comparable<T>를 들 수 있다. 이것은 다음과 같이 정의된다.

interface Comparable<T> {
    int compareTo(T other);
}

이 인터페이스의 역할은 앞의 compareTo와 정렬에 관련된 포스트에서 살펴보았다. 이것을 implements한 클래스들은 compareTo 메소드를 가져야 한다. 그런데 여기서 중요한 것은 T에 들어갈 타입이 무엇인가 이다. 다음 코드에서 그 답을 알 수 있다.

class Student implements Comparable<Student> {
    public int compareTo(Student other) {
    }
}

compareTo 메소드의 목적은 다른 Student 객체와 this 객체를 비교하여 큰지 같은지 작은지를 돌려주는 메소드이다. 그러므로 여기서 compareTo의 타입 매개변수는 Student인 것이 맞다. 그리고 implements한 인터페이스의 T 자리에 Student를 가지는 것이다. 이와 같이 Student가 Comparable 인터페이스를 구현하면 Student의 ArrayList인 stList에 대해 Collections.sort(stList);와 같이 호출할 수 있다. Integer의 ArrayList<Integer>도 정렬할 수 있다. 왜냐하면 Integer 클래스는 이미 Comparable 인터페이스를 구현해 둔 라이브러리 클래스이기 때문이다.

이런 식으로 제너릭은 여러 타입의 데이터에 대해 사용할 수 있는 클래스를 만들 때 많이 사용된다. 즉 여러 타입에 대해 다 적용할 수 있는 코드인데, 그렇다고 여러 타입이 섞여서 사용되는 것이 아니라 하나의 타입으로 정해서 쓸 수 있는 경우다. 제너릭을 얘기할 때 많이 드는 예가 스택, 큐, 리스트 같은 콜렉션 클래스인데, 이런 자료구조를 사용하는 알고리듬을 타입에 상관없이 제너릭하게 작성하여 제공하고 그것을 실제 사용하는 곳에서 어떤 타입의 요소에 대해 이것을 사용할 것인지 정하고 쓰면 된다.(스택의 push()나 pop()을 다시 구현할 필요가 없다.) 그래서 제너릭은 컬렉션 라이브러리 클래스에서 매우 많이 사용된다. 

앞의 Comparabl<T>의 예 외에도 인터페이스를 이용하는 곳에서도 많이 사용된다. Iterator<T>는 요소 타입 T의 여러 객체를 차례로 반복하게 해주는 추상클래스다. 이것을 이용하면 위에서 선언된 Integer의 ArrayList인 nList에 대해 다음과 같은 while 루프를 돌릴 수 있다. 즉 ArrayList 클래스는 그 리스트의 요소를 차례로 반복하게 해주는 Iterator를 돌려주는 iterator()라는 메소드를 가진다.

ArrayList<integer> list = new ArrayList<>();
int sum = 0;
Iterator<integer> it = list.iterator();
while (it.hasNext()) {
	sum += it.next();
}

 이것은 Scanner에서 봤던 그 hasNext()와 next() 메소드와 같은 것이다. 끝날 때까지 차례로 하나씩 받아다 반복하는 패턴(반복자라고 한다)에서 이 인터페이스를 사용하게 된다. 사실 for each에서 콜론(:)을 통해 하나씩 돌려주는 것도 이 Iterator 기능을 이용하는 것이다. for (Integer n : nList) { ... } 은 다음 코드를 간략하게 표현하는 syntactic sugar다. 

for (Iterator<Integer> it = nList.iterator(), Integer n = it.next(); it.hasNext(); n = it.next())
   sum += n;

이와 같이 우리는 알게 모르게 제너릭을 포함한 라이브러리 클래스의 기능을 많이 사용하고 있다. 제너릭은 다형성을 제공하면서 코드의 안정성을 보장하는 자바의 아주 중요한 기능요소(feature)다. 

[주] for :이 다음과 같은 형태로 해석될 것이라고 생각될 것이다. 

for (int i = 0, Integer n = nList.get(0); i < nList.size();i++, n = nList.get(i)) { ... }

물론 이렇게 봐도 되지만 i가 차례로 증가되어 순차 방문할 수 없는 콜렉션도 있다. 이터레이터의 역할은 어떤 콜렉션이 됐든 그 콜렉션이 원하는 순서대로 for 루프를 돌리게 해주는 것이다. 예를 들어 트리 콜렉션이라고 생각해 보자.  그 트리의 노드 방문 순서에 따라 터미널 노드만 차례로 방문하게 이터레이터를 작성할 수 있다. 그 트리 클래스에서 터미널 노드를 어떤 순서로 방문할지 정한다. 이터레이터를 사용하는 측에서는 일정한 순서로 돌려주는 노드에 대해 할 일만 하면 된다. 또는 해시맵이라면 키의 어떤 순서에 따라 방문하게 할 수도 있다.  이런 것을 구현할 수 있는 곳이 이터레이터다. 사실 이터레이터는 최근에 대두되는 함수형 프로그래밍에서 매우 중요한 개념이다. 이에 대해서는 다음에 기회가 되면 한번 정리하려고 한다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함