본문으로 건너뛰기
Advertisement

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

고수 팁

인터페이스 설계 원칙:

  1. 작게 유지: 인터페이스는 작을수록 좋습니다 (인터페이스 분리 원칙 - ISP). 큰 인터페이스보다 작고 응집된 여러 인터페이스가 낫습니다.

  2. default 메서드 남용 금지: default 메서드는 하위 호환성을 위한 것이지, 복잡한 비즈니스 로직을 담는 곳이 아닙니다.

  3. 함수 합성 패턴: Function.andThen(), Predicate.and() 같은 합성 메서드를 적극 활용하면 코드를 선언형으로 작성할 수 있습니다.

    // 파이프라인 스타일 (읽기 쉬움)
    Predicate<String> validEmail =
    ((Predicate<String>) s -> s.contains("@"))
    .and(s -> s.contains("."))
    .and(s -> s.length() > 5);
Advertisement