자바 객체 지향 핵심: 다형성, 추상 클래스, 인터페이스의 이해

객체 지향 프로그래밍(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 메서드도 가질 수 있습니다.
  • 인터페이스는 생성자를 가질 수 없습니다.

예시: 멀티미디어 장치 인터페이스


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' 관계를 나타냅니다. 클래스가 특정 '능력' 또는 '행동'을 수행할 수 있음을 보장하는 규약 역할을 합니다. 서로 다른 계층의 클래스들도 동일한 인터페이스를 구현하여 같은 동작을 수행할 수 있습니다. (예: '날 수 있는', '소리 낼 수 있는' - 인터페이스)

이러한 객체 지향 개념들을 이해하고 적절히 활용하면, 견고하고 유연하며 유지보수가 쉬운 소프트웨어를 설계할 수 있습니다.

태그: java Polymorphism AbstractClass Interface OOP

7월 25일 00:02에 게시됨