관리 메뉴

개발노트

이펙티브 자바 규칙 - 추상 클래스 대신 인터페이스를 사용하자 본문

Java

이펙티브 자바 규칙 - 추상 클래스 대신 인터페이스를 사용하자

YoonGwon 2024. 6. 20. 16:53

자바에는 여러 구현을 허용하는 자료형을 만드는 방법이 두가지 있습니다.

 

추상형 클래스와 인터페이스입니다.

 

이 두가지의 차이점은 추상클래스는 구현된 메소드를 포함할 수 있지만, 인터페이스는 아니라는 점입니다... 는 점이었지만

 

자바 1.8부터 default 를 활용하여 언터페이스도 body를 가질 수 있게 되었습니다!

 

public interface Test {
    public void existingMethod();
    default public void newDefaultMethod() {
        System.out.println("New default method is added in interface");
    }
}

덕분에 우리는 공부할 게 늘었습니다.

 

추상형 클래스는 구현하려면 상속(계승)을 써야합니다.

 

자바에서는 멀티계승을 허용하지 않기 때문에 추상형 클래스로 유연한 자료형을 사용하려 한다면 많은 제한이 따르게 됩니다.

 

하지만 인터페이스는 클래스 계층(hierarchy)에 상관없이 복수로 구현이 가능합니다.

 

또한 기존의 클래스를 인터페이스 구현에 추가하는 일도 간단합니다.

 

인터페이스를 만든 후 클래스의 메소드를 확인해서 추가한다음

 

implements 만 붙여 주어 구현하면 됩니다.

 

하지만 상속을 전제로 하는 추상형 클래스에는 제한이 붙게됩니다.

 

 

인터페이스의 장점으로 돌아가보자면

 

public interface Singer {
    AudioClip sing(Song s);
}
public interface Songwriter {
    Song compose(boolean hit);
}

가수와 작곡가 클래스가 있습니다.

 

하지만 가수 중에는 작곡가를 겸하는 분들도 있죠. 

 

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

그럴 때는 인터페이스를 extends 한 형태로 만들어 주면 됩니다.

 

이렇듯 인터페이스는 계층에 상관없이 자료형 프레임워크를 만들 수 있는 아주 유연한 기능을 제공합니다

 

 

만약 이런 자료형을 구현하고 싶은데 인터페이스가 없다면?

 

SingerSongwriter 라는 클래스를 구현하고 그 안에 songwritersinger를 넣어놔야합니다.

 

3개니까 이렇게 끝나지만, 만약 백개쯤 된다면? 

 

조합을 맞춘 클래스가 수천개는 가볍게 넘을 겁니다. 이런 현상을 조합 폭증(combinatiorial explosion)이라고 합니다.

 

이러한 현상을 가볍게 해결해주는 인터페이스는 아주 유용한 도구죠.

 

인터페이스안에서는 메소드 구현을 둘 수 없지만, 그렇다고 프로그래머가 사용할 수 있는 코드를 제공할 방법이 없는 것은 아닙니다.

 

추상 골격 구현(abstract skeletal implementation) 클래스를 중요 인터페이스 마다 두면, 인터페이스의 ㅈ아점과 추상 클래스의 장점을 살릴 수 있습니다.

 

책을 이해한 대로 코드를 한번 만들어봤습니다.

 

public interface A {
    void aa();
    void bb();
}

A는 인터페이스 입니다.

 

public abstract class B implements A {
    @Override
    public void aa(){
        System.out.println("aa");
    }
}

B는 A를 상속하는 추상 클래스입니다.

 

추상클래스이기에 구현을 강제하지는 않습니다만

 

override는 가능합니다. 간단하게 aa를 출력해볼까요.

 

public class C extends B{
@Override
public void bb() {
this.aa();
}
}

이번엔 추상클래스 B를 상속하는 C를 만들어봅시다.

 

B 추상클래스는 bb를 오버라이드 하지 않았기 때문에

 

bb의 오버라이드가 강제됩니다. aa를 호출해봅시다.

 

public class D {
    public static void main(String[] args){
        C c = new C();
        c.aa();
    }
}

그리고 C 객체를 생성해서

aa를 호출해보죠.

 

 

 

정상적으로 aa가 나오게 됩니다.

 

이 코드에서 말하고 싶은 점은

 

C의 aa는 바디가 있는 추상클래스에서, bb는 인터페이스에서 가져옵니다.

 

C가 객체를 인터페이스를 구현하는 효과를 가지지만 구체화 된 바디가 있는 메소드를 그대로 가져올 수 있죠.

 

관습적으로 이러한 골격 ㄱ구현 클래스의 이름을 AbstractInterface라고 정의합니다.

 

자바 1.8버전에는 interface에서 default를 사용할 수 있으니

 

바디가 있는 유연성 있는 구현을 하고싶다면 굳이 이 추상골격구현을 안써도 되겠네요.

방법의 하나로만 알아두기로 합시다.

 

자바의 API에는 이러한 추상골격구현 방법으로 만들어진 클래스도 있다고 합니다

(https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/keyvalue/AbstractMapEntry.html)

 

 

 

 

이펙티브 자바에서 꼽는 인터페이스의 단한가지 단점은 추상클래스가 인터페이스보다 발전시키기 쉽다는 성격입니다.

 

추상클래스의 구현은 계층(hierarchy)에 들어가기 때문에 위에서 메소드를 새로 추가한다면

 

밑의 모든 클래스들은 그 메소드를 사용할 수 있습니다.

 

인터페이스는 인터페이스에 새로운 메소드를 추가하면 밑에서 자료형이 깨지죠. 새로운 메소드를 구현하라고 컴파일 에러를 날리죠.

 

하지만 이것도 옛말이 됬네요.

 

저도 default를 써본적 없으니 한번 써보려고 코드를 짜봤습니다.

 

public interface A {
    void aa();
    void bb();
}

aa, bb라는 메소드를 가지는 A 인터페이스가 있습니다.

 

public class B implements A{
    @Override
    public void aa() {

    }
    @Override
    public void bb() {

    }
}

 

A를 구현하는 B를 만들면, 두개를 오버라이드 하라고 컴파일 에러를 줘서 구현을 해줘야 합니다.

 

여기서 A에 메소드를 추가해볼까요?

 

 

 

 

cc를 추가해봤습니다.

 

 

 

B에서 당연히 컴파일 에러가 나지요?

 

 

 

 

다시 A로 돌아가서

 

 

 

default를 만들고 바디를 붙여줍시다.

 

 

 

 

 

 

 

 

에러가 사라집니다.

 

 

 

 

B에서 구현하지 않더라도

 

새로운 클래스를 만들어서 A인터페이스에서 정의한 내용 그대로 사용이 가능합니다.

 

 

 

이렇듯 public 인터페이스를 구현하는 기존 클래스를 깨뜨리지 않고 

 

새로운 메소드를 인터페이스에 추가할 방법은 없다는 이야기도 옛말이 되어버렸습니다.

 

왜 default를 만들었나 잠깐 생각해보았는데, 추상 골격 클래스를 사용해도

결국 단일 상속만 허용하는 자바에서는 추상골격 클래스도 유연성이 떨어지는 반쪽자리 방법이라 판단했나봅니다.

 

Interface 를 사용하는 방법이 설계상 더 낫다.

왜인지 생각해보자!

아래와 같이 배달앱 서비스가 있다고 가정해보면

public class Restaurant extends Delivery {
    @Override
    public void info() {
        super.info();
    }
}    
public abstract class Delivery {
    public void info(){
        System.out.println("배달주문이 들어왔습니다");
    }
}

원래는 배달 기능만 있었는데 포장 기능이 추가되어야 하는 상황이라면 ?
추상 클래스의 자식 클래스는 하나의 클래스만 extends 할 수 있어서 문제가 생김

인터페이스로 대체

public class Restaurant implements Delibariable, Packable {
  
}
public interface Delibariable {
    default void deliveryOrder(){
        System.out.println("배달주문이 들어왔습니다");
    }
}
public interface Packable {
    default void packOrder(){
        System.out.println("포장 주문이 들어왔습니다");
    }
}

인터페이스에서 default method 사용시,
두 인터페이스의 default method 이름이 같다면 해당 인터페이스를 상속받는 클래스에서 diamond problem 생길 수 있다는 점도 유의해야합니다.

default method가 아니면 해당 문제 발생하지 않음

짚고 넘어가는 diamod problem

Why doesn't the diamond problem occur with interfaces in Java?

The diamond problem is what happens when you inherit multiple implementations for the same method and it’s ambiguous as to which you should choose.
Interfaces don’t result in the same ambiguity because they have no implementations.

Diamond problem은 추상적인 메서드에 대해 여러 구현체가 있을 때 발생함

원래 인터페이스는 구현체가 없기 때문에 이러한 모호성이 발생하지 않음
인터페이스에서 실체가 있는 메서드인 default method 사용시 diamond problem 발생하는 이유

추상 클래스를 이용할 때 diamond problem 발생 예시

abstract class Animal { 
	public asbtract void speak(); 
} 
class Dog extends Animal { 
	public void speak() { 
		print("Woof!"); 
	} 
} 
class Cat extends Animal { 
	public void speak() { 
		print("Meow!"); 
	} 
} 
class Catdog extends Cat, Dog {} 
 
a = new Catdog(); 
a.speak(); // the diamond problem: what does it say? 

 

참조 : https://meaownworld.tistory.com/entry/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EA%B7%9C%EC%B9%99-18-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%8C%80%EC%8B%A0-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90

 

이펙티브 자바 규칙 18 - 추상 클래스 대신 인터페이스를 사용하자

자바에는 여러 구현을 허용하는 자료형을 만드는 방법이 두가지 있습니다. 추상형 클래스와 인터페이스입니다. 이 두가지의 차이점은 추상클래스는 구현된 메소드를 포함할 수 있지만, 인터페

meaownworld.tistory.com

참조 : https://velog.io/@haden/effective-java-5-Abstract-class-%EB%B3%B4%EB%8B%A4%EB%8A%94-Interface%EB%A5%BC-%EC%9A%B0%EC%84%A0%ED%95%98%EB%9D%BC

728x90