Item 20
추상 클래스보다는 인터페이스를 우선하라
🟧 추상 클래스, 인터페이스 - 상속과 확장
추상 클래스
- 다중 상속이 불가능하다.
- 한 번 상속하면 이후에 확장하기 힘들다.
인터페이스
- 다중 상속이 가능하다.
- 기능을 추가하기 용이하다.
🤔
그러면 계속해서 확장가능한 인터페이스만 사용하면 되잖아 ??
(제목부터 인터페이스를 우선시하라는데 ??)
왜 그런 말이 나왔는지 일단 더 알아보자구요 ~
🙅♂️ 먼저 추상 클래스와 인터페이스의 목적은 다릅니다. 🙅♂️
🟧 목적이 다르고 차이점이 존재한다.
추상 클래스
- 상속받아 구현된 기능을 이용하고, 확장시킨다.
인터페이스
- 구현을 강제하기 때문에 같은 동작을 보장한다.
추상 클래스는 기존 기능을 이용해서 확장하고
인터페이스는 구현을 강제한다고..?
추상 클래스와 인터페이스는 목적이 다르니
그에 맞게 각각 제한이 되어있다.
첫 번째, 상속(다중, 단일)
두 번째, 프로퍼티를 이용하는 법
세 번째, 메서드를 이용하는 법
상속은 위에서 알아봤으니
추상 클래스와 인터페이스의 프로퍼티, 메서드의 차이점을 알아보자.
01.
추상 클래스는 다양한 프로퍼티를 정의할 수 있다.
인터페이스는 클래스 상수 필드만 정의할 수 있다.
인터페이스는 클래스 상수 필드만 public으로 선언할 수 있다.
추상 클래스는 다양한 프로퍼티를 정의할 수 있다.
- 클래스 상수 필드 (public, protected, package-private, private)
- 클래스 필드 (public, protected, package-private, private)
- 인스턴스 상수 필드 (public, protected, package-private, private)
- 인스턴스 필드 (public, protected, package-private, private)
interface Interface_ {
// === 클래스 상수 필드 ===
// public static final int staticFinalField01 = 0;
// 위, 아래는 동일하다.
int staticFinalField01 = 0;
}
abstract class Abstract_ {
// === 클래스 상수 필드 ===
public static final int publicStaticFinalField = 0;
protected static final int protectedStaticFinalField = 0;
static final int defaultStaticFinalField = 0;
private static final int privateStaticFinalField = 0;
// === 클래스 필드 ===
public static int publicStaticField;
protected static int protectedStaticField;
static int defaultStaticField;
private static int privateStaticField;
// === 인스턴스 상수 필드 ===
public final int publicFinalInstanceField;
protected final int protectedFinalInstanceField;
final int defaultFinalInstanceField;
private final int privateFinalInstanceField;
public Abstract_(int publicFinalInstanceField, int protectedFinalInstanceField, int defaultFinalInstanceField, int privateFinalInstanceField) {
this.publicFinalInstanceField = publicFinalInstanceField;
this.protectedFinalInstanceField = protectedFinalInstanceField;
this.defaultFinalInstanceField = defaultFinalInstanceField;
this.privateFinalInstanceField = privateFinalInstanceField;
}
// === 인스턴스 필드 ===
public int publicInstanceField;
protected int protectedInstanceField;
int defaultInstanceField;
private int privateInstanceField;
}
추상 클래스를 이용한다면
-> 상속한 구현 클래스는 추상 클래스가 가진 프로퍼티를 활용하여 기능을 확장한다.
인터페이스를 이용한다면
-> 기존의 무언가를 활용하여 기능을 확장하는 것이 아닌 기능 자체를 구현한다.
02.
추상 클래스는 구현된 메서드를 가질 수 있다.
인터페이스는 JDK 8부터 default 메서드가 제공됐다.
인터페이스
- JDK 8 : 인터페이스에서 default 메서드 구현을 제공한다.
- JDK 9 : 인터페이스에 private 메서드를 정의할 수 있다.
- 접근 제어자 public, private 만 가능하다.
추상 클래스
- 메서드 구현 (public, protected, package-private, private)
(추상 클래스 구현된 메서드 != 인터페이스 default 메서드 != 인터페이스 private 메서드)
interface Interface_ {
// === static 메서드 ===
public static void publicStaticMethod() {}
private static void privateStaticMethod() {}
// === JDK 8 : default 메서드 ===
public default void publicDefaultMethod() {}
// === JDK 9 : private 메서드 구현 ===
private void privateMethod() {}
// === 추상 메서드 ===
public abstract void abstractMethod();
}
abstract class Abstract_ {
// === 구현된 static 메서드 ===
public static void publicStaticMethod() {}
protected static void protectedStaticMethod() {}
static void defaultStaticMethod() {}
private static void privateStaticMethod() {}
// === 구현된 메서드 ===
public void publicMethod() {}
protected void protectedMethod() {}
void defaultMethod() {}
private void privateMethod() {}
// === 추상 메서드 ===
public abstract void publicAbstractMethod();
protected abstract void protectedAbstractMethod();
abstract void defaultAbstractMethod();
}
추상 메서드는 상속받은 하위 클래스에서 구현해야 하므로 private 접근제어자가 불가능하다.
추상 클래스를 이용하면
-> package-private, protected 접근 제어자를 오버라이딩하여 다양하게 활용할 수 있다는 장점이 있다.
인터페이스는
-> 외부에서 사용하기 위한 추상 메서드를 기본적으로 제공하고 있다.
-> default method를 사용할 수 있게 됐지만 잘못 사용하게 되면 매우 복잡해질 위험성이 있다.
🟧 결국 차이점과 목적을 정리하면 다음과 같다.
- 추상 클래스 목적과 특징
- 구현된 기능을 상속받아 확장시킨다.
- 다양한 프로퍼티를 정의할 수 있다.
- 다양한 메서드를 정의할 수 있다.
- 단일 상속만 가능하다.
- 인터페이스 목적과 특징
- 구현을 강제하여 같은 동작을 하도록 만든다.
- 클래스 상수 필드만 정의할 수 있다.
- 추상 메서드를 이용할 수 있다.
- default 메서드, private 메서드가 추가되었지만 무작정 사용하면 복잡한 코드가 만들어진다.
- 다중 상속이 가능하다.
이제 서로 어떤 차이점을 가지고 있는지 알고 있다.
그렇다면 왜 인터페이스를 우선하라는 걸까 ?
여러 예시를 들면서 인터페이스의 장점 설명하겠다.
예시는 다음과 같다.
- 믹스인 정의
- 계층구조가 없는 타입 프레임워크
- default 메서드
- 인터페이스와 추상 골격 구현 클래스
🟧 1. 믹스인 정의 (mixin)
믹스인 정의 ?
- 다른 클래스에서 "사용"할 목적으로 만들어진 클래스를 의미한다.
- 특정 클래스의 주 기능에 추가적인 기능을 제공하는 방법이다.
이전 예시와 같은 상황이다.
- 플레이어가 있다.
- 플레이어는 사람이다.
- 플레이어는 도박을 한다.
플레이어(Player)는 자신의 주 기능에 추가적으로 사람(Person)의 기능, 도박꾼(Gambler)의 기능을 선택해서 갖는 믹스인 예시를 보자.
🙆♂️ : case01. 인터페이스로는 믹스인이 가능하다.
아래 코드에서 interface Gambler, interface Person 을 구현해서 이용한다.
Player와 Dealer 같은 다양한 클래스에서 재사용 가능한 기능을 제공하고 있는데
이것을 믹스인이라고 한다.
// === Gambler의 기능과 Person의 기능을 재사용한다. ===
class Dealer implements Gambler, Person {
@Override
public void hit() {
System.out.println("딜러가 히트 한다.");
}
@Override
public void join() {
System.out.println("딜러가 참여 한다.");
}
}
// === Gambler의 기능과 Person의 기능을 재사용한다. ===
class Player implements Gambler, Person {
@Override
public void hit() {
System.out.println("플레이어가 히트 한다.");
}
@Override
public void join() {
System.out.println("플레이어가 참여 한다.");
}
}
interface Gambler {
void hit();
}
interface Person {
void join();
}
🙅♂️ : case02. 추상 클래스로는 믹스인이 불가능하다.
추상 클래스로 불가능한 이유 ?
- 클래스는 두 부모를 가질 수 없다.
다른 방식을 이용하면 추상 클래스로 가능하지만 "2. 계층구조"에서 설명하겠다.
🟧 2. 계층구조가 없는 타입 프레임워크
계층구조가 없는 타입 프레임워크 ?
- 객체 간 상속이나 상속 계층을 갖지 않고 타입을 정의하는 방식이다.
- 타입 사이의 관계가 간단해서 의존성이 상대적으로 적다.
자바는 계층 구조를 갖는 객체 지향 언어이다.
그러나 일부는 계층구조가 없는 타입 프레임워크이어도 잘 사용할 수 있다.
먼저 추상 클래스를 이용한 계층구조가 있을 때를 보자.
🙅♂️ : case01. 추상 클래스를 이용한 계층구조
- 새로운 클래스가 계속 만들어진다.
- 점점 많은 조합이 필요해지므로 복잡도가 올라간다.
class Dealer extends GamblerPerson {}
class Player extends GamblerPerson {}
// GamblerPerson -> 단일 상속이기 때문에 새로운 추상 클래스를 만든다.
abstract class GamblerPerson {
// 조합이 계속 늘어날 수 있다.
Gambler gambler;
Person person;
}
abstract class Gambler {}
abstract class Person {}
🙆♂️ : case02. 인터페이스를 이용한 계층구조 없는 타입 프레임워크
- 계층구조가 없기 때문에 간단하게 사용할 수 있습니다.
- 다중 상속이 가능하므로 재사용하기 쉽습니다.
// Dealer - 계층구조가 없는 타입 프레임워크
class Dealer implements Gambler, Person {
}
// Player - 계층구조가 없는 타입 프레임워크
class Player implements Gambler, Person {
}
interface Gambler {
}
interface Person {
}
두 개의 케이스 모두 인터페이스를 선호하고 있어서 헷갈릴 수 있다.
두 케이스를 정리하면
믹스인과 계층구조가 없는 타입 프레임워크는 비슷하지만 다르다.
믹스인
- 클래스에서 다른 클래스의 기능을 재사용하는 방법
계층 구조가 없는 타입 프레임워크
- 여러 개의 독립된 기능을 조합하여 하나의 프로그램을 만드는 방법
공통
- 재사용성을 높이기 위한 방법
🟧 3. default 메서드
추상 클래스에 구현된 메서드가 있듯이
인터페이스에는 default 메서드가 있다.
구현 내용이 명확하다면 인터페이스에서도 사용이 가능하다.
interface Person {
default void join() {
System.out.println("참여한다.");
}
}
🟧 4. 인터페이스와 추상 골격 구현 클래스
추상 골격 구현 클래스 ?
- 인터페이스의 장점과 추상 클래스의 장점 모두 가져갈 수 있다.
Step01.
Gambler 인터페이스를 정의한다.
구현체로는 Dealer와 Player가 있다.
Dealer와 Player가 오버라이딩한 bet, bust 메서드가 중복됨을 알 수 있다.
interface Gambler {
void bet();
void hit();
void bust();
}
class Dealer implements Gambler {
// === Player와 동일하다 ===
@Override
public void bet() {
System.out.println("배팅하다.");
}
@Override
public void hit() {
System.out.println("딜러의 규칙대로 히트하다.");
}
// === Player와 동일하다 ===
@Override
public void bust() {
System.out.println("버스트 되다.");
}
}
class Player implements Gambler {
// === Dealer와 동일하다 ===
@Override
public void bet() {
System.out.println("배팅하다.");
}
@Override
public void hit() {
System.out.println("플레이어의 규칙대로 히트하다.");
}
// === Dealer와 동일하다 ===
@Override
public void bust() {
System.out.println("버스트 되다.");
}
}
Step02.
중복을 해결하기 위해 "추상 골격 구현 클래스"를 이용한다.
// 추상 골격 구현 클래스 - 중복된 메서드르 제거한다.
abstract class AbstractGambler implements Gambler {
// 같은 동작을 하는 메서드 bet
@Override
public void bet() {
System.out.println("배팅하다.");
}
// 같은 동작을 하는 메서드 bust
@Override
public void bust() {
System.out.println("버스트 되다.");
}
}
Step03.
이제 다음과 같이 Player를 재구현하면 된다.
- 중복된 메서드를 제거한다.
- 추상 클래스를 이용해서 접근 제어자 (protected, package-private)을 이용할 수 있다.
class Player extends AbstractGambler implements Gambler {
// bet 메서드는 AbstractGambler에 구현되어 있다.
// bust 메서드는 AbstractGambler에 구현되어 있다.
@Override
public void hit() {
System.out.println("플레이어의 규칙대로 히트하다.");
}
}
네 가지 케이스
1. 믹스인 // 2. 계층 구조가 없는 프레임워크 // 3. default 메서드 // 4. 추상 골격 구현 클래스
결과적으로 추상 클래스보다 인터페이스가 더 제한이 없고 활용하기에 쉬웠다.
🟧 정리하자면
- 추상 클래스보다 인터페이스를 우선시하라.
- 추상 클래스보다 인터페이스가 더 재사용하기 좋다.
- 다중 상속
- 계층 구조 없는 타입 프레임워크
- "추상 골격 구현 클래스"를 이용해서 추상 클래스, 인터페이스의 두 장점 모두 가져가자.
'💬 언어 > 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] Item 23 - 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2023.04.14 |
---|---|
[이펙티브 자바] Item 22 - 인터페이스 타입을 정의하는 용도로만 사용하라 (1) | 2023.03.20 |
[이펙티브 자바] Item 49 - 매개변수가 유효한지 검사하라 (0) | 2023.03.06 |
[이펙티브 자바] Item 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2023.03.05 |
[이펙티브 자바] Item 26 - Raw Type은 사용하지 말라 (0) | 2023.02.22 |