Skip to content

Latest commit

 

History

History
280 lines (229 loc) · 10.2 KB

추상_클래스보다는_인터페이스를_우선하라.md

File metadata and controls

280 lines (229 loc) · 10.2 KB

다중 구현 메커니즘

  • 자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상클래스 두가지
  • 자바 8부터 인터페이스도 디폴트 메서드를 제공
  • 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있음

추상 클래스 방식과 인터페이스 방식의 차이점

  • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 함
  • 반면 인터페이스는 선언한 메서드를 모두 정의했다면 어떤 클래스를 구현했던 같은 타입으로 취급됨
  • 인터페이스는 기존 클래스에 확장 구현이 쉽다
  • 반면 추상 클래스는 부모 자식 단일 계층 구조를 강제함
  • 따라서 추상 클래스는 이후에 생길 자손 클래스들에게도 공통된 조상을 강제한다 (다중 상속 불가)

믹스인 정의

  • 인터페이스는 믹스인 정의에 안성맞춤
  • 믹스인이란 클래스가 구현할 수 있는 타입을 말함
  • 믹스인을 구현한 클래스는 특정 선택적 행위를 제공한다고 선언하는 효과를 줌
public  interface Singer { AudioClip sing(Song s); }
public  interface Songwriter { Song compose(int chartPosition); }
public  interface SingerSongwriter extends Singer, Songwriter { 
	AudioClip strum(); 
	void actSensitive(); 
}
  • 위의 코드에서 SingerSongwriter는 Singer 기능과 Songwriter 기능을 제공함을 손쉽게 알 수 잇다.
  • 이렇게 선택적 기능을 혼합할 수 있다는 의미에서 믹스인이라고 불린다.

추상 클래스의 단점 (믹스인 구현 힘듦)

  • 위와 같은 믹스인 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의해야 함
  • 고도비만 계층구조 탄생
  • 지원할 수 있는 선택적 속성이 N개라면 조합의 수는 2^n개나 됨
  • 이와 같은 현상을 조합 폭발이라고 함
  • 추상 클래스 방식에서는 두 부모를 섬길 수 없기에 믹스인은 인터페이스만의 장점
  • 즉 클래스 구조와 달리 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다

인터페이스는 기능 향상에서 안전하고 강력한 수단

  • 만약 래퍼 클래스와 같이 타입을 클래스로 정의한다고 하자
  • 타입을 추상 클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속 뿐
  • 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기는 더 쉽다.
  • 인터페이스는 확장이 쉽고 제한이 적음
@jdk.internal.ValueBased  
public final class Integer extends Number  
  implements Comparable<Integer>, Constable, ConstantDesc {  
  /**  
 * A constant holding the minimum value an {@code int} can  
 * have, -2<sup>31</sup>. */  @Native public static final int MIN_VALUE = 0x80000000;
  • 위는 Integer 클래스의 구현 정보
  • 여러 기능을 구현하는 것에 상속보다 제약이 적은 것을 확인할 수 있음

인터페이스의 디폴트 메서드

  • 인터페이스 메서드 중 구현 방법이 명백한 것이 있다면 디폴트 메서드로 제공할 수 있음
  • 디폴트 메서드를 제공할 때는 상속하려는 사람을 위한 설명을 @implSpec 자바 독 태그를 붙여 문서화해야 한다
  • 다음은 implSpec을 통해 디폴트 메서드를 설명하는 예시
/**  
 * Receives notification when the state of a recording changes. * <p> * Callback is invoked when a recording reaches the {@code RUNNING},  
 * {@code STOPPED} and {@code CLOSED} state.  
 * * @implNote The implementation of this method should return as soon as possible  
 *           to avoid blocking normal operation of Flight Recorder. * 
 * * @implSpec The default implementation of this method is empty.  
 * * @param recording the recording where the state change occurred, not  
 *        {@code null}  
 * * @see FlightRecorder#addListener(FlightRecorderListener)  
 * @see RecordingState  
  *  
 */default void recordingStateChanged(Recording recording) {  
}

인터페이스의 제약

  • 인스턴스 필드를 가질 수 없음
  • public이 아닌 정적 멤버도 가질 수 없다(단, private 정적 메서드는 예외)
  • 이는 추상 클래스에 비해 인터페이스가 가지지 못하는 제약점이라고 볼 수 있음

인터페이스에서 추상 클래스의 장점을 취할 수 있는 방법

  • 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 추상 클래스와 인터페이스의 장점을 함께 취할 수 있다.
  • 인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드 몇개도 함께 제공한다.
  • 그리고 골격 구현 클래스는 나머지 메서드들까지 구현한다.
  • 이렇게 해두면 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다.
  • 관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다.
  • 제대로 설계했다면 골격 구현은 그 인터페이스로 나름의 구현을 만들려는 프로그래머의 일을 상당히 덜어준다.

골격 구현과 그 예시 코드로 보는 장점

  • 다음은 Map을 구현하는 골격 클래스 예시
public abstract class AbstractMap<K,V> implements Map<K,V> {  
  /**  
 * Sole constructor.  (For invocation by subclass constructors, typically * implicit.) */  protected AbstractMap() {  
 }  
  // Query Operations  
  
 /** * {@inheritDoc}  
 * * @implSpec  
  * This implementation returns {@code entrySet().size()}.  
 */  public int size() {  
  return entrySet().size();  
 }
  • 다음은 완벽히 동작하는 List 구현체를 반환하는 정적 팩터리 메서드
  • AbstractList를 골격 구현으로 활용했다.
static List<Integer> intArrayAsList(int[] a) {
	Objects.requireNonNull(a);
	
	return new AbstractList<Integer>() {
		@Override public Integer get(int i) {
			return a[i];
		}
		@Override public Integer set(int i, Integer val) {
			int oldVal = a[i];
			a[i] = val;
			return oldVal;
		}
		@Override public int size() {
			return a.length;
		}
	};
}
  • 위 코드는 골격 구현의 힘을 잘 보여주는 예시
  • 이 예시는 우선 int 배열을 받아 Integer 인스턴스의 리스트 형태로 보여주는 어댑터 이기도 함
  • 골격 구현 클래스는 추상 클래스처럼 구현을 도와 줌
  • 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유로움

골격 구현 작성 과정

  • 골격 구현 작성 과정은 상대적으로 쉽다.
  • 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드 선정
  • 이 기반 메서드들은 추상 메서드가 될 것
  • 기반 메서드들을 사용해 직접 구현할 수 있는 메서드들은 모두 디폴트 메서드로 제공
  • 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아 있다면 인터페이스를 구현하는 골격 클래스에 넣는다.

골격 구현 클래스 작성 예시

public interface Athlete { 
    
    void 근육을_키우자(); 
    void 지구력을_기르자(); 
    void 연습하자(); 
    void 루틴();
}

public class SoccerPlayer implements Athlete { 
    
    @Override 
    void 근육을_키우자() { 
        System.out.println("헬스장 레츠고"); 
    }

    @Override 
    void 지구력을_기르자() { 
        System.out.println("청계천 러닝"); 
    }

    @Override 
    void 연습하자() { 
        System.out.println("슈팅 연습"); 
    }

    @Override
    void 루틴() {
        근육을_키우자();
        지구력을_기르자();
        연습하자();
    }
}

public class BaseballPlayer implements Athlete { 
    
    @Override 
    void 근육을_키우자() { 
        System.out.println("헬스장 레츠고"); 
    }

    @Override 
    void 지구력을_기르자() { 
        System.out.println("청계천 러닝"); 
    }

    @Override 
    void 연습하자() { 
        System.out.println("배팅 연습"); 
    }

    @Override
    void 루틴() {
        근육을_키우자();
        지구력을_기르자();
        연습하자();
    }
}
  • 위 코드는 골격 클래스 없이 인터페이스 구현을 통해 다형성을 구현한 것
  • 중복되는 부분이 보인다.
  • 예로 SoccerPlayer, BaseballPlayer 모두 헬스장으로 근육을 키우고 청계천 러닝을 통해 지구력을 기른다.
public interface Athlete {

    void 근육을_키우자();

    void 지구력을_기르자();

    void 연습하자();

    void 루틴();
}

public abstract class WangsimniAthlete implements Athlete {
    
    @Override
    public void 근육을_키우자() {
        System.out.println("헬스장 레츠고");
    }

    @Override
    public void 지구력을_기르자() {
        System.out.println("청계천 러닝");
    }

    @Override
    public void 루틴() {
        근육을_키우자();
        지구력을_기르자();
        연습하자();
    }
}

public class SoccerPlayer extends WangsimniAthlete implements Athlete {

    @Override
    public void 연습하자() {
        System.out.println("슈팅 연습");
    }
}

public class BaseballPlayer extends WangsimniAthlete implements Athlete {

    @Override
    public void 연습하자() {
        System.out.println("배팅 연습");
    }
}

public class Main {
    
  public static void main(String[] args) {

      List<Athlete> athletes = new ArrayList<>();
      Athlete soccerPlayer = new SoccerPlayer();
      Athlete baseballPlayer = new BaseballPlayer();
      athletes.add(soccerPlayer);
      athletes.add(baseballPlayer);
  
      for (Athlete athlete : athletes) {
        athlete.루틴();
      }
  }
}
  • 위 코드는 골격 구현으로 중복을 제거하고 공통된 부분을 WangsimniAthlete으로 추상화한 결과
  • 골격 구현은 기본적으로 상속해서 사용하는 것을 확인할 수 있음

참고

https://velog.io/@injoon2019/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C20.-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%B3%B4%EB%8B%A4%EB%8A%94-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%9A%B0%EC%84%A0%ED%95%98%EB%9D%BC