6. Java 객체지향 프로그래밍


javalogo

Overview

이 포스트에서는 Java 프로그래밍 언어의 특성과 객체지향 개념이 어떻게 프로그램에 적용되는지 Java 프로그램을 통해 간단히 살펴본다.

객체지향 프로그램의 가장 기본적인 요소는 객체로, 객체는 데이터와 연산자가 묶여있는 구조이다.

또한 객체는 템플릿(template)으로 생각할 수 있는 클래스로부터 생성되어 클래스의 인스턴스가 객체가 되며 모든 객체는 클래스로 분류된다.


Java로 구현한 객체지향 프로그래밍

그래픽스 프로그램에서 사용하는 모든 사각형들을 다음 프로그램과 같이 Rectangle 클래스로 분류하여 정의할 수 있다.

class Rectangle {
    private int x;
    private int y;
    private int width;
    private int height;
    
    public Rectangle() {
        x = y = width = height = 0;
    }
    
    public Rectangle(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    public void draw() {
        System.out.println("Rectangle : " + x + ", " + y + ", " 
            + width + ", " + height);
    }

    public static void main(String args[]) {
        Rectangle r1 = new Rectangle();
        Rectangle r2 = new Rectangle(0, 8, 100, 150);
        
        r1.draw();
        r2.draw();
    }
}

Java에서 사각형은 화면의 왼편 꼭대기에 기준점 좌표(x, y)를 명세하고 가로(width)와 세로(height)를 명세하는 것으로 정의된다.

다음 그림은 Java에서의 사각형 그림의 예를 보여준다.

100x150 Rectangle

위 프로그램을 실행시키기 위해 먼저 Java 컴파일러 javac를 실행시킨다.

명령문 javac Rectangle.javaRectangle.class라는 파일을 생성하게 되는데 이것은 소스코드를 바이트코드(bytecode)라는 명령어로 변환시킨 것이다.

이것은 다시 명령문 java Rectangle에 의해 실행된다.

여기서 java는 Java 바이트코드 번역기이고 Rectangle은 컴파일된 클래스 이름이다.

실행의 첫 번째 단계를 Rectangle.main() 명령문으로 main() 메소드를 실행시키는 것으로 시작된다.

실행이 완료되면 다음과 같이 출력된다.

Rectangle : 0, 0, 0, 0
Rectangle : 0, 8, 100, 150

이 프로그램에서 알 수 있듯이 클래스 정의는 클래스 선언과 데이터 필드, 그리고 메소드들로 구성된다.

객체의 데이터 필드에 대한 접근은 클래스 설계자의 의도에 따라 다른 객체들에 허용할 수 있다.

이 접근은 접근 명세자 키워드로 정의하는데 데이터 필드나 메소드 앞에 위치한다.

예를 들어 2-5행의 데이터 필드들은 private로 선언되어 클래스 내부의 메소드들만이 데이터를 접근할 수 있도록 하여 데이터 필드를 보호할 수 있다.

프로그램의 7행과 10행의 Rectangle()은 클래스 이름과 같다는 것을 볼 수 있다.

이것은 생성자(constructor)라는 특별한 메소드로서 생성된 객체를 다른 코드들이 사용하기 전에 먼저 초기화를 수행한다.

이것은 데이터 필드가 적절히 초기화되기 전에 사용되지 않도록 보장하는 역할을 한다.

이 생성자는 위 프로그램과 같이 필요에 따라 한 클래스에 여러 개가 있을 수 있다.

메소드나 생성자의 매개 변수는 지역 변수와 같은 역할을 한다.

이들은 메소드나 생성자라 호출될 때 사용된 인자값으로 초기화 된다.

10행의 Rectangle() 생성자의 매개변수들은 Rectangle 클래스의 데이터 필드와 같은 이름을 가지고 있는데, 이렇게 하면 지역 변수가 같은 이름의 Rectangle 데이터 필드들을 접근할 수 없게 은닉시키는 경과가 된다.

그러나 이 은닉된 클래스 데이터 필드들을 접근할 필요가 있을때는 매개변수와 구별하기 위해 앞에 this를 붙여 표현한다.

this는 모든 객체의 메소드 내에서 객체 자신을 가리키기 위해 묵시적으로 정의되어 있는 참조 변수이다.

이 변수는 그 객체를 다른 메소드에 전달할 때나 이 프로그램에서와 같이 그 객체에 있는 데이터 필드들을 명시적으로 참조하기 위해 사용된다.

또한 클래스의 한 생성자에서 다른 생성자를 호출해야할 때 this 를 사용한다.

예를 들어 가로, 세로를 100으로 초기화 하는 Rectangle 클래스에 대한 또다른 생성자를 만든다고 가정할때 기존의 생성자 코드를 복사해서 사용할 수 있지만 다음과 같이 호출을 하여 생성자 코드를 재사용할 수 있다.

public Rectangle(int x, int y) {
    //  width와 height를 100으로 초기화 하는 생성자
    this(x, y, 100, 100);
    // 다른 생성자 Rectangle(int x, int y, int width, int height)를 호출
}

이 생성자 예에서 유의해야할 것은 같은 클래스내에 같은 이름의 여러 메소드가 있을때, 기본적으로 메소드 이름이 같더라도 매개변수의 시그니처(signature)가 다르기만 하면 시스템은 이들을 식별할 수 있게 된다.

이것이 바로 메소드 오버로딩(method overloading)의 특징이다.

이 기능은 메소드들이 개념적으로 모두 같은 작업을 수행하지만 이들이 필요로 하는 데이터가 상이한 경우 유용하다.

위의 프로그램은 매개변수가 없고 사각형의 특성을 String으로 스크린에 프린트하는 Rectangle에 대한 draw() 메소드를 정의하고 있다.

이때, 이 사각형을 Graphics 클래스를 이용하여 스크린에 그림을 그리길 원한다면 다음 draw() 메소드를 추가시킬 수 있다.

public void draw(Graphics g) {
    g.drawRect(x, y, width, height);
    // 사각형을 그래픽스로 표현
}

이렇게 되면 다음과 같이 두 버전의 draw() 메소드를 원하는대로 선택해서 실행시킬 수 있다. 여기서 rect는 Rectangle 클래스의 객체를 값으로 갖는 참조 변수이다.

rect.draw();    // 사각형을 스트링으로 스크린에 프린트
Graphics g;
rect.draw(g);   // 사각형을 그래픽스로 표현

예를 들어 여러 도형을 그리는 그래픽스 프로그램에서 원(circle)을 표현하는 클래스를 추가시키려 한다고 할때, 객체지향의 중요한 개념중인 하나인 상속(inheritance)을 이용할 수 있다.

먼저 그래픽스 프로그램이 필요로 하는 핵심적이고 공통적인 데이터 필드와 도형을 그리는 draw() 메소드로 구성된 Shape 라는 클래스를 만든다.

draw() 메소드는 각 도형에 따라 상이하게 구현되기 때문에 abstract 메소드로 선언만 하면 되고 자연스럽게 Shape 클래스는 추상 클래스가 된다.

Rectangle 과 Circle 클래스를 Shape 클래스의 서브 클래스로 선언 한다면, 그래픽스 프로그램은 일련의 Shape 객체를 갖게 될 것이고 스크린에 각 도형을 그리는 draw() 메소드를 사용할 수 있게 된다.

그래픽스 프로그램은 각 도형이 내부적으로 어떻게 표현되는지, 그리는지를 알 필요가 없으며 이는 각 객체가 처리할 문제가 된다.

아래 프로그램은 상속을 이용한 그래픽스 프로그램을 보여주고 있다.

메소드 오버라이딩(method overriding)을 이용해 슈퍼 클래스에 있는 코드를 재사용할 수 있으면서 클래스의 기능을 확장하였다.

import java.awt.*;

abstract class Shape {
    protected Color color;
    protected int x;
    protected int y;

    protected Shape(Color c, int x, int y) {
        color = c;
        this.x = x;
        this.y = y;
    }

    public abstract void draw();
}

class Rectangle extends Shape {
    private int width;
    private int height;
    public Rectangle(Color c, int x, int y, int width, int height) {
        super(c, x, y);
        this.width = width;
        this.height = height;
    }

    public void draw() {
        System.out.println("Rectangle : " + x + ", " + y
            + ", " + width + ", " + height);
    } 
}

class Circle extends Shape {
    private int radius;
    public Circle(Color c, int x, int y, int radius) {
        super(c, x, y);
        this.radius = radius;
    }

    public void draw() {
        System.out.println("Circle : " + x + ", " + y
            + ", " + radius);
    }
}

public class GraphicsProgram {
    public static void main(String args[]) {
        Shape s1 = new Rectangle(Color.red, 0, 5, 200, 300);
        Shape s2 = new Circle(Color.green, 20, 30, 100);

        s1.draw();
        s2.draw();
    }
}

위 프로그램의 참조변수 s1, s2는 모두 Shape 클래스의 참조 변수로 선언되어 있지만 각가 Shape의 서로 다른 서브 클래스 객체로 초기화 된다.

s1의 경우에는 Rectangle 서브 클래스의 객체를 참조하고 있고, s2는 Circle 서브 클래스의 객체를 참조하고 있다.

따라서 s1.draw()가 실행될 때 Rectangle의 draw() 메소드가 호출되고, s2.draw()가 실행될 때는 Circle의 draw() 메소드가 호출된다.

따라서 이 프로그램이 실행되면 다음과 같은 결과가 출력된다.

Rectangle : 0, 5, 200, 300
Circle : 20, 30, 100

슈퍼 클래스로부터 상속받은 데이터 필드에 새로운 필드를 추가해서 확장시킨 서브 클래스에 대한 생성자는 추가된 자기 자신의 데이터 필드를 초기화해야 될 뿐 아니라, 상속받은 슈퍼 클래스의 데이터 필드도 초기화해야 한다.

따라서 서브 클래스의 생성자가 포함해야 되는 첫 번째 명령문은 슈퍼 클래스의 생성자를 실행시켜 상속받은 데이터 필드를 초기화 하는 것이다.

이때 이 슈퍼 클래스 생성자 호출 형식은 슈퍼 클래스 생성자 이름 대신 super라는 키워드를 사용한다.

위 프로그램에서 Rectangle과 Circle 클래스의 생성자들은 모두 super를 사용하여 슈퍼클래스 Shape의 생성자를 호출하는 것을 볼 수 있다.

super 키워드는 서브 클래스에서 슈퍼 클래스의 데이터 필드를 접근할 때나 메소드를 호출할 때도 사용한다.

이 때 super는 현 클래스가 상속받은 슈퍼 클래스의 한 인스턴스를 나타내어, 메소드 기동 형식 super.<메소드>(인자); 는 항상 슈퍼 클래스에 있는 메소드를 호출한다.


참고 문서

  • 이석호. (2004). 자료 구조와 JAVA. 정익사.





© 2019. by RaP0d

Powered by aiden