객체 지향 프로그래밍(OOP)의 중요한 세 가지 개념인 다형성(Polymorphism), 추상 클래스(Abstract Class), 인터페이스(Interface)는 코드의 유연성, 확장성, 재사용성을 높이는 데 필수적입니다. 이 글에서는 각 개념의 원리와 활용법을 상세히 설명합니다.
1. 다형성(Polymorphism)
다형성은 '여러 가지 형태를 가질 수 있는 성질'을 의미합니다. 자바에서는 하나의 객체가 여러 타입의 형태를 가질 수 있는 능력을 말합니다. 이는 부모 클래스 타입의 참조 변수가 자식 클래스의 인스턴스를 가리키거나, 인터페이스 타입의 참조 변수가 해당 인터페이스를 구현한 클래스의 인스턴스를 가리킬 때 나타납니다.
다형성의 특징
- 멤버 메서드 호출 시: 컴파일 시에는 참조 변수(왼쪽)의 타입을 기준으로 유효성을 검사하고, 런타임 시에는 실제 인스턴스(오른쪽)의 재정의된 메서드가 호출됩니다. (메서드는 런타임에 실제 객체 타입에 따라 동작)
- 멤버 변수 접근 시: 컴파일 시와 런타임 시 모두 참조 변수(왼쪽)의 타입을 기준으로 접근합니다. (변수는 항상 참조 변수의 타입에 따라 접근)
예시: 차량 다형성
class Vehicle {
String model = "기본 모델";
int maxSpeed = 120;
public void start() {
System.out.println("차량이 시동을 겁니다.");
}
}
class Car extends Vehicle {
String model = "스포츠 세단";
int maxSpeed = 200;
@Override
public void start() {
System.out.println("자동차가 엔진을 가동합니다.");
}
public void drive() {
System.out.println("자동차가 도로를 주행합니다.");
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
Vehicle myVehicle = new Car(); // 다형성: 부모 타입 참조가 자식 인스턴스를 가리킴
myVehicle.start(); // Car 클래스의 start() 메서드 호출 ("자동차가 엔진을 가동합니다.")
System.out.println("모델: " + myVehicle.model); // Vehicle 클래스의 model ("기본 모델")
System.out.println("최고 속도: " + myVehicle.maxSpeed); // Vehicle 클래스의 maxSpeed (120)
// myVehicle.drive(); // 컴파일 오류: Vehicle 타입에는 drive() 메서드가 없음
}
}
위 예시에서 `myVehicle.start()`는 `Car` 클래스의 재정의된 `start()` 메서드를 호출하지만, `myVehicle.model`과 `myVehicle.maxSpeed`는 `Vehicle` 클래스의 필드에 접근합니다. 이는 메서드는 런타임 객체에 따라 동적으로 바인딩되지만, 필드는 참조 변수 타입에 따라 정적으로 바인딩되기 때문입니다.
다형성의 전제 조건
- 상속 관계(클래스) 또는 구현 관계(인터페이스)가 있어야 합니다.
- 메서드 오버라이딩(재정의)이 있어야 합니다.
- 부모 타입의 참조 변수가 자식 타입의 객체를 가리켜야 합니다.
예시: 인터페이스를 활용한 다형성
interface Actionable {
void execute(); // 모든 Actionable 객체는 execute() 메서드를 가져야 함
}
class Robot implements Actionable {
@Override
public void execute() {
System.out.println("로봇이 작업을 수행합니다.");
}
public void chargeBattery() {
System.out.println("로봇이 배터리를 충전합니다.");
}
}
public class InterfacePolymorphism {
public static void main(String[] args) {
Actionable agent = new Robot();
agent.execute(); // Robot의 execute() 호출
// agent.chargeBattery(); // 컴파일 오류: Actionable 인터페이스에는 chargeBattery() 메서드가 없음
// 다형성의 단점: 자식 객체의 고유한 기능을 직접 호출할 수 없음
}
}
다형성의 장점과 단점
- 장점:
- 코드의 유연성과 확장성이 향상됩니다. 새로운 자식 클래스가 추가되어도 부모 타입으로 참조하면 기존 코드를 수정할 필요가 없습니다.
- 코드의 재사용성이 높아집니다. 공통된 기능을 부모 타입으로 처리하여 코드를 간결하게 유지할 수 있습니다.
- 단점:
- 자식 클래스에만 존재하는 고유한 기능을 부모 타입 참조 변수로 직접 호출할 수 없습니다. 이를 위해서는 명시적인 타입 캐스팅(downcasting)이 필요합니다.
타입 캐스팅 (Upcasting & Downcasting)
- 업캐스팅 (Upcasting): 자식 객체를 부모 타입으로 참조하는 것은 자동적으로 이루어집니다. 다형성의 기본 형태입니다.
Vehicle vehicle = new Car(); // Car 객체가 Vehicle 타입으로 업캐스팅 - 다운캐스팅 (Downcasting): 부모 타입으로 참조되던 객체를 다시 자식 타입으로 변환하는 것입니다. 명시적 캐스팅이 필요하며, 런타임에 ClassCastException이 발생할 수 있으므로 주의해야 합니다.
Vehicle vehicle = new Car(); Car car = (Car) vehicle; // Vehicle을 Car로 다운캐스팅 car.drive(); // 이제 Car 고유의 drive() 메서드 호출 가능 Vehicle genericVehicle = new Vehicle(); // Car anotherCar = (Car) genericVehicle; // 런타임 오류: ClassCastException 발생! // genericVehicle은 실제 Car 인스턴스가 아님
2. 추상 클래스(Abstract Class)
추상 클래스는 미완성된 메서드(추상 메서드)를 포함할 수 있는 클래스입니다. 특정 개념의 공통된 특징과 동작을 정의하지만, 일부 동작은 구체적인 구현을 자식 클래스에게 맡기고 싶을 때 사용됩니다.
추상 클래스의 특징
abstract키워드를 사용하여 선언합니다.- 추상 메서드(메서드 바디가 없는 메서드)를 가질 수 있으며, 추상 메서드가 하나라도 있으면 반드시 추상 클래스로 선언해야 합니다.
- 추상 클래스는 인스턴스화할 수 없습니다 (
new키워드로 객체 생성 불가). - 자식 클래스가 추상 클래스를 상속받을 경우, 부모의 모든 추상 메서드를 반드시 오버라이딩하여 구현해야 합니다. 그렇지 않으면 자식 클래스 또한 추상 클래스로 선언해야 합니다.
- 일반 멤버 변수, 일반 메서드, 생성자를 가질 수 있습니다.
예시: 작업자 추상 클래스
abstract class Worker {
private String name;
private int id;
public Worker(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() { return name; }
public int getId() { return id; }
public abstract void performWork(); // 추상 메서드: 하위 클래스에서 반드시 구현
public void takeBreak() { // 일반 메서드
System.out.println(name + "이(가) 잠시 휴식을 취합니다.");
}
}
class Engineer extends Worker {
public Engineer(String name, int id) {
super(name, id);
}
@Override
public void performWork() {
System.out.println(getName() + "이(가) 코드를 작성하고 시스템을 설계합니다.");
}
}
class Designer extends Worker {
public Designer(String name, int id) {
super(name, id);
}
@Override
public void performWork() {
System.out.println(getName() + "이(가) 사용자 인터페이스를 디자인합니다.");
}
}
public class AbstractClassDemo {
public static void main(String[] args) {
// Worker worker = new Worker("미상", 0); // 컴파일 오류: 추상 클래스는 인스턴스화 불가
Worker eng = new Engineer("김철수", 101);
Worker des = new Designer("이영희", 102);
eng.performWork();
eng.takeBreak();
des.performWork();
des.takeBreak();
}
}
추상 클래스의 제약 사항
abstract키워드는private,final,static과 함께 사용할 수 없습니다.private abstract: 추상 메서드는 자식에서 구현되어야 하므로 private일 수 없습니다.final abstract: 추상 클래스는 상속되어야 하므로 final일 수 없습니다.static abstract: 추상 메서드는 인스턴스에 종속적인 동작을 정의하므로 static일 수 없습니다. (JDK 8부터 인터페이스에 static, default 메서드가 도입되었지만, 추상 클래스의 추상 메서드는 여전히 동일)
3. 인터페이스(Interface)
인터페이스는 추상 클래스보다 더 추상화된 개념으로, 클래스가 구현해야 할 '규약' 또는 '청사진'을 정의합니다. 모든 메서드가 기본적으로 추상 메서드(JDK 8부터 default, static 메서드 가능)이며, 모든 필드는 상수입니다.
인터페이스의 특징
interface키워드를 사용하여 선언합니다.- 클래스는
implements키워드를 사용하여 인터페이스를 구현합니다. - 인터페이스는 인스턴스화할 수 없습니다.
- 인터페이스의 모든 필드는
public static final특성을 가지는 상수로 간주됩니다 (생략 가능). - 인터페이스의 모든 메서드는
public abstract특성을 가지는 추상 메서드로 간주됩니다 (생략 가능).- JDK 8부터
default메서드를 통해 인터페이스에 구현된 메서드를 추가할 수 있으며,static메서드도 가질 수 있습니다.
- JDK 8부터
- 인터페이스는 생성자를 가질 수 없습니다.
예시: 멀티미디어 장치 인터페이스
interface Playable {
String PLAY_MODE_DEFAULT = "Normal"; // public static final
void play(); // public abstract
default void stop() { // default 메서드 (JDK 8부터)
System.out.println("재생을 중지합니다.");
}
}
interface Streamable {
void streamContent(); // public abstract
}
class MultimediaPlayer implements Playable, Streamable {
@Override
public void play() {
System.out.println("멀티미디어 콘텐츠를 재생합니다.");
}
@Override
public void streamContent() {
System.out.println("온라인 콘텐츠를 스트리밍합니다.");
}
// Playable 인터페이스의 default 메서드 stop()을 재정의하지 않아도 됨
}
class MusicPlayer implements Playable {
@Override
public void play() {
System.out.println("음악을 재생합니다.");
}
}
public class InterfaceDemo {
public static void main(String[] args) {
System.out.println("기본 재생 모드: " + Playable.PLAY_MODE_DEFAULT);
MultimediaPlayer multiPlayer = new MultimediaPlayer();
multiPlayer.play();
multiPlayer.streamContent();
multiPlayer.stop();
MusicPlayer musicPlayer = new MusicPlayer();
musicPlayer.play();
musicPlayer.stop(); // default 메서드 호출
}
}
인터페이스와 클래스의 관계
- 클래스 간 관계: 상속(
extends) 관계, 단일 상속만 가능 (다중 상속 불가). - 클래스-인터페이스 관계: 구현(
implements) 관계, 다중 구현 가능. 하나의 클래스가 여러 인터페이스를 구현할 수 있습니다 (예:class MyClass implements InterfaceA, InterfaceB {}). - 인터페이스 간 관계: 상속(
extends) 관계, 다중 상속 가능. 인터페이스는 여러 인터페이스를 상속받아 새로운 인터페이스를 정의할 수 있습니다.
인터페이스의 설계 철학
- 규약 정의: 클래스가 특정 기능을 수행하려면 반드시 구현해야 하는 '규약'을 제공합니다. (예: USB 인터페이스처럼 표준화된 연결 규약)
- 확장성: 기존 클래스를 수정하지 않고 새로운 기능을 추가하는 데 용이합니다.
- 느슨한 결합: 구현 세부 사항이 아닌 기능 자체에 의존하게 하여 코드 간의 결합도를 낮춥니다.
- 다중 기능 부여: 클래스가 여러 인터페이스를 구현함으로써 다양한 역할을 수행할 수 있도록 합니다 (자바의 단일 상속 단점 보완).
추상 클래스와 인터페이스의 비교
둘 다 객체를 직접 생성할 수 없으며, 미완성된 부분을 포함하여 하위 클래스에서 구현하도록 강제한다는 공통점이 있습니다.
주요 차이점
- 멤버 구성:
- 추상 클래스: 일반 필드, 상수, 추상 메서드, 일반(구현된) 메서드, 생성자를 모두 가질 수 있습니다.
- 인터페이스: 모든 필드는
public static final상수이며, 모든 메서드는public abstract추상 메서드입니다 (JDK 8 이후 default, static 메서드 가능). 생성자를 가질 수 없습니다.
- 관계 및 상속:
- 추상 클래스:
extends키워드로 상속받으며, 단일 상속만 가능합니다. - 인터페이스:
implements키워드로 구현하며, 다중 구현이 가능합니다. 인터페이스는extends키워드로 다른 인터페이스를 다중 상속할 수 있습니다.
- 추상 클래스:
- 설계 의도:
- 추상 클래스: 'is-a' 관계를 나타냅니다. 동일한 종류의 클래스들이 공통적으로 가지는 특징과 동작을 정의하고, 일부 미완성된 부분을 자식에게 맡겨 계층 구조를 만듭니다. (예: '동물은 움직이고 먹는다' - 동물 추상 클래스)
- 인터페이스: 'has-a' 또는 'can-do' 관계를 나타냅니다. 클래스가 특정 '능력' 또는 '행동'을 수행할 수 있음을 보장하는 규약 역할을 합니다. 서로 다른 계층의 클래스들도 동일한 인터페이스를 구현하여 같은 동작을 수행할 수 있습니다. (예: '날 수 있는', '소리 낼 수 있는' - 인터페이스)
이러한 객체 지향 개념들을 이해하고 적절히 활용하면, 견고하고 유연하며 유지보수가 쉬운 소프트웨어를 설계할 수 있습니다.