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

[Java] 자바로 프로그래밍 입문하기: 3.2. 자료형 생성하기 (3)

복소수

  복소수는 x + iy의 형태를 지닌 수입니다. x와 y는 실수이며, i는 -1의 제곱근이죠. x는 복소수의 실수부라고 하며, y는 복소수의 허수부라고 합니다. 복소수는 -1의 제곱근은 허수여야 한다는 생각으로부터 출발합니다. 실수에는 이러한 수가 없기 때문이죠. 복소수는 전형적인 수학적 추상화입니다: -1의 제곱근이 물리적으로 말이 되는지는 믿거나 말거나지만, 이는 자연 세계를 이해하는데 큰 도움이 됩니다.

  복소수는 회로에서 음파, 전자기장에 이르기까지 물리 체계를 모델링하는데 쓰이고 있습니다. 이러한 모델들은 잘 정의된 수학적 연산을 기반으로 복소수를 다루기 때문에, 이러한 연산을 좀 더 쉽게 할 수 있도록 컴퓨터 프로그램을 작성해보려고 합니다. 곧, 새로운 자료형이 필요하겠네요.

  복소수 자료형을 개발하는 것은 객체지향 프로그램의 모범 예제입니다. 어떤 프로그래밍 언어도 우리가 필요한 모든 수학적 추상화의 구현을 제공하지 않습니다. 하지만 자료형을 구현할 수 있는 능력은, 우리에게끔 복소수, 다항식, 벡터, 행렬 등 수많은 것들로부터 자유로워지게 됩니다.

  필요한 복소수의 기본 연산들은 선형대수학의 덧셈, 곱셈과 교환법칙, 결합법칙, 분배법칙입니다. 크기(magnitude)를 계산하기 위해, 다음 등식에 따라 허수부와 실수부를 추출합니다. 다음 그림을 참고하세요.

  예를 들어 a = 3 + 4i 이고 b = -2 + 3i 라면, a+b = 1 + 7i, a*b = -18 + i, Re(a) = 3, Im(a) = 4, |a| = 5입니다.

  이러한 기초 정의로, 복소수 자료형을 구현하는 방법은 명쾌해졌습니다. 다음 API로 시작해봅시다.

<Complex>의 API

    <Complex>는 위의 API를 구현하는 클래스입니다. <Charge>, 그리고 수많은 자료형 구현들과 동일한 구성을 지녔죠. 인스턴스 변수(re와 im), 생성자, 인스턴스 메소드(plus(), times(), abs(), re(), im(), toString()), 테스트 클라이언트를 지녔습니다. 테스트 클라이언트는 먼저 z와 z0을 1 + i로 설정하고, 다음을 시행합니다:


프로그램 3.2.6: Complex numbers

public class Complex {
    private final double re;   // the real part
    private final double im;   // the imaginary part

    // create a new object with the given real and imaginary parts
    public Complex(double real, double imag) {
        re = real;
        im = imag;
    }

    // return a new Complex object whose value is (this + b)
    public Complex plus(Complex b) {
        Complex a = this;             // invoking object
        double real = a.re + b.re;
        double imag = a.im + b.im;
        return new Complex(real, imag);
    }

    // return a new Complex object whose value is (this * b)
    public Complex times(Complex b) {
        Complex a = this;
        double real = a.re * b.re - a.im * b.im;
        double imag = a.re * b.im + a.im * b.re;
        return new Complex(real, imag);
    }
    
    // return abs/modulus/magnitude
    public double abs() {
        return Math.hypot(re, im);
    }

    // return the real or imaginary part
    public double re() { return re; }
    public double im() { return im; }  

    // return a string representation of the invoking Complex object
    public String toString() {
        if (im == 0) return re + "";
        if (re == 0) return im + "i";
        if (im <  0) return re + " - " + (-im) + "i";
        return re + " + " + im + "i";
    }

    // sample client for testing
    public static void main(String[] args) {
    	Complex z0 = new Complex(1.0, 1.0);
    	Complex z = z0;
    	z = z.times(z).plus(z0);
    	z = z.times(z).plus(z0);
    	StdOut.println(z);
    }
}
% java Complex
-7.0 + 7.0i

  위 코드는 여러분이 이제껏 이 장에서 봐왔던 코드들과 유사합니다. 단 한가지만 빼면 말이죠: 다른 객체의 값에 접근한다는 점입니다.


복소수 자료형 객체의 인스턴스 변수에 접근하기

  plus()와 times()는 두 객체의 값에 접근해야 합니다: 인자로 전달되는 객체와, 메소드를 호출하는 객체입니다. 만약 a.plus(b)를 호출한다면, a 자신의 인스턴스 변수인 re와 im에는 접근할 수 있습니다. 그저 가져다 쓰면 되는 것이죠. 하지만 b의 인스턴스 변수에는 어떻게 접근할 수 있을까요? 바로 b.re와 b.im을 이용하여 접근할 수 있습니다. 인스턴스 변수를 private으로 선언한다는 것은, 클라이언트 코드가 다른 클래스의 인스턴스 변수를 직접 참조할 수 없게 만드는 것입니다. (같은 클래스에서는 언제든지 참조할 수 있습니다)


체이닝(Chaining)

  main() 함수를 보면, 두 메소드의 호출이 하나의 간단한 표현식으로 나타나는 것을 볼 수 있습니다: z.times(z).plus(z0)은 곧 z^2 + z0가 되죠. 이런 용법은 중간 값을 또 변수로 담지 않아도 되기 때문에 편리합니다. 모호성도 없죠: 좌측에서 우측으로 움직이며, 각 메소드는 다음 메소드를 호출할 때 사용할 <Complex> 객체에 대한 참조를 반환합니다. 원한다면 괄호로 우선순위를 덧붙일 수 있습니다. z.times(z.plus(z0))은 곧 z(z + z0)이죠.


새로운 객체를 생성하여 반환하기

  plus()와 times()는 클라이언트에게 값을 반환합니다: 클라이언트는 <Complex> 반환값이 필요하죠. 따라서 실수부와 허수부를 계산하고, 그 값을 이용해 새로운 객체를 생성한 뒤, 해당 객체의 참조를 반환합니다. 이러한 방식은 클라이언트로 하여금 지역 변수를 조작하여 복소수를 다룰 수 있게 합니다.


불변 값(Final values)

  <Complex>의 인스턴스 변수들은 전부 final입니다. 이 의미는 <Complex> 객체가 생성되면 그 생명주기 동안 절대로 해당 값이 변하지 않는다는 것입니다. 왜 이것을 사용하는지에 대해서는 3.3절에서 다뤄보도록 하겠습니다.


  복소수는 응용 수학에서 정교한 계산을 위한 기초입니다. 몇몇 프로그래밍 언어는 복소수를 기본 자료형으로 내장하고 있기도 합니다. 물론 *나 + 연산자 역시 복소수 범위에서 제공하죠. 자바는 내장 연산자들의 오버로딩을 지원하지 않습니다. 대신 자바의 자료형에 대한 지원은 좀 더 범용적이고, 우리에게 수많은 추상화를 구현할 수 있도록 하죠. <Complex>는 하나의 예제에 불과합니다.


망델브로 집합(Mandeolbrot set)

  망델브로 집합은 브누아 망델브로가 고안한 복소수의 집합입니다. 이는 매우 흥미로운 속성을 지녔는데요. 이전에 우리가 알아보았던 반슬리 고사리, 시에르핀스키 삼각형, 브라우니안 브릿지 등과 연관된 프랙탈 패턴입니다. 이러한 종류의 패턴들은 자연 현상에서 손쉽게 찾아볼 수 있습니다. 또한 이러한 모델들은 현대 과학에서 굉장히 중요한 역할을 합니다.

  망델브로 집합에서 점들은 하나의 수학 등식으로 표현될 수 없습니다. 대신 알고리즘에 의해 정의되므로, 복잡한 <Complex> 클라이언트에 적합한 후보입니다.

  해당 복소수가 망델브로 집합에 포함 되는지 여부를 정하는 규칙은 간단합니다: 다음 식을 만족하는 복소수 수열을 생각해봅시다. zt+1 = (zt)^2 + z0. 예를 들어, 다음 표는 z0 = 1 + i에 해당하는 수열의 처음 부분을 나타냅니다.

망델브로 수열의 계산

  만약 수열 |zt|이 무한으로 발산하면, z0은 망델브로 집합에 속하지 않는 복소수입니다. 수열이 유계(bounded) 수열이면, z0은 망델브로 집합에 속합니다. 검증은 간단한 경우도 있고, 아주 많은 계산이 필요한 경우도 있습니다. 다음 표를 참조하세요.

망델브로 수열의 몇몇 시작점


  몇몇 경우에, 우리는 수가 집합에 포함되는지 안 되는지를 쉽게 증명할 수 있습니다. 예를 들어, 0+0i의 경우 망델브로 집합에 속합니다. 모든 수열의 크기가 0이기 때문입니다. 2+0i의 경우 망델브로 집합에 속하지 않습니다. 2를 계속 제곱해나가면 무한으로 발산하기 때문입니다. 

  다른 몇몇 경우에는 증가함이 명백할 때도 있습니다: 1+i는 누가봐도 집합에 속하지 않을 것 같죠.

  반복되는 주기를 갖는 경우도 있습니다. i의 경우 -1+i와 -i가 반복되는 주기를 갖죠. 이러한 주기는 굉장히 길어지는 경우도 있습니다.

  망델브로 집합을 시각화하기 위해, 복소수의 점을 실함수(real-valued function)의 점으로 추출(sample)해야 합니다. 각 복소수 x+iy는 평면 위의 점 (x, y)에 대응합니다. 주어진 해상도 N에서, 우리는 N*N 픽셀의 격자를 상상하고 해당하는 위치의 점이 망델브로 집합이라면 검은 점을, 그렇지 않다면 흰 점을 칠하는 것을 생각해볼 수 있습니다. 이렇게 표현하면 상당히 낯설고 경이로운 모양이 나타나게 됩니다. 모든 검은 점은 서로 연결 되어 있고, 대부분이 2*2 사각형 안에 존재하게 됩니다.

  해상도가 커지면 커질수록 더 많은 계산을 요구합니다. 자세히 보면, 군데 군데에서 자기 유사성이 드러납니다. 예를 들어, 똑같이 둥글납작한 모양이 심장형 영역의 윤곽에 나타납니다. 심장형 곡선의 경계를 확대해보면, 또 다른 심장형 곡선이 나타나죠!

  하지만 어떻게 정확히 그래프를 그릴 수 있을까요? 실제로는 해당 복소수가 정말로 망델브로 집합에 포함되는지 확인할 수 있는 간단한 방법이라는 것은 없습니다. 단순히 주어진 복소수 점에서 일정량 수열을 계산해보고, 이것이 발산하는지 아닌지 판단할 뿐이죠: 어떤 수이든 크기가 2(예를 들어, 2 + 0i)보다 커지면, 해당 수는 반드시 발산합니다.


프로그램 3.2.7: Mandelbrot set

import java.awt.Color;

public class Mandelbrot {
    private static int mand(Complex z0, int max) {
	Complex z = z0;
	for (int t = 0; t < max; t++) {
	    if (z.abs() > 2.0)
		return t;
	    z = z.times(z).plus(z0);
	}
	return max;
    }

    public static void main(String[] args) {
	double xc = Double.parseDouble(args[0]);
	double yc = Double.parseDouble(args[1]);
	double size = Double.parseDouble(args[2]);
	int N = 512;
	Picture pic = new Picture(N, N);
	for (int i = 0; i < N; i++)
	    for (int j = 0; j < N; j++) {
		double x0 = xc - size / 2 + size * (double)i / N;
		double y0 = yc - size / 2 + size * (double)j / N;
		Complex z0 = new Complex(x0, y0);
		int t = 255 - mand(z0, 255);
		Color c = new Color(t, t, t);
		pic.set(i, N - 1 - j, c);
	    }
	pic.show();
    }
}

%java Mandelbrot .13 -.643 .01

%java Mandelbrot .13 -.643 .1

%java Mandelbrot .13 -.643 1.0


  <Mandelbrot>은 이러한 검증을 거쳐서 망델브로 집합을 시각화합니다. 우리는 해당 점이 망델브로 집합인지 확신할 수 없으므로, 그레이스케일을 이용하여 표현합니다. mand() 함수는 <Complex> 인자 z0와 정수형 인자 max를 받아 해당 복소수의 크기가 2를 넘어갈 때까지 max번 검증합니다. t는 검증한 횟수이며, 크기가 2를 넘어가면 t를 반환합니다.

  각 픽셀에서 <Mandelbrot>은 255 - mand(z0, 255)의 값으로 그레이스케일 컬러를 칠합니다. 즉, 검정색인 경우에는 mand(z0, 255)의 반환값이 255라는 의미이며, 이는 255번의 시행동안 크기가 2를 넘지 않았다는 의미입니다. 반면 검정색이 아닌 경우에는 255번의 시행 안에 크기가 2를 넘었다는 의미입니다. 

  <Mandelbrot>은 <Complex>의 클라이언트입니다. <Complex>를 구현하였기에, <Mandelbrot>를 손쉽게 만들 수 있었죠. 여러분은 <Complex>가 없더라도, <Mandelbrot>을 만들 수 있습니다. 하지만 <Complex>의 코드와 <Mandelbrot>의 코드가 한 프로그램에 자리했겠죠. 이는 분명 코드를 이해하기 어렵게 만들었을 것입니다. "프로그램에서 작업의 단위를 나눌 수 있다면, 그렇게 해야 합니다."


상업적 자료 처리

  객체지향 프로그래밍 개발의 원동력 중 하나는 신뢰성 있는 상업적 자료 처리 소프트웨어의 필요입니다. 따라서 금융 기관에서 고객의 정보를 유지 관리하는 데 사용되는 자료형 예제에 대해서 알아보려고 합니다.

  주식 중개인이 다양한 주식을 지닌 고객의 계좌들을 유지해야 한다고 가정해봅시다. 중개인이 필요한 값의 집합은 고객의 이름, 각 주식의 수와 종목코드, 계좌의 총 주식수 등이 필요하겠네요. 계좌를 처리하기 위해, 중개인은 최소한 다음 API에 정의된 연산들 정도는 필요할 것입니다.

<StockAccount>의 API


  중개인은 사고, 팔고, 고객에게 보고해야 합니다. 하지만 이러한 종류의 자료 처리를 이해하는 첫 번째 핵심은 StockAccount() 생성자와 write() 메소드입니다. 고객의 정보는 긴 기간동안 유지되어야 하고, 파일이나 데이터베이스로 저장되어야 합니다. 계좌를 처리하기 위해, 클라이언트 프로그램은 해당 파일의 정보를 읽어야 합니다. 그리고 정보가 변경되면, 다시 파일에 작성해주어야 하죠.. 이러한 처리를 가능하게 만들기 위해, 계좌 정보에 대한 파일 형식(file format)과 내부 표현(internal representation), 자료 구조(data strructure) 등이 필요합니다. 

  조금은 엉뚱한 예시로, 중개인이 컴퓨팅의 아버지인 앨런 튜링(Alan Turing)의 주식 포트폴리오를 관리한다고 해보죠. 여담이지만, 튜링의 인생은 멋진 삶을 살았습니다. 암호화 기술을 통해 2차 세계 대전을 끝낼 수 있게 해주었죠. 최초의 컴퓨터를 만들었고, 인공지능의 선구자였습니다. 아마 앨런 튜링은 오늘날의 전산 소프트웨어에 긍정적으로 생각했을 것이고, 주식에 작게나마 투자했을 가능성이 높지 않겠어요?


파일 형식

  현대 시스템은 보통 텍스트 파일을 이용합니다. 형식에 대한 의존성을 최소화하기 위함이죠. 간단하게, 예금주의 이름(문자열), 잔액(부동소수점 수), 주식의 수(정수), 종목코드를 목록에 써넣어봅시다. <Name>이나 <Number of shares>와 같은 태그(tags)를 이용하는 것이 의존성을 더 최소화하는 영리한 방법이겠지만, 이번에는 넘어가도록 합시다.

파일 형식

자료 구조

  자바 프로그램에서 처리를 위한 정보를 표현할 때, 우리는 객체 인스턴스 변수를 사용합니다. 이것들은 정보의 자료형을 명세하고, 코드에서 명확하게 참조할 수 있도록 구조를 제공합니다. 우리 예제의 경우에는 다음을 명확히 해야겠죠:

  • String 값의 예금주
  • double 값의 잔액
  • int 값의 총 주식 수
  • String 값 배열의 주식 심볼
  • int 값 배열의 주식 수
public class StockAccount {
    private final String name;
    private double cash;
    private int N;
    private int[] shares;
    private String[] stocks;
    ...
}

  우리는 <StockAccount>에서 이러한 인스턴스 변수들을 선언할 것입니다. 배열 stocks[]와 shares[]는 평행 배열(parallel arrays)라고 합니다. 주어진 인덱스 i에서 stocks[i]는 주식의 종목 코드를, shares[i]는 해당 종목 코드의 주식 수를 나타내죠. 이러한 방법 대신 주식의 종목 코드와 주식 수를 담는 새로운 자료형을 정의하여 사용하는 방법도 있습니다.

  <StockAccount>는 생성자를 포함합니다. 파일을 읽어들여 객체의 내용을 채우죠. 또한 웹에서 주식의 가격을 알아내는 valueOf()의 메소드 구현은 <StockQuote>에서 사용했던 것입니다. 예를 들어, 중개인은 고객에게 정기 보고를 하기 위해, printReport()를 사용할 수 있을 것입니다.


public void printReport() { 
    StdOut.println(name); 
    double total = cash; 
    for (int i = 0; i < n; i++) {
        int amount = shares[i];
        double price = StockQuote.price(stocks[i]);
        total += amount * price;
        StdOut.printf("%4d %5s ", amount, stocks[i]);
        StdOut.printf("%9.2f %11.2f\n", price, amount * price);
    }
    StdOut.printf("%21s %10.2f\n", "Cash: ", cash);
    StdOut.printf("%21s %10.2f\n", "Total:", total);
} 

  한편, 이 클라이언트는 1950년대, 컴퓨터 발전의 주요 원동력 중 하나인 프로그램 종류입니다. 은행과 같은 회사들은 컴퓨터를 일찍이 구매해 사용했죠.  예를 들어, 형식 지정자를 이용해 문자열을 출력하는 것은 이러한 응용프로그램에서 처음으로 도입되었습니다. 또한 이 클라이언트는 브라우저 없이 웹의 정보를 직접 가져온다는 점에서, 현대의 웹 중심 컴퓨팅의 훌륭한 예가 되기도 합니다.

  buy()와 sell()의 구현은 4.4절에서 다룰 기초 메커니즘을 사용합니다. 따라서 이들에 대한 이야기는 다음으로 미루도록 하죠. 


프로그램 3.2.8: Stock account

public class StockAccount { 
    private final String name;     // customer name
    private double cash;           // cash balance
    private int n;                 // number of stocks in portfolio
    private int[] shares;          // shares[i] = number of shares of stock i
    private String[] stocks;       // stocks[i] = symbol of stock i

    // build data structure from file
    public StockAccount(String filename) {
        In in = new In(filename);
        name = in.readLine(); 
        cash = in.readDouble(); 
        n = in.readInt(); 
        shares = new int[n]; 
        stocks = new String[n]; 
        for (int i = 0; i < n; i++) {
            shares[i] = in.readInt(); 
            stocks[i] = in.readString(); 
        } 
    } 

    // print a report to standard output
    public void printReport() { 
        StdOut.println(name); 
        double total = cash; 
        for (int i = 0; i < n; i++) {
            int amount = shares[i];
            double price = StockQuote.price(stocks[i]);
            total += amount * price;
            StdOut.printf("%4d %5s ", amount, stocks[i]);
            StdOut.printf("%9.2f %11.2f\n", price, amount * price);
        }
        StdOut.printf("%21s %10.2f\n", "Cash: ", cash);
        StdOut.printf("%21s %10.2f\n", "Total:", total);
    } 

    // value of account
    public double valueOf() { 
        StdOut.println(name); 
        double total = cash; 
        for (int i = 0; i < n; i++) {
            int amount = shares[i];
            double price = StockQuote.price(stocks[i]);
            total += amount * price;
        }
        return total;
    } 

    // test client
    public static void main(String[] args) { 
        String filename = args[0];
        StockAccount account = new StockAccount(filename); 
        account.printReport(); 
    } 
} 
% more Turing.txt
Turing, Alan
10.24
5
100 ADBE
25 GOOG
97 IBM
250 MSFT

% java StockAccount Turing.txt
Turing, Alan
 100   ADBE    495.77    49577.00
  25   GOOG   2421.28    60532.00
  97    IBM    145.72    14134.84
 250   MSFT    247.30    61825.00
                Cash:       10.24
                Total:  186079.08


  2장에서는 프로그램의 곳곳에서 사용될 수 있는 함수를 정의하는 방법에 대해 배웠습니다.  이를 통해 여러분들은 단일 파일의 구문 목록에 불과한 프로그램으로부터 떠나 모듈화 프로그래밍의 세계에 도달할 수 있었습니다. 그 여정에는 "프로그램에서 작업의 단위를 나눌 수 있다면, 그렇게 해야 합니다." 라는 주문이 함께했죠. 

  이번 장을 통해, 여러분들은 자료가 반드시 기초적인 자료형들로 표현되어야 하는 세계에서 나만의 자료형을 정의할 수 있는 세계로 떠날 수 있었습니다. 이 굉장한 능력은 여러분들의 프로그래밍 영역을 확장시켜줍니다. 함수의 개념과 자료형을 구현하고 사용하는 것. 이걸 사용하지 않았던 원시 프로그램들은 상상하기도 힘들죠.

  하지만 객체지향 프로그래밍은 자료를 구조화하는 것 그 이상입니다. 이는 우리에게끔 관련 있는 자료를 연관짓고 그 자료들을 처리하는 연산들을 독립된 모듈 하나에 분리할 수 있게 만듭니다. 객체지향 프로그래밍과 함께할 때, 우리의 주문은 다음과 같습니다: "자료 및 해당 자료와 관련된 연산들을 명확하게 분리할 수 있다면, 그렇게 해야 합니다." 

  우리가 함께 배웠던 예제들은 분명 객체지향 프로그래밍이 넓은 범위의 분야에서 중요한 역할을 할 수 있음을 증명합니다. 물리적 인공물을 만들고, 소프트웨어 시스템을 개발하고, 자연에 대해 이해하고, 정보를 처리하고 했던 시도의 첫 번째 단계는 적절한 추상화를 정의하는 것입니다. 물리적 인공물의 기하학적 표현, 소프트웨어 시스템의 모듈화 설계, 자연 세계의 수학적 모델, 정보의 자료 구조와 같은 것들이죠. 잘 정의된 추상화만 있다면, 우리는 그것을 자바 클래스의 자료형으로 정의하고 자바 프로그램을 작성해 자료들을 손쉽게 다룰 수 있습니다.

  다른 클래스들에서 특정 자료형의 객체를 생성하고 다루기 위해, 해당 자료형의 클래스를 개발할 때마다 우리는 더 높은 추상화 계층에서 프로그래밍하게 되는 것입니다. 다음 절에서는 이러한 프로그래밍에 내재된 설계 문제들에 대해서 다뤄볼 것입니다.


끝.

댓글

이 블로그의 인기 게시물

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

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

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