// 확장 가능한 열거 타입을 위한 인터페이스
interface Animal {
    void sound();
}

// 기본 열거 타입 구현
enum DefaultAnimal implements Animal {
    DOG {
        @Override
        public void sound() {
            System.out.println("Woof");
        }
    },
    CAT {
        @Override
        public void sound() {
            System.out.println("Meow");
        }
    },
    BIRD {
        @Override
        public void sound() {
            System.out.println("Tweet");
        }
    }
}

// 새로운 열거 타입을 추가하기 위해 Animal을 구현하는 열거 타입 정의
enum CustomAnimal implements Animal {
    LION {
        @Override
        public void sound() {
            System.out.println("Roar");
        }
    },
    ELEPHANT {
        @Override
        public void sound() {
            System.out.println("Trumpet");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // 기본 열거 타입 사용
        DefaultAnimal.DOG.sound();  // 출력: Woof
        DefaultAnimal.CAT.sound();  // 출력: Meow

        // 새로운 열거 타입 사용
        CustomAnimal.LION.sound();  // 출력: Roar
        CustomAnimal.ELEPHANT.sound();  // 출력: Trumpet
    }
}

 

이 예시 코드에서는 Animal 인터페이스를 정의하여 열거 타입이 구현하도록 하였다. 그리고 DefaultAnimal이라는 기본 열거 타입과 CustomAnimal이라는 새로운 열거 타입을 정의하고 각각의 상수에 대해 다른 동작을 구현하였다. 클라이언트는 Animal 인터페이스를 사용하여 두 열거 타입의 인스턴스를 다룰 수 있다.

 

 

핵심 정리 : 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거타입을 함께 사용해 같은 효과를 낼 수 있다. 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자신만의 열거 타입(혹은 다른 타입)을 만들 수 있다. 그리고 API가 (기본 열거 타입을 직접 명시하지 않고) 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스를 대체해 사용

인터페이스 : 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할.

즉, 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다.

 

인터페이스는 위의 지침에 맞지 않는 예로 상수 인터페이스라는 것이 있다. 상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.

 

상수 인터페이스 안티패턴의 예시

// 상수 인터페이스 예시
public interface CarConstants {
    int MAX_SPEED = 240;
    String ERROR_MESSAGE = "Operation not allowed";
}

// 위의 상수 인터페이스는 이런식으로 사용될 수 있다.
public class SportsCar implements CarConstants {
    public void drive() {
        System.out.println("Driving at speed: " + MAX_SPEED);
    }
}

 

이러한 패턴은 상수 인터페이스 안티패턴이다 - 절대 사용하면 안된다.

 

안티패턴을 사용하지 않고 상수를 공유하기 위한 더 좋은 방법은 열거형(enum)을 사용하거나, 클래스 내부에 상수를 정의하는 것이다.

public final class CarUtils {
    private CarUtils() {} // 인스턴스화 방지

    public static final int MAX_SPEED = 240;
    public static final String ERROR_MESSAGE = "Operation not allowed";
}

 

결론 : 인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자.

 

디폴트 메서드 : 인터페이스 내에서 구현 코드를 가진 메서드

추상 메서드 : 선언만 있고 구현이 없는 메서드

예시 : 

interface MyInterface {
    // 추상 메서드
    void abstractMethod();

    // 디폴트 메서드
    default void defaultMethod() {
        System.out.println("This is a default method.");
    }
}

// MyInterface를 구현하는 클래스
class MyClass implements MyInterface {
    @Override
    public void abstractMethod() {
        System.out.println("Abstract method implementation.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.abstractMethod(); // "Abstract method implementation." 출력
        myClass.defaultMethod(); // "This is a default method." 출력
    }
}

 

과거에는 모든 인터페이스의 메서드가 추상 메서드 이어야 했지만 이제는 디폴트 메서드의 도입으로 인해 인터페이스에 메서드의 기본 구현을 제공할 수 있게 되었다. 기존 인터페이스에 메서드를 추가하는 길이 열렸지만 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다. 디폴트 메서드는 범용적이라 대부분 상황에서 잘 작동한다.

하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다.

즉 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다. 디폴트 메서드는 꼭 필요한 경우가 아니면 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는일을 피해야 한다. 추가할경우 디폴트 메서드가 기준 구현체들과 충돌하는지도 체크해야 한다. 디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아니다. 새로운 인터페이스를 만들 경우 표준적인 메서드 구현을 제공할때는 아주 유용한 수단이다.

새 인터페이스라면 릴리스 전 반드시 테스트를 거쳐야 한다. 최소 세가지는 구현해봐야 한다. 각 인터페이스의 인스턴스를 다양한 작업에 활용하는 클라이언트도 여러 개 만들어봐야 한다. 릴리스하기 전, 즉 바로잡을 기회가 아직 남아있을 때 결함을 찾아내야 한다. 인터페이스를 릴리스한 후라도 결함을 수정하는게 가능한 경우도 있지만 어려운 방법이니 피하자.

자바가 제공하는 다중 구현 메커니즘은 인터페이스추상 클래스 두가지이다. 둘의 가장 큰 차이는

추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다. 자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는데 큰 제약을 안게되는 셈이다.

반면 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다.

다중 구현 메커니즘 : 한 클래스가 둘 이상의 인터페이스를 구현할 수 있는 기능

 

인터페이스의 장점 : 

- 인터페이스는 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.

- 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.

- 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.

 

결론 : 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법이 좋다. 골격 구현은 가능한 한 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든곳에서 활용하도록 하는 것이 좋다. 가능한 한 이라고 한 이유는, 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.

+ Recent posts