Java

[Java] 추상 클래스와 인터페이스의 개념과 차이점

nooblette 2023. 11. 15. 22:34

목차

    배경

    최근 Java 언어에 대한 기본기가 부족하다고 생각해서 기본서를 다시 읽어보며 그 개념을 학습하는 중인데, (이전글 참고) 이번에는 그 중 추상 클래스와 인터페이스의 개념 그리고 차이점에 대해서 살펴보고 정리한 내용을 공유하였다. 실제로 신입 개발자 면접에서 단골 질문으로도 나오는 개념임에도 불구하고 기본 개념을 다시 훑어보고 고민하다보니 그동안 나는 이 두 개념과 그 차이점에 대해 잘못 이해했던 부분이 있었던 것 같고, 실제로 이러한 오해들로 정리된 글도 많이 봐왔던 것 같아 이번 기회에 다시 정리해보고자 한다.

     

    내가 잘못 이해했던 부분과 추상 클래스와 인터페이스의 차이점에 대해 설명하기에 앞서, 두 개념에 대해 간략하게 설명하고 넘어가는게 좋을 것 같다.

    추상 클래스

    추상화된 클래스라는 의미인데, 여기서 추상화란 객체들간 공통적인 필드와 메서드만을 추출하여 일반화한 클래스 정도로 설명할 수 있을 것 같다. 아래 이미지에서 Animal 클래스는 Dog, Cat, Squirrel의 부모이자 추상 클래스 역할을 한다.

     

    추상 클래스는 구체적인 구현 클래스의 부모 역할을 하며, 추상 클래스 그 자체로는 인스턴스화 될 수 없고 반드시 상속을 통해 구현 클래스를 작성해야한다. 추상 클래스를 바로 new 연산자로 인스턴스화 하면 다음과 같은 ‘추상클래스’ is abstract; cannot be instantiated 컴파일 에러가 발생한다.

     

    추상 클래스를 선언하는 방법은 class 앞에 abstract 키워드를 선언하면 된다.

    public abstract class Animal {
    	...
    }

     

    이렇게 선언된 추상 클래스는 extends 키워드를 통해 자식 클래스에서 상속받아 사용할 수 있다. 

    public class Dog extends Animal {
    	...
    }

     

    자식 클래스 Dog는 실제 자바 메모리 상으로 아래 이미지와 같이 존재한다. 따라서 자식 클래스를 생성할때 추상 클래스의 생성자를 먼저 호출한다. 이 때, 자식 클래스의 생성자에 부모 클래스의 생성자를 별도로 작성하지 않으면 컴파일러가 자동으로 부모 클래스의 생성자를 호출하는 super() 메서드를 가장 상단에 추가하게 되는데, 이는 부모 클래스의 매개변수 없는 기본 생성자를 호출한다. 만일 부모 클래스인 추상 클래스의 생성자가 매개변수를 필요로 한다면 개발자가 직접 super() 메서드를 자식 클래스의 가장 상단에 추가하고 매개변수를 넘겨주어야한다.

    추상 메서드

    추상 메서드는 자식 클래스의 공통 메서드라는 것만 정의할 뿐, 다른 어떠한 동작을 수행하지 않는다. 다시 말해 메서드의 실행 내용을 가지지 않으며 중괄호{} 도 없고 메서드 body가 비어있어야 한다. 아래 이미지에서 Animal 추상 클래스의 eat()과 sleep() 메서드는 모든 동물들의 공통 역할을 수행하므로 추상 메서드가 된다.

    public abstract class Animal {
    	// 추상 메서드 선언
    	abstract void eat();
     	abstract void sleep();
    }

     

    추상 메서드에 중괄호나 함수 body를 작성하면 Abstracts methods cannot have a body 컴파일 에러가 발생한다. 

    이러한 추상 메서드는 자식 클래스에서 Override하여 재정의해서 사용할 수 있다. 이때 @Override 어노테이션을 붙여주어 메서드 오버라이딩을 명시한다. @Override 어노테이션은 컴파일시 메서드 오버라이딩 규칙을 준수하였는지 체크하여 휴먼에러를 방지하는 역할을 한다.

    인터페이스

    인터페이스의 역할과 용도는 다형성과 밀접한 관련이 있는데, 그 개념은 다음과 같다.

    (추상 클래스도 상속을 통해 다형성을 구현해줄 수는 있지만 실제로는 인터페이스가 다형성 구현의 주요한 기술로 사용된다.)

     

    다형성

    다형성(polymorphism, 多形性)이란 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미한다.

    어떤 기능이 상황에 따라 여러 형태를 가질 수 있다는 말은 동일한 사용 방법을 통해 기능을 동작했음에도 상황에 따라 다른 결과를 낳는다는 의미가 되는데, 이를 조금 더 프로그래밍 관점에서 얘기해보자면 동일한 메서드(= 동일한 사용방법)를 호출하여도 메서드의 구현 내용(= 상황에 따라)에 따라 다른 동작(= 기능의 다른 형태)을 낳는다는 의미이다.

     

    인터페이스는 다음과 같이 두개의 접근 제한으로 선언할 수 있다. 첫번째 선언은 default 접근 제한자로 인터페이스를 선언한 패키지와 같은 패키지에서만 접근할 수 있으며, 두번째 선언은 모든 클래스에서 접근 할 수 있다.

    interface 인터페이스명 {
    	...
    }
    
    public interface 인터페이스명 {
    	...
    }

    즉, 인터페이스는 프로그래밍에서 두 객체를 연결하고 다형성을 구현하는 기술로 사용된다. 

    구현 클래스 선언

    인터페이스는 추상클래스와 같이 그 자체로 인스턴스화 할 수 없으며 이를 구현하는 클래스(= 구현 클래스)를 선언하여 사용해야한다. 구현 클래스 선언은 다음과 같이 implements 키워드를 통해 선언한다.

    public class A implements Foo{
    	...
    }

     

    인터페이스도 하나의 타입이므로 참조 변수의 타입으로 사용할 수 있다. (그리고 참조 변수이기 때문에 아무 힙 영역도 참조하고 있지 않다는 의미로 null을 대입할 수 있다.)

     

    인터페이스는 추상 클래스와 달리 다중 구현이 가능하다(java에서는 다중 상속을 지원하지 않아 추상 클래스는 다중 상속을 받을 수 없다.) 다중 구현은 implements 키워드 뒤에 구현할 인터페이스를 나열하면 되며, 이 때 구현할 대상이 되는 각 인터페이스의 모든 메서드를 구현 클래스에서 재정의 해줘야 한다.

    public class ClassName implements InterfaceA, InterfaceB {
    	// 모든 추상 메서드 오버라이딩
    }
    

     

    이렇게 구현한 클래스는 다음과 같이 각 인터페이스의 참조 변수에 대입할 수 있다.

    IntefaceA interfaceA = new ClassName();
    IntefaceB interfaceB = new ClassName();
    

     

    추상 메서드

    인터페이스의 필드와 메서드는 public으로만 접근 제한자를 갖는다. 그 이유는 인터페이스는 구체적인 구현 클래스가 어떤 인터페이스를 구현할때 특정한 규약(메서드 시그니처)을 따르도록 강제하여 동일한 역할을 수행할 수 있음을 보장하기 위해 존재하기 때문이다. 모든 메서드와 필드가 기본적으로 public으로 선언되고 만일 접근제한자를 지정하지 않으면 javac가 자동으로 public 접근 제한자(필드일 경우 상수 public static final로)로 지정하여 선언한다.(즉, public 키워드는 생략할 수 있다.)

    public interface InterfaceA {
    	// 추상 메서드
    	void methodA();
    }

     

    인터페이스에서 선언한 추상 메서드는 구현 클래스에서 Override하여 사용할 수 있다.

    public class InterfaceAImpl {
    	// 추상 메서드 구현
    	@Override
    	void methodA() {
        	System.out.println("methodA call");
        }
    }

     

     

    java 9 이후부터 private과 private static 메서드도 인터페이스에 선언할 수 있지만, 이를 제공하는 이유는 인터페이스를 조금 더 유연하게(코드 중복을 줄인다든지..) 사용하기 위함이지, 인터페이스의 존재 목적이 달라지는 것은 아니라고 생각한다.

    public interface Service {
    	// 디폴트 메서드
    	default void defaultMethod1() {
    		System.out.println("defaultMethod1 메서드");
    		commonPrivateMethod();
    	}
    
    	default void defaultMethod2() {
    		System.out.println("defaultMethod2 메서드");
    		commonPrivateMethod();
    	}
        
    	// private 메서드
    	private void commonPrivateMethod(){
    		System.out.println("commonPrivateMethod");
    	}
    }

     

     

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

    우선 추상 클래스와 인터페이스에 차이에 대해 내가 잘못 이해했던 내용과 정정한 내용은 다음과 같다.

     

    • 오해 1. 추상클래스는 미완성 클래스(일부는 구체적인 메서드로, 일부는 추상 메서드로 구현)이며 인터페이스는 전부 추상 메서드로 구현되어 있어 구현 클래스에서 모두 구현해주어야한다?
      → java 8부터 인터페이스에 default 키워드를 제공하여 인터페이스에서도 모든 구현 클래스에서 동일한 동작을 하는 인스턴스 메서드는 정의할 수 있다.
    • 오해 2. 인터페이스는 public 접근제한자로만 필드와 메서드 선언이 가능하다.
      → java 9부터 인터페이스에서만 호출할 수 있는 private 접근 제한의 메서드 선언이 가능하다(인터페이스의 구현 객체에서는 호출 불가하다)

     

    그래서 추상 클래스와 인터페이스의 차이는 그 존재 목적 자체가 다르다고 생각한다. 추상 클래스는 부모 클래스를 상속 받아 부모 클래스의 기능을 확장하기 위해 존재하는데 반해, 인터페이스는 이 인터페이스를 구체화하는 클래스에서 구현해야하는 메서드를 강제하여, 이 인터페이스를 구현한 클래스는 모두 동일한 역할을 수행할 수 있음을 보장하기 위해 존재한다. 결국 두 개념의 차이가 뭐냐는 질문에 존재 목적 자체가 다르다고 답할 수 있을 것 같다. (이와 같이 서로 존재 목적이 다르기 때문에 추상 클래스는 다중 상속이 불가능하지만, 인터페이스는 다중 구현이 가능하다고 생각한다.)

     

    예전에는 나 또한 두 추상 클래스와 인터페이스의 차이를 기능적으로만 생각했던 것 같다. 흔히 추상 클래스는 메서드의 일부 추상 클래스에서 구현하고 일부는 자식 클래스에 구현을 위임하는 미완성 설계도이며 인터페이스는 모든 메서드를 구현 클래스가 작성해야하는 백지 설계도라는 비유를 들었던 적이 있는데 사실 두 차이에 대해서 이런 기능적인 관점보다는 목적에 대해 먼저 생각했다면 더 이해를 잘 했을 것 같다.


     

    출처

    • 이것이 자바다