나는 인터페이스를 꽤 자주 사용한다.
최근사용은 랜덤값을 테스트 하기위해 전략패턴을 사용했는 데 그 때 아주 잘 사용했다.
그렇지만 막사용하지도 않는다.
글을 먼저 작성하기전에 개념부터 알아보자
제목 자체가 추상 클래스보다는 인터페이스이다.
그렇다면 추상 클래스는 뭐고 인터페이스는 무엇인지 먼저 알아보자
그렇다면 책에서는 왜 추상 클래스보다 인터페이스를 우선하라고 하는 걸까? 어떤 장점이 있어서 이렇게 말하는 걸까?
추상 클래스(Abstract Class)
자바에서 추상 클래스는 "공통된 뼈대"를 정의하고 구체적인 구현은 자식 클래스에게 맡기는 데 사용한다.
- 추상 메서드(몽통 없는 메서드 선언) 하나 이상을 포함할 수 있다.
- 인스턴스(객체)를 직접 만들 수 없다.
- 다른 클래스가 상속받아 구현(override)해 주어야 한다.
예를 들어, 게임기에서 모든 "몬스터"가 갖는 기본 특성이 있다고 가정해보자.
public abstract class Monster {
// 모든 몬스터가 가지고 있을 최소한의 체력
protected int health;
// 공통된 동작인 '공격'을 추상 메서드로 선언
public abstract void attack();
// 체력이 감소하는 일반 메서드
public void takeDamage(int damage) {
health -= damage;
System.out.println("몬스터가 " + damage + "만큼의 피해를 입었습니다.");
}
}
- Monster 클래스 자체로는 “몬스터”를 만들 수 없다.
- Monster를 상속받은 Orc, Dragon 같은 구체적인 몬스터 클래스들은 attack() 메서드를 어떻게 구현할지 결정하게 된다.
인터페이스(Interface)
인터페이스는 클래스가 “이 기능(메서드)을 꼭 제공해야 한다!”라는 계약을 정의한다.
- 메서드의 “시그니처(이름, 매개변수, 반환타입)”만 정하고, 실제 구현은 없다.(자바 8 이후 디폴트/정적 메서드는 예외).
- 여러 인터페이스를 동시에 구현(다중 구현)할 수 있어 “유연한 다형성”을 실현할 수 있다.
public interface Flyable {
void fly();
}
public interface Walkable {
void walk();
}
public class Bird implements Flyable, Walkable {
@Override
public void fly() {
System.out.println("새가 날아다닌다.");
}
@Override
public void walk() {
System.out.println("새가 걷는다.");
}
}
public class Human implements Walkable {
@Override
public void walk() {
System.out.println("사람이 걷는다.");
}
}
인터페이스 장점
1. 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
인터페이스가 요구하는 메서드를 추가하고, 클래스 선언에 implements 구문만 추가하면 끝이다.
예를 들어, 이미 Person이라는 클래스가 존재한다고 해보죠 그런데 새롭게 Singer라는 인터페이스를 만들어 “노래” 기능을 추가하고 싶다면, 어떻게 해야 할까요?
public interface Singer {
void sing();
}
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void sayHello() {
System.out.println(name + " says hello!");
}
}
여기서 Person이 Singer 인터페이스까지 구현하도록 만들려면 이렇게 고쳐주면 된다.
public class Person implements Singer {
private String name;
public Person(String name) {
this.name = name;
}
public void sayHello() {
System.out.println(name + " says hello!");
}
// Singer 인터페이스가 요구하는 메서드 구현
@Override
public void sing() {
System.out.println(name + " is singing!");
}
}
자바에서 자주 쓰는 인터페이스인 Comparable<T>나 Iterable<T>도 마찬가지이다. implements를 활용하여 해당 인터페이스를 추가하고 필요한 메서드(compareTo, iterator 등)를 추가하면 바로 인터페이스를 구현할 수 있다.
그렇다면 추상 클래스는 어떨까?
추상 클래스를 새로 추가해서 기존 클래스에 추가 기능을 얹는 일은 훨씬 까다롭다.
- 자바는 단일 상속만 지원하므로, 어떤 클래스가 이미 다른 클래스를 상속받고 있다면 새로운 추상 클래스를 끼워 넣기가 불가능할 수 있다.
- 두 클래스가 같은 추상 클래스를 확장하길 원한다면 그 추상 클래스가 "공통 조상" 위치에 있어야 한다.
- 만약 기존 계층구조와 전혀 맞지 않는 추상 클래스를 중간에 억지로 넣어버리면, 그 추상 클래스의 모든 자손 클래스가 다 함께 바뀐 추상 클래스를 물려받게 된다.
2. 믹스인(mixin)
대상 타입의 주된 기능에 선택적 기능을 "혼합(mixed in)"한다고 해서 믹스인이라 부른다.
추상 클래스로는 믹스인을 정의할 수 없다.
왜냐하면 기존 클래스에 덧씌울 수 없기 때문이다.
클래스는 두 부모를 섬길 수 없고, 클래스 계층구조에는 믹스인을 삽입하기에 합리적인 위치가 없기 때문이다.
믹스인의 예는 이와 같다.
ex) Comparable, Iterable, AutoCloseable
추상 클래스는 이미 다른 클래스를 상속하는 클래스의 경우, 해당 클래스가 두 부모를클래스를 가질 수 없으므로 믹스인으로 사용될 수 없다.
그렇다면 믹스인이 왜 필요할까?
자바에는 다중 상속이 없으니, 하나의 클래스가 여러 부모 클래스로부터 로직이나 필드를 동시에 상속받을 수 없다.
대신 인터페이스를 이용하면, 여러 인터페이스를 동시에 구현할 수 있어 "다중 구현"자체는 가능하다.
자바 8부터는 인터페이스 내에 디폴트 메서드를 구현할 수 있게 되었다.
결과적으로, 여러 인터페이스에서 정의된 로직(디폴트 메서드)을 하나의 클래스에서 가져다 쓰는 방식으로, "여러 기능을 섞어 넣는"것과 유사한 효과를 낼 수 있다.
예를 들어, 오디오 파일을 다루는 프로그램에서
조작 가능(Playable): 재생, 정지 등의 기능을 할 수 있음
타이머 기능(Timed): 현재 재생 시간을 기록하거나, 특정 구간에 도달하면 알람을 주는 기능이 있음
두 가지 기능을 모두 활용하고 싶다면, 아래와 같은 인터페이스를 만들 수 있다.
public interface Playable {
default void play() {
System.out.println("재생 시작!");
}
default void stop() {
System.out.println("재생 멈춤!");
}
}
public interface Timed {
default void setTimer(long seconds) {
System.out.println(seconds + "초 뒤에 알림을 설정합니다.");
}
default void alert() {
System.out.println("설정된 타이머 시간이 지났습니다!");
}
}
public class AudioFile implements Playable, Timed {
private String fileName;
public AudioFile(String fileName) {
this.fileName = fileName;
}
public void info() {
System.out.println("오디오 파일: " + fileName);
}
public static void main(String[] args) {
AudioFile audio = new AudioFile("music.mp3");
audio.info();
audio.play();
audio.setTimer(5);
audio.stop();
audio.alert();
}
}
AudioFile 클래스는 Playable과 Timed가 제공하는 모든 디폴트 메서드를 “섞어넣어” 바로 사용할 수 있게 됩니다. 이처럼 “인터페이스 여러 개 + 디폴트 메서드 = 믹스인” 패턴을 활용해서, 클래스가 필요한 기능들을 손쉽게 합칠 수 있습니다.
3. 디폴트 메서드
인터페이스의 메서드 중 구현 방법이 명확한 메서드가 있다면, 디폴트 메서드를 활용할 수 있다.
- 단, `equals()`, `hashCode()`,와 같이 `Object`에서 제공하는 메서드는 디폴트 메서드로 제공해선 안됩니다.
- 단, `public`이 아닌 정적 멤버도 가질 수 없습니다.
/**
* Removes all of the elements of this collection that satisfy the given
* predicate. Errors or runtime exceptions thrown during iteration or by
* the predicate are relayed to the caller.
*
* @implSpec
* The default implementation traverses all elements of the collection using
* its {@link #iterator}. Each matching element is removed using
* {@link Iterator#remove()}. If the collection's iterator does not
* support removal then an {@code UnsupportedOperationException} will be
* thrown on the first matching element.
*
* @param filter a predicate which returns {@code true} for elements to be
* removed
* @return {@code true} if any elements were removed
* @throws NullPointerException if the specified filter is null
* @throws UnsupportedOperationException if elements cannot be removed
* from this collection. Implementations may throw this exception if a
* matching element cannot be removed or if, in general, removal is not
* supported.
* @since 1.8
*/
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
위 코드는 `Collection` 인터페이스 내부의 `removeIf()`입니다. 위와 같이 `@implSpec`을 이용해 상속하려는 사람을 위한 설명을 문서화해두는 것이 좋다.
📚 Reference
- Effective java Item 20
'JAVA' 카테고리의 다른 글
조합 ? 상속 ? (1) | 2025.03.12 |
---|---|
isBlank() vs isEmpty() (0) | 2025.02.28 |
Comparable 인터페이스 너 뭐야! (0) | 2025.02.26 |