hanker

Java - 상속과 다형성의 활용 본문

JAVA

Java - 상속과 다형성의 활용

hanker 2025. 2. 5. 00:00
반응형

자바에서 객체지향 프로그래밍(OOP) 개념에서 상속과 다형성은 매우 중요하다.

 

이번 글에서 어떤 개념인지, 어떻게 활용하는지에 대해서 알아보자.

 


1. 추상 클래스와 인터페이스

https://hanke-r.tistory.com/entry/JAVA-JAVA%EC%97%90%EC%84%9C-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

 

JAVA - JAVA에서 인터페이스와 추상 클래스의 차이점은 무엇일까?

Java에서 인터페이스와 추상 클래스의 차이점에 대해 알아보자. 두 개념 모두 객체 지향 프로그래밍에서 중요한 역할을 하며, 공통된 특성은 있지만, 서로 다른 사용 목적과 특징을 가지고 있다.

hanke-r.tistory.com

 

1-1. 추상 클래스(Abstract Class)

- abstract 키워드를 사용해 직접 객체를 생성할 수 없는 클래스

- 일부 메서드는 구현할 수 있고, 일부는 abstract 키워드로 선언해 하위 클래스에서 구현을 강제할 수 있음

 

실무 활용 방법

- 기본적인 동작을 포함하지만, 일부 기능을 하위 클래스에서 변경해야 할 경우

- 공통 코드가 많고, 일부만 변경되는 경우 적합

abstract class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(name + " is eating.");
    }

    abstract void makeSound(); // 하위 클래스에서 반드시 구현해야 함
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " barks!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog("Buddy");
        myDog.eat(); // 공통 기능 사용 가능
        myDog.makeSound(); // 오버라이드된 메서드 실행
    }
}

 

 

1-2. 인터페이스 (Interface)

- 모든 메서드가 기본적으로 abstract (jdk8 이후 default와 static 메서드 사용 가능)

- 다중 구현이 가능하여 여러 기능을 조합할 때 유리하다.

 

실무 활용 방법

- 특정 기능을 강제할 때 사용 (Java의 Comparable 인터페이스)

- 다중 상속이 필요한 경우 인터페이스로 해결

// Flyable 인터페이스 선언: 'fly' 메서드를 구현해야 함을 명시
interface Flyable {
    void fly();
}

// Swimmable 인터페이스 선언: 'swim' 메서드를 구현해야 함을 명시
interface Swimmable {
    void swim();
}

// Bird 클래스 선언: Flyable 인터페이스를 구현하여 'fly' 기능을 제공
class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Bird is flying.");
    }
}

// Duck 클래스 선언: Flyable과 Swimmable 인터페이스를 모두 구현하여
// 오리의 'fly'와 'swim' 기능을 제공
class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("Duck can fly.");
    }

    @Override
    public void swim() {
        System.out.println("Duck can swim.");
    }
}

// 메인 클래스: 프로그램 실행 진입점
public class Main {
    public static void main(String[] args) {
        // Duck 객체 생성
        Duck duck = new Duck();
        // Duck 객체의 'fly' 메서드 호출
        duck.fly();
        // Duck 객체의 'swim' 메서드 호출
        duck.swim();
    }
}

 

비교 항목추상 클래스인터페이스

비교 항목 추상 클래스 인터페이스
다중 상속 지원 단일 상속만 가능 여러 인터페이스 구현 가능
접근 제한자 가능 (private, protected 등) 불가능 (모든 메서드는 public이어야 함)
기본 메서드 제공 가능 (일반 메서드 포함 가능) JDK 8부터 default 메서드 사용 가능
사용 목적 공통 기능 제공 + 일부 구현 강제 기능 규약 제공 (다형성 극대화)

 


2. 인터페이스의 기본 메서드와 정적 메서드

 

JDK 8 이후, 인터페이스에 default 메서드static 메서드를 추가할 수 있게 되었다.

 

2-1. default 메서드

- 인터페이스를 구현하는 모든 클래스에서 공통적으로 사용할 수 있는 기본 메서드를 정의할 수 있음

- 기존 구현체의 변경 없이 새로운 기능 추가 가능

// Vehicle 인터페이스 정의
interface Vehicle {
    // 추상 메서드 move 선언
    void move();

    // 디폴트 메서드 stop 정의
    default void stop() {
        // 디폴트 메서드의 기본 구현: "Stopping the vehicle..." 출력
        System.out.println("Stopping the vehicle...");
    }
}

// Vehicle 인터페이스를 구현하는 Car 클래스 정의
class Car implements Vehicle {
    // move 메서드 구현
    @Override
    public void move() {
        // "Car is moving." 출력
        System.out.println("Car is moving.");
    }
}

// 메인 클래스 정의
public class Main {
    // 메인 메서드: 프로그램의 시작 지점
    public static void main(String[] args) {
        // Car 클래스의 인스턴스 생성
        Car car = new Car();
        // car 객체의 move 메서드 호출
        car.move();
        // car 객체의 stop 메서드 호출 (디폴트 메서드 실행)
        car.stop();
    }
}

 

 

2-2. static 메서드

- 인터페이스에서 직접 호출할 수 있음

- 공통적으로 사용될 유틸리티 성격이 메서드를 제공할 때 사용

// MathUtils 인터페이스 정의
interface MathUtils {
    // 두 정수의 합을 반환하는 정적 메서드 add 정의
    static int add(int a, int b) {
        return a + b;
    }
}

// 메인 클래스 정의
public class Main {
    public static void main(String[] args) {
        // MathUtils 인터페이스의 정적 메서드 add를 호출하여 결과를 result에 저장
        int result = MathUtils.add(10, 20);
        // result 값을 출력
        System.out.println("Result: " + result);
    }
}

 


3. 업캐스팅(Upcasting)과 다운캐스팅(Downcasting)

 

업캐스팅과 다운캐스팅은 객체 지향 프로그래밍에서 클래스 간의 형 변환을 의미하며, 주로 상속 관계에 있는 클래스들 사이에서 발생한다.

 

3-1. 업캐스팅 (Upcasting)

- 하위 클래스 객체를 부모 타입(상위 타입)으로 변환

- 다형성을 활용할 때 필수적으로 사용된다.

class Parent {
    void show() {
        System.out.println("Parent class method");
    }
}

class Child extends Parent {
    @Override
    void show() {
        System.out.println("Child class method");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child(); // 업캐스팅
        obj.show(); // Child의 오버라이드된 메서드 실행
    }
}

 

3-2. 다운캐스팅(Downcasting)

- 부모 타입을 다시 자식 타입으로 변환

instanceof 연산자로 타입 체크 후 캐스팅 하는 것이 안전하다.

* instanceof 연산자 : 객체가 특정 클래스, 상속받은 클래스를 확인하기 위해 사용

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child(); // 업캐스팅
        if (obj instanceof Child) {
            Child childObj = (Child) obj; // 다운캐스팅
            childObj.show();
        }
    }
}

- 다운캐스팅은 잘못 사용하면 ClassCastException 이 발생하니 반드시 instanceof 연산자를 활용해 확인 후 캐스팅해야한다. 

 


4. 다형성의 활용

 

4-1. 전략 패턴 (Strategy pattern)

- 다형성을 활용한 대표적인 디자인 패턴

- 런타임에서 동적으로 객체의 행동을 변경 가능

// 결재 방식을 정의하는 인터페이스
interface PaymentStrategy {
	// 구현하는 클래스에서 실제 결제 로직을 정의한다.
    void pay(int amount);
}

// 전략 1: 신용카드 결제
class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        // 신용카드로 결제하는 방식의 출력문 작성
        System.out.println("Paid " + amount + " using Credit Card.");
    }
}

// 전략 2: 현금 결제
class CashPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        // 현금으로 결제하는 방식의 출력문 작성
        System.out.println("Paid " + amount + " using CashPayment.");
    }
}

// 위 전략 2개를 실행하는 컨텍스트
class PaymentContext {
    // 결제 전략을 저장할 변수
    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
        // 실행할 전략을 설정
        this.strategy = strategy;
    }

    public void executePayment(int amount) {
        // 현재 설정된 전략의 pay 메서드 실행
        strategy.pay(amount);
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentContext context = new PaymentContext(new CreditCardPayment());
        context.executePayment(1000);

        context = new PaymentContext(new CashPayment());
        context.executePayment(2000);
    }
}

위 방식은 새로운 결제 방법을 추가해도 기존 코드의 수정 없이 확장이 가능하다 (OCP 원칙 준수)

 


정리

 

- 추상 클래스와 인터페이스를 적절히 활용하면 코드의 유지보수성이 높아진다.

- 업캐스팅과 다운캐스팅을 활용해 유연한 설계를 할 수 있다. (나중에 이 내용으로 보다 더 심도있게 다룰 예정)

- 전략 패턴 같은 디자인 패턴을 적용하면 실무에서도 효과적으로 활용 가능하다.

반응형