Java

추상클래스, 인터페이스의 문법적,의미적 차이

shininghyunho 2024. 5. 22. 12:50

클래스는 아무것도 붙지않은 일반 클래스

abstart 가 붙은 추상 클래스

interface 가 붙은 인터페이스로 구분됩니다. (엄격하게 따지면 인터페이스는 클래스가 아닙니다.)

 

그래서 일반 클래스, 추상 클래스, 인터페이스의 차이점을 명확하게 해야합니다.

문법적인 차이 설명

  • 일반 클래스
    • 홀로 독자적인 객체를 가질 수 있음.
    • 자식 클래스는 부모의 메서드 사용가능.
    • 부모의 메서드 오버라이드 가능함.(필수x)
    • 자식 메서드로 자식 클래스가 만들지 않은 부모 클래스도 사용 가능함.
    • extends 로 상속.
  • 추상 클래스
    • 추상 메서드가 존재함.
    • 일반 메서드도 존재 가능함.
    • 홀로 독자적인 객체를 가지지 못함.
    • public, protected, private 변수 가능. (static, final 필수 x)
    • 자식 클래스는 부모의 추상 메서드를 반드시 구현해야함. (필수 o)
    • 일반 메서드와 오버라이드한 추상 메서드가 공존.
    • extends 로 상속.
  • 인터페이스
    • 추상 메서드만 존재함.
      (단 자바 8 이후에 디폴트 메서드가 추가됨.
      일반 메서드처럼 사용.)
    • public static final 변수만 가능함.
    • 추상 클래스인데 일반 메서드는 없이 추상 메서드만 가진다고 생각하면 됨.
    • implements 로 구현.

즉 엄청 간단하게 말해서

  • 일반 클래스 : 추상 메서드 x
  • 추상 클래스 : 추상 메서드 o
  • 인터페이스 : only 추상 메서드, public static final 변수

이다.

 

추상메서드는 반드시 구현해줘야하는 특징이 있습니다.

그래서 여러개의 클래스(인터페이스)로부터 추상 메서드를 구현해야한다면

이름만 중복되기 때문에 문제가 안생깁니다.

어느 인터페이스의 메서드를 구현하는지는 중요하지 않죠.

interface Picture {
    void sound();
}
interface Video {
    void sound();
}
class Phone implements Picture, Video {
    @Override
    public void sound() {
        System.out.println("김치~");
    }
}
public class 다중상속 {
    public static void main(String[] args) {
        Phone phone=new Phone();
        phone.sound();
    }
}
/*출력결과
김치~
*/

 

 

그러나 일반 메서드가 포함되는 일반 클래스와 추상 클래스에서는 다릅니다.

두개 클래스의 메서드가 구현되어있을때는

메인 메서드에서는 어떤 메서드를 호출해야할까요?

abstract class Picture {
    void sound() {
        System.out.println("찰칵");
    }
}
abstract class Video {
    void sound() {
        System.out.println("띨롱");
    }
}
class Phone extends Picture,Video { // 다중상속이 불가능!
}
public class 다중상속 {
    public static void main(String[] args) {
        Phone phone=new Phone();
        phone.sound(); // 찰칵? 띨롱?
    }
}
/*
컴파일 불가
*/

 

즉 일반 메서드 때문에 여러개의 클래스로부터 상속이 불가능해집니다.

폰 사운드를 뭘로 해야할지 난감하죠.

그래서 이러한 상황을 원천 차단하고자 일반 메서드가 포함된

일반 클래스, 추상 클래스는 다중 상속 자체가 불가능합니다.

 

디폴트 메서드

자바 8부터 갑자기 인터페이스의 구현이 되어있는 메서드가 등장합니다.

이 디폴트 메서드는 인터페이스의 추상메서드와 다르게 구현이 되어있어

굳이 재정의할 필요가 없습니다.

interface Picture {
    default void sound() {
        System.out.println("찰칵 찰칵");
    }
}
class Phone implements Picture {
    // sound 구현 안해도 됨.
}
public class 다중상속 {
    public static void main(String[] args) {
        Phone phone=new Phone();
        phone.sound();
    }
}
/*출력결과
찰칵 찰칵
*/

 

즉 일반 메서드와 큰 차이가 없어진거죠.

다시 다중상속의 문제가 생겨버립니다.

위처럼 한개의 디폴트 메서드만 있을때는 상관이 없는데

디폴트 메서드의 이름이 겹친다면 추상 메서드처럼 반드시 구현을 해줘야합니다.

 

기능적인 설명

그럼 언제 추상 클래스, 인터페이스를 써야하지? 기준이 뭐야?

사실 문법적인면만 보면 언제 어떻게 사용해야할지가 헷갈립니다.

 

위에서는 단순히 문법적인 차이가 어떤지를 설명했지만

명확한 사용처가 있습니다.

 

목적

추상 클래스의 목적은 코드 재사용입니다.

그래서 상속했을때 중복되고 공통된 기능을 재구현할 필요가 없어집니다.

일반 클래스도 공통된 기능을 한다고 생각할 수 있지만

추상 메서드를 통해 다형성의 특징을 사용할 수도 있습니다.

 

인터페이스의 목적은 기능 구현에 대한 약속입니다. (프로토콜이라고도 합니다.)

해당 클래스가 반드시 어떤 기능을 해야한다면 인터페이스를 통해

기능 구현을 강제화 할 수 있습니다.

 

관계

관계라는 관점에서 본다면,

추상 클래스는 상속한다는 측면에서 is-a 관계입니다.

벤다이어그램을 그려 포함 관계에 있을때 사용됩니다.

 

여러 자동차중에 현대,대우 종류가 있을것입니다.

이때 추상 클래스는 자동차가 될것이고

이를 상속한 클래스는 현대, 대우가 될것입니다.

한글로 풀이하면 현대,대우는 자동차다. (The Hyundai is a car. 반대로 Car is the Hyundai는 성립이 안됩니다.)

 

인터페이스는 has-a 관계입니다.

해당 기능을 구현해야 할때 사용합니다.

어떤 클래스가 가지는 기능을 정의할때 인터페이스를 사용합니다.

 

자동차라면 움직여야하고 멈출 수 있어야합니다.

그래서 움직임, 멈춤이 인터페이스로 사용될 수 있습니다.

 

아래의 예시를 살펴보면 어떤 느낌인지 확 와닿을것입니다.

 

먼저 기능을 설명하는 인터페이스입니다.

/**
 * 기능에 초첨을 맞춘 interface
 * 구현체와 has-a 관계를 가짐.
 * "구현체는 해당 인터페이스 기능이 있습니다." 를 의미
 */
interface Rushable {
    int speed = 10; // 반드시 public static final (명시안해도 설정되있음)
    void moveForward(); // 반드시 public
    void moveBackward();
}
interface Stopable {
    void slowStop();
    void suddenStop();
}

 

다음은 해당 기능을 갖는 추상 클래스 정의입니다.

(인터페이스가 인터페이스를, 추상 클래스가 다른 추상 클래스,인터페이스를 구현/상속이 가능합니다.)

인터페이스를 정의하면 해당 기능을 갖는 다른 클래스도 정의가 가능해집니다.

/**
 * 코드 재사용에 초점을 맞춘 abstract
 * 구현체와 is-a 관계를 가짐.
 * "구현체는 해당 추상 클래스의 종류 중 하나입니다." 를 의미
 */
abstract class Car implements Rushable,Stopable {
    public int carNumber = 0; // static, final 이 아니어 됨
    protected int serialNumber; // 변수의 제한자는 protected, private 도 모두 가능
    public Car(int serialNumber) {
        setSerialNumber(serialNumber);
    }
    private void setSerialNumber(int serialNumber) { // private 메서드 가능, abstract 클래스에서만 사용
        this.serialNumber=serialNumber;
    }
    protected void printSerialNumber() { // 메서드의 제한자 protected, private 도 모두 가능
        System.out.println("시리얼 번호 : "+serialNumber);
    }
    abstract void setCarNumber(int carNumber); // 추상 메서드를 사용해 구현 강제화, 예를들어 제조사마다 랜덤함수 다르게 구현
}
// 비행기도 움직임, 멈춤의 기능을 가집니다.
abstract class Airplane implements Rushable,Stopable {
    public int flightNumber = 0;
}

 

 

이를 상속한 구현체 현대와 대우입니다.

실제로 어떤 기능을 하는지는 이제 인터페이스와 추상클래스를 확인해야합니다.

해당 추상 메서드를 재정의 하지 않는다면 컴파일이 불가능합니다.

(구현은 비워뒀습니다.)

class Hyundai extends Car {
    public Hyundai(int serialNumber) {
        super(serialNumber);
    }

    @Override
    public void moveForward() {

    }

    @Override
    public void moveBackward() {

    }

    @Override
    public void slowStop() {

    }

    @Override
    public void suddenStop() {

    }

    @Override
    void setCarNumber(int carNumber) {

    }
}
class Daewoo extends Car {
    public Daewoo(int serialNumber) {
        super(serialNumber);
    }

    @Override
    public void moveForward() {

    }

    @Override
    public void moveBackward() {

    }

    @Override
    public void slowStop() {

    }

    @Override
    public void suddenStop() {

    }

    @Override
    void setCarNumber(int carNumber) {

    }
}

 

메인 함수에서 호출한다면 다음과 같이 사용될 수 있습니다.

자동차의 움직임, 멈춤 기능이 있는 Car 타입의 현대와 대우가 있습니다.

이 두 객체는 Car 추상 클래스를 통해, 시리얼 넘버 호출 기능을 재사용하여 호출이 가능합니다.

public class Main {
    public static void main(String[] args) {
        Car hyundai=new Hyundai(123);
        hyundai.printSerialNumber();
        Car daewoo=new Daewoo(456);
        daewoo.printSerialNumber();
    }
}
/* 출력결과
시리얼 번호 : 123
시리얼 번호 : 456
*/

그러면 무조건 추상화는 좋은가?

MVC 패턴으로 Service 계층을 구현할때

어떤 책에서는 CarService 인터페이스를 만들고

CarServiceImpl 구현체를 만들라고합니다.

 

해당 기능을 강제화한다는 측면에서 맞는거같습니다.

그러나 막상 구현하면 CarService는 CarRepository를 호출하는 역할을 하기에

그 구현이 여러버전으로 만들필요가 없습니다. 대체될일이 없죠.

이럴때는 굳이 인터페이스를 만들 필요가 없다고 생각합니다.

 

또한 설계적인 측면에서도 과도하게 추상화를 진행한다면

매우 많은 추상클래스와 인터페이스가 생겨나 오히려 복잡성만 증가시킵니다.

성능또한 클래스가 많아질수록 단계가 늘어나 악영향을 끼치죠.

 

그래서 추상화는 추상화 클래스와 인터페이스의 본질적인 목적을 생각하고 사용해야합니다.

 

참고

Interfaces (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance) (oracle.com)

[Java] 언제 추상 클래스(Abstract Class) 또는 인터페이스(Interface)를 사용해야 하는가? - MangKyu's Diary (tistory.com)

'Java' 카테고리의 다른 글

[객체지향설계원칙] LSP  (0) 2024.05.29
[객체지향설계원칙] OCP  (0) 2024.05.23
[객체지향설계원칙] SRP  (0) 2024.05.14
객체지향을 사용하는 이유 (절차지향과 비교)  (0) 2024.04.28
GC는 어떻게 이루어지는가?  (0) 2024.01.29