티스토리 뷰

이번 포스트에서는 앞의 글에 이어서 인터페이스의 구체적인 예제를 살펴보겠다. 그리고 그 인터페이스를 이용하여 실제 클래스를 만드는 것도 살펴본다. 여기서는 실제 동작하는 프로그램을 만들기 위해 데이터를 읽고 출력하고 검색하는 기능을 만들어보겠다.

그것을 위해서 먼저 실제 인터페이스를 하나 정의한다. 이 인터페이스는 읽고 출력하고 비교하는 기능을 가지고 이것을 관리하는 클래스를 만들 것이므로 이름을 Manageable이라고 붙이려고 한다.

interface Manageable {
    void read(Scanner scan);
    void print();
    boolean compare(String kwd);
}

그럼 이 인터페이스를 구현한 클래스를 예로 들어 보자.

class Book implements Manageable {
    String title;
    int year;
    String author;
    @Override
    public void read(Scanner scan) {
        title = scan.next();
        year = scan.nextInt();
        author = scan.next();
    }
    @Override
    public void print() {
         System.out.printf("%s (%d년) by %s%n", title, year, author);
    }
    @Override
    public boolean compare(String kwd) {
        if (title.contains(kwd)) return true;
        if (author.equals(kwd)) return true;
        return false;
    }
}

다음으로 이런 Book 클래스를 여러 개 가지는 BookShelf를 다음과 같이 만들어보자.  Bookshelf.java

Bookshelf.java
0.00MB

이와 같이 여러 개 입력받고 전부 출력하고 검색하는 기능을 하는 클래스를 Book의 관리자 클래스라고 하자. 우리 프로젝트에서 이런 일을 여러 클래스에 대해 자주 해야 한다면? 그런 관리해야 할 객체마다 따로 관리자 클래스를 만들려면 코드의 중복이 많아질 것이다. 여기서 readAll(), printAll(), findBook() 같은 함수가 다 비슷한(사실 클래스 이름만 다르지 거의 똑같은) 코드를 가질 가능성이 높다. 이러한 코드 중복을 피하기 위해 우리는 Manager라는 클래스를 다음과 같이 만들려고 한다.

class Manager { 
	ArrayList>Manageable> mList = new ArrayList<>();
	Scanner scan = new Scanner(System.in);
	void readAll(Scanner scan) {
		System.out.print("개수 : "); 
		int n = scan.nextInt();
		Manageable m  = null;
		for (int i = 0; i < n; i++) {
			m = new Manageable();  // *** 컴파일 오류
			m.read(scan);
			mList.add(b);
		}
	}
	void printAll() {
		for (Manageable m : mList)
			m.print();
	}
	Manageable find(String kwd) {
		for (Manageable m : mList)
			if (m.compare(kwd)) return m;
		return null;
	}
}

여기서 한 가지 문제가 생긴다. 즉 new Manageable을 할 수가 없다는 점이다. 인터페이스는 new를 할 수 없으므로... 이 문제를 해결하기 위해 등장하는 것이 Factory 패턴이다. 이것은 새로운 인터페이스를 하나 더 만들어서 객체를 생성하는 기능만 가지게 하고 그것을 new가 필요한 자리에서 쓴다. 그리고 실제 new를 할 수 있는 클래스에서 그 Factory 인터페이스를 구현해 주어야 한다.

interface Factory {
    Manageable create();
}

그러면 이제 BookShelf 클래스는 상당히 간단하게 만들어진다.

class BookShelf extends Manager implements Factory {
    @Override
    public Book create() {
          return new Book();
    }
    public static void main() {
          BookShelf shelf = new BookShelf();
          shelf.doit();
    }
    void doit() {
          shelf.readAll(scan);
          shelf.printAll();
          String kwd = scan.next();
          Book b = shelf.findBook(kwd);
          b.print();
    }
}

위 코드에서 오버라이드의 한 가지 새로운 점이 있다. 즉 Manageable을 반환하는 create()에 대해 오버라이드한 메소드가 반환형을 Book으로 해도 된다는 점이다. 이것은 상속해서 오버라이드할 때 자주 발생하는 형태여서 자바에서 특별히 오버라이드로 인정해 주는 경우다.

다음으로 우리가 주차 등록 시스템을 만들어서 자동차를 읽어들이고 출력하고 검색하고 싶다고 해 보자.

class ParkingMgr extends Manager implements Factory { ...

이렇게 main과 create만으로 우리는 쉽게 프로그램을 완성할 수 있다. (물론 Car 클래스가 있고 그것이 Manageable을 구현했어야 한다.) 또는 학생을 관리하는 클래스를 만들고 싶다고 해 보자.

class StudentMgr extends Manager implements Factory { ...

이와 같이 Manager를 재사용하면 쉽게 기본적인 기능까지는 만들 수 있다. 그리고 여기에 추가적으로 각 관리자 클래스에서 고유한 일을 하는 코드를 추가할 수 있을 것이다. 다음 예처럼 StudentMgr 클래스는 학생의 점수로 최우수 학생을 찾아 출력하는 기능을 추가하고 싶다. Manageable은 점수에 관한 기능은 갖지 않고 Student만 가지고 있다. 그러므로 우리는 다음과 같이 Student로 객체를 캐스팅하여 사용해야 한다. 그런데 문제는 다음과 같은 코드에서 발생한다.

void printBestStudent() {
    Student best = (Student) mList.get(0);
    for (Manageable m : mList) {
         if (best.getScore() < ((Student)m).getScore())
              best = (Student) m;
    }
}

즉 학생 중에서 점수가 제일 높은 학생을 찾아서 이름을 출력하고 싶다면 점수를 가져오는 메소드가 필요하고 이것은 Manageable에 없다. 그러므로 우리는 Manageable을 가지고 있는 mList에서 하나씩 꺼내서 학생으로 바꾸어서 점수를 물어봐야 한다. 여기서 ((Student) m).getScore()처럼 다운캐스팅이 계속 필요하다. 캐스팅해야 학생에 해당하는 메소드나 필드를 사용할 수 있게 되고 학생에 관련된 일을 할 수 있다. 

마찬가지로 Manager를 상속한 다른 클래스에서도 Manageable에 없는 기능을 이용하려면 다운캐스팅을 통해 해당 클래스 객체로 바꾼 다음에 그 메소드를 이용하게 된다. 

이러한 다운캐스팅 코드가 자주 발생하는 것이 인터페이스 사용의 중요한 문제점 중 하나다. 우리가 앞의 다운캐스팅 절에서 살펴본 바와 같이 Manageable인 m은 그것을 상속한 클래스 객체 중 어느 것이든 될 수 있다. 즉 여기서는 Car거나 Book이 될 수 있다. 특히 하나의 프로그램 안에서 이런 여러 가지 클래스들을 다 다룬다면 이것들이 섞여서 들어갈 가능성이 있다(일부러 그러진 않겠지만 실수할 가능성이 많이 있다). 그랬을 때 다운캐스팅 코드는 프로그램이 예외를 일으키고 종료하는 원인이 될 수 있다. 

즉 위와 같이 Manageable 인터페이스를 사용하는 코드에서 문제는 Manager가 가지고 있는 mList에 Manageable을 상속한 클래스들이 섞어서 들어갈 수 있다는 점이다. 컴파일러가 타입에 대한 구분을 못하고 집어넣을 때 Manageable이기만 하면 add가 된다. 그런데 꺼낼 때 다운캐스팅을 해야 된다면 컴파일러로서는 캐스팅하려는 타입이 맞는지 틀린지 확인할 방법이 없다. 즉 위의 코드에서 m이 Student 타입의 객체가 맞는지 확인할 방법이 없으므로 프로그래머에게 책임을 맡기게 된다. 프로그래머에게 맡기는 것은 항상 오류(실수)의 가능성이 있다. 이러한 문제를 해결하기 위해 등장한 것이 제너릭이다. 뒤에서 설명할 제너릭 방법은 컴파일러가 Manager 객체에 어떤 타입이 들어가고 나와야 되는지 알 수 있게 해준다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함