[JAVA] 추상클래스와 인터페이스

JHJeong·2024년 4월 10일
0

추상 클래스와 인터페이스는 객체 지향 프로그래밍에서 중요한 역할을 하며, 둘 다 추상화를 제공한다. 그러나 사용하는 시나리오와 제공하는 기능에 차이가 좀 있는데, 이를 선택하는 기준을 이해하기 위해서 추상클래스와 인터페이스가 뭔지 소개한다.

추상클래스

  • 추상 클래스는 하나 이상의 추상 메서드(구현이 없는 메서드)를 포함할 수 있는 클래스이다.
  • 상속받는 하위 클래스는 추상 클래스의 추상 메서드를 "모두" 구현해야한다.
  • 상태(필드)와 구현된 메서드(비추상 메서드)를 포함할 수 있다.

인터페이스

  • 메서드 선언만 포함하고, 모든 메서드는 기본적으로 public이며, 추상적이다.(JAVA 8이후로는 default 메서드와 static 메서드를 포함할 수 있음)
  • 상태(필드)를 포함할 수 없거나, 있더라고 public static final 변수(상수)로 제한된다.
  • 클래스는 여러 인터페이스를 구현할 수 있으며, 인터페이스는 다중 상속을 지원한다.

선택 기준

1. 공통 API를 정의할 때

  • 인터페이스 사용 : 다양한 구현이 필요할 때 인터페이스를 사용한다. 인터페이스는 다중 구현을 지원하므로, 여러 클래스가 동일한 인터페이스를 구현할 수 있다. 예를 들어서, 여러 종류의 지불 방식을 지원하는 결제 처리 시스템에서 각 지불 방식(신용카드, 현금, 네이버페이, 카카오페이, 토스.. 등등)를 공통 인터페이스로 구현할 수 있다.
interface PaymentProcessor {
    void processPayment(double amount);
}

class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("신용카드로 " + amount + "원 결제 처리됨.");
    }
}

class NaverPayProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("네이버페이로 " + amount + "원 결제 처리됨.");
    }
}

public class PaymentTest {
    public static void main(String[] args) {
        PaymentProcessor creditCard = new CreditCardProcessor();
        PaymentProcessor naverPay = new NaverPayProcessor();
        
        creditCard.processPayment(10000);
        naverPay.processPayment(20000);
    }
}

출력 결과
신용카드로 10000.0원 결제 처리됨.
네이버페이로 20000.0원 결제 처리됨.

2. 코드 재사용이 중요할 때

  • 추상 클래스 사용 : 추상 클래스는 공통된 코드(상태 관리나 메서드 구현)를 재사용하고 싶을 때 유용하다. 예를 들어, 여러 동물의 공통 특성을 모델링하는 경우, "Animal" 추상 클래스에서 공통적인 필드(나이, 무게, 키)와 메서드(move, eat 등)을 정의하고, 구체적인 동물 클래스인 Dog 클래스 혹은 Fish 클래스에서 이를 상속받아서 사용하게 된다.
abstract class Animal {
    int age;
    double height;
    double weight;

    public Animal(int age, double height, double weight) {
        this.age = age;
        this.height = height;
        this.weight = weight;
    }

    abstract void eat();
    abstract void move();

    void sleep() {
        System.out.println("동물이 잠을 잡니다.");
    }
    
    void displayStats() {
        System.out.println("나이: " + age + ", 키: " + height + "cm, 몸무게: " + weight + "kg");
    }
}

class Dog extends Animal {
    public Dog(int age, double height, double weight) {
        super(age, height, weight);
    }

    @Override
    void eat() {
        System.out.println("개가 먹습니다.");
    }
    
    @Override
    void move() {
        System.out.println("개가 달립니다.");
    }
}

class Fish extends Animal {
    public Fish(int age, double height, double weight) {
        super(age, height, weight);
    }

    @Override
    void eat() {
        System.out.println("물고기가 먹습니다.");
    }
    
    @Override
    void move() {
        System.out.println("물고기가 헤엄칩니다.");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Animal dog = new Dog(5, 30.0, 20.0);
        Animal fish = new Fish(1, 5.0, 0.2);
        
        System.out.println("Dog:");
        dog.displayStats();
        dog.eat();
        dog.move();
        dog.sleep();
        
        System.out.println("\nFish:");
        fish.displayStats();
        fish.eat();
        fish.move();
        fish.sleep();
    }
}

출력 결과
Dog:
나이: 5, 키: 30.0cm, 몸무게: 20.0kg
개가 먹습니다.
개가 달립니다.
동물이 잠을 잡니다.

Fish:
나이: 1, 키: 5.0cm, 몸무게: 0.2kg
물고기가 먹습니다.
물고기가 헤엄칩니다.
동물이 잠을 잡니다.

3. 확장성이 중요할 때

  • 인터페이스 사용 : 시스템을 확장해야 할 가능성이 있거나, 여러 구현이 필요한 경우 인터페이스를 사용한다. 인터페이스는 클래스가 여러 인터페이스를 구현할 수 있기 때문에, 추상 클래스보다 유연하다. 아래 소스는 여러 데이터 소스로부터 데이터를 읽어오는 애플리케이션인 경우, DataSource 인터페이스를 정의하여 다양한 데이터의 소스의 구현을 가능하게 하는 소스이다.
interface DataSource {
    String getData();
}

class DatabaseSource implements DataSource {
    @Override
    public String getData() {
        return "데이터베이스로부터 데이터를 가져옵니다.";
    }
}

class APISource implements DataSource {
    @Override
    public String getData() {
        return "API로부터 데이터를 가져옵니다.";
    }
}

public class DataSourceTest {
    public static void main(String[] args) {
        DataSource database = new DatabaseSource();
        DataSource api = new APISource();
        
        System.out.println(database.getData());
        System.out.println(api.getData());
    }
}

출력결과
데이터베이스로부터 데이터를 가져옵니다.
API로부터 데이터를 가져옵니다.

4. 다중 상속의 필요성

  • 인터페이스 사용 : 자바 언어에서는 클래스가 다중 상속을 지원하지 않는다. 클래스가 다양한 타입으로 작동해야 하고 다중 상속이 필요한 경우, 인터페이스를 사용한다. 아래 소스는 로그 기록과 데이터 검증 기능을 모두 제공해야하는 서비스를 개발해야할 때, "Logger" 클래스와 "Validator"라는 두 인터페이스를 정의하고, 하나의 클래스에서 두 인터페이스를 모두 구현할 수 있는 걸 보여주는 예제 소스이다.
interface Logger {
    void log(String message);
}

interface Validator {
    boolean validate(String data);
}

class Service implements Logger, Validator {
    @Override
    public void log(String message) {
        System.out.println("로그: " + message);
    }
    
    @Override
    public boolean validate(String data) {
        return data != null && !data.isEmpty();
    }
}

public class ServiceTest {
    public static void main(String[] args) {
        Service service = new Service();
        service.log("서비스 시작");
        System.out.println("데이터 검증 결과: " + service.validate("테스트 데이터"));
    }
}

출력 결과
로그: 서비스 시작
데이터 검증 결과: true

결론

  • 인터페이스는 여러 구현체에 대한 공통된 계약을 정의할 때 사용하며, 특히 다중 구현이 필요할 때 유용하다.
  • 추상클래스는 코드 재사용을 목적으로 할 때 사용하며, 하위 클래스에 특정 메서드나 필드를 상속하고 싶을 때 적합하다.

실제로 이 둘 사이의 선택은 프로젝트의 요구사항과 개발 팀의 선호에 따라 달라질 수 있다.

profile
이것저것하고 싶은 개발자

0개의 댓글