interface에 메서드 본문이 들어간 이유 — default method와 다중 구현의 충돌
Java 8 이전의 interface는 순수한 명세였다 — 메서드 선언만 있고 본문이 없는 계약서. 그런데 라이브러리를 배포했는데, 나중에 새 메서드를 추가해야 한다면? 그 인터페이스를 구현한 모든 클래스가 컴파일 에러로 깨진다. 이 문제를 해결하기 위해 default method가 도입됐다 — interface에 본문이 있는 메서드를 넣을 수 있게 됐다. (JEP-126, Java 8) "이미 구현체를 가진 메서드를 interface에 넣을 수 있다"는 작은 변화가, Java 컬렉션 API 전체에 함수형 프로그래밍을 가능하게 했다.
하지만 두 interface가 같은 이름의 default method를 가지면 어떻게 되는가? 이 글은 interface와 abstract class의 차이, default method 충돌 해결, 그리고 "is-a" vs "can-do" 선택 기준을 풀어간다.
abstract class — 부분 구현을 가진 클래스
abstract 클래스는 인스턴스를 직접 생성할 수 없고, 하위 클래스가 상속하여 구현해야 하는 클래스다. (JLS §8.1.1.1)
// Java 25
public abstract class Shape {
// 일반 필드 (가능)
protected String name;
public Shape(String name) {
this.name = name;
}
// 구현된 메서드 (하위 클래스가 공유)
public String getName() {
return name;
}
// abstract 메서드 (하위 클래스가 반드시 구현)
public abstract double area();
public abstract double perimeter();
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) {
super("Circle");
this.radius = radius;
}
@Override
public double area() { return Math.PI * radius * radius; }
@Override
public double perimeter() { return 2 * Math.PI * radius; }
}
// Shape s = new Shape("X"); // 컴파일 에러: abstract 클래스 인스턴스화 불가
Shape s = new Circle(5.0);
System.out.println(s.area()); // 78.539...
abstract class의 핵심:
- 인스턴스화 불가 (
new AbstractClass()컴파일 에러) - 필드(상태)를 가질 수 있다 — interface는 (static final이 아닌) 인스턴스 필드를 가질 수 없다
- 생성자를 가질 수 있다 — interface는 생성자가 없다
- abstract 메서드와 구현된 메서드를 섞을 수 있다
- 단일 상속만 가능
interface — 다중 구현 타입의 계약
interface는 구현이 없는 메서드 명세의 집합이다. Java 8 이후로 default/static/private 메서드 본문도 가질 수 있게 됐지만, 본질은 타입 계약이다. (JLS §9)
// Java 25
public interface Comparable<T> {
int compareTo(T other); // abstract 메서드 (구현 없음)
}
public interface Drawable {
void draw();
// default method (Java 8+) — 본문이 있음
default void drawTwice() {
draw();
draw();
}
// static method (Java 8+)
static Drawable empty() {
return () -> {}; // 아무것도 그리지 않는 Drawable
}
// private method (Java 9+) — default 메서드 간 공통 로직
private void log(String msg) {
System.out.println("[Drawable] " + msg);
}
}
interface가 가질 수 있는 것과 없는 것
| 요소 | interface | abstract class |
|---|---|---|
| 인스턴스 필드 | X (상수 static final만) |
O |
| 생성자 | X | O |
| abstract 메서드 | O | O |
| 구현된 메서드 | O (default/static/private) | O |
| 다중 구현/상속 | O (여러 interface 구현 가능) | X (단일 상속) |
| 접근 제어자 | public (명시적 지정 불필요) | public/protected/private/package-private |
다중 구현 — interface의 핵심 이점
// Java 25 — 한 클래스가 여러 interface를 구현
public class TextBox implements Drawable, Comparable<TextBox> {
private String text;
public TextBox(String text) { this.text = text; }
@Override
public void draw() {
System.out.println("[" + text + "]");
}
@Override
public int compareTo(TextBox other) {
return this.text.compareTo(other.text);
}
}
TextBox는 Drawable이기도 하고 Comparable<TextBox>이기도 하다. 이것이 interface가 해결하는 다중 타입 문제다 — 단일 상속만으로는 두 개의 다른 타입 계약을 동시에 만족시킬 수 없다.
default method — interface 진화의 열쇠
default method는 interface에 본문이 있는 메서드를 추가하는 기능이다. (JLS §9.4)
// Java 25
public interface List<E> {
// ... 기존 abstract 메서드들 ...
// Java 8에서 추가된 default method
default void forEach(Consumer<? super E> action) {
for (E e : this) { // this = List 자신
action.accept(e);
}
}
default boolean isEmpty() {
return size() == 0;
}
}
List 인터페이스에 forEach를 default method로 추가하면, 이미 List를 구현한 모든 클래스(ArrayList, LinkedList 등)가 코드 변경 없이 forEach를 사용할 수 있다. 이것이 default method의 존재 이유다 — interface의 하위 호환성을 유지하면서 진화.
Java 8에서
Collection.forEach(),Stream관련 default method들이 대거 추가됐다. Java의 기존 컬렉션 API가 함수형 프로그래밍을 지원할 수 있게 된 것이 default method 덕분이다.
default method 충돌 — 다이아몬드 문제의 귀환
두 interface가 같은 이름의 default method를 가지고, 한 클래스가 두 interface를 모두 구현하면 충돌이 발생한다.
// Java 25 — default method 충돌
interface A {
default void hello() { System.out.println("A"); }
}
interface B {
default void hello() { System.out.println("B"); }
}
// class C implements A, B { } // 컴파일 에러: 충돌!
// C inherits unrelated defaults for hello() from types A and B
// 해결: 명시적으로 override
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // A의 hello() 선택 (또는 B.super.hello())
}
}
충돌 해결 규칙 (JLS §9.4.1):
- 클래스가 이긴다 — class의 구현이 interface의 default보다 우선
- 더 구체적인 interface가 이긴다 —
B extends A일 때B의 default가 우선 - 충돌 시 명시적 override — 위 규칙으로 결정할 수 없으면 컴파일 에러 → 개발자가 직접 override
A.super.hello()문법으로 특정 interface의 default method를 명시적으로 호출할 수 있다. 이것은 interface 다중 구현에서만 사용하는 특수 문법이다.
abstract class vs interface — 언제 무엇을?
| 기준 | abstract class | interface |
|---|---|---|
| 관계 | "is-a" (강한 결합) | "can-do" (약한 결합) |
| 상태(필드) | 필요함 | 불필요 (타입 계약만) |
| 다중 상속 | 불가 | 가능 |
| 공통 구현 | 필드 + 메서드 공유 | 메서드 본문만 공유 (default) |
| 진화 | 새 메서드 추가 시 하위 클래스 영향 | default method로 하위 호환 유지 |
실용적 가이드:
- 공통 상태(필드)와 구현을 공유해야 한다 → abstract class
- 타입 역할(역할, 능력)을 정의 → interface
- 여러 타입을 한 객체에 부여해야 한다 → interface (다중 구현)
- 라이브러리 API 설계 → interface (default method로 진화 가능)
Java 컬렉션 프레임워크의 설계가 이 원칙을 잘 보여준다:
AbstractList(abstract class)가 공통 구현을 제공하고,List(interface)가 타입 계약을 정의한다.ArrayList는List를 구현하면서AbstractList의 구현을 재사용한다.
sealed interface (Java 17+, JEP-409) — 닫힌 타입 계층
interface는 기본적으로 "누구나 구현할 수 있는" 열린(open) 계약이다. 누가 구현할지 미리 알 수 없다. sealed는 이것을 제한한다 — 허용된 타입만 구현(또는 상속)할 수 있다. (JEP-409)
// Java 25 — sealed interface로 닫힌 계층 정의
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// 다른 패키지의 클래스가 Shape를 구현하려고 하면 컴파일 에러
// class Hexagon implements Shape {} // 허용되지 않음
sealed의 규칙:
permits에 명시된 타입만 하위 타입이 될 수 있다- 허용된 타입은
final,sealed, 또는non-sealed중 하나여야 한다 final= 더 이상 하위 타입 없음sealed= 또 다른 sealed 계층non-sealed= 열림 (이 하위 트리에서는 누구나 상속 가능)
sealed interface는 switch expression의 exhaustiveness(완전성) 검사와 결합할 때 가장 강력하다 — default 없이도 컴파일러가 모든 case를 처리했는지 검증한다 (03-operators-control.md 참조).
함수형 인터페이스(functional interface) — default method의 부산물
정확히 하나의 abstract 메서드를 갖는 interface를 함수형 인터페이스라 한다. (JLS §9.8) 람다식의 타겟 타입이 된다.
// Java 25
@FunctionalInterface // 컴파일러 검증 (abstract 메서드가 1개 초과 시 에러)
public interface Predicate<T> {
boolean test(T t);
// default 메서드는 abstract 카운트에 포함 안 됨
default Predicate<T> and(Predicate<? super T> other) {
return t -> this.test(t) && other.test(t);
}
default Predicate<T> negate() {
return t -> !this.test(t);
}
}
@FunctionalInterface는 선택적이지만, 실수로 abstract 메서드를 추가하는 것을 막아주므로 항상 붙이는 것이 좋다. 함수형 프로그래밍과 람다의 자세한 내용은 12-lambda-functional.md에서 풀어간다.
실습 — default method 충돌 직접 체험
// Java 25 — InterfaceDemo.java
interface Walker {
default void move() { System.out.println("걷는다"); }
}
interface Swimmer {
default void move() { System.out.println("헤엄친다"); }
}
public class InterfaceDemo implements Walker, Swimmer {
@Override
public void move() {
System.out.print("양서류: ");
Walker.super.move(); // Walker의 move() 선택
System.out.print("양서류: ");
Swimmer.super.move(); // Swimmer의 move()도 호출
}
public static void main(String[] args) {
new InterfaceDemo().move();
}
}
java InterfaceDemo.java
양서류: 걷는다
양서류: 헤엄친다
확인할 것: 두 interface의 같은 이름 default method가 충돌하지만, 명시적으로 override하여 둘 다 호출한다. Walker.super.move()와 Swimmer.super.move()로 각각의 default method를 지정한다.
요약 — 이 글의 결론
- interface는 타입 계약이고, abstract class는 부분 구현이다. interface는 다중 구현이 가능하고 상태가 없다. abstract class는 단일 상속이지만 필드와 생성자를 가질 수 있다.
- default method(Java 8+)는 interface의 진화를 가능하게 했다. 기존 구현체를 깨지 않고 새 메서드를 추가할 수 있다. Java 컬렉션 API의 함수형 확장이 이것 덕분이다.
- default method 충돌은 컴파일 타임에 잡힌다. 두 interface의 같은 이름 default method를 구현할 때는 명시적으로 override하고
Interface.super.method()로 원하는 것을 선택한다. - "is-a"는 abstract class, "can-do"는 interface. 공통 상태와 구현이 필요하면 abstract class, 역할/능력 정의가 필요하면 interface. 실제로는 interface를 우선하고, 필요할 때 abstract class로 보완하는 것이 유연하다.
@FunctionalInterface를 붙여 람다 타겟을 명시한다. abstract 메서드가 1개임을 컴파일러가 검증한다.
생각해 볼 문제
interface A { default void m() {} }를 구현하는 클래스에서m()을 override하지 않으면 어떻게 되는가?m()이 abstract인지 아닌지를 어떻게 알 수 있는가?- interface에
private메서드(Java 9+)를 추가한 이유는 무엇인가? default 메서드 간 중복 코드를 어떻게 줄이는가? Comparator.comparing()은 static 메서드다. 이 메서드가 interface에 정의될 수 있는 이유를 설명하라.- abstract class와 interface를 둘 다 쓰는 상황을 설계해 보자. (예: template method 패턴)
- sealed interface는 다중 구현에 어떤 영향을 주는가? sealed interface의 하위 타입을 switch에서 처리할 때의 이점은?
참고
- JLS §9 - Interfaces - 접근 2026-07-10
- JLS §8.1.1.1 - abstract Classes - 접근 2026-07-10
- JLS §9.4 - Interface Method Bodies - 접근 2026-07-10
- JLS §9.4.1 - Inheritance and Overriding - 접근 2026-07-10
- JLS §9.8 - Functional Interfaces - 접근 2026-07-10
- JEP-126: Lambda Expressions & Default Methods - 접근 2026-07-10
'Develop Artifacts > Java' 카테고리의 다른 글
| Java - 09. exception (0) | 2026.07.12 |
|---|---|
| Java - 08. record sealed enum (0) | 2026.07.12 |
| Java - 06. inheritance and polymorphism (0) | 2026.07.12 |
| Java - 05. class and object (0) | 2026.07.12 |
| Java - 04. array and string (0) | 2026.07.12 |
