{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[Java] 자바로 프로그래밍 입문하기: 3.3. 자료형 설계하기 (3)

 상속(Inheritance)

  자바에서는 객체 간의 관계를 정의할 수 있습니다. 바로 상속입니다. 소프트웨어 개발은 이 메커니즘을 널리 사용하며, 여러분이 소프트웨어 공학에 관심이 있다면 자세히 알아야 하는 부분 중 하나입니다. 이러한 메커니즘을 효과적으로 활용하는 방법에 대한 것은 이 강의의 범위를 벗어나지만, 어차피 여러분들이 곧 직면하게 될 요소이므로 간단하게 설명하려고 합니다.


인터페이스(Interfaces)

  인터페이스는 관련되지 않은 클래스들 간의 관계를 명세하는 메커니즘입니다. 구현 클래스(implementing class)는 반드시 포함해야 하는 메소드들이 존재하죠. 우리는 이를 인터페이스 상속이라고 말합니다. 구현 클래스는 API에 없는 메소드를 인터페이스로부터 상속 받기 때문입니다. 이러한 방식은 우리에게 다양한 자료형의 객체를 다룰 수 있는 클라이언트 프로그램을 작성할 수 있게 합니다. 인터페이스의 메소드를 호출하기만 하면 되죠. 새로운 프로그래밍의 개념들을 접하면 처음엔 혼란스럽긴 하겠습니다만, 계속 예제를 보다보면 감이 올 것입니다.


Comparable

  여러분이 접하기 쉬운 인터페이스 중 하나는 자바의 Comparable입니다. 다음에 기술될 API인 compareTo() 메소드를 통해서 자료형의 자연 순서(natural order)를 연관 짓는 인터페이스입니다.

자바의 Comparable 인터페이스 API

  <Key> 부분은 4.3절에서 다뤄볼 표현법이며, 비교가 될 두 객체가 같은 자료형임을 보장하는 것입니다. a와 b가 같은 객체라는 전제 하에, a.compareTo(b)는 반드시 다음을 반환합니다:

  • b보다 작으면 음수
  • b보다 크면 양수
  • b와 같으면 0

  추가적으로, compareTo() 메소드는 일관성을 지녀야합니다: 예를 들어, a가 b보다 작다면, 반드시 b는 a보다 커야합니다. Comparable 인터페이스를 구현하는 클래스(String, Integer, Double 등)은 compareTo() 메소드를 반드시 포함하는 약속을 지켜야만 합니다. 예상하시겠지만, 문자열의 자연 순서는 알파벳 순서, 정수는 오름차순입니다.

  이러한 인터페이스들의 범용성을 느끼기 위해, String 객체의 정렬 필터로부터 시작해보죠. 다음 코드는 명령행으로부텉 정수 N을 받아 N개의 문자열을 표준 입력으로 받으며, 그들을 정렬해 알파벳 순서로 표준 출력에 문자열들을 다시 출력합니다.


import java.util.Arrays;

public class SortClient {
    public static void main(String[] args) {
        int N = Integer.parseInt(args[0]);
        String[] names = new String[N];
        for (int i = 0; i < N; i++)
            names[i] = StdIn.readString() ;
        Arrays.sort(names);
        for (int i = 0; i < N; i++)
            StdOut.println(names[i]);
    }
}

  정렬의 세부사항에 대해서는 4.2절에서 다뤄볼 것이며, 잠깐 동안은 java.uitil 라이브러리의 정적 메소드인 Array.sort()를 사용하도록 합시다. 중요한 것은 이제부터입니다. 배열의 입력 부분인 세 줄을 바꿔봅시다. String을 Integer로 바꾸는 것이죠.

Integer[] names = new Integer[N];
for (int i = 0; i < N; i++)
    names[i] = Stdln.readlnt() ;

  SortClient는 표준 입력으로부터 읽은 Integer 값을 정렬합니다. 아마 여러분은 Arrays.sort() 메소드가 해당 구현을 오버로딩했을 것이라고 추측하신 분들도 있을 것입니다. String에 대한 메소드와 Integer에 대한 메소드를 둘 다 만드는 것이죠. 하지만 그렇지 않습니다. Arrays.sort()는 어떤 자료형이든 Comparable 인터페이스를 구현하기만 한다면(심지어 정렬 메소드를 작성할 때 고려되지 않은 자료형들조차) 그 배열을 정렬할 수 있기 때문이죠.

  Array.sort()는 변수를 String, Integer, Double, 그 외의 Comparable 인터페이스를 구현하는 모든 자료형들을 대표하는 Comparable형으로 선언해 다룹니다. 이러한 변수의 compareTo()를 호출하면, 자바는 해당 변수의 자료형을 알고 있기 때문에, 어떤 compareTo() 메소드를 호출해야하는지 알고 있습니다. 이런 강력한 프로그래밍 메커니즘은 다형성(polymorphism) 혹은 동적 디스패치(dynamic dispatch)라고 말합니다.

  Comparable 인터페이스를 구현하는 클래스를 만들기 위해, implements Comparable이라는 문구를 클래스 정의 다음에 포함해야 합니다.또한 compareTo() 메소드를 추가해야 하죠. 예를 들어, Counter(프로그램 3.3.2)를 다음처럼 변경할 수 있습니다:

public class Counter implements Comparable<Counter> {
    ...
    public int compareTo(Counter b) {
        if (count < b.count) 
            return -1
        else if (count > b.count) 
            return +1
        else 
            return 0
    }
    ...
}

  여러분이 compareTo()를 구현한 이후라면, 어떤 정렬 순서든 특정할 수 있는 유연성을 갖추게 된 것입니다. 예를 들어, 클라이언트들이 Counter 값을 내림차순으로 정렬할 수 있는 버전을 만든다고 하면, compareTo() 메소드를 다음과 같이 바꿀 수 있습니다:


public int compareTo(Counter b) {
    if (count < b.count) 
        return +1
    else if (count > b.count) 
        return -1
    else 
        return 0
} 


함수로 계산하기

  종종, 과학 분야에서는 함수를 통한 계산이 필요합니다. 미적분을 계산한다거나 제곱근을 찾는 것 등이죠. 함수형 프로그래밍 언어(functional programming languages)라고 불리는 몇몇 프로그래밍 언어는 이러한 요구와 일치하죠. 클라이언트 코드를 단순하게 만들기 위함입니다. 

적분의 근사

  안타깝게도, 자바에서 메소드는 일급 객체(first-class objects)가 아닙니다. 예를 들어, 구간이 (a,b)인 양함수에서 적분(곡선 아래의 넓이)을 추정하는 문제를 생각해보죠. 이러한 게산은 수치 적분(numerical integration) 혹은 구적법(quadrature)이라고 말합니다. 적분 값을 추정하는 간단한 방법은 사각형법(rectangle rule)을 이용하는 것입니다: 전체 구간을 같은 폭으로 N개 만큼 나누어서 사각형을 그리는 것이죠. 이를 구현하기 위해, 다음과 같이 코드를 작성해볼 수 있습니다.

public double integrate(double f(double x), double a, double b, int N) {
	double delta = (a - b) / N;
	double sum = 0.0;
	for (int i =0; i < N; i++)
		sum += delta * f(a + delta * (i + 0.5));
}

  클라이언트 코드에서는 다음과 같이 작성해볼 수 있겠죠.


integrate(Gaussian.phi(), a, b, N)

  불행하게도, 이러한 코드는 자바에서 유효하지 않습니다. 메소드를 인자로 넘겨줄 수 없죠. 우리는 이러한 제한을 극복하기 위해서 인터페이스를 정의하고 해당 인터페이스가 구현하는 evaluate() 메소드를 사용할 수 있을 것입니다. 다음 두 코드 조각을 참고하세요.

public interface Function {
    public abstract double evaluate(double x);
}
 
public class RectangleRule {
   /**********************************************************************
    * Integrate f from a to b using the rectangled rule.
    * Increase n for more precision.
    **********************************************************************/
    public static double integrate(Function f, double a, double b, int n) {
        double delta = (b - a) / n;           // step size
        double sum = 0.0;                     // area
        for (int i = 0; i < n; i++) {
            sum += delta * f.evaluate(a + delta*(i + 0.5));
        }
        return sum;
    }

    // sample client program
    public static void main(String[] args) { 
        double a = Double.parseDouble(args[0]);
        double b = Double.parseDouble(args[1]);
        Function f = new GaussianPDF();
        StdOut.println(integrate(f, a, b, 1000));
    }
}


사건 기반 프로그래밍(Event-based programming)

  인터페이스 상속의 가치에 대한 또 다른 훌륭한 예시 중 하나는 사건 기반 프로그래밍입니다. 친근한 예시를 생각해보죠. 마우스 클릭이나 키보드 입력 등 사용자가 발생 시키는 입력에 반응해 그리기를 확장해야 하는 문제가 있습니다. 이를 해결하는 방법 중 하나는, 그리기 패키지가 사용자 입력 시 어떠한 메소드를 호출해야하는지 명세하는 인터페이스를 정의하는 것입니다.

  콜백은, 인터페이스를 통해 한 클래스의 메소드로부터 다른 클래스의 메소드를 호출하는 것을 일컫습니다. 여러분은 이 강의의 웹사이트에서 DrawListener 인터페이스 예시를 접해볼 수 있으며, Draw.java에서 어떻게 사용자의 입력에 대응하는지를 알 수 있습니다. 여러분들은 Draw 메소드가 사용자의 이벤트에 대응하는 여러분의 메소드를 호출할 수 있도록, 인터페이스를 잘 기술해 Draw 객체를 생성해낼 수 있습니다.


서브타이핑(Subtyping)

  코드 재사용을 실현하는 또다른 접근법은 바로 서브타이핑입니다. 이는 프로그래머가 처음부터 전체 클래스를 다시 작성하지 않아도 클래스의 행위를 변경하거나, 기능을 추가할 수 있는 강력한 기술입니다. 이 개념은 다른 클래스(super class 또는 base class)의 인스턴스 변수와 인스턴스 메소드를 상속받는 새로운 클래스(subclass 또는 derived class)를 정의하는 것입니다.

  자식 클래스(subclass)는 부모 클래스(super class)보다 더 많은 메소드를 갖고 있습니다. 서브타이핑은 이른바 확장 가능한 라이브러리를 만들기 위해 시스템 프로그래머들에게서 널리 사용되고 있습니다. 이 개념을 통해 한 프로그래머가 다른 프로그래머에 의해 만들어진 라이브러리에 메소드를 추가할 수 있으며, 잠재적으로 거대한 라이브러리를 효과적으로 재사용할 수 있죠.

  이러한 접근법은 널리 사용되며, 특히 사용자 인터페이스를 개발하는 분야에서 사용자가 기대한(드롭-다운 메뉴, 복사-붙여넣기, 파일 접근 등) 모든 기능을 제공하는 거대한 양의 코드가 재사용될 수 있습니다. 

  서브타이핑의 사용은 시스템 프로그래머들로부터 갑론을박되는 문제이기도 합니다. 우리는 이러한 것을 사용하지 않을 것입니다. 일반적으로 캡슐화와 거리가 멀기 때문이죠. 서브타이핑은 모듈화 프로그래밍을 두 가지 이유로 더욱 어렵게 만듭니다. 

  첫째, 부모 클래스의 변화는 모든 자식 클래스에게 영향을 미칩니다. 자식 클래스는 부모 클래스로부터 독립적으로 개발될 수 없습니다. 실제로, 자식 클래스는 부모 클래스에게 완전 종속됩니다. 이 문제는 fragile base class(취약한 기반 클래스) 문제로도 잘 알려져 있습니다.

  둘째, 인스턴스 변수에 접근하는 자식 클래스의 코드는 부모 클래스 코드의 의도를 완전히 뒤바꿀 수 있습니다. 예를 들어, Vector와 같은 클래스의 설계자는 Vector를 불변형으로 만들기 위해 많은 노력을 들였을 수도 있습니다. 하지만 인스턴스 변수에 완전한 접근을 지닌 자식 클래스가 그들을 단숨에 바꿔버릴 수 있죠. 

  하지만, 이런 접근법들은 자바에 내장되어 있으므로 피할 순 없습니다. 특히, 자바의 모든 클래스는 Object 클래스의 서브타입입니다. 이러한 구조는 모든 클래스가 toString(), equals(), hashCode() 등 메소드의 구현을 포함하게 하는 일종의 "관습"이죠. 모든 클래스는 이러한 메소드들을 Object로부터 서브타이핑을 통해 상속받습니다.


계속.

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기