인터페이스 & 추상 클래스 / 인터페이스 default 메서드, private 메서드 / 인터페이스를 타입으로 사용하기

손효재·2023년 4월 6일

상속 vs 구현

상속(extends) : is Kind Of , IS - A “~이다”

  • 자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용하며 자식 클래스에서 추가 및 확장을 할 수 있다.
    이로인해 중복이 최소화 되어 재사용성이 증가한다.
  • 존재의 정의
  • 일반 클래스, 추상(abstract) 클래스 기반으로 구현한다.

구현(implements) : be Able to, HAS - A “~을 할 수 있는”

  • 상속과 달리 반드시 부모 인터페이스의 메서드를 재정의하여 구현해야 한다.
  • 능력의 정의
  • 인터페이스 기반으로 구현한다.

추상클래스 (abstract class)

동작이 정의되지 않은 하나 이상의 추상 메서드를 포함하는 클래스이다.
상속(extends)을 통해 자손 클래스에서 재정의(Override)하여 사용할 수 있기에, 인스턴스 생성 불가

  • 추상 메서드 (abstract method) : 메서드의 선언부만 작성하고, 구현부는 작성하지 않고, 자식 클래스에서 반드시 override 해야 사용할 수 있다. 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있을때 사용한다. (다형성을 가지는 메서드의 집합을 정의)

추상 클래스를 상속받는 자식 클래스가 반드시 추상 메서드를 구현하도록 하기 위한 목적으로 사용한다.

인터페이스 (interface)

인터페이스는 클래스가 “무엇을 할 수 있다”라고 하는 기능을 구현하도록 강제하는 특징이다.
동일한 목적 하에 동일한 기능을 수행하도록 강제하여, Java의 다형성을 통해 유지보수성을 높인다.
ex) Serializable, Comparable, Runnable, Trainable, Petable

인터페이스는 클래스에 다중 구현을 지원하고, 인터페이스끼리 다중 상속이 가능하다.

인터페이스의 다중상속이 가능한 이유

기존 Java 클래스 다중 상속의 문제점 : 상속한 클래스들이 동일한 형태의 메서드를 가졌을때,
어떤 메서드를 실행해야하는지 판단할 수 없었다.

인터페이스의 메서드는 모두 추상 메서드이기 때문에, 겹치는 메서드이더라도,
인터페이스를 상속한 클래스에서 해당 메서드를 최종 구현하기에 다중 상속이 가능하다.

인터페이스도 다중 상속이 안될 수 있다 (feat. default method)

Java 8 부터 인터페이스에 default 메서드를 명시하여 선언할 수 있으므로,
동일한 default 메서드 명을 가진다면 다중 상속이 불가능하다.

해결 방법 3가지

  1. 중복되는 default 메서드를 override 하여 구현해준다.
  2. 부모 인터페이스 메서드를 선택해서 호출한다. ex) A인터페이스.super.메서드명()
  3. B인터페이스가 A인터페이스를 상속했다면, 더 구체적인 B의 default 메서드 호출 (Java의 우선순위 규칙)

인터페이스에서 default 메서드 구현 가능이 추가된 이유

“하위 호환성”을 위해 default 메서드가 구현 가능해졌다.

인터페이스를 보완하거나 수정하는 과정에서 추가로 구현해야할 필수적인 메서드가 생길때,
이미 인터페이스를 구현한 모든 클래스에서 추가된 추상 메서드를 구현해줘야 한다.
ex) Java 8의 Collection 인터페이스에 forEach(), stream() 등을 추가

따라서, default 메서드를 추가하고, 이를 정의하면 하위 호환성이 유지되며 인터페이스를 보완할 수 있다.
* 그대로 사용해도 되고, 재정의해서 사용 가능

interface Trainable {
    void train(String command);

    default void reset() {
        System.out.println("기본 초기화!");
    }
}

// 1. 그냥 상속 - default 메서드 그대로 사용
class Dog implements Trainable {
    public void train(String command) { System.out.println(command + " 완료!"); }
    // reset() 따로 구현 안 해도 됨 ✅
}

// 2. 오버라이드 - default 메서드 재정의
class Cat implements Trainable {
    public void train(String command) { System.out.println("..."); }

    @Override
    public void reset() {             // 필요하면 재정의 가능
        System.out.println("Cat 전용 초기화!");
    }
}

인터페이스의 선언 가능 범위

  • Java 8 이전 : 상수 (public static final) , 추상 메서드 (public abstract)
  • Java 8 : default 메서드 (클래스 인스턴스 메서드), static 메서드 (클래스 정적 메서드)
  • Java 8 이후 : private 정적 메서드
    * 정적 필드와 정적 멤버 클래스는 여전히 public 이어야 한다.
interface Trainable {
    int MAX_LEVEL = 10;                    // 상수 (Java 8 이전부터)
    void train(String command);            // 추상 메서드 (Java 8 이전부터)

    // ✅ Java 8 추가
    default void reset() {                 // default 메서드 (구현 포함)
        System.out.println("훈련 초기화!");
    }

    // ✅ Java 8 추가
    static Trainable of(String type) {    // static 메서드
        if (type.equals("dog")) return new Dog("unknown");
        throw new IllegalArgumentException();
    }

    // ✅ Java 9 추가
    private void log(String msg) {        // private 메서드 (내부 공통 로직용)
        System.out.println("[LOG] " + msg);
    }
}

static 메서드는 재정의 할 수 없는데, 그 이유는 무엇인가?

static은 인스턴스가 아닌, 클래스/인터페이스에 귀속되기 때문에 다형성 대상이 아니다.
컴파일 시점에 타입이 고정되기 때문에 재정의 할 수 없다.

그렇다면, static 메서드가 추가된 이유는 무엇인가?

인터페이스와 강하게 관련된 유틸리티 기능을 인터페이스 내부에 함께 두기 위해서 추가됐다.
인스턴스 객체 필요없이, 필요한 유틸 기능을 인터페이스 내부에 포함시켜, 불필요한 객체 생성 로직이 없어진다.
ex) Comparator.comparing / List.of

private 메서드는 왜 예외적으로 허용됐을까?

private 메서드가 없었다면 log() 같은 공통 로직을
인터페이스 내부에서 중복 작성해야 했기 때문에 Java 9에서 예외적으로 허용했다.

interface Animal {

    default void breathe() {
        log("호흡 시작");       // 공통 로직 재사용
        System.out.println("숨을 쉽니다");
        log("호흡 완료");
    }

    default void sleep() {
        log("수면 시작");       // 공통 로직 재사용
        System.out.println("잠을 잡니다");
        log("수면 완료");
    }

    // ✅ default 메서드들의 공통 로직을 묶기 위해 Java 9에서 허용
    // 외부에서는 호출 불가, 인터페이스 내부에서만 사용
    private void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
}

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

  • 추상 클래스 : 클래스 내에 추상 메서드가 하나 이상 포함되거나, 클래스가 abstract로 정의된 경우
    자신의 기능들을 하위로 확장시킨다.
    각 객체들의 공통점을 찾아 추상화 시켜놓은 것으로, 부모 클래스가 가진 기능을 구현해야할 때 사용
  • 인터페이스 : 모든 메서드가 추상 메서드이다. (Java 8에서 default 키워드를 통해 일반 메서드 구현이 가능)
    인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 동일한 기능으로 구현한다.
    다른 부모 클래스를 상속하더라도, 같은 기능이 필요한 경우 사용
추상 클래스인터페이스
의미~이다 (정체성)~할 수 있다 (능력)
키워드extends (상속, 확장의 느낌)implements (상속, 구현의 느낌)
다중 상속XO
가질 수 있는 것일반 메서드, 추상 메서드, 일반 변수, 생성자상수, 추상 메서드, default 메서드(일반 메서드)

ex) Animal 클래스 예시
추상클래스 : Creature - Human, Animal / 인터페이스 : Trainable, Petable, Flyable, Swimable

abstract class Animal {           // 모든 동물의 공통 본질
    String name;
    abstract void speak();        // 모든 동물은 소리를 낸다 ✅
}

interface Trainable {             // 훈련 가능한 능력
    void train(String command);   // 되는 애만 구현하면 됨
}

interface Petable {               // 쓰다듬을 수 있는 능력
    void pet();
}

class Dog extends Animal implements Trainable, Petable {
    @Override void speak()               { System.out.println("멍멍!"); }
    @Override public void train(String c){ System.out.println(c + " 완료!"); }
    @Override public void pet()          { System.out.println("꼬리를 흔듭니다"); }
}

class Cat extends Animal implements Petable {  // Trainable 없음!
    @Override void speak()               { System.out.println("야옹!"); }
    @Override public void pet()          { System.out.println("그루밍합니다"); }
}

class Snake extends Animal {  // 둘 다 없음!
    @Override void speak()               { System.out.println("쉬이익.."); }
}

인터페이스를 활용한 능력 기반의 분류

train()이 추상 클래스였다면, 아래와 같이 List 같은 능력 기반의 분류가 어렵다.

// 훈련 가능한 동물만 모아서 훈련시키기
List<Trainable> trainees = new ArrayList<>();
trainees.add(new Dog("바둑이"));
trainees.add(new Dog("흰둥이"));
// trainees.add(new Cat("나비"));  // ❌ Cat은 Trainable이 아님!

for (Trainable t : trainees) {
    t.train("앉아");
}

인터페이스를 타입으로 사용하는 방법 (Java의 다형성)

Dog 객체 하나가 4가지 타입 모두로 참조 될 수 있다.

// 이 모든 것이 가능합니다
Animal a = new Dog("바둑이");          // 추상 클래스 타입
Trainable t = new Dog("바둑이");       // 인터페이스 타입
Petable p = new Dog("바둑이");         // 인터페이스 타입
Dog d = new Dog("바둑이");             // 구체 클래스 타입

타입에 따라 사용할 수 있는 메서드가 달라진다.

Dog dog = new Dog("바둑이");

// Animal 타입으로 보면 → Animal 메서드만 접근 가능
Animal a = dog;
a.speak();              // ✅
a.breathe();            // ✅
a.train("앉아");        // ❌ 컴파일 에러! Animal엔 train 없음

// Trainable 타입으로 보면 → Trainable 메서드만 접근 가능
Trainable t = dog;
t.train("앉아");        // ✅
t.speak();              // ❌ 컴파일 에러! Trainable엔 speak 없음

// Dog 타입으로 보면 → 전부 접근 가능
Dog d = dog;
d.speak();              // ✅
d.train("앉아");        // ✅
d.pet();                // ✅

그렇다면, 왜 구체 타입(Dog) 대신 인터페이스를 사용할까?
→ 구현체에 종속되지않고, “훈련가능한 것”이라는 기능에 의존하여 유지보수성이 올라간다.

// ❌ 구체 타입 사용 - Dog에만 종속됨
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog("바둑이"));
// dogs.add(new Wolf("늑대"));  훈련 가능한 Wolf가 생겨도 추가 불가!

// ✅ 인터페이스 타입 사용 - 훈련 가능한 건 뭐든 OK
List<Trainable> trainees = new ArrayList<>();
trainees.add(new Dog("바둑이"));
trainees.add(new Wolf("늑대"));    // Wolf가 Trainable이면 추가 가능!
trainees.add(new Dolphin("돌고래")); // Dolphin도 Trainable이면 추가 가능!

for (Trainable t : trainees) {
    t.train("앉아");   // 종류에 상관없이 동일하게 처리
}

자료구조의 List 또한 인터페이스이다. ArrayList, LinkedListList 인터페이스를 구현한 구체 클래스

// List 선언할 때도 인터페이스 타입 권장
List<String> list = new ArrayList<>();      // ✅ 좋은 방식
ArrayList<String> list = new ArrayList<>(); // ❌ 구체 타입에 종속

// 나중에 LinkedList로 바꿔도 선언부만 그대로 유지됨
List<String> list = new LinkedList<>();     // ✅ 선언부 변경 없음!

인터페이스의 모든 추상 메서드를 구현하지 못하면 추상 클래스로 선언해야한다.

인터페이스를 구현하는 구현 클래스는 반드시 인터페이스의 모든 추상 메서드를 실체 메서드로 구현해야 한다.

추상 메서드를 전부 구현하면, 일반 클래스로 선언하고, 인스턴스화 가능하다.
모든 추상 메서드가 구현되지 않으면, 추상 클래스로 선언하고, 미구현 메서드는 자식 클래스에게 위임해야 한다.

interface Trainable {
    void train(String command);
}

interface Petable {
    void pet();
}

// ✅ 두 인터페이스의 추상 메서드 전부 구현
class Dog implements Trainable, Petable {
    public void train(String command) { System.out.println(command + " 완료!"); }
    public void pet()                 { System.out.println("꼬리를 흔듭니다!"); }
}

// ❌ pet() 빠짐 → 컴파일 에러!
class Cat implements Trainable, Petable {
    public void train(String command) { System.out.println("..."); }
    // pet() 빠짐!
}

// ✅ 일부만 구현하려면 abstract 선언
abstract class Cat implements Trainable, Petable {
    public void train(String command) { System.out.println("..."); }
    // pet()은 미구현 → Cat을 상속받는 자식 클래스에게 위임
}

이는 구현이 없는 메서드가 호출되는 상황을 컴파일 시점에 미리 막기 위한 규칙이다.

// 만약 이게 허용된다면?
class Cat implements Trainable, Petable {
    public void train(String command) { ... }
    // pet() 없음

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.pet();   // 😱 구현이 없는데 호출됨 → 런타임 에러 대참사!
    }
}

0개의 댓글