본문 바로가기
Develop/Java

[Java] 인터페이스를 사용하자!

by 연로그 2022. 3. 18.
반응형

자바는 다중 구현을 위해 인터페이스, 추상 클래스 기능을 제공한다.

그러면 어느 것을 사용하는게 더 좋을지 한번 고민해보자!

 

🤗 인터페이스의 장점

자유로운 추가

자바는 단일 상속이 조건이다.

이러한 관점에서 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약이다.

 

예를 들어 개발 도구는 컴퓨터, 안드로이드 개발자에게는 삼성폰, IOS 개발자에게는 아이폰 등으로 나눌 수 있다.
그렇다면 'Computer ⊂ DevelopmentTool'로 표현할 수 있다.

이번에는 게임기를 생각해보자.

게임기에는 컴퓨터, 스위치, 닌텐도, 플스 등등 여러가지가 있다.
그렇다면 'Computer ⊂ GameMachine'로 볼 수도 있겠다.

그런데 위에서 말했다시피 자바는 단일 상속이 조건이라 아래와 같은 표현이 불가능하다.

class Computer extends DevlopmentTool, GameMachine { ... }

 

반면 인터페이스는 다른 인터페이스가 존재 하더라도 새롭게 추가할 수 있다.

class Computer implements DevlopmentTool, GameMachine { ... }

 

❗ 계층구조가 없는 타입 프레임워크 생성

타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있다.

하지만 현실에는 이를 구분하기 어려운 경우가 많다.

 

예를 들어 가수(Singer)와 작곡가(Songwriter)가 있다고 하자.

가수는 노래를, 작곡가는 작곡을 한다고 정의한다.

public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(int chartPosition);
}

 

하지만 가수가 작곡하는 경우도 있지 않은가?

클래스는 Singer, Songwirter 인터페이스를 둘 다 implements할 수 있다.

또는 아래와 같은 새로운 타입 프레임워크를 구현할 수 있다.

public interface SingerSongwirter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

 

래퍼 클래스 관용구와의 조합

래퍼 클래스와 인터페이스 조합은 안전하고 강력하다.

타입을 매개변수로 받아 래퍼 클래스로 유연하게 대처할 수 있다.

 

추상 클래스로 저장된 타입에 기능을 추가하려면 상속을 해야한다.

이는 래퍼 클래스보다 활용도가 떨어지고 깨지기 쉽다.

 

👉 이에 대해서는 Effective Java - item 18을 참고하자.

관련 글: https://yeonyeon.tistory.com/206

 

[Java] Inheritance(상속) vs Composition(조합)

목차 상속; Inheritance 조합; Composition 질문 사항 상속보다는 컴포지션을 써야한다? 상속 설계 시 hook을 주의하자. 상속용 클래스의 생성자에서 재정의 가능한 메서드 호출 금지 Cloneable과 Serializable

yeonyeon.tistory.com

 

믹스인 정의에 안성맞춤

믹스인; mixin: 클래스가 구현할 수 있는 타입

👉 믹스인을 구현한 클래스에 원래의 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언

 

example - Comparable

👉 자신을 구현한 클래스의 인스턴스들끼리 순서를 정할 수 있다고 선언

 

😣 인터페이스 단점

  • 인스턴스 필드 불가
  • private 정적 메서드 외의 public이 아닌 정적 멤버 불가

 

😊 디폴트 메서드

  • 인터페이스 메서드 중 구현 방법이 명백하다면 디폴트 메서드를 이용하자. (Java 8부터 이용 가능)
  • @implSpec을 붙여 문서화할 것 (Effective Java - item 19)

 

제약 사항

  • Objects의 equals와 hashCode 같은 메서드는 디폴트로 제공 X
  • 직접 생성하지 않은 인터페이스에는 디폴트 메서드 추가 X

 

😆 골격 구현 클래스

  • 인터페이스와 추상 클래스의 장점을 모두 취하는 방법

  • 인터페이스: 타입 정의 (+필요 시 디폴트 메서드 추가)
  • 골격 구현 클래스: 나머지 메서드 정의
  • 👉 템플릿 메서드 패턴
이름을 보통 Abstract~로 짓는다.
ex: 인터페이스 이름: List, 골격 구현 클래스 이름: AbstractList

 

🔻 골격 구현 작성 방법

더보기
  1. 다른 메서드 구현에 사용되는 기반 메서드 선정
  2. 1을 사용해 직접 구현 가능한 메서드들을 디폴트 메서드로 제공
  3. 남아있는 메서드가 있다면 이 인터페이스를 구현하는 골격 구현 클래스를 생성해 남은 메서드들 작성

 

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {

    // Map.Entry의 기반 메서드: getKey(), getValue()
    // setValue()도 선택적으로 기반 메서드
    @Override
    public V setValue(V value) {
    	throw new UnsupportedOperationException();
    }
    
    // Object 관련 메서드
    // -> default 메서드로 제공 X
    // Map.Entry.equals의 일반 규약 정의
    @Override
    public boolean equals(Object obj) {
    	if (obj == this) return true;
        if (!(obj instanceof Map.Entry)) return false;
        Map.Entry<?, ?> e = (Map.Entry) obj;
        return Objects.equals(e.getKey(), getKey())
        	&& Objects.equals(e.getValue(), getValue());
    }
    
    // ...
}

 

example - intArrayAsList

// List 구현체를 반환하는 정적 팩토리 메소드
// List: 인터페이스. Integer라는 타입을 정의
static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
    
    // AbstractList: 골격 구현체. int[]를 List<Integer>로 변환
    // Java 8 이하인 경우 AbstractList<Integer>으로 수정
    return new AbstractList<>() {
        @Override
        public Integer get(int i) {
            return a[i];	// 오토박싱(Effective Java - item 6)
        }
        
        @Override
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;	// 오토언박싱
            return oldVal;	// 오토박싱
        }

        @Override
        public int size() {
            return a.length;
        }
    };
}
  • List 구현체를 반환하는 정적 팩토리 메서드
  • int[]를 List<Integer> 형태로 보여주는 어댑터이기도 함

 

시뮬레이트한 다중 상속; simulated multiple inheritance

골격 구현 클래스를 우회적으로 이용하는 것도 가능하다.

  1. 인터페이스를 구현한 클래스에서 골격 구현을 확장한 private 내부 클래스를 정의
  2. 각 메서드 호출을 내부 클래스의 인스턴스에게 전달
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    public void clear() {
        s.clear();
    }
  
    // ...
}

 

단순 구현; Simple Implementation

  • 골격 구현의 작은 변종으로 동작하는 가장 단순한 구현을 의미
  • 골격 구현 같이 상속을 위해 인터페이스 구현
  • != 추상 클래스

 

example - AbstractMap.SimpleEntry

// Entry interface의 단순 구현
// SimpleEntry를 참조가 살아있는 동안 Map의 내용이 변경되지 않음을 보여주고
// extra map 조회를 피하기 위해 entry의 값을 캐싱할 수도 있음
public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable {

    private static final long serialVersionUID = -8499721149061103585L;

    private final K key;
    private V value;

    public SimpleEntry(K key, V value) {
        this.key   = key;
        this.value = value;
    }

    public SimpleEntry(Entry<? extends K, ? extends V> entry) {
        this.key   = entry.getKey();
        this.value = entry.getValue();
    }

    public K getKey() {...}

    public V getValue() {...}

    public V setValue(V value) {...}

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        return eq(key, e.getKey()) && eq(value, e.getValue());
    }

    public int hashCode() {...}

    public String toString() {...}
}

 

📔 글을 마치며...

여태까지 상속 하지마!!! 인터페이스 써!!! 라는 뉘앙스의 글을 많이 봐왔다.

공감 가지 않았다. 사실 지금도 딱히 100% 공감하지는 않는 중이다.

캡슐화를 해친다는 입장에서 자주 쓰면 안좋기야 하겠다만은 추상 클래스도 필요한 경우가 분명 존재한다.

상황에 따라 잘 판단해보자.

여러 상황을 겪으며 코딩하다 보면 잘 판단할 때가 오겠지 :)


참고

반응형