[Java] Interface (default method) VS Astract Class

General Dong·2024년 10월 24일
post-thumbnail

Interface default method의 등장

Java 8부터 인터페이스에서 default method의 경우 구현이 가능해지면서 추상 클래스와의 비슷한 점이 많아졌다.
둘의 경계가 모호해졌기 때문에, 우선 둘을 비교해보고 사용 방식에 대해 알아보자.

Interface VS Abstract Class

Interface Abstract Class
상속 키워드 implements extends
다중 상속 다중 상속 지원 (여러 개의 인터페이스 상속 가능) 지원하지 않음 (단일 상속만 가능)
Fields 상수만 가질 수 있음 (final, static) 상수와 인스턴스 변수도 선언 가능
접근 제어자 public, default, private 가능
(상수는 private 불가능)
메소드와 필드에 다양한 접근 제어자 사용 가능
생성자 생성자 생성 불가 생성자 선언 가능
사용 목적 각 클래스의 목적에 맞게 기능을 구현 공통된 기능들을 제공하고 하위 클래스로 확장할 때 유리
공통점
  • 추상 메소드 선언 가능
  • default 메소드 선언 가능 (method 구현 가능) (Java 8+)
  • 정적 메소드 선언 가능 (Java 8+)
  • private 메소드 선언 가능 (Java 9+)
  • 인스턴스화 불가능 (new 키워드 사용)
  • 클래스에서 구현 가능

Interface

인터페이스를 상속하는 키워드 implements처럼 인터페이스는 구현이라는 개념이 더 강하다.
Class상속 (extends)처럼 의미있는 연관 관계보다는 비교적 자유로운 관계를 만든다.
다음 코드를 보며 이해해 보자.

낮은 결합도로 인한 유연성

interface Swimable {
    void swim();
}

interface Flyable {
    void fly();
}

class Elephant implements Swimable {
    @Override
    public void swim() {
        System.out.println("Elephant is swimming.");
    }
}

class Duck implements Swimable, Flyable {
    @Override
    public void swim() {
        System.out.println("Duck is swimming.");
    }

    @Override
    public void fly() {
        System.out.println("Duck is flying.");
    }
}

위의 코드와 같이 Swimable 인터페이스의 경우 ElephantDuck 클래스에서 구현되고 있다.
위 코드에서는 "수영을 할 수 있다"라는 공통점을 제외하면 둘의 연관성은 크지 않다.

이처럼 인터페이스다중 상속이 가능하기 특정 구현에 의존하지 않고, 동일한 인터페이스를 통해 다양한 클래스를 만들어 낼 수 있다.
따라서 시스템의 유연성이 높아지고, 변경이나 확장이 용이해질 수 있다.

Methods

public method

public interface Animal {
	// public 메소드 
    public void introduce();	// = public abstract void introduce();

    // 위와 같음
    void move();
}

인터페이스에서는 public, abstract 키워드 생략이 가능하며, Animal상속하는 하위 클래스에서 구현을 강제한다.

default method

public interface Animal {
    // default 메소드
    default void makeSound() {
        System.out.println("Animal sound");
    }

    // 추상 메소드
    void move();
}

구체적인 구현을 따로 하지 않아도 된다.
수정이 필요한 경우, Override 하여 재정의 할 수 있다.

static method

/* 요청 값의 양식을 설정 */
public interface RestDocsFromatGenerator {
	// static 메소드
    static Attributes.Attribute phoneNumberFormat() {
        return key("format").value("000-0000-0000");
    }

    static Attributes.Attribute emailFormat() {
        return key("format").value("example-email@example.com");
    }

    static Attributes.Attribute imageUrlFormat() {
        return key("format").value("https://example.com/{image-path}");
    }
}

static 메소드는 default 메소드와 비슷하게 사용 가능하다.
인스턴스를 생성하지 않아도 메소드를 호출할 수 있기 때문에, 관련성이 높은 메소드를 모아 높은 응집도로 만들 수 있다.
그러나 메소드 재정의는 불가능하다.

private method

public interface Animal {
    // default 메소드
    default void makeSound() {
    	commonBehavior();
        System.out.println("Animal sound");
    }

    // 추상 메소드
    void move();

    // private 메소드 (Java 9 이상)
    private void commonBehavior() {
        System.out.println("Common behavior");
    }
}

인터페이스 내부에서만 사용 가능하다.
default 메소드를 구현하면서 반복되는 로직이나 긴 제어문을 구현하는데 좋다.

Abstract Class

인터페이스 부분에서 말했던 것처럼 추상 클래스는 인터페이스보다 결합도가 높다.
그렇지만 추상 클래스에서는 공통의 필드에 대해 각 객체마다 다른 값을 가질 수 있도록 한다.
이를 통해 코드의 중복을 줄이고, 관련된 기능을 그룹화하여 재사용성을 높일 수 있다.

공통 필드와 메소드 구현 / 추상화

public abstract class Animal {
    protected String name;
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 추상 메소드
    public abstract void move();
}

// 구체적인 클래스
public class Snake extends Animal {
    public Snake(String name, int age) {
        super(name, age);
    }

    @Override
    public void move() {
        System.out.println("Snake slithers");
    }
}

public class Horse extends Animal {
    public Horse(String name, int age) {
        super(name, age);
    }

    @Override
    public void move() {
        System.out.println("Horse gallops");
    }
}

Animal의 경우 이름과 나이의 경우, 공통 사항으로 볼 수 있다.
그러나 move()의 경우, Animal마다 다를 수 있다.
예를 들어 Snake는 기어가는 로직을 구현해야 하고, Horse는 네 발로 달리는 로직을 구현해야 한다.

이처럼 공통 사항은 미리 구현하고 하위 계층마다 다를 수 있는 경우에는 추상화하도록 하면 코드의 중복을 많이 줄일 수 있다.
그러나 추상 클래스단일 상속만 가능하므로 더 강한 결합을 형성하게 된다.

Interface + Abstract Class

인터페이스의 다중 상속추상 클래스의 공통의 필드, 메소드 상속의 기능을 함께 하고자 할 때 두 가지가 같이 사용된다.

interface Driver {
    void drive();
}

interface Pilot {
    void fly();
}

// 추상 클래스 Person
abstract class Person {
    protected String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    // 추상 메소드
    public abstract void work();
}

class TaxiDriver extends Person implements Driver {
    public TaxiDriver(String name) {
        super(name);
    }

    @Override
    public void drive() {
        System.out.println(name + " is driving a taxi.");
    }

    @Override
    public void work() {
        System.out.println(name + " is working as a taxi driver.");
    }
}

class AirplanePilot extends Person implements Pilot, Driver {
    public AirplanePilot(String name) {
        super(name);
    }

    @Override
    public void fly() {
        System.out.println(name + " is flying a plane.");
    }
    
    @Override
    public void drive() {
        System.out.println(name + " can drive a car.");
    }

    @Override
    public void work() {
        System.out.println(name + " is working as an airplane pilot.");
    }
}

위의 코드에서 TaxiDriverAirplanePilotPerson의 특화된 유형이다. Person 클래스는 공통 속성인 name과 모든 하위 클래스에서 구현해야 하는 추상 메소드 work()를 포함 된다.

TaxiDriver 클래스는 Driver 인터페이스를 구현하여 drive() 행동을 수행할 수 있다. 반면 AirplanePilot 클래스는 PilotDriver 두 인터페이스를 구현하여 비행기를 fly()할 수 있고, 자동차를 drive()할 수 있는 유연성을 보여준다.


참고

Java Interfaces vs. Abstract Classes
인터페이스 vs 추상클래스 용도 차이점 - 완벽 이해 | 인파
Spring Rest Docs 적용 | 우아한 기술 블로그

profile
개발에 대한 기록과 복습을 위한 블로그 | Back-end Developer

0개의 댓글