인터페이스와 추상 클래스 비교

실무 관점에서 본 인터페이스 vs. 추상 클래스 비교

인터페이스와 추상 클래스는 객체지향 설계에서 핵심적인 개념이며, 실무에서는 상황에 따라 적절히 선택해야 합니다. 아래에서는 언제 인터페이스를 사용하고, 언제 추상 클래스를 사용하는지, 그리고 실제로 더 자주 사용하는 개념과 그 이유를 정리해 보겠습니다.


🚀 인터페이스 vs. 추상 클래스 실무 비교

비교 요소 인터페이스 (Interface) 추상 클래스 (Abstract Class)

상속 구조 다중 구현 가능 (클래스는 여러 개의 인터페이스를 구현 가능) 단일 상속만 가능 (다른 클래스를 동시에 상속받을 수 없음)
상태(State) 유지 불가능 (멤버 변수 선언 불가, Java 8 이후 default 메서드로 일부 구현 가능) 가능 (멤버 변수 선언 및 상태 유지 가능)
기본 구현 기본적으로 없음, Java 8 이후 default 메서드 사용 가능 일부 메서드를 구현 가능 (공통된 기능 제공)
사용 목적 특정 기능을 강제하는 용도로 사용 (Comparable, Runnable) 공통된 기능을 제공하면서 일부 메서드를 구현 강제 (Animal → Dog, Cat)
유연성 높은 확장성 (다양한 클래스에서 같은 동작을 구현 가능) 공통 기능을 포함한 특정 계열의 클래스 설계에 적합
성능 인터페이스는 기본적으로 가상 호출이 많아 성능 오버헤드가 발생할 수 있음 직접적인 상속 구조로 인해 성능이 조금 더 유리

✅ 실무에서의 선택 기준

상황 인터페이스 선택 추상 클래스 선택

     
상황  인터페이스 선택  추상 클래스 선택
여러 클래스가 공통된 동작(기능)을 가져야 하나, 계층 구조가 아닐 때
여러 클래스가 공통된 속성(필드)행동(메서드)을 공유해야 할 때
특정 동작을 강제하고, 다양한 구현이 필요할 때 (예: Comparable, Serializable)
동일한 동작을 하는 다양한 클래스가 있고, 기본 구현을 제공해야 할 때
다중 상속이 필요한 경우 (여러 개의 기능을 조합)
프레임워크나 라이브러리에서 기능을 확장할 때 (예: Spring의 BeanPostProcessor)
특정 클래스를 기반으로 몇 개의 서브클래스를 만들고 싶을 때

💡 실무에서 더 자주 사용하는 것은?

인터페이스가 실무에서 더 자주 사용됩니다.

🔹 이유 1: 다중 상속 지원

  • Java는 클래스의 다중 상속을 지원하지 않지만, 인터페이스는 다중 구현 가능
  • 예를 들어, Runnable, Serializable, Cloneable과 같은 동작을 동시에 가질 수 있음
  • Spring, JPA 등 주요 프레임워크에서도 인터페이스 기반 설계가 많음

🔹 이유 2: 유연한 설계

  • 인터페이스를 사용하면 서로 다른 클래스에서도 공통 기능을 강제할 수 있어 확장성이 뛰어남
  • 실무에서는 비즈니스 로직이 확장될 가능성이 높기 때문에 인터페이스 중심의 설계가 선호됨

🔹 이유 3: SOLID 원칙 준수

  • OCP(Open-Closed Principle, 개방-폐쇄 원칙): 인터페이스를 사용하면 새로운 기능을 추가할 때 기존 클래스를 변경하지 않고 확장 가능
  • DIP(Dependency Inversion Principle, 의존 역전 원칙): 고수준 모듈이 저수준 모듈에 직접 의존하지 않고, 인터페이스를 통해 의존성을 분리

 

🚀 인터페이스 vs. 추상 클래스 장단점 정리

📌 인터페이스의 장점

1. 유연성과 확장성 (OCP: 개방-폐쇄 원칙 준수)

  • 인터페이스를 사용하면 새로운 기능을 추가할 때 기존 코드를 수정할 필요가 없음

2. 다중 구현 가능 (다중 상속 대체)

  • 자바는 클래스의 다중 상속을 지원하지 않지만, 인터페이스는 여러 개를 구현할 수 있음

3. 객체 간의 결합도를 낮춤 (DIP: 의존 역전 원칙 적용)

  • 인터페이스를 활용하면 특정 구현체에 의존하지 않으므로, 코드 유지보수가 용이

📌 인터페이스의 단점

1. 구현 강제성 (불필요한 메서드 구현 발생)

  • 인터페이스를 구현하는 모든 클래스는 정의된 메서드를 반드시 구현해야 함
  • 만약 일부 클래스에서 필요하지 않은 메서드가 있을 경우, 빈 구현을 해야 하는 경우가 발생
  • 🔍 해결 방법: 인터페이스를 분리하여 ISP(인터페이스 분리 원칙) 적용

2. 코드 중복 가능성 (기본 구현이 없음)

  • 인터페이스는 기본적으로 메서드 구현을 제공하지 않기 때문에,동일한 기능이 여러 구현체에서 필요할 경우 중복 코드가 발생할 수 있음
  • (Java 8 이후 default 메서드를 지원하여 일부 해결 가능)

📌 추상 클래스의 장점

1. 기본 동작 제공 (코드 재사용 가능)

  • 공통 기능을 제공하여 중복 코드 감소

2. 상태(필드) 유지 가능

  • 인터페이스는 멤버 변수를 가질 수 없지만, 추상 클래스는 상태를 유지할 수 있음

📌 추상 클래스의 단점

1. 단일 상속의 한계

  • 자바에서는 하나의 클래스만 상속 가능
  • 만약 여러 개의 부모 클래스를 상속하고 싶다면, 추상 클래스를 사용할 수 없음

2. 확장성이 떨어짐 (OCP 원칙 위반)

  • 새로운 기능을 추가하려면 기존 추상 클래스를 수정해야 함
  • 인터페이스 기반 설계보다 유연성이 낮음

3. 구현 클래스 간 결합도가 높아짐

  • 추상 클래스를 변경하면, 이를 상속하는 모든 클래스가 영향을 받음

🚀 실무에서는 어떻게 선택해야 할까?

비교 요소 인터페이스 추상 클래스

유연성 ✅ 높음 (OCP 준수, 변경 용이) ❌ 낮음 (추가 시 기존 클래스 수정 필요)
코드 재사용 ❌ 없음 (중복 가능성 존재) ✅ 있음 (기본 구현 제공)
상태 유지 ❌ 불가능 ✅ 가능 (멤버 변수 포함 가능)
다중 상속 ✅ 가능 (여러 인터페이스 구현 가능) ❌ 불가능 (단일 상속 제한)
결합도 ✅ 낮음 (느슨한 결합) ❌ 높음 (변경 시 전체 영향)
구현 강제성 ✅ 필요 (모든 메서드 구현) ✅ 필요 (추상 메서드 구현)

🚀 실무 예제: 인터페이스 vs. 추상 클래스 적용 사례

📌 인터페이스 사용 예 (Spring에서 Repository 패턴 적용)

Spring 프레임워크에서는 Repository 계층을 인터페이스로 설계하여, 다양한 데이터베이스 구현체를 쉽게 교체할 수 있도록 합니다.

// 인터페이스 정의 (다양한 DB 구현체를 만들 수 있음)
public interface UserRepository {
    User findById(Long id);
    void save(User user);
}

// MySQL 구현체
public class MySQLUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        System.out.println("Fetching user from MySQL...");
        return new User(id, "MySQL User");
    }

    @Override
    public void save(User user) {
        System.out.println("Saving user to MySQL...");
    }
}

// MongoDB 구현체
public class MongoUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        System.out.println("Fetching user from MongoDB...");
        return new User(id, "MongoDB User");
    }

    @Override
    public void save(User user) {
        System.out.println("Saving user to MongoDB...");
    }
}

// 서비스 계층 (인터페이스를 통해 종속성 주입)
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void processUser(Long id) {
        User user = userRepository.findById(id);
        System.out.println("Processing user: " + user.getName());
    }
}

장점:

  • MySQL, MongoDB 등의 구현을 쉽게 교체 가능
  • 확장성이 뛰어나며, 새로운 DB 지원이 필요할 경우 기존 코드를 수정하지 않고 확장 가능

📌 추상 클래스 사용 예 (공통 동작을 가지는 동물 클래스)

java
복사편집
// 추상 클래스: 공통 속성과 메서드 제공
abstract class Animal {
    protected String name;

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

    public void sleep() {
        System.out.println(name + " is sleeping...");
    }

    // 추상 메서드 (각 동물이 다르게 구현해야 함)
    public abstract void makeSound();
}

// 하위 클래스
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

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

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

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

// 실행 코드
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog("Buddy");
        dog.sleep();
        dog.makeSound();

        Animal cat = new Cat("Kitty");
        cat.sleep();
        cat.makeSound();
    }
}

장점:

  • Animal 클래스에서 공통된 동작(sleep())을 제공하고, 하위 클래스에서 makeSound()를 반드시 구현하도록 강제
  • 공통 속성(name)을 관리할 수 있어 중복 코드 감소

📌 결론

실무에서는 인터페이스를 더 많이 사용!

  • 유연성이 뛰어나고 유지보수성이 높음
  • 다중 구현 가능하여 확장성 증가
  • Spring, JPA, REST API 등 대부분의 프레임워크에서 인터페이스 기반 설계 적용

추상 클래스는 특정한 경우에만 사용

  • 공통된 상태(필드)와 기본 동작을 제공할 때 적합
  • 인터페이스만으로 해결하기 어려운 경우 보완적으로 활용

👉 즉, 기본적으로 인터페이스를 사용하고, 필요한 경우에만 추상 클래스를 활용하는 것이 실무에서 가장 효과적인 방식! 🚀

'Java' 카테고리의 다른 글

인터페이스에 대한 생각 정리  (0) 2025.02.03
JVM 내부 구조  (0) 2024.02.23