추상클래스와 인터페이스 정리

김동규·2024년 5월 9일
0

추상클래스와 인터페이스

인터페이스 (Interface)

  1. 내부의 모든 메서드는 public abstract로 정의 (default 메서드 제외)
  2. 내부의 모든 필트는 public static final = 상수
  3. 클래스에 다중 구현 지원.
  4. 인터페이스끼리는 다중 상속을 지원
  5. 인터페이스에도 static, default, private 접근 제어자를 붙여서 클래스 가이 구체적인 메서드를 가질 수 있다. 하위멤버의 중복 메서드를 일부분 통합할 순 있지만, 필드는 상수이기 때문에 중복 필드 통합은 불가능하다.
  6. 부모 자식 관계인 상속에 얽메이지 않고, 공통 기능이 필요할 때 마다 추상 메서드를 정의 한 후 구현(implement)하는 방식으로 추상 클래스보다 자유롭게 사용할 수 있다.
  7. 클래스와 별도로 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용하는 것에 초점이 맞춰져있음
  8. 다중 구현을 통해, 내부 멤버가 없는 빈 인터페이스를 선언하여 마커 인터페이스로서 이용이 가능
  9. OOOable 형식으로 네이밍 규칙을 따른다.

추상 클래스 (abstract)

  1. 하위 클래스들의 공통점을 모아 추상화 하여 만든 클래스
  2. 다중 상속이 불가능하며, 단일 상속만 허용한다.
  3. 추상 메서드 외에 일반 클래스와 같이 일반적인 필드, 메서드, 생성자를 가질 수 있다.
  4. 추상화(추상 메서드)를 하면서 중복되는 클래스 멤버들을 통합하고 확장을 할 수 있다.
  5. 추상클래스는 클래스 간의 연관 관계를 구축하는 것에 촛점을 둔다.

추상 메서드(abstract method)

추상 메서드(abstract method)란 자식 클래스에서 반드시 오버라이딩 해야만 사용할 수 있는 메서드를 의미한다.

추상 메서드를 선언하여 사용하는 목적은 추상 메서드가 포함된 클래스를 상속받는 자식 클래스가 반드시 추상 메서드를 구현하도록 하기 위함이다.

예로 모듈처럼 중복되는 부분이나, 공통적인 부분은 미리 다 만들어진 것을 사용하고 이를 받아 사용하는 쪽에서 자신에게 필요한 부분만 재정의하여 사용함으로써 생산성이 향상되고 배포등이 수위지기 때문이다.

추상메서드는 선언부만 존재하며, 구현부는 작성되어 있지 않다.
추상 메서드는 작성되지 않은 구현부를 자식클래스에서 오버라이딩 해서 사용하는 것이다.

추상 메서드는 다음과 같은 문법으로 선언한다
abstract 반환타입 메서드명 ();

선언부만 있고 구현부가 없다는 의미로 선언부 끝에 세미콜론(;)을 추가한다. 중괄호 {} 또한 필요없다.

인터페이스와 추상클래스의 각 사용처

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

하지만 둘의 분명한 고유 특징들을 가지고 있으며, 이로 인해서 사용처가 갈리게 된다. 대표적으로 '다중 상속' 기능 여부의 차이가 있지만, 이에 따른 사용 목적이 다르다는 것이 포인트이다.

인터페이스(Interface): implements 라는 키워드처럼 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 기능을 구현하는 느낌

추상 클래스(Abstract): extends 키워드를 사용해서 자신의 기능들을 하위 클래스로 확장 시키는 느낌

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

  1. 상속 받을 클래스들이 공통으로 가지는 메서드와 필드가 많아 중복 멤버 통합을 할 때

  2. 멤버에 public 이외의 접근자(protected, private) 선언이 필요한 경우

  3. non-static, non-final 필드 선언이 필요한 경우 (각 인스턴스에서 상태 변경을 위한 메서드가 필요한 경우)

  4. 요구 사항과 함께 구현 세부 정보의 일부 기능만 지정했을 때

  5. 하위 클래스가 오버라이드하여 재정의하는 기능들을 공유하기 위한 상속 개념을 사용할 때

  6. 추상클래스는 이를 상속할 각 객체들의 공통점을 찾아서 추상시켜 놓은 것으로, 상속 관계를 타고 올라 갔을 때 같은 부모 클래스를 상속하며 부모 클래스가 가진 기능들을 구현해야 할 경우에 사용한다.

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

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

다만, 추상클래스는 미리 논리적인 클래스 상속 구조를 만들어 놓고 사용이 결정되는 느낌이 강하다.

결론적으로 추상클래스를 통한 다형성을 이용할땐, 부모 추상 클래스와 논리적으로 관련이 있는 확장된 자식 클래스를 다루는 점에서 의미적으로 관계가 묶여있다고 볼 수 있다.

명확한 계층 구조 추상화

중복된 멤버를 제거하는 것을 떠나서, 클래스끼리의 명확한 계층 구조가 필요한 경우에도 추상 클래스를 사용한다.

추상클래스나 인터페이스나 추상 매서드를 이용한 구현 원칙을 강제하는 것은 같지만, 추상클래스는 결국 '클래스'이며 클래스와 의미있는 연관관계를 구축할때 사영된다 볼 수 있다.

의미있는 연관 관계란, 부모와 자식 간의 논리적으로 묶인 관계라 볼 수 있다. 개, 고양이, 사자 를 동물이라고 관계로 묶는 것 과 같이, 단어 그 자체의 논리성과 의미성이 있는 연관 관계로 연결지으면 된다.

추상 클래스 직접 사용 예시

가게를 운영한다고 가정하고 MarketStaff 추상 클래스는 Manager와 PartTimer의 하위 클래스를 가지고 있다.

매니저와 아르바이트생은 각각 이름과, 직함이 있으므로 상위클래스인 추상 클래스의 필드로 공통적으로 선언해준다.

일반 메서드인 greeting()의 경우 추상클래스에서 추상 매서드와 다르게 구현 내용이 포함되어 있어야 한다. 매니저든, 알바생이든 "안녕하세요"라고 인사하는 것은 공통의 동작이므로 추상메서드가 아닌 일반 메서드로 구현한다. 또한 하위 클래스에서 이를 Overriding하여 재정의 할 수 있고, 그대로 쓸 수도 있다.

추상 메서드인 working(); 과 cleaning();의 경우 매니저와 알바생 모두 하는 일들이지만, 직함이 다르므로 다른 업무를 할 것이라고 여겨지기 때문에 구현체가 없는 추상 메서드로 구현을 한다. 하위 클래스의 경우 상위클래스의 추상 메서드들을 반드시 Override 하여 구현체를 정의해야한다.

추상 클래스에서 일반 메서드를 구현하는 이유

  1. 공통된 동작의 구현: 추상 클래스는 하위 클래스들이 공통으로 사용하는 동작을 구현할 수 있다. 이는 반복되는 코드를 피하여 코드의 재사용성을 높이며, 공통된 동작을 중복해서 하위의 클래스에서 구현하지 않기 때문에 코드를 간결하게 유지가 가능하다.

  2. 확장성과 유연성 제공: 추상클래스의 일반 메서드는 하위 클래스에서 필요에 따라 재정의(Override)할 수 있다. 이는 하위클래스의 기본 동작을 변경하거나 확장의 유연성을 제공한다. 또한, 추상 클래스의 일반 메서드를 상속 받을 때 구현체가 똑같으므로 공통되어 코드의 일관성을 유지할 수 있게 한다.

  3. 구현 세부 사항 은닉: 추상 클래스의 일반 메서드는 하위 클래스에서 구현되지 않는 경우도 있다. 이는 상위 클래스에서 필요한 기능을 추상화하여 하위 클래스에 구현 세부 사항을 숨기는 데 도움을 준다. 이러한 추상화는 클래스 간의 결합도를 낮추고 코드를 더 관리하기 쉽게 만듭니다.

결론적으로 추상 클래스에 일반 메서드를 정의함으로써 코드의 재사용성, 유연성, 유지보수성을 높일 수 있습니다. 이는 객체 지향 프로그래밍의 원칙 중 하나인 코드 재사용성과 모듈화를 따르게 되는 방향이다.

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

  1. 어플리케이션의 기능을 정의해야 하지만, 그 구현 방식이나 대상에 대해 추상화를 할 때

  2. 서로 관련성이 없는 클래스들을 묶어주고 싶을 때 (형제관계)

  3. 다중 상속(구현)을 통한 추상화 설계를 해야할 때

  4. 특정 데이터 타입의 행동을 명시하고 싶지만, 어디서 그 행동이 구현되는지는 신경쓰지 않는 경우

  5. 클래스와 별도로 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용한다.

자유로운 타입 묶음

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

상속(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() 메서드를 각 자식 클래스에 추가하려 할 경우 나중에 확장을 위한 추상화 원칙 을 따라야 한다고 한다. 그러면 부모 클래스인 Animal이나 조상 클래스인 Creature에 추상 메서드를 추가해야 하는데, swimming은 고래와 사람만 할 수 있기 때문에 이둘을 포함하는 조상 클래스인 Creature에 추상 메서드를 추가해야 한다.

하지만 조상 클래스인 Creature에 추상 메서드를 추가하게 될 경우, 모든 자손/ 자식 클래스에서 반드시 추상 메서드를 구현해야 하는 규칙 때문에 수영이 불가능한 앵무새나 호랑이도 swimming 메서드를 구체화 해야하는 일이 생겨버린다. 당연히 일반 메서드로 넣기에도 swimming 자체를 할 수 없는 종들이기에 문제가 생긴다.

메서드를 선언하기만 하고 빈칸으로 둘 수 있으나, 이는 객체 지향적 설계에 위반될 뿐 아니라 유지보수 측면에서도 좋지 않다.

이를 해결하기 위해 인터페이스에 추상 메서드를 선언하고 이를 구현(implements)하면 자유로운 타입 묶음을 통해 추상화를 이룰 수 있다.


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() {}
}

이렇듯 사람과 고래의 공통된 매서드를 인터페이스를 통해 자유롭게 자식 클래스에 상속을 시킴으로써 구조화된 객체 지향 설계를 추구할 수 있다.

인터페이스 다형성 이용 설계

추상클래스의 경우 클라이언트에서 자료형을 사용하기 전 미리 논리적으로 클래스의 상속 구조를 만들어 놓은 후 사용을 결정하는 느낌이면, 인터페이스의 경우 반대로 미리 만들든 나중에 만들든 그때 필요에 따라 구현해서 자유롭게 붙이거나 떼어낼 수 있는 느낌으로 볼 수 있다.

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

마커 인터페이스

일반적인 인터페이스와 같지만, 사실상 아무 메서드도 선언하지 않은 빈 껍데이 인터페이스를 말한다.

}

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

마커 인터페이스 예시

상위 클래스 Animal을 만들고 하위로 Lion, Chicken 등 여러가지 동물 클래스들을 만들어 상속을 시켰다. 이때 born() 매서드의 경우 Animal 타입의 매개변수를 받고 새끼를 낳는지, 알을 낳는지, instanceof 연산자로 클래스 타입을 구분했다.

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 Chicken extends Animal { }
class Snake extends Animal { }

인터페이스 + 추상클래스 조합하여 사용

추상클래스와 인터페이스는 같이 조합되어 많이 사용되기도 한다. 가장 큰 특징이라고 볼 수 있는 추상 클래스의 중복 멤버 통합 + 인터페이스의 다중 상속 기능 을 동시에 사용하기 위함이다. 이 둘을 같이 사용하는 여러 코드 패턴들이 나왔고, 이것이 디자인 패턴의 근간이 되었다 볼 수 있다.

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

추상클래스에 인터페이스를 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 Rectangle 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(); 는 구체화 안함
}

실제 구현 클래스인 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);
    }

이런 방식으로 자바에서 제공하는 재료를 각자의 위치에 맞게 조합적인 코드를 짜는 것이 디자인 패턴이라고 한다.

위의 패턴은 중복제거에는 효과적이지만, 클래스 상속을 기반으로 하고 있으므로 다른 클래스를 상속 받아야 하는 경우 이 패턴을 못쓴다는 단점이 있다.

참조 출처: https://inpa.tistory.com/entry/JAVA-☕-인터페이스-vs-추상클래스-차이점-완벽-이해하기 [Inpa Dev 👨‍💻:티스토리]

profile
안녕하세요~

0개의 댓글