7.5 인터페이스 심화 (Interface Advanced)
Java 8 이후로 인터페이스는 단순한 "규약"을 넘어 구현 코드도 포함할 수 있게 되었습니다. default 메서드, static 메서드, private 메서드가 추가되어 인터페이스의 활용 범위가 크게 넓어졌습니다.
1. default 메서드 (Java 8+)
인터페이스에 기본 구현을 제공합니다. 구현 클래스에서 선택적으로 오버라이딩할 수 있습니다.
왜 필요한가?
기존 인터페이스에 새 메서드를 추가하면 이를 구현한 모든 클래스가 깨집니다. default 메서드는 하위 호환성을 유지하면서 인터페이스를 확장하기 위해 도입되었습니다.
interface Vehicle {
void move(); // 추상 메서드 (반드시 구현)
default void stop() { // default 메서드 (선택적 오버라이딩)
System.out.println("차량이 멈춥니다.");
}
default void fuel() {
System.out.println("일반 연료를 사용합니다.");
}
}
class ElectricCar implements Vehicle {
@Override
public void move() {
System.out.println("전기 모터로 달립니다.");
}
@Override
public void fuel() { // 필요한 경우에만 오버라이딩
System.out.println("전기로 충전합니다.");
}
// stop()은 오버라이딩 하지 않으면 default 구현이 사용됨
}
ElectricCar car = new ElectricCar();
car.move(); // 전기 모터로 달립니다.
car.stop(); // 차량이 멈춥니다. (default 구현)
car.fuel(); // 전기로 충전합니다. (오버라이딩된 구현)
다이아몬드 문제 해결
여러 인터페이스의 default 메서드가 충돌하면, 구현 클래스에서 직접 오버라이딩해야 합니다.
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // 명시적으로 A의 default 메서드 호출
// 또는 완전히 새로운 구현 제공
}
}
2. static 메서드 (Java 8+)
인터페이스에도 static 유틸리티 메서드를 정의할 수 있습니다. 인스턴스 없이 인터페이스명.메서드명()으로 호출합니다. 오버라이딩 불가합니다.
interface Validator<T> {
boolean validate(T value);
// 팩토리 메서드 패턴으로 유용하게 활용
static <T> Validator<T> of(Validator<T> validator) {
return validator;
}
static Validator<String> notEmpty() {
return s -> s != null && !s.isBlank();
}
static Validator<Integer> positive() {
return n -> n > 0;
}
}
// 사용
Validator<String> nameValidator = Validator.notEmpty();
System.out.println(nameValidator.validate("홍길동")); // true
System.out.println(nameValidator.validate("")); // false
Validator<Integer> ageValidator = Validator.positive();
System.out.println(ageValidator.validate(25)); // true
System.out.println(ageValidator.validate(-1)); // false
3. private 메서드 (Java 9+)
인터페이스 내부에서만 사용하는 공통 헬퍼 코드를 추출할 수 있습니다. default 메서드 간에 코드 중복을 제거하는 데 활용합니다.
interface Logger {
void log(String message);
default void logInfo(String message) {
log(formatMessage("INFO", message)); // private 메서드 호출
}
default void logError(String message) {
log(formatMessage("ERROR", message)); // private 메서드 호출
}
// private: 인터페이스 외부에서는 보이지 않음
private String formatMessage(String level, String message) {
return String.format("[%s] %s: %s",
java.time.LocalTime.now(), level, message);
}
}
4. 함수형 인터페이스와 람다식
추상 메서드가 딱 하나인 인터페이스를 함수형 인터페이스라고 합니다. @FunctionalInterface 어노테이션으로 이를 명시하며, 람다식으로 간결하게 구현할 수 있습니다.
@FunctionalInterface
interface StringTransformer {
String transform(String input);
// 추상 메서드 1개만 허용 (default, static은 여럿 있어도 됨)
default StringTransformer andThen(StringTransformer after) {
return s -> after.transform(this.transform(s));
}
}
// 람다식으로 구현
StringTransformer toUpper = s -> s.toUpperCase();
StringTransformer exclaim = s -> s + "!!!";
StringTransformer trim = String::trim; // 메서드 참조
// andThen으로 체이닝
StringTransformer pipeline = trim.andThen(toUpper).andThen(exclaim);
System.out.println(pipeline.transform(" hello ")); // HELLO!!!
java.util.function 패키지 주요 함수형 인터페이스 총정리
import java.util.function.*;
import java.util.List;
import java.util.Arrays;
// --- Predicate<T>: T를 받아 boolean 반환 ---
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> hasA = s -> s.contains("a");
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.and(hasA).test("banana")); // true (and 조합)
System.out.println(isLong.or(hasA).test("ok")); // false (or 조합)
System.out.println(isLong.negate().test("Hi")); // true (부정)
// --- Function<T, R>: T를 받아 R로 변환 ---
Function<String, Integer> strLen = String::length;
Function<Integer, String> intToStr = n -> "숫자: " + n;
Function<String, String> combined = strLen.andThen(intToStr); // 합성
System.out.println(combined.apply("Hello")); // 숫자: 5
Function<String, String> composed = strLen.compose(s -> s + "!!"); // 역순 합성
// compose: s -> s + "!!" 먼저 실행 → 그 결과를 strLen에 전달
// --- BiFunction<T, U, R>: 두 개를 받아 변환 ---
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("자바! ", 3)); // 자바! 자바! 자바!
// --- Consumer<T>: T를 받아 소비 (반환값 없음) ---
Consumer<String> print = System.out::println;
Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> printAndLog = print.andThen(log); // 순차 실행
printAndLog.accept("테스트"); // 두 가지 동작 순서대로 실행
// --- BiConsumer<T, U>: 두 개를 받아 소비 ---
BiConsumer<String, Integer> printPair = (k, v) -> System.out.println(k + " = " + v);
printPair.accept("나이", 30); // 나이 = 30
// --- Supplier<T>: 아무것도 받지 않고 T를 공급 ---
Supplier<List<String>> listFactory = ArrayList::new;
List<String> newList = listFactory.get(); // 새 ArrayList 생성
// --- UnaryOperator<T>: T를 받아 T로 반환 (Function<T,T> 특화) ---
UnaryOperator<String> addBracket = s -> "[" + s + "]";
UnaryOperator<Integer> doubleIt = n -> n * 2;
System.out.println(addBracket.apply("자바")); // [자바]
// --- BinaryOperator<T>: T 두 개를 받아 T로 반환 ---
BinaryOperator<Integer> sum = (a, b) -> a + b;
BinaryOperator<String> concat = String::concat;
System.out.println(sum.apply(10, 20)); // 30
System.out.println(concat.apply("Hello", " World")); // Hello World
인터페이스 설계 원칙:
-
작게 유지: 인터페이스는 작을수록 좋습니다 (인터페이스 분리 원칙 - ISP). 큰 인터페이스보다 작고 응집된 여러 인터페이스가 낫습니다.
-
default 메서드 남용 금지:
default메서드는 하위 호환성을 위한 것이지, 복잡한 비즈니스 로직을 담는 곳이 아닙니다. -
함수 합성 패턴:
Function.andThen(),Predicate.and()같은 합성 메서드를 적극 활용하면 코드를 선언형으로 작성할 수 있습니다.// 파이프라인 스타일 (읽기 쉬움)
Predicate<String> validEmail =
((Predicate<String>) s -> s.contains("@"))
.and(s -> s.contains("."))
.and(s -> s.length() > 5);