[OOP][JAVA] 추상클래스와 인터페이스

장인석·2022년 8월 28일
1

자바를 다시 공부하면서 추상클래스와 인터페이스가 어떤 차이가 있는지 헷갈리기 시작했다. 둘 다 추상메서드를 포함하는 것은 동일한데, 언제 추상클래스를 사용해야 하고 언제 인터페이스를 사용해야 하는 지를 판단을 못 하고 있었다. 따라서 포스팅을 통해 전체적인 개념과 사용법에 대해 정리해보고자 한다.

추상메서드

먼저 둘 다 가진다는 추상메소드에 대해 알아보자.
추상메서드는 미구현 상태의 메서드를 의미한다.

{
	...
    public abstract void abstractMethod(int p1, int p2);
    ...
}

위의 코드처럼 메서드명과 파라미터는 정의되어있지만 구현부가 없다.
메서드는 객체가 어떠한 행위를 하고자 할 때 사용하는 것인데,
이처럼 구현을 하지 않고 정의만 해두는 이유는 무엇일끼?

객체지향 프로그래밍의 핵심 중 하나인 다형성을 사용하기 위해서이다.
현실세계의 한 사례를 예시로 한 번 살펴보자.

동물 중 포유류와 어류는 둘 다 호흡을 하지만
포유류는 폐를 사용해 호흡을 하고 어류는 아가미를 사용해 호흡을 한다.
그리고 둘 다 위를 사용해 소화를 시킨다.
이를 코드로 아래에 표현해보았다.

// 포유류
class Mammal{
	// 호흡
	void breath(Air air){
    	// 폐를 사용해서 공기를 호흡
    	lung.use(air);
    }
    
    // 소화
    void digest(Food food){
    	// 위를 사용해서 소화
    	stomach.use(food);
    }
}

// 어류
class Fish{
	// 호흡
	void breath(Air air){
    	// 아가미를 사용해서 공기를 호흡
    	gill.use();
    }
    
    // 소화
    void digest(Food food){
    	// 위를 사용해서 음식을 소화
    	stomach.use(food);
    }
}

포유류와 어류 모두 breath, digest라는 메서드를 가지고 있다.
그렇지만 breath 메서드 안에서의 동작은 다르다.

중복되는 코드가 있어서 보기 안좋으니
동물(Animal) 클래스를 새로 만들어 모듈화를 시켜보도록 하겠다.
포유류와 어류 모두 동물이기 때문에 호흡과 소화는 필수불가결이다.
그렇기에 동물로부터 digest만 상속하는 것은 좋지 않아보인다.
그렇지만 둘 다 상속시키기엔 breath 메서드 안의 동작이 다르다.
이를 해결 해줄 수 있는 것이 추상메서드이다.

abstract class Animal{
	// 호흡
    abstract void breath(Air air);
    
    // 소화
	void digest(Food food){
    	// 위를 사용해서 소화
        stomach.use(food);
    }
}

위는 동물(Animal)을 코드로 표현해본 것이다.
이전의 Mamal와 Fish의 코드와 유사하지만 여기서는 breath 메서드 앞에 abstract 키워드가 들어간다.
그리고 Air 인자를 받는다고 정의되어 있지만 동작에 대한 구현부는 없다.
메서드명과 반환형, 인자는 정의가 되어있지만 구현부는 존재하지 않는 메서드
이것이 바로 추상메서드이다.

그렇다면 이 Animal 클래스를 상속하여 Mamal과 Fish를 다시 만들어보면 어떻게 될까?

// 포유류
class Mammal extends Animal{
	// 호흡
    @Override
    void breath(Air air){
    	// 폐를 사용해서 공기를 호흡
        lung.use(air);
    }
}

// 어류
class Fish extends Animal{
	// 호흡
    @Override
    void breath(Air air){
    	// 아가미를 사용해서 공기를 호흡
        gill.use(air);
    }
}

digest 메서드는 Animal에서 구현 되어있기 때문에 상속을 받을 수 있고
breath 메서드는 Mamal과 Fish 각각 동작이 다르기 때문에
Animal에서의 breath 추상메서드를 Override 해주었다.
그렇게 함으로써 이전의 코드보다 더욱 간결해진 것을 볼 수 있다.

그런데 만약 여기서 breath 추상메서드를 Override를 안 해주면 어떻게 될까?
Mamal과 Fish는 Animal을 상속 받는데, 부모클래스의 추상메서드를 Override해주지 않으면 컴파일부터 되지 않는다.

이처럼 어느 추상적인 동작이 하위 클래스에 필요하지만 구체적인 동작이 하위클래스마다 다를 경우에 하위클래스에서 동작에 대한 구현을 강제 할 수 있도록 하는 것이 추상메서드이다.

추상클래스

그렇다면 추상클래스는 무엇 인가?
간단하다.

추상메서드를 가지고 있는 클래스를 추상클래스라고 한다.

추상클래스와 인터페이스의 공통점

  • 추상메서드를 가지고 있다.
  • 새로운 객체를 생성할 수 없다.

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

인터페이스를 아직 설명 안 했기에, 인터페이스를 설명하면서 추상클래스와 비교해보자.
인터페이스는 추상클래스보다 더욱 추상적인 형태를 가지고 있다.

interface Interface{
	public static Class<T> variable;
	public abstract void method(Param p);
}

자바 기준으로 인터페이스에서 변수는 무조건 public static, 메서드는 public abstract 키워드를 사용해야한다.
그렇기에 생성자 메서드를 가질 수 없다.
그리고 추상클래스는 extends 를 통해 상속받지만 인터페이스는 implements로 상속받는다.
추상클래스는 하나만 상속이 가능하지만 인터페이스는 다중상속이 가능하다.

class Class extends AbstractClass implements InterfaceOne, InterfaceTwo{
	/**
     * 구현부
     */
}

그렇다면 언제 추상클래스를 사용하고, 언제 인터페이스를 사용해야 할까?

When? How?

꼭 언제 어떻게 써야 된다라는 규정이 있지는 않다.
사실 추상클래스와 인터페이스도 결국 문법의 일종이기 때문에
개발자가 그 문법을 언제 어떻게 사용하는 지는 자유이다.
그렇지만 자유롭다 하더라도 패러다임이라는 것은 존재하기 때문에
대중적인 패러다임들 몇 가지를 알아보고자 한다.

isKindof 와 isAbleto

추상클래스도 결국 클래스이기 때문에 ~의 종류(isKindof)로 정의할 수 있다.
동물의 종류에는 포유류, 어류, 조류 등이 있는 것 처럼 말이다.

class Mamal extends Animal{}

class Fish extends Animal{}

인터페이스는 종류라기 보다는 ~을 할 수 있는(isAbleto)으로 정의할 수 있다.
참치와 고래는 숨을 쉴 수 있고, 수영을 할 수 있다.

class Tuna implements Breathable, Swimable {}

class Wale implements Breathable, Swimable {}

그렇지만 이 구분법은 실제 코드와는 좀 동떨어졌다고 느껴진다.
대부분의 개발자들이 인터페이스의 명칭을 ~할 수 있는으로 명명하지 않고,
일반적인 클래스와 유사한 형태로 명명하기 때문이다.

상속과 의존

추상클래스는 일반 클래스처럼 상속을 할 수 있다.
그렇기 때문에 추상클래스에 추상메서드와 일반메서드를 정의하고
클래스에서 추상클래스를 상속하게 되면
일반메서드는 클래스에 구현할 필요가 없어지고,
추상메서드 부분만 구현을 해주면 된다.

abstract class AbClass{
	void method(){
    	System.out.println("inheritance");
    }
    abstract void abMethod();
}

class NormalClass extends Abclass{
	void abMethod(){
    	System.out.println("impleemnts");
    }
}

그러면 다음으로 동물이 무슨 종류인지 판단하는 기계를 만든다고 하자.
우선 인터페이스를 사용하지 않고 기계를 만들어보자.
우리는 메서드 오버로딩을 통해 기계를 만들 수 있을 것이다.

class Machine{
	void whatIs(Mamal mamal){
    	System.out.println("this is Mamal");
    |
    void whatIs(Fish fish){
    	System.out.println("this is Fish");
    }
}

만든 기계를 실행해보자.

Mamal a = new Mamal();
Fish b = new Fish();

Machine.whatIs(a);
Machine.whatIs(b);

위의 코드를 실행하면 터미널에 출력값으로 다음과 같이 나타날 것이다.

this is Mamal
this is Fish

그리고 인터페이스를 사용해서 다시 기계를 만들어보자.

class Machine{
	void whatIs(Animal animal){
		animal.thisIs();
	}
}

그리고 Animal 인터페이스를 구현하는 Mamal과 Fish를 새로 만들어보자.

interface Animal{
	public abstract void thisIs();
}

class Mammal implemnts Animal{
	@Override
    void thisIs(){
    	System.out.println("this is Mammal");
    }
}

class Fish implemnts Animal{
	@Override
    void thisIs(){
    	System.out.println("this is Fish");
    }
}

그리고 새로 만든 기계로 실행해보자.

Animal a = new Mamal();
Animal b = new Fish();

Machine.whatIs(a);
Machine.whatIs(b);

위의 코드를 실행하면 터미널에 출력값으로 다음과 같이 나타날 것이다.

this is Mamal
this is Fish

아까전과 결과는 동일하다.
전의 인터페이스를 사용하지 않고 구현하는 것이 클래스도 여러 개 안만들어도 되고
편해보일 것이다.
그런데 왜 이런 귀찮아 보이는 구현방식을 선택한 걸까?

만약 조류(Bird)라는 클래스를 새로 만들고 이또한 기계에서 판단을 해줘야한다고 보자.
그러면 인터페이스를 사용하지 않은 기계라면 기계를 업그레이드 해주어야 할 것이다.

class Machine{
	void whatIs(Mamal mamal){
    	System.out.println("this is Mamal");
    |
    void whatIs(Fish fish){
    	System.out.println("this is Fish");
    }
    void whatIs(Bird bird){
    	System.out.println("this is Bird");
    }
}

그렇지만 인터페이스를 사용한 기계라면 기계의 업그레이드 필요 없이
그냥 Bird 클래스가 Animal의 thisIs만 구현해줘도 문제없이 기계가 작동할 것이다.

class Bird implements Animal{
	@Override
    void thisIs(){
    	System.our.println("this is Bird");
    }
}

이러면 기계는 어떤 동물들이던 상관없이 판단할 수 있을 것이다.
이렇게 구현부분과 설계부를 나누는 것을 의존관계라고 표현한다.
설계부와 구현부분을 나누게 되면 확장이 용이해지고 코드관리가 편해진다.

실코드의 대부분에서 이런 형태로 개발을 하고 있다.

결론

원래 이 포스팅의 목적은 언제 추상클래스를 사용하고,
언제 인터페이스를 사용하는 지를 구별하기 위해 시작하였었지만
결국 원칙적으로 나눌 수 있는 방법은 없다 라는 것을 알게 되었다.

그렇지만 사람들이 많이 사용하는 패러다임이라는 것이 있고,
그것에 맞추어 개발하여 다른 개발자들과 원활하게 협력할 수 있도록
하는 것이 중요하다고 생각한다.

참고

https://wildeveloperetrain.tistory.com/112
https://myjamong.tistory.com/150
https://medium.com/webeveloper/%EC%9E%90%EB%B0%94-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4-6eecbe5d6350

profile
사람을 좋아하는 개발자

0개의 댓글