JAVA

[JAVA] 추상클래스와 인터페이스의 차이점?

sagecode 2024. 12. 1. 02:10

추상클래스의 경우 이전 글에서 '상속'을 포스트 할 때 설명을 했으니 인터페이스에 대한 설명을 먼저 시작하겠다.

 

1. 인터페이스

인터페이스(Interface)는 개발 코드와 객체가 서로 통신하는 접점 역할을 합니다. 개발 코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출시킵니다. 그렇기 때문에, 개발 코드는 객체 내부의 구조를 알 필요가 없고 인터페이스의 메소드만 알고 있으면 됩니다.

 

  • 인터페이스 선언 : 인터페이스는 클래스와 선언하는 방법이 같다. 클래스는 필드, 생성자, 메소드를 구성 멤버로 가지는데 비해, 인터페이스는 상수 필드와 추상 메소드만을 구성 멤버로 가진다. 또한, 객체를 생성할 수 없기 때문에 생성자를 가질 수 없다.
interface RemoteControl {
// 상수
    public int MIN_VOLUME = 0;
    public int MAX_VOLUME = 10;
    
// 추상메소드
    public void turnOn();
    public void turnOff();
    public void setVolume(int volume);
}

인터페이스는 객체 사용 방법을 정의한다. 상수 필드는 인터페이스에 고정된 값으로 실행 시에 데이터를 바꿀 수 없다.

따라서 인터페이스에 상수필드를 선언할 경우, public static final을 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.

인터페이스를 통해 호출된 메소드는 최종적으로 객체에서 실행되므로, 인터페이스 내의 메소드는 실행 블록이 필요 없는 추상 메소드로 선언한다. public abstract를 생략하더라도 컴파일 과정에서 자동으로 붙게된다.

 

  • 인터페이스 구현 : 코드에서 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 그 때, 객체는 인터페이스에서 정의된 추상 메소드와 동일한 타입의 실체 메소드를 가지고 있어야된다.  그 객체를 구현 객체, 객체를 만드는 클래스를 구현 클래스 라고 한다.

구현 클래스의 경우 보통의 클래스 선언문에 implements를 붙인 다음 뒤에 인터페이스 이름을 작성한다. 그럴경우 그 구현 클래스는 implement한 인터페이스의 추상 메소드와 상수 필드를 사용 가능하다.

public class TV implements RemoteControl {
    private int volume;
    
    public void turnOn() {
    System.out.println("TV를 켠다");
    
    public void turnOff() {
    System.out.println("TV를 끈다")
    
    public void setVolume(int volume) {
    	if (volume > RemoteControl.MAX_VOLUME)
            this.volume = RemoteControl.MAX_VOLUME;
        else if (volume < RemoteControl.MIN_VOLUME)
            this.volume = RemoteControl.MIN_VOLUME;
        else
            this.volume = volume;
        System.out.println("현재 TV 볼륨 : " + this.volume);
    }
}

 

구현 클래스를 작성하면 new 연산자로 객체를 생성할 수 있다. 인터페이스로 구현 객체를 사용하려면 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다.

public class RemoteControlEx {
	public static void main(String[] args) {
    	RemoteControl rc;
        rc = new Television();
        rc = new Audio();
    }
}

 

인터페이스는 한 객체에서 다중으로 구현이 가능하다. 다중 인터페이스를 구현하기 위해선 구현 클래스는 모든 인터페이스의 추상 메소드에 대해 실체 메소드를 작성해야 한다.

public interface Searchable {
	void search(String url);
}

public class SmartTV implements RemoteControl, Searchable {
    private int volume;
    
    // RemoteControl의 추상 메소드에 대한 실체 메소드
    public void turnOn() {
    System.out.println("TV를 켠다");
    
    public void turnOff() {
    System.out.println("TV를 끈다")
    
    public void setVolume(int volume) {
    	if (volume > RemoteControl.MAX_VOLUME)
            this.volume = RemoteControl.MAX_VOLUME;
        else if (volume < RemoteControl.MIN_VOLUME)
            this.volume = RemoteControl.MIN_VOLUME;
        else
            this.volume = volume;
        System.out.println("현재 TV 볼륨 : " + this.volume);
    }
    
    // Searchable의 추상 메소드에 대한 실체 메소드
    public void search(String url) {
    	System.out.println(url + "를 검색합니다.");
    }
}

 

  • 인터페이스 사용
    1. 인터페이스가 필드 타입으로 사용될 경우, 필드에 구현 객체를 대입할 수 있다.
    2. 인터페이스가 생성자의 매개 변수타입으로 사용될 경우, new 연산자로 객체를 생성할 때 구현 객체를 생성자의 매개값으로 대입할 수 있다.
    3. 인터페이스가 로컬 변수 타입으로 사용될 경우, 변수에 구현 객체를 대입할 수 있다.
    4. 인터페이스가 메소드의 매개 변수 타입으로 사용될 경우, 메소드 호출 시 구현 객체를 매개값으로 대입할 수 있다.
public class MyClass {

    RemoteControl rc = new TV(); // 1
    
    MyClass(RemoteControl rc) { // 2
    	this.rc = rc;
    }
    
    void methodA() { // 3
    	RemoteControl rc = new Audio();
    }
    
    void methodB(RemoteControl rc) { // 4
    	...
    }
}

 

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

인터페이스나 추상클래스나 둘이 똑같은 추상 메소드를 통해 상속/구현을 통해서 메소드 강제 구현 규칙을 가지는 추상화 클래스이다. 인터페이스는 각 클래스의 목적에 맞게 기능을 구현하는 느낌이라면, 추상 클래스는 상속을 통해 자신의 기능들을 하위 클래스로 확장 시키는 느낌이다.

 

  • 왜 Java 는 클래스의 다중상속을 지원하지 않는가?

다중 상속을 지원하게 되면 하나의 클래스가 여러 상위 클래스를 상속 받을 수 있습니다. 이런 특징 때문에 발생하게 되는 문제가 있는데, 바로 '다이아몬드 문제' 입니다.

만약 이렇게 Son 클래스가 2개의 Father 클래스를 상속받고 있다면 어떤 부모의 method를 사용해야 될까?

class GrandFather {
    void myMethod() {
    	System.out.println("GrandFather");
    }
}

class FatherA extends GrandFather {
	@Override
    void myMethod() {
    	System.out.println("FatherA");
    }
}

class FatherB extends GrandFather {
	@Override
    void myMethod() {
    	System.out.println("FatherB");
    }
}

class Son extends FatherA, FatherB {
    void myMethod() {
    	super.myMethod();
    }
}

class Son 에서 myMethod를 실행할 경우 당연하게도 컴파일 되지 않지만, 구조적으로 봤을 때, 어떤 부모의 method를 사용해야 되는지 충돌이 생긴다. 그렇기 때문에 Java는 개발자에게 일임하기 보다는 컴파일 단계에서 막고 있으며, 만약 여러가지 기능을 상속하고 싶을 경우 다중상속이 가능한 인터페이스를 사용한다.

interface GrandFather {
    void myMethod();
}

interface FatherA extends GrandFather {
    @Override
    void myMethod();
}

interface FatherB extends GrandFather {
    @Override
    void myMethod();
}

interface Son extends FatherA, FatherB {
    @Override
    void myMethod();
}

인터페이스의 경우에는 선언만 하기 때문에 다이아몬드 문제가 발생하지 않는다.

 

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

추상클래스는 is a ~ .

인터페이스는 is able to (has a) ~

두 방식을 구분하는 이유는 다중상속의 가능 여부에 따라서 용도를 정했다. Java는 특성상 한개의 클래스만 상속이 가능한데, 해당 클래스의 구분(분류)를 추상클래스의 상속을 통해 해결하고, 할 수 있는 기능들을 인터페이스로 구현한다.

추상 클래스와 인터페이스 차이 예시

public abstract class Creature {
	private int age;
    
    public Creature(int age) {
    	this.age = age;
    }
    
    public void age() {
    	age++;
    }
    
    public abstract void attack(); // 추상 메소드
}

모든 생명체가 갖는 공통적인 특성인 age 필드와 1년이 지나면 나이를 한 살 먹는 age() 메소드를 정의하였다.

추상 메소드 attack()의 경우에는 사람이냐 동물이냐에 따라서 다른 결과를 낼 수 있으므로 추상 메소드로 정의하였다.

 

public abstract class Animal extends Creature {
	public Animal(int age) {
    	super(age);
    }
    
    @Override
    public void attack() {
    	System.out.println("몸으로 공격");
    }
}

public interface Talkable {
	abstract void talk();
}

public abstract class Human extends Creature implements Talkable {
	public Human(int age) {
    	super(age);
    }
    
    @Override
    public void attack() {
    	System.out.println("도구를 사용");
    }
    
    @Override
    public void talk() {
    	System.out.println("말을 할 수 있다.");
    }
}

Animal 추상 클래스는 생명체이기 때문에 Creature 추상 클래스를 상속했다. 또한 동물은 몸을 사용하여 공격하기 때문에 attack() 메소드를 오버라이딩 했다. 

Human 추상 클래스도 생명체이기 때문에 Creature 추상 클래스를 상속했다. 하지만 사람은 도구를 사용하여 공격하기 때문에 attack() 메소드를 오버라이딩 했지만 결과물이 Animal과는 다르다. 인간은 동물과 달리 말을 할 수 있으므로 Talkable 인터페이스를 구현하면서 talk() 메소드를 오버라이딩 했다.

 

public interface Flyable {
    void fly();
}

public class Bird extends Animal implements Flyable {
    public Bird(int age) {
    	super(age);
    }
    
    @Override
    public void fly() {
    	System.out.println("날았다");
    }
}

Bird 클래스는 일반클래스이다. 동물 클래스를 상속하고 날 수 있는 동물이기에 Flyable 인터페이스를 구현하여 fly() 메소드를 오버라이드해서 사용한다.

 

public interface Swimable {
	void swim();
}

public interface Programmer {
	void coding();
}

public Fish extends Animal implements Swimable {
    public Fish(int age) {
    	super(age);
    }
    
    @Override
    public void swim() {
    	System.out.println("수영한다");
    }
}

public James extends Animal implements Swimable, Programmer {
    public James(int age) {
    	super(age);
    }
    
    @Override
    public void swim() {
    	System.out.println("수영한다");
    }
    
    @Override
    public void coding() {
    	System.out.println("Hello World!");
    }
}

Fish 클래스도 Animal 클래스 상속, Swimable 인터페이스 구현을 해주면서 swim() 메소드를 오버라이드해서 사용할 수 있다.

James 클래스는 Animal 클래스 상속과 Swimable, Programmer 다중 구현을 통해 수영도 할 수 있고 코딩도 할 수 있다는 것을 구현할 수 있다.

 

결국, 추상 클래스는 다중 상속이 불가능하기 때문에 공통된 특성을 하위 클래스에 완벽하게 상속할 때 사용하고 인터페이스는 여러가지 기능들을 목적에 맞게 구현하고 싶을 때 사용한다.