자바 - 추상 클래스 vs 인터페이스

2024. 3. 28. 11:21백엔드/자바

자바 - 추상 클래스 vs 인터페이스


추상 클래스 vs 인터페이스 비교

추상 클래스와 인터페이스를 동시에 공부했다면 문득 궁금해진다. '그게 그거 아닌가?' 사실 인터페이스에 default 메서드와 static 메서드가 생기면서 인터페이스가 추상 클래스의 상위 호환이 된 느낌이 없지 않아 있지만 그래도 분명 차이점은 존재한다. 지금부터 추상 클래스와 인터페이스를 비교해 가며 무엇이 같고 무엇이 다른지 살펴보자.

 

가슴이 웅장해진다...

 

  추상 클래스 인터페이스
사용 키워드 abstract interface
사용 가능 변수 제한 없음 static final (상수)
사용 가능 접근 제어자 제한 없음 (public, private, protected, default) public
사용 가능 메소드 제한 없음 abstract method, default method, static method, private method
상속 키워드 extends implements
다중 상속 가능 여부 불가능 가능 (클래스에 다중 구현, 인터페이스 끼리 다중 상속)
공통점 1. 추상 메소드를 가지고 있어야 한다.

2. 인스턴스화 할 수 없다 (new 생성자 사용 X)

3. 인터페이스 혹은 추상 클래스를 상속받아 구현한 구현체의 인스턴스를 사용해야 한다.

4. 인터페이스와 추상클래스를 구현, 상속한 클래스는 추상 메소드를 반드시 구현하여야 한다.

 

추상 클래스 정리

  • 추상 클래스는 하위 클래스들의 공통점을 모아 추상화하여 만든 클래스
  • 추상 클래스는 다중 상속이 불가능하여 단일 상속만 허용한다.
  • 추상 클래스는 추상 메서드 외에 일반 클래스와 같이 일반적인 필드, 메서드, 생성자를 가질 수 있다.
  • 이러한 특징으로, 추상 클래스는 추상화(추상 메서드)를 하면서 중복되는 클래스 멤버들을 통합 및 확장을 할 수 있다.
  • 같은 추상화인 인터페이스와 다른점은, 추상 클래스는 클래스 간의 연관 관계를 구축하는 것에 초점을 둔다.

 

인터페이스 정리

  • 내부의 모든 메서드는 `public abstract`로 정의 (default 메서드 제외)
  • 내부의 모든 필드는 `public static final` 상수
  • 클래스에 다중 구현 지원
  • 인터페이스 끼리는 다중 상속 지원
  • 인터페이스에도 `static`, `default`, `private`제어자를 붙여 클래스 같이 구체적인 메서드를 가질 수 있음. 따라서 하위 멤버의 중복 메서드 통합을 어느 정도 할 수는 있겠지만, 필드는 상수이기 때문에 중복 필드 통합은 불가능
  • 인터페이스는 부모 자식 관계인 상속에 얽매이지 않고, 공통 기능이 필요할때 마다 추상 메서드를  정의해 놓고 구현(implement)하는 식으로 추상 클래스보다 자유롭게 붙였다 뗐다 사용
  • 인터페이스는 클래스와 별도로 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용하는 것에 초점
  • 다중 구현이 된다는 점을 이용해, 내부 멤버가 없는 빈 껍데기 인터페이스를 선언하여 마커 인터페이스로서 이용 가능
  • 보통 `Verbable` 같은 형식으로 인터페이스 네이밍 규칙을 따름

 


추상 클래스 vs 인터페이스 쓰임새

인터페이스나 추상 클래스나 둘 다 똑같이 추상 메서드를 통해 상속/구현을 통한 메서드 강제 구현 규칙을 가지는 추상화 클래스이다.

 

다만 이 둘은 각각 고유한 특징을 몇 가지 가지고 있는데, 이러한 특징으로 쓰임새가 달라지게 된다. 또한 기능적인 부분뿐만 아니라 인터페이스와 추상 클래스가 내포하고 있는 논리적인 의미로서도 사용처가 나뉜다.

 

예를 들어 이 둘은 대표적으로 `다중 상속` 기능 여부의 차이가 있긴 하지만, 중요한 점은 고유 기능보다 사용 목적의 차이에 있다.

 

  • 인터페이스: `implements`라는 키워드처럼 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 기능을 구현하는 느낌
  • 추상 클래스: `extends` 키워드를 사용해서 자신의 기능들을 하위 클래스로 확장시키는 느낌

 

추상클래스를 사용하는 경우

  • 상속 받을 클래스들이 공통으로 가지는 메서드와 필드가 많아 중복 멤버 통합을 할 때
  • 멤버에 public 이외의 접근자(protected, private) 선언이 필요한 경우
  • non-static, non-final 필드 선언이 필요한 경우 (각 인스턴스에서 상태 변경을 위한 메서드가 필요한 경우)
  • 요구사항과 함께 구현 세부 정보의 일부 기능만 지정했을 때(interface의 일부만 구현한 경우)
  • 추상 클래스는 이를 상속할 각 객체들의 공통점을 찾아 추상화시켜 놓은 것으로, 상속 관계를 타고 올라갔을 때 같은 부모 클래스를 상속하며 부모 클래스가 가진 기능들을 구현해야 할 경우 사용한다.
1. 중복 멤버 통합

중복되는 멤버를 통합해 주는 기능은 추상 클래스만의 기능이라기보단 상속의 기능이라 보는 것이 맞다.

 

다만 같은 추상화 개념인 인터페이스와 차이점을 두기 위해, 상수 밖에 정의 못하는 인터페이스에서는 할 수 없는 기능이 추상 클래스 중복 멤버 통합이라고 보면 된다.

 

2. 추상클래스의 다형성 이용 설계

추상 클래스의 다형성이나 인터페이스의 다형성이나 둘 다 클래스 타입을 통합한다는 취지의 기능은 똑같다.

 

다만, 다형성을 사용하는 순서의 차이로 추상 클래스와 인터페이스의 다형성을 분리할 수 있다.

 

이 부분은 이론적인 면이 강해 이해하는 데 어려울 수 있다. 하지만 확실히 이해하면 언제 추상 클래스를 사용하고, 인터페이스를 사용할지 결정을 내리는 데 도움이 됨으로 이해하도록 노력하자.

 

아래의 도식을 보자.

 

 

추상 클래스는 클라이언트(ExamConsole)에서 자료형을 사용하기 전에 미리 논리적인 클래스 상속 구조를 만들어 놓고 사용이 결정되는 느낌이라고 보면 된다.

 

예를 들어 위의 Exam 주제의 논리적인 상속 구조의 객체들을 클라이언트(ExamConsole)에서 다형성으로 통합적으로 받아 사용할 때, 필드로 추상 클래스 타입으로 선언하고 생성자에서 매개변수로 `new NewlecExam()` 혹은 `new YBMExam()` 객체로 받아와 업캐스팅으로 초기화함으로써, 다양한 Exam 자식 클래스들을 다형성으로 다룰 수 있게 된다.

 

public class ExamConsole {
    Exam exam; // 상위 추상 클래스 타입으로 선언
    
    // 생성자 매개 변수로 new NewlecExam() 혹은 new YBMExam() 생성자가 들어와 필드를 초기화
    ExamConsole(Exam e) {
        this.exam = e; // 업캐스팅 초기화
    }
    
    void input() {}
    void print() {}
}

 

정리하자면, 인터페이스나 추상 클래스나 다형성을 이용할 수 있는데, 추상 클래스를 통한 다형성을 이용할 때에는, 부모 추상 클래스와 논리적으로 관련이 있는 자식 클래스를 다룬다는 점에서, 클라이언트와 추상화 객체들은 의미가 있는 관계로 묶여 있다고 볼 수 있다.

 

3. 명확한 계층 구조 추상화

단순한 중복 멤버 제거를 떠나서, 클래스끼리 명확한 계층 구조가 필요할 때도 추상 클래스를 사용한다.

 

공통된 기능 구현이 필요하거나, 공통으로 지켜야 할 규칙이 있을 때 상속을 통해 구조화하여 재정의(Overriding)을 통해 구현한다.

 

이는 기능이라기보단 설계 원칙 이론에 가까운데, 아직 객체 지향의 추상화에 익숙치 않으면 인터페이스와의 차이점이 애매래 잘 와닿지 않을 수도 있다.

 

주목해야 할 부분은 추상 클래스나 인터페이스나 추상 메서드를 이용한 구현 원칙을 강제한다는 점은 같지만, 추상 클래스는 '클래스로서' 클래스와 의미 있는 연관 관계를 구축할 때 라고 보면 된다. 

 

예를 들어 대용량 SMS sender를 구현하는데 여러 통신사들이 다른 통신탑(tower)을 갖고 있어서 접속하는 데 있어 각각 다른 구현이 필요하며(establish Connection With Your Tower), 공통으로 지켜야 할 규칙인 방해 금지 모드(check If Do Not Disturb Mode)도 있는 스펙일 때 먼저 추상 클래스로 공통분모를 추상화로 구현하고, 상속을 통해 여러 통신사 클래스를 확장하여 구현하는 식으로 객체 지향 설계 원칙대로 프로그램을 구성할 수 있다.

 

/* SMS를 보내는 추상화된 코드 */
abstract class SMSSender {

    abstract public void establishConnectionWithYourTower();

    public void sendSMS() {
        establishConnectionWithYourTower();
        checkIfDoNotDisturbMode();
        // ...
        destroyConnectionWithYourTower();
    }

    abstract public void destroyConnectionWithYourTower();

    public void checkIfDoNotDisturbMode() {
        // 추상 클래스 안에서 구현
    }
}

 

/* SMSSender를 통신사 클래스들이 상속 */
class SKT extends SMSSender {
    @Override
    public void establishConnectionWithYourTower() {
        // SKT 방식으로 커넥션 맺기
    }

    @Override
    public void destroyConnectionWithYourTower() {
        // SKT 방식으로 커넥션 종료
    }
}

class LG extends SMSSender {
    @Override
    public void establishConnectionWithYourTower() {
        // LG 방식으로 커넥션 맺기
    }

    @Override
    public void destroyConnectionWithYourTower() {
        // LG 방식으로 커넥션 종료
    }
}

 

인터페이스를 사용하는 경우

  • 어플리케이션의 기능을 정의해야 하지만 그 구현 방식이나 대상에 대해 추상화할 때
  • 서로 관련성이 없는 클래스들을 묶어 주고 싶을 때 (형제 관계)
  • 다중 상속(구현)을 통한 추상화 설계를 해야 할 때
  • 특정 데이터 타입의 행동을 명시하고 싶은데, 어디서 그 행동이 구현되는지 신경 쓰지 않는 경우
  • 클래스와 별로도 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용
 
자유로운 타입 묶음

인터페이스의 가장 큰 특징은 상속에 구애 받지 않은 상속(구현)이 가능하다는 것이다.

 

상속(extends)은 클래스끼리 논리적으로 타입을 묶는데의 의의가 있다면, 구현(implements)은 자유로이 타입을 묶을 수 있다는 것에 의의가 있다. 그래서 서로 논리적이지 않고 관련이 적은 클래스끼리 필요에 의해 형제 타입처럼 묶을 수 있다.

 

다음과 같이 Creature라는 최상위 추상 클래스와 그 하위 추상 클래스인 Animal, Fish 가 있고, 각 추상 클래스를 구체적으로 의미 부여해 구현한 Parrot, Tiger, People 클래스와 Whale 클래스가 있다고 하자.

 

 

위의 상속 관계도를 코드로 표현하면 다음과 같다.

 

// 추상 클래스 (조상 클래스)
abstract class Creature { }

// 추상 클래스 (부모 클래스)
abstract class Animal extends Creature { }
abstract class Fish extends Creature { }

// 자식 클래스
class Parrot extends Animal { }
class Tiger extends Animal { }
class People extends Animal { }

class Whale extends Fish { }

 

이렇게 상속 관계를 설정해 놓고 동작을 하는 메서드를 추가해야 하는데, 만일 수영 동작을 하는 `swimming()` 메서드를 각 자식 클래스에 추가해야 한다고 하자.

 

이때 나중에 확장을 위해 추상화 원칙을 따라야 한다. 그럼 부모나 조상 클래스에 추상 메서드를 추가해야 하는데, 수영은 고래(Whale)와 사람(People)만 할 수 있으니 이를 동시에 포함하는 Creature 추상 클래스에서 추상 메서드를 추가해야 한다. (호랑이와 앵무새는 수영을 못한다고 가정. 고래도 Fish라고 가정.) 

 

하지만 Creature 추상 클래스에 추상 메서드를 추가하면, 곧 이를 상속하는 모든 자손/자식 클래스에서 반드시 메서드를 구체화해야 한다는 규칙 때문에 실제로 수영을 못하는 호랑이(Tiger)와 앵무새(Parrot) 클래스에서도 메서드를 구현해야 하는 강제성이 생긴다.

// 추상 클래스 (조상 클래스)
abstract class Creature { 
	abstract void swimming(); // 수영 동작을 하는 추상 메소드
}

// 추상 클래스 (부모 클래스)
abstract class Animal extends Creature { }
abstract class Fish extends Creature { }

// 자식 클래스
class Parrot extends Animal {
	void swimming() {} // 앵무새는 수영을 할수 없지만 상속 관계로 인해 강제적으로 메소드를 구현해야하는 사태가 일어난다.
}
class Tiger extends Animal {
	void swimming() {} // 호랑이는 수영을 할수 없지만 상속 관계로 인해 강제적으로 메소드를 구현해야하는 사태가 일어난다.
}
class People extends Animal {
	void swimming() {
    	// ...
    } 
}

class Whale extends Fish {
	void swimming() {
		// ...
    } 
}

 

물론 메서드를 선언하기만 하고 빈칸으로 놔두어도 코드는 문제없이 돌아가겠지만, 이는 객체 지향 설계에 위반될 뿐만 아니라 나중에 유지보수면에서도 생산성이 떨어진다.

 

따라서 상속에 얽매이지 않는 인터페이스에 추상 메서드를 선언하고 이를 구현(implements)하면서 자유로운 타입 묶음을 통해 추상화를 이루게 하는 것이다. 

 

abstract class Creature { }

abstract class Animal extends Creature { }
abstract class Fish extends Creature { }

// 수영 동작 추상 메소드를 따로 인터페이스를 만들어 넣는다.
interface Swimmable {
    void swimming();
}

class Tiger extends Animal { }
class Parrot extends Animal { }
class People extends Animal implements Swimmable{ // 인터페이스를 구현함으로써 동작이 필요한 클래스에만 따로 상속에 구애받지않고 묶음
    @Override
    public void swimming() {}
}

class Whale extends Fish implements Swimmable{ // 인터페이스를 구현함으로써 동작이 필요한 클래스에만 따로 상속에 구애받지않고 묶음
    @Override
    public void swimming() {}
}

 

 

이외에도, 날아다니는 동작 메서드나, 말하는 동작 메서드를 각각 인터페이스마다 분리하여 선언하고 이를 각 자식 클래스에 자유롭게 상속시킴으로써 보다 구조화된 객체 지향 설계를 추구할 수 있다.

 

 

abstract class Creature { }

abstract class Animal extends Creature { }
abstract class Fish extends Creature { }

interface Flyable {
    void flying();
}

interface Talkable {
    void talking();
}

interface Swimmable {
    void swimming();
}

class Tiger extends Animal { }
class Parrot extends Animal implements Talkable{
    @Override
    public void talking() {
        
    }
}
class People extends Animal implements Talkable, Swimmable{ // 필요에 따라 적재적소에 다중으로 여러개 추가(구현)이 가능함
    @Override
    public void talking() {}

    @Override
    public void swimming() {}
}

class Whale extends Fish implements Swimmable{
    @Override
    public void swimming() {}
}
 
인터페이스 다형성 이용 설계

 

이 파트는 위의 추상 클래스의 다형성 이용 설계 파트에서 이어진다. 

 

추상 클래스는 클라이언트에서 자료형을 사용하기 전에 미리 논리적인 클래스 상속 구조를 만들어 놓고 사용하는 느낌이라면, 인터페이스는 언제든 상관없이 상황에 맞춰 필요에 따라 구현하고 자유롭게 붙였다 뗐다하는 느낌이다.

 

외부 파일에 데이터를 저장하는 라이브러리를 가져와 사용한다고 가정하자.

 

이 라이브러리 안에는 Filesave 클래스를 이용해 외부 파일로 데이터를 저장한다. 이때 Filesaver 클래스를 보면 필드 변수로 Storable 인터페이스 타입의 객체를 선언하여 `save()` 메서드에서 인터페이스 타입 객체를 이용하는 것을 볼 수 있다.

 

즉, Filesaver 클래스는 구체적인 클래스 타입으로 통신하는 것이 아닌 인터페이스라는 중개타입을 이용하여 통신하는 것이다. 

 

// 외부 파일에 클래스 데이터를 저장 가능하도록 하는 인터페이스
interface Storable {
    // 클래스 데이터를 반환하는 메서드
    int getData();
}

// 외부 파일에 데이터를 저장하는 클래스
class FileSaver {
    Storable target; // 인터페이스를 구현한 객체를 필드로 선언
    
    // FileSaver 생성자, Storable 타입의 객체를 매개변수로 받음
    FileSaver(Storable target) {
        this.target = target; // 주어진 객체를 필드에 할당
    }
    
    // 데이터를 저장하는 메서드
    int save() {
        int data = target.getData(); // Storable 인터페이스의 메서드를 호출하여 데이터를 가져옴
        // 데이터를 저장하는 로직 구현
        // ...
    }
}

 

서로 다른 클래스인 Exam, File, Rectangle 등을 FileSaver 클래스에서 파일로 저장하려면, 이들을 공통 인터페이스로 묶어 타입을 일관성 있게 정의할 수 있다. 이렇게 하면 상속 관계없이도 FileSaver 클래스의 인터페이스 타입 필드로 전달할 수 있어, 유연하게 다형성을 활용할 수 있다.

 

 

class Exam implements Storable { // 인터페이스를 구현함으로서 클래스끼리 형제 관계를 맺음 
    int kor;
    int eng;
    int math;

    void total(){}
    void avg(){}

    // 인터페이스 통신 메소드 구현
    int getData(){
        // ...
    }
}

class File implements Storable { // 인터페이스를 구현함으로서 클래스끼리 형제 관계를 맺음 
    String src;
    
    boolean isFile(){}
    void mkDir(){}
    
    // 인터페이스 통신 메소드 구현
    int getData(){
        // ...
    }
}

 

만일 분석 라이브러리를 사용한다고 했을때 Analyzer 클래스에서 통신으로 사용되는 Calculateable 인터페이스 타입 객체 필드에 Exam 클래스를 전달하기 위해 역시 다중 구현이 가능하다는 점을 이용해 Caculatable 인터페이스를 implements만 하면 된다.

 

 

interface Caculatable {
    void total();
    void avg();
}

class Analyzer {
	Caculatable caculate; // Caculatable 인터페이스 타입으로 필드 선언
    
    void result() {}
    // ...
}

class Exam implements Storable, Caculatable { // 인터페이스를 구현함으로서 클래스끼리 형제 관계를 맺음
    int kor;
    int eng;
    int math;

    void total(){}
    void avg(){}
    int getData(){}
}

 

정리하자면, 인터페이스의 다형성은 부모 자식 클래스와 달리 논리적으로 관련이 없는 별개의 클래스들을 다룬다는 점에서, 상속 관계에 얽매이지 않고 구현(implements)만 하면 자유롭게 다형성을 코드에 적용할 수 있다.

 
마커 인터페이스

자바의 마커 인터페이스는 일반적인 인터페이스와 동일하지만 아무 메서드도 선언하지 않은 빈 껍데기 인터페이스를 말한다.

 

interface inevitable {} // 아무 내용이 없는 빈 껍데기 인터페이스

 

이는 마치 구현체에 대한 역할을 정의하는 인터페이스의 역할을 다하지 못한 것처럼 보인다.

 

마커 인터페이스는 인터페이스가 다중 상속이 가능하다는 점을 이용해 그저 표시하는 용도로 쓰는 것이다.

 

아무 내용도 없어서 쓸모가 없어 보이지만, 마커 인터페이스의 역할은 객체의 타입과 관련된 정보만을 제공해 주는 것이다.

 

따라서 컴파일러와 JVM은 이 마커 인터페이스를 통해 객체에 대한 추가적인 정보를 얻을 수 있다.

 

다음 예시 코드를 보자.

 

class Animal {
    public static void born(Animal a) {
        if(a instanceof Lion) {
            System.out.println("새끼를 낳았습니다.");
        } else if(a instanceof Chicken) {
            System.out.println("알을 낳았습니다.");
        } else if (a instanceof Snake) {
            System.out.println("알을 낳았습니다.");
        }
        // ...
    }
}

class Lion extends Animal { }
class Chicken extends Animal { }
class Snake extends Animal { }

 

하지만 이러한 방식은 자식 클래스의 개수가 많을수록 코드가 난잡하고 길어진다는 단점이 있다.

 

따라서 아무런 내용이 없는 빈 껍데기 인터페이스를 선언하고 적절한 클래스에 implements 시킴으로써, 단순히 타입 체크용으로만 사용하는 것이다.

 

마커 인터페이스를 사용한 아래의 코드를 보자.

 

//새끼를 낳을 수 있다는 표식 역할을 해주는 마커 인터페이스
interface Breedable {}

class Animal {
    public static void born(Animal a) {
        if(a instanceof Breedable) {
            System.out.println("새끼를 낳았습니다.");
        } else {
            System.out.println("알을 낳았습니다.");
        }
    }
}

class Lion extends Animal implements Breedable { }
class Chiken extends Animal { }
class Snake extends Animal { }

 

이러한 마커 인터페이스의 대표적인 자바 인터페이스로는 Serializabe, Cloneable 정도가 있다.

 


 

인터페이스 + 추상 클래스 조합

이처럼 각각 인터페이스와 추상 클래스의 차이점을 알기 위해 각 고유한 특징에 따른 쓰임새에 대해 알아봤지만, 사실 이 둘은 같이 조합되어서도 많이 쓰인다.

 

각각의 고유한 특징인 추상 클래스의 중복 멤버 통합과 인터페이스의 다중 상속 기능을 동시에 사용하기 위해서다. 이에 따라 둘을 같이 사용하는 여러 가지 코드 패턴들이 나왔고, 이것이 디자인 패턴의 근간이 되었다고 보면 된다.

 

추상 클래스에 인터페이스 일부 구현 방법

추상 클래스에 인터페이스를 implements 하고, 인터페이스의 추상 메서드를 아예 구현하지 않거나, 혹은 일부만 구현하는 식으로 통합된 추상화 클래스를 만들 수 있다.

 

interface Animal {
    void walk();
    void run();
    void breed();
}

// Animal 인터페이스를 일부만 구현하는 포유류 추상 클래스
abstract class Mammalia implements Animal {
    public void walk() { ... }
    public void run() { ... }
    // breed() 메서드는 자식 클래스에서 구체적으로 구현하도록 일부로 구현하지 않음
}
 
 // 인터페이스 + 추상 클래스를 상속하여 사용
class Lion extends Mammalia
    @Override
    public void breed() { ... }
}

 

Interface - Abstract - Concrete Class 디자인 패턴

객체 지향 프로그래밍을 해보면 디자인 패턴이라는 단어를 한번쯤은 들어본 적이 있을 것이다.

 

디자인 패턴은 어떤 이론이라기보단 관습에 가깝다. 

 

인터페이스는 정말 강력하지만 아쉽게도 필드는 상수만 사능하여, 중복된 필드가 있을 경우 인터페이스로 해결할 수 없다는 단점이 있다. 이때는 어쩔 수 없이 추상 클래스를 사용하여야 한다. 그렇다고 추상 클래스를 남용하면 단일 상속만 되는 제한 때문에 클래스의 의존성이 커지게 된다.

 

따라서 이러한 서로 간의 제약들을 극복할 방법이 인터페이스 - 추상 클래스 - 클래스 구현 디자인 패턴이다.

 

예를 들어 다른 개발자로부터 다음과 같은 표준화 인터페이스 코드를 전달받았다고 가정하다.

 

interface IShape {
    void setOpacity(double opacity); //도형 투명도 지정
    void setColor(String color); // 도형 색깔 지정
    void draw(); // 도형 그리기
}

 

우리는 이 Shape 인터페이스에 적힌 스펙대로 도형 클래스를 설계해야 한다.

 

그래서 설계에 맞게 다음과 같이 Rectangle 과 Square 클래스를 만들고 인터페이스를 구현하여 추상 메서드를 구체화하였다.

 

interface IShape {
    void setOpacity(double opacity); //도형 투명도 지정
    void setColor(String color); // 도형 색깔 지정
    void draw(); // 도형 그리기
}

// 인터페이스 설계서에 따라 클래스를 구현
class Rectangle implements IShape {
    double opacity; // ! 중복
    String color; // ! 중복
    
    public void setOpacity(double opacity) { // ! 중복
        this.opacity = opacity;
    }
    public void setColor(String color) { // ! 중복
        this.color = color;
    }
    
    public void draw() {
        System.out.println("draw Rectangle with");
        System.out.println(opacity);
        System.out.println(color);
    }
}

// 인터페이스 설계서에 따라 클래스를 구현
class Square implements IShape {
    double opacity; // ! 중복
    String color; // ! 중복
    
    public void setOpacity(double opacity) { // ! 중복
        this.opacity = opacity;
    }
    public void setColor(String color) { // ! 중복
        this.color = color;
    }
    
    public void draw() {
        System.out.println("draw Square with");
        System.out.println(opacity);
        System.out.println(color);
    }
}

public class Pattern {
    public static void main(String[] args) {
        IShape[] rec = { new Rectangle(), new Square() };
        
        rec[0].setOpacity(0.7);
        rec[0].setColor("red);
        rec[0].draw();
        
        rec[1].setOpacity(0.3);
        rec[1].setColor("yellow);
        rec[1].draw();
    }
}

 

하지만 한 눈에 봐도 중복된 코드가 많이 보인다.

 

인터페이스는 기본적으로 중복되는 멤버에 대해 클래스와 같이 묶어주는 역할을 못한다. 인터페이스에 선언할 수 있는 필드는 오직 상수이며, 디폴트 메서드가 있더라도 한계가 있다.

 

이런 문제점을 해결할 수 있는 방법이 인터페이스(interface)와 구체 클래스(concrete class) 중간에 추상 클래스(abstract class)를 하나 두고 공통된 부분을 모아두는 것이다.

 

위의 예제에서 공통된 부분을 추상 클래스로 따로 뽑아 모으면 다음과 같아진다.

 

abstract class Shape implements IShape { // 인터페이스를 상속하는 추상 클래스
    // 중복되는 멤버들을 모아놓고
    protected double opacity;
    protected String color;
    
    public void setOpacity(double opacity) {
        this.opacity = opacity;
    }
    public void setColor(String color) {
        this.color = color;
    }
    
    // void draw(); 는 구체화 x
}

 

그리고 실제 구현 클래스인 Rectangle과 Square 클래스에서는 추상 클래스 Shape를 extends 하고 `draw()` 메서드 부분만 구체화해 주면 된다.

 

class Rectangle extends Shape {
    public void draw() {
        System.out.println("draw Rectangle with");
        System.out.println(opacity);
        System.out.println(color);
    }
}

class Square extends Shape {
    public void draw() {
        System.out.println("draw Rectangle with");
        System.out.println(opacity);
        System.out.println(color);
    }
}

 

이런 식으로 자바에서 제공하는 재료들을 이용하여 각자의 위치에 맞게 여러 조합적인 코드를 짜는 것이 디자인 패턴이다.

 

다만 위의 방법은 중복 제거에 효과적이지만, 클래스 상속을 기반으로 하고 있기 때문에 다른 클래스를 상속받아야 하는 경우엔 이 패턴을 활용할 수 없다는 단점이 존재한다.

 

이런 경우에는 유명한 디자인 패턴인 Adapter 패턴을 이용하기도 한다.

 

 


원본 게시글

https://inpa.tistory.com

 

☕ 인터페이스 vs 추상클래스 용도 차이점 - 완벽 이해

인터페이스 vs 추상클래스 비교 이 글을 찾아보는 독자분들은 아마도 이미 인터페이스와 추상클래스 개념을 학습한 뒤에 이 둘에 대하여 차이의 모호함 때문에 방문 했겠지만, 그래도 다시한번

inpa.tistory.com