
Java 8부터 인터페이스에서 default method의 경우 구현이 가능해지면서 추상 클래스와의 비슷한 점이 많아졌다.
둘의 경계가 모호해졌기 때문에, 우선 둘을 비교해보고 사용 방식에 대해 알아보자.
| Interface | Abstract Class | |
|---|---|---|
| 상속 키워드 | implements |
extends |
| 다중 상속 | 다중 상속 지원 (여러 개의 인터페이스 상속 가능) | 지원하지 않음 (단일 상속만 가능) |
| Fields | 상수만 가질 수 있음 (final, static) |
상수와 인스턴스 변수도 선언 가능 |
| 접근 제어자 | public, default, private 가능 (상수는 private 불가능) |
메소드와 필드에 다양한 접근 제어자 사용 가능 |
| 생성자 | 생성자 생성 불가 | 생성자 선언 가능 |
| 사용 목적 | 각 클래스의 목적에 맞게 기능을 구현 | 공통된 기능들을 제공하고 하위 클래스로 확장할 때 유리 |
| 공통점 |
|
|
인터페이스를 상속하는 키워드 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 인터페이스의 경우 Elephant와 Duck 클래스에서 구현되고 있다.
위 코드에서는 "수영을 할 수 있다"라는 공통점을 제외하면 둘의 연관성은 크지 않다.
이처럼 인터페이스는 다중 상속이 가능하기 특정 구현에 의존하지 않고, 동일한 인터페이스를 통해 다양한 클래스를 만들어 낼 수 있다.
따라서 시스템의 유연성이 높아지고, 변경이나 확장이 용이해질 수 있다.
public interface Animal {
// public 메소드
public void introduce(); // = public abstract void introduce();
// 위와 같음
void move();
}
인터페이스에서는 public, abstract 키워드 생략이 가능하며, Animal을 상속하는 하위 클래스에서 구현을 강제한다.
public interface Animal {
// default 메소드
default void makeSound() {
System.out.println("Animal sound");
}
// 추상 메소드
void move();
}
구체적인 구현을 따로 하지 않아도 된다.
수정이 필요한 경우, Override 하여 재정의 할 수 있다.
/* 요청 값의 양식을 설정 */
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 메소드와 비슷하게 사용 가능하다.
인스턴스를 생성하지 않아도 메소드를 호출할 수 있기 때문에, 관련성이 높은 메소드를 모아 높은 응집도로 만들 수 있다.
그러나 메소드 재정의는 불가능하다.
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 메소드를 구현하면서 반복되는 로직이나 긴 제어문을 구현하는데 좋다.
인터페이스 부분에서 말했던 것처럼 추상 클래스는 인터페이스보다 결합도가 높다.
그렇지만 추상 클래스에서는 공통의 필드에 대해 각 객체마다 다른 값을 가질 수 있도록 한다.
이를 통해 코드의 중복을 줄이고, 관련된 기능을 그룹화하여 재사용성을 높일 수 있다.
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 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.");
}
}
위의 코드에서 TaxiDriver와 AirplanePilot은 Person의 특화된 유형이다. Person 클래스는 공통 속성인 name과 모든 하위 클래스에서 구현해야 하는 추상 메소드 work()를 포함 된다.
TaxiDriver 클래스는 Driver 인터페이스를 구현하여 drive() 행동을 수행할 수 있다. 반면 AirplanePilot 클래스는 Pilot와 Driver 두 인터페이스를 구현하여 비행기를 fly()할 수 있고, 자동차를 drive()할 수 있는 유연성을 보여준다.
Java Interfaces vs. Abstract Classes
인터페이스 vs 추상클래스 용도 차이점 - 완벽 이해 | 인파
Spring Rest Docs 적용 | 우아한 기술 블로그