Language/자바&코틀린

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

코딩공장공장장 2023. 11. 15. 20:26

인터페이스 구현체들의 동일한 행위를 보장한 추상 자료형으로 일종의 계약서 또는 설계서이다.

추상클래스하나 이상의 추상 메서드를 포함한 클래스를 추상 클래스라고 하며 복제와 확장의 목적으로 쓰인다.

(추상 메서드 없이 추상 클래스 선언 가능하나 구체 클래스와 차이가 없음)

 

인터페이스와 추상클래스는 모두 인스턴스화가 불가하고 구현체에서 추상 메서드를 반드시 오버라이딩 해야한다는 공통점이 있다.

허나, 인터페이스와 추상 클래스는 구조적으로 큰 차이가 있고, 이로인해 그 사용성에도 큰 차이점이 있다.

 

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


1. 구현부의 존재여부 : 캡슐화

인터페이스는 메서드의 구현부가 없는 추상 메서드로만 이루어져있다.

이에 반면 추상 클래스는 메서드의 구현부가 존재한다.

"추상 클래스"의 구현부는 하위 클래스에 그대로 복제된다.

코드가 그대로 복제 확장되기에 캡슐화가 깨지며, 구현부에 의존하는 결합도가 높은 구조를 갖게 된다.

추상 클래스에는 메서드의 구현부만 존재하는게 아니라 속성(프로퍼티)까지 존재하기 굉장히 높은 결합도를 유발한다.

구현부와 속성의 변경사항은 모든 하위 클래스에 영향을 끼치게 되는 것이다.

 

"인터페이스" 추상 메서드에만 의존하고 구현부가 존재하지 않으므로 캡슐화가 깨지지 않고, 결합도가 낮은 구조를 가질 수 있다.

추상 클래스와 비교해 상대적으로 상위 타입에 의존하는 자원이 적다.

 

2. 다중상속 여부 : 다이아몬드 문제

인터페이스의 경우 다중 상속이 가능하나, 추상 클래스는 다중 상속이 불가능하다.

(구체 클래스 또한 다중 상속 불가)

이또한 사실 구현부의 존재여부 차이에 의해 파생된 또다른 차이점이다.

 

 

위와 같이 Person이라는 추상 클래스가 존재하고 이를 상속 받은 Father, Mother이라는 구현 클래스가 존재한다고 하자.

Father, Mother은 추상 메서드인 move를 오버라이딩 하였다.

이때 Child가 Father, Mother를 상속 받으려고 한다면 둘 중 누구의 move 메서드가 복제될지 결정할 수 없다.

 

자바는 이러한 구조적 문제로 클래스의 다중 상속을 지원하지 않는다.

 

허나, 인터페이스의 경우 이러한 문제가 생겨나지 않는다.

위와 같이 Father와 Mother가 모두 인터페이스라고 한다면 구현부가 존재하지 않기에

어떤 추상 메서드를 상속 받아도 문제 될일이 없고 Child에서 반드시 오버라이딩을 진행하므로 구조적 문제가 발생하지 않는다.

허나, 자바 8 이후 인터페이스에 등장한 default 메서드의 경우 추상 클래스처럼 다이아몬드 문제가 발생한다.

 

[클래스와 인터페이스 상속시 오버라이딩 우선순위]

 

 

  • [클래스와 인터페이스 상속] -> 클래스 우선
  • [계층 구조의 인터페이스 상속] -> 상속 받는 default 메서드 우선
  • [인터페이스 다중 상속] -> 오버라이딩을 통한 재정의 그렇지 않으면 컴파일 에러

=> 공통적으로 구현부가 있으면 구현부를 우선적으로 상속 받음

 

아래와 같이 명시적으로 누구의 default 메서드를 사용할지 지정할 수 있고 또는 아에 새롭게 오버라이딩할 수 있다.

public class Child implements Father, Uncle {

   @Override

   public void move() {

       Father.super.move();

   }

}

 

참고로 자바8 부터 인터페이스에 static 메서드가 추가됬는데, static 메서드는 다이아몬드 문제에 고려하지 않아도 된다.

static은 애초에 오버라이딩이 없다. 인스턴스화 해서 사용하는게 아니고 Class.move()와 같이 클래스에 직접 접근하니 문제 될게 없다.

 

3. 클래스 폭발 문제

클래스 폭발 문제는 상속에서만 나타나는 현상이다.

여리 기능을 조합해서 사용해야하는 경우 다중 상속이 불가하기에 수직적인 상속관계를 만든 클래스들이 조합의 수 만큼 생성되는 문제이다.

 

예를들어 아래와 같이 a 기능을 가진 A 클래스가 있고 b 기능을 가진 B 클래스가 있다고 할 때,
a, b 기능을 모두 사용하기 위해선 다중 상속이 불가하기에 새로운 AB 클래스를 생성해야하는 것이다.
따라서 a 기능, b 기능, (a, b)기능을 제공할 수 있는 3개의 클래스가 필요 하고,

 

만약에 3가지 기능이 있다면 3C1+3C2+3C3=7으로 가능한 조합을 모두 제공하려면 7개의 클래스가 필요로 한다. 

 

허나, 인터페이스에서는 이러한 패턴이 보이지 않는다.

인터페이스는 합성 패턴을 이용하여 사용하는 곳에서 필요로한 객체를 주입시키는 방식으로 사용할 수 있기 때문이다.

class Caller {
    private final A aImpl;
    private final B bImpl;
    private final C cImpl;

    public Caller(A aImpl, B bImpl, C cImpl) {
        this.aImpl = aImpl;
        this.bImpl = bImpl;
        this.cImpl = cImpl;
    }

    void run() {
        aImpl.runA();
        bImpl.runB();
        cImpl.runC();
    }

}

 

4. is-a 관계와 can-do 관계

추상 클래스의 상속관계는 is-a, 인터페이스의 상속관계를 can-do의 특성을 갖는다.

추상 클래스는 속성과 행위를 하위 구현체에 그대로 물려주는 계층 구조를 형성한다.

인터페이스는 추상메서드만 존재하기에 ‘can-do’라는 행위를 할 수 있는지 여부로만 구현을 결정할 수 있다.

 

인터페이스는 행위의 가능 여부를 따지기에 작게 분리시키는것이 가능하지만,

추상 클래스는 속성과 행위가 하나로 묶여있기에 작게 분리하는 것이 어렵다. 

다중상속이 불가능하기에 계층 구조를 형성해야하고 상위타입의 추상 클래스는 필요한 모든 요소를 갖추고 있어야한다.

 

따라서 추상 클래스를 설계할 때에는 ‘하위 클래스는 상위 클래스이다’라는 엄격한 is-a 관계를 고려해야한다.

추상클래스에서 정의한 속성과 행위, 그리고 하위 구현체에서 오버라이딩 하는 행위들이 모두 일관성있게 정의되야 한다.

인터페이스에 비해 추상 클래스의 is-a관계가 엄격하게 강조되는 이유는

추상 클래스에 정의된 모든 속성과 행위가 계층 구조를 형성하는데 일관성이 있어야 하기 때문이다.

 

[인터페이스와 추상클래스 비교]

  추상 클래스 인터페이스
선언 abstract interface
메서드 제한 없음 추상 메서드
static 메서드(자바8)
default메서드(자바8)
private 메서드(자바9)
변수 제한 없음 상수(static final)만 가능
접근제어자 제한없음 public만 가능(생략가능)
결합도 높음(구현부 존재) 낮음(구현부 미존재)
이슈 - 다이아 몬드 문제
- 클래스 폭발
- 디폴트 메서드의 경우 다이아 몬드 문제

 

상속보다 합성을


합성이란 인터페이스를 통해 의존관계를 맺어 객체를 주입받아 사용하는 방식이다.

인터페이스를 통한 의존관계는 캡슐화가 지켜지고 상속에 비해 결합도가 낮다.

"상속보다 합성을" 이라는 토픽은 인터페이스와 추상클래스에 차이에서 비롯한 주제이다.

위에서 설명했듯이 상속은 클래스를 통한 강한 결합을 갖추기에 변경이 어렵고, 

합성은 추상 메서드에만 의존하므로 변경사항에 유연하게 대처하기 쉽다.

또한 기능 확장시 상속은 클래스 폭발 문제를 일으킬 수 있지만, 합성은 사용하는 객체에 끼워넣는 방식으로 사용하면 되기에 클래스 폭발 문제 또한 발생하지 않는다.

 

합성이 변경사항에 유연한 대처를 할 수 있으므로 상속 보다 합성을 사용하는 것을 추천한다.

 

(코드의 복제가 필요하다면 추상 클래스를 사용하는 것이 좋다.

코드의 복제를 통한 코드 중복제거는 추상 클래스의 가장 큰 장점이자 주 사용 목적이다.)

 

========================================================================================

 

[번외] 자바 인터페이스에default, static, private 메서드가 추가된 이유

default 메서드
첫번째 주요한 이유는 하위 호환이다.
기존에 제공되고 있는 인터페이스의 결함이나 비효율으로 변경이 필요한 경우 기존 메서드를 default 메서드로 변경하여 이전에 작성된 구현체들과 호환을 이루도록 하고, 새로운 추상 메서드를 제공하여 이후의 구현을 강제화 할 수 있다.
 
대표적인 예로 스프링의 ApplicationEventPublisher 있다.
ApplicationEventPublisher의 publishEvent(ApplicationEvent event)메서드는 ApplicationEvent 하위 타입의 객체 대해서만 이벤트를 발행하도록 하였다.
허나, 스프링에서 ApplicationEvent 상속 없이도 모든 객체에 대하여 이벤트를 발행할 수 있는 구조를 제공하고자기존의 publishEvent(ApplicationEvent event)를 default 메서드로 바꿔 이전에 작성된 구현체들과 호환을 이루고 
새롭게 정의한 publish(Object event)를 통해  ApplicationEvent 상속 없이도 사용 가능하게 하였다.

두번째 이유는 선택적 구현 목적이다.
예시를 통해 설명하겠다. 스프링의 HandlerInterceptor는 컨트롤러 아래와 같은 세개의 메서드가 존재한다.
preHandle : 컨트롤러 이전 실행
postHandle : 컨트롤러 정상 처리 이후 실행
afterCompletion : 예외가 터지더라도 컨트롤러 이후 반드시 실행
HandlerInterceptor는 컨트롤러 이전 이후에 실행할 수 있는 로직을 구현할 수 있게 해주는 인터페이스이다.
세개의 메서드 모두 default 메서드로 구현되어있기에 개발자는 로직을 추가할 필요한 시점에 실행될 메서드를 선택적으로 구현할 수 있다.

 

static 메서드
static메서드는 클래스에 고정된 메서드이다. 오버라이딩 가능하지도 않고, 인스턴스를 통해 접근하는 메서드 또한 아니다. 오버라이딩을 통한 하위 구현체에서 다형성을 표현할 수 없는 static 메서드가 굳이 인터페이스에 없을 이유가 없었다. 자바의 지나친 인터페이스 추상화가 static 메서드의 부재를 만들었고 8 이후에 static 메서드가 추가 되었다.

 

private 메서드
private 메서드는 default 메서드와 static 메서드의 관심사를 분리하기 위한 목적으로
default 메서드와 static 메서드에 모든 로직을 구현하면 메서드가 비대해질 수 있으므로 private 메서드를 통해 구현을 분리하기 위하여 자바9 부터 추가 되었다.

 

반응형