6. Java 객체지향 프로그래밍
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에서의 사각형 그림의 예를 보여준다.
위 프로그램을 실행시키기 위해 먼저 Java 컴파일러 javac를 실행시킨다.
명령문 javac Rectangle.java
는 Rectangle.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. 정익사.