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

artp·2025년 3월 5일

java

목록 보기
22/32
post-thumbnail

인터페이스와 추상 클래스의 차이점

구분추상 클래스 (abstract class)인터페이스 (interface)
사용 키워드abstractinterface
변수(필드) 사용일반 변수(인스턴스 변수), static 변수, final 변수 모두 가능public static final(상수)만 가능 (자동 적용)
메서드 사용 가능 여부일반 메서드 (구현 O), abstract 메서드 (구현 X)abstract 메서드 (자동 public abstract), default 메서드 (자바 8부터), static 메서드 (자바 8부터), private 메서드 (자바 9부터)
사용 가능한 접근 제어자public, protected, default, private (제한 없음)public (모든 메서드와 변수에 자동 적용)
상속 키워드extends (클래스를 상속)implements (클래스가 인터페이스 구현), extends (인터페이스 간 다중 상속)
다중 상속 가능 여부불가능 (extends로 하나의 부모 클래스만 상속 가능)가능 (인터페이스끼리 다중 상속 가능, 클래스에서 다중 구현 가능)
생성자 사용 가능 여부생성자 사용 가능생성자 사용 불가능
객체 생성 가능 여부단독으로 객체 생성 불가능 (추상 클래스이므로)단독으로 객체 생성 불가능 (규약만 정의)
사용 목적공통된 속성과 동작을 가진 클래스의 “부모 역할” → 코드 재사용성을 높이기 위해 사용됨클래스 간의 “규약(프로토콜)“을 정의 → 클래스 간 결합도를 낮추고 확장성을 높이기 위해 사용됨

추상 클래스 (abstract class)

  • “일반 메서드(구현 O)“와 “추상 메서드(구현 X)“를 함께 가질 수 있는 클래스입니다.
  • “부모 클래스” 역할을 하며, 일부 기능을 제공하고, 나머지는 자식 클래스에서 구현하도록 강제합니다.
  • 공통적인 상태(인스턴스 변수)와 동작(메서드)을 정의할 수 있습니다.
  • 추상 메서드 외에도 필드, 메서드, 생성자를 가질 수 있습니다.
  • 생성자가 존재하므로, 자식 클래스가 super()를 통해 부모 생성자를 호출할 수 있습니다.
  • 추상 클래스의 객체 생성은 불가능합니다.
  • 추상 클래스 내부에서 static 메서드를 선언할 수 있습니다.
  • 자식 클래스에서 extends 키워드를 사용하여 상속할 수 있습니다.
  • 단, 다중 상속이 불가능하며, 단일 상속만 허용됩니다.
abstract class Animal {
    String name; // 인스턴스 변수 가능

    // 생성자 사용 가능
    public Animal(String name) {
        this.name = name;
    }

    // 일반 메서드 (구현 O)
    public void eat() {
        System.out.println(name + "가 음식을 먹습니다.");
    }

    // 추상 메서드 (구현 X, 반드시 자식 클래스에서 오버라이딩 필요)
    public abstract void makeSound();
}

// 자식 클래스에서 추상 메서드 구현 필수
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

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

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("바둑이");
        dog.eat();       // "바둑이가 음식을 먹습니다."
        dog.makeSound(); // "멍멍!"
    }
}

인터페이스 (interface)

  • 클래스가 따라야 할 “규약(프로토콜)“을 정의합니다.
  • 내부의 모든 필드public static final 상수입니다.
  • 구현 클래스에서 반드시 구현해야 하는 abstract 메서드를 포함합니다.
  • 자바 8 이후부터 default, static, private 메서드도 포함할 수 있습니다.
  • 여러 인터페이스를 동시에 implements하여 “다중 구현”이 가능합니다.
  • 인터페이스 간 extends를 사용하여 “다중 상속”이 가능합니다.
  • 인터페이스의 객체 생성은 불가능합니다.
  • 인터페이스를 구현한 클래스에서 implements 키워드를 사용하여 해당 규약을 따라야 합니다.
  • static 메서드인터페이스 자체에서 호출할 수 있으며, 구현 클래스에서 오버라이딩할 수 없습니다.
  • private 메서드인터페이스 내부에서만 사용할 수 있으며, default 또는 static메서드에서 호출 가능합니다.
interface Animal {
    // 추상 메서드 (자동 public abstract 적용)
    void makeSound();

    // 자바 8 이후 `default` 메서드 (구현 가능)
    default void eat() {
        System.out.println("음식을 먹습니다.");
    }

    // 자바 8 이후 `static` 메서드 (구현 가능)
    static void info() {
        System.out.println("이것은 Animal 인터페이스입니다.");
    }
}

// 인터페이스를 구현하는 클래스
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.makeSound(); // "멍멍!"
        dog.eat();       // "음식을 먹습니다."
        Animal.info();   // "이것은 Animal 인터페이스입니다."
    }
}

언제 추상 클래스를 쓰고, 언제 인터페이스를 쓸까?

사용 조건추상 클래스 (abstract class)인터페이스 (interface)
클래스의 공통적인 속성과 동작을 정의해야 하는 경우적합 (변수 + 메서드 포함 가능)부적합 (상태 저장 불가)
공통적인 행동(메서드)을 강제하고 싶을 경우가능가능
여러 개의 부모를 가져야 하는 경우불가능 (단일 상속)가능 (다중 구현 가능)
객체의 상태(변수)를 저장해야 하는 경우가능불가능 (static final 상수만 가능)
코드 재사용성을 높이고 싶은 경우가능 (일반 메서드 제공)구현 클래스에서 직접 구현해야 함

인터페이스를 사용하는 경우

인터페이스는 클래스가 따라야 할 규약(프로토콜)을 정의합니다.

즉, 구현 방식은 신경쓰지 않고, "어떤 기능을 제공해야 하는지"를 명확하게 지정하는 것이 목적입니다.

1. 애플리케이션의 기능을 정의해야 하지만, 그 구현 방식이나 대상에 대해 추상화할 때

  • 예를 들어, DataLoader라는 인터페이스를 만들고 이를 파일, 데이터베이스, API 등에서 데이터를 로드하는 다양한 클래스에서 구현할 수 있습니다.
  • 클래스마다 구현 방식은 다를 수 있지만, 데이터를 로드해야 한다규약은 동일합니다.
  • 즉, "어떤 기능을 수행해야 한다"는 점만 정하고, 실제 구현은 다양한 방식으로 가능하도록 추상화하는 것입니다.
interface DataLoader {
    void loadData();  // 데이터를 로드하는 규약 정의
}

class FileDataLoader implements DataLoader {
    @Override
    public void loadData() {
        System.out.println("파일에서 데이터를 로드합니다.");
    }
}

class DatabaseDataLoader implements DataLoader {
    @Override
    public void loadData() {
        System.out.println("데이터베이스에서 데이터를 로드합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        DataLoader fileLoader = new FileDataLoader();
        DataLoader dbLoader = new DatabaseDataLoader();
        
        fileLoader.loadData();  // "파일에서 데이터를 로드합니다."
        dbLoader.loadData();    // "데이터베이스에서 데이터를 로드합니다."
    }
}
  • 여기서 DataLoader는 “데이터를 로드한다”는 기능만 규약하고, 실제 구현 방식은 다양할 수 있습니다.

2. 서로 관련성이 없는 클래스들을 하나의 그룹으로 묶고 싶을 때

  • 예를 들어, Animal이라는 추상 클래스가 Dog와 Cat을 상속할 수 있지만, Robot까지 포함하려면 논리적으로 맞지 않습니다.
  • 이 경우, “움직일 수 있다”는 개념을 별도로 추상화하여 Movable 인터페이스를 만들면 해결할 수 있습니다.
  • 이렇게 하면 “움직일 수 있는 모든 객체 (Car, Animal, Robot)“을 하나의 그룹으로 묶을 수 있습니다.
interface Movable {
    void move();
}

class Car implements Movable {
    @Override
    public void move() {
        System.out.println("자동차가 도로를 달립니다.");
    }
}

class Robot implements Movable {
    @Override
    public void move() {
        System.out.println("로봇이 이동합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Movable m1 = new Car();
        Movable m2 = new Robot();
        
        m1.move(); // "자동차가 도로를 달립니다."
        m2.move(); // "로봇이 이동합니다."
    }
}
  • Car와 Robot은 전혀 관련이 없지만, Movable 인터페이스를 통해 같은 타입으로 묶을 수 있습니다.
  • 즉, 인터페이스는 “논리적인 부모-자식 관계”가 아닌, “공통된 특징을 가진 그룹”을 만들 때 유용합니다.

3. 다중 구현을 통한 추상화 설계가 필요할 때

  • 자바에서는 클래스 다중 상속이 불가능하지만, 인터페이스 다중 구현은 가능합니다.
  • 여러 개의 기능을 조합해야 할 때 각각의 역할을 나누어 인터페이스로 설계하면 유용합니다.
  • 예를 들어, “날 수 있다”, “수영할 수 있다”, “걷을 수 있다”는 각각의 개념을 인터페이스로 분리하면 필요에 따라 조합하여 사용 가능합니다.
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("오리가 납니다.");
    }

    @Override
    public void swim() {
        System.out.println("오리가 헤엄칩니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Duck duck = new Duck();
        duck.fly();  // "오리가 납니다."
        duck.swim(); // "오리가 헤엄칩니다."
    }
}

4. 특정 데이터 타입의 행동을 정의해야 하지만, 어디서 그 행동이 구현되는지는 신경 쓰지 않을 때

  • 예를 들어, Comparable<T> 인터페이스를 구현하면, 어떤 클래스든 비교 기능을 가질 수 있습니다.
  • Comparable<T> 인터페이스를 사용하면, 특정한 비교 기준을 정의할 수 있습니다.
class Person implements Comparable<Person> {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age); // 나이 기준 정렬
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Bob", 25);

        System.out.println(p1.compareTo(p2)); // 1 (p1이 p2보다 나이가 많음)
    }
}
  • Person 클래스가 Comparable을 구현함으로써, 객체 간 비교가 가능해졌습니다.
  • Person 클래스는 Comparable<Person>을 구현했기 때문에, compareTo() 메서드를 반드시 포함해야 합니다.
  • 하지만 compareTo()가 “나이(age)를 기준으로 비교할지, 이름(name)을 기준으로 비교할지는” 각 클래스에서 결정할 수 있습니다.
  • 즉, 인터페이스를 통해 특정한 동작을 보장하며 어디서 그 동작이 구현되는지는 신경쓰지 않습니다.

5. 클래스와 별도로 특정 객체들이 같은 동작을 한다는 것을 보장해야 할 때

  • 예를 들어, Runnable 인터페이스를 구현하면, 해당 객체가 반드시 run() 메서드를 가지게 됩니다.
  • 이는 멀티스레드 환경에서 실행할 수 있는 객체임을 보장하는 역할을 합니다.
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("작업이 실행됩니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyTask());
        thread.start();  // "작업이 실행됩니다."
    }
}
  • Runnable을 구현한 클래스는 Thread 객체를 통해 실행될 수 있습니다.
  • 즉, 특정 객체들이 동일한 동작을 수행해야 한다는 것을 보장할 때 인터페이스를 사용합니다.

인터페이스의 가장 큰 특징은 상속 구조와 상관없이 다중 구현이 가능하다는 점입니다.
extends논리적인 부모-자식 관계를 의미하지만, implements는 단순히 "이 기능을 지원한다"는 의미입니다.

따라서, 논리적으로 관련이 없는 클래스들도 필요에 따라 같은 인터페이스를 구현할 수 있습니다.

interface Movable {
	void move();
}

class Car implements Movable {
	@Override
    public void move() {
    	System.out.println("자동차가 움직입니다.");
    }
}

Class Animal implements Movable {
	@Override
    public void move() {
    	System.out.println("동물이 이동합니다.");
    }
}

public class Main {
	public static void main(String[] args) {
    	Movable m1 = new Car();
        Movable m2 = new Animal();
        
        m1.move(); // 자동차가 움직입니다.
        m2.move(); // 동물이 이동합니다.
    }
}
  • Car와 Animal은 서로 관련 없는 클래스지만, Movable 인터페이스를 통해 같은 그룹으로 묶일 수 있습니다.

추상 클래스를 사용하는 경우

추상 클래스는 클래스 간 공통된 속성과 동작을 정의하고, 일부 메서드는 하위 클래스에서 구현하도록 강제하는 클래스입니다.

즉, 어떤 기능을 수행해야 하는지뿐만 아니라, 어떤 속성과 기본 동작을 제공할지도 결정할 수 있습니다.

1. 상속받을 클래스들이 공통으로 가지는 메서드와 필드가 많을 때 (중복 멤버 통합)

  • 여러 클래스가 공통으로 가지는 메서드가 많다면, 추상 클래스를 사용하여 중복 코드를 줄일 수 있습니다.
  • 공통된 메서드는 추상 클래스에서 직접 구현하고, 차이가 있는 부분만 추상 메서드로 정의하여 하위 클래스에서 구현하도록 할 수 있습니다.

예제

abstract class Animal {
	String name;
    
    public Animal(String name) {
    	this.name = name;
    }
    
    // 모든 동물이 공통적으로 먹는 기능 제공 (구현 완료된 메서드)
    public void eat() {
    	System.out.println(name + "가 음식을 먹습니다.");
    }
    
    // 동물마다 소리가 다르므로 추상 메서드로 선언 (하위 클래스에서 구현 필수)
    public abstract void makeSound();
}

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

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

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

    @Override
    public void makeSound() {
        System.out.println("야옹~");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("바둑이");
        Cat cat = new Cat("나비");

        dog.eat();        // "바둑이가 음식을 먹습니다."
        dog.makeSound();  // "멍멍!"

        cat.eat();        // "나비가 음식을 먹습니다."
        cat.makeSound();  // "야옹~"
    }
}
  • Animal 클래스는 eat() 메서드와 같은 공통적인 동작을 미리 구현할 수 있습니다.
  • 하지만, makeSound()는 동물마다 다르므로 추상 메서드로 선언하여 하위 클래스에서 구현하도록 강제합니다.
  • 이를 통해 중복되는 코드가 줄어들고, 가독성이 향상됩니다.

2. 멤버에 public 이외의 접근 제어자(protected, private)를 선언해야 할 때

  • 인터페이스에서는 모든 필드가 public static final(상수)로 선언되지만, 추상 클래스에서는 private, protected 접근 제어자를 사용할 수 있습니다.
  • 공통된 데이터를 보호하면서 하위 클래스에 접근할 수 있도록 protected를 활용할 수 있습니다.

예제

abstract class Vehicle {
	protected int speed; // 하위 클래스(자식 클래스)에서 접근 가능
    
    public Vehicle(int speed) {
    	this.speed = speed;
    }
    
    // 현재 속도 출력 (공통 기능 제공)
    public void showSpeed() {
    	System.out.println("현재 속도: " + speed + "km/h");
    }
    // 가속 기능 (하위 클래스에서 다르게 구현)
    public abstract void accelerate();
}

class Car extends Vehicle {
	public Car(int speed) {
    	// 자식 클래스의 생성자가 실행될 때, 부모 클래스의 생성자가 먼저 실행됨
    	super(speed); // 부모 클래스에서 정의된 필드(name)를 자식 클래스가 초기화
    }
    
    @Override
    public void accelerate() {
    	speed += 10;
        System.out.println("자동차가 가속합니다. 새로운 속도: " + speed + "km/h");
    }
}

public class Main {
	public static void main(String[] args) {
    	Car car = new Car(50);
        car.showSpeed(); // 현재 속도: 50km/h
        car.accelerate(); // 자동차가 가속합니다. 새로운 속도: 60km/h
    }
}
  • protected 필드를 사용하면 하위 클래스에서는 접근 가능하지만, 외부에서는 직접 변경할 수 없습니다.
  • 이는 데이터를 보호하면서도, 하위 클래스에서 필요한 경우 수정할 수 있도록 하는 유용한 방법입니다.

3. 인스턴스 변수를 포함하고, 상태 변경이 필요한 경우

  • 인터페이스에서는 static final(상수) 필드만 가능하지만, 추상 클래스에서는 일반적인 필드를 선언하고 값을 변경할 수 있습니다.

    즉, 객체의 상태를 유지하면서 동작을 정의할 때 추상 클래스가 적합합니다.

예제

abstract class Car {
	int speed = 0; // 인스턴스 변수 선언 가능
    
    // 공통 기능 제공
    public void showSpeed() {
    	System.out.println("현재 속도: " + speed + "km/h");
    }
    
    // 가속 기능 (하위 클래스에서 다르게 구현)
    public abstract void accelerate();
}

class SportsCar extends Car {
	@Override
    public void accelerate() {
    	speed += 20;
        System.out.println("스포츠카가 가속합니다. 새로운 속도: " + speed + "km/h");
    }
}

public class Main {
	public static void main(String[] args) {
    	SportsCar ferrari = new SportsCar();
        ferrari.showSpeed(); // 현재 속도: 0km/h
        ferrari.accelerate(); // 스포츠카가 가속합니다. 새로운 속도: 20km/h
    }
}
  • speed 변수를 통해 각 객체가 상태를 가지며 변경할 수 있도록 설계할 수 있습니다.
  • 인터페이스에서는 이러한 기능을 제공할 수 없기 때문에, 상태 변경이 필요한 경우에는 추상 클래스가 적합합니다.

4. 요구사항과 함께 구현 세부 정보의 일부 기능만 지정해야 할 때

  • 추상 클래스는 일부 기능을 직접 구현하면서도, 하위 클래스에서 필수적으로 구현해야 할 기능을 강제할 수 있습니다.
  • 즉, 공통적인 동작은 추상 클래스에서 구현하고, 개별적인 동작은 하위 클래스에서 구현하도록 할 수 있습니다.

    공통적인 로직을 제공하면서도, 특정 동작을 하위 클래스에서 다르게 구현해야 할 때 추상 클래스를 사용합니다.

abstract class Game {
	// 공통적인 게임 시작 로직 제공 (구현 완료)
    public void start() {
    	System.out.println("게임을 시작합니다.");
        play(); // 하위 클래스에서 구현해야 하는 메서드 호출
        end(); // 
    }
    
    public abstract void play();
    
    public void end() {
    	System.out.println("게임이 종료되었습니다.");
    }
}

class Chess extends Game {
	@Override
    public void play() {
    	System.out.println("체스를 둡니다.");
    }
}

public class Main {
	public static void main(String[] args) {
    	Chess chess = new CHess();
        chess.start();
        // 게임을 시작합니다.
        // 체스를 둡니다.
        // 게임이 종료되었습니다.
    }
}
  • 게임의 시작과 종료는 모든 게임이 공통적으로 가지는 기능이므로 추상 클래스에서 미리 구현합니다.
  • 하지만 play() 메서드는 게임마다 다르게 동작하므로, 추상 메서드로 선언하여 하위 클래스에서 반드시 구현하도록 강제합니다.
profile
donggyun_ee

0개의 댓글