F-LAB JAVA · 3주차 · Phase 4 · 추상화의 두 도구
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Java 8 의 default 메서드는 "인터페이스에 구현을 추가" 할 수 있게 한 혁명이다.
기존 인터페이스를 깨지 않고 새 메서드를 추가할 수 있게 되어,
Collection.stream(),Comparator.reversed()같은 풍부한 API 가 가능해졌다.
다중 상속이 가능해지면서 Diamond Problem 이 부활했지만, 명시적 충돌 해결 규칙 으로 안전하게 처리한다.
Java 9+ 의 private 메서드까지 더해져 인터페이스는 추상클래스와 거의 동등한 표현력을 갖게 되었다.
Java 1.0 ~ 7 (순수 명세):
매뉴얼 = 해야 할 일 목록만
"draw() 를 구현하라"
→ 구현 방법은 각자 알아서
Java 8 (default 추가):
매뉴얼 = 해야 할 일 목록 + 기본 방법 가이드
"draw() 를 구현하라"
"기본 방법: 이렇게 하면 됨 (default)"
→ 원하면 따르고, 원하면 자신만의 방법
→ default = 기본 구현 제공 + 선택적 오버라이드.
1. default 메서드의 등장 배경
2. default 메서드의 문법과 의미
3. Diamond Problem 의 부활과 해결
4. 인터페이스의 static 메서드
5. private 메서드 (Java 9+)
6. 자바 표준 라이브러리의 default 활용
7. default vs 추상클래스 구현 메서드
8. default 메서드의 단점과 주의
9. 면접 + 자기 점검
Java 1.2 (1998): Collection 인터페이스
public interface Collection<E> {
boolean add(E e);
boolean remove(Object o);
// ... 기존 메서드들
}
Java 8 (2014) 에서 추가하고 싶었던 메서드들:
- forEach(Consumer) — 람다 순회
- removeIf(Predicate) — 조건 삭제
- stream() — Stream API 진입점
- parallelStream()
- spliterator()
문제:
Collection 인터페이스에 새 메서드 추가하면?
→ 모든 구현체가 컴파일 에러
→ ArrayList, HashSet, LinkedList, ... 모두 깨짐
→ 사용자 코드의 Collection 구현체 깨짐
→ 거대한 호환성 문제
1. 소스 호환성 (Source Compatibility):
- 기존 소스 코드 재컴파일 가능
- 컴파일 에러 없음
2. 바이너리 호환성 (Binary Compatibility):
- 기존 .class 파일이 새 JVM 에서 실행
- 링크 에러 없음
Java 8 의 도전:
Collection 에 stream() 메서드 추가하면서
둘 다 깨지 않기.
// 가능했던 방법 1:
public interface Collection<E> { /* 기존 */ }
public interface CollectionV2<E> extends Collection<E> {
void forEach(Consumer<? super E> action);
Stream<E> stream();
// ...
}
// 단점:
// - 사용자가 어느 것을 쓸지 혼란
// - 기존 코드가 새 메서드 못 씀
// - 라이브러리 분열
// 가능했던 방법 2:
public abstract class AbstractCollection<E> {
abstract Iterator<E> iterator();
abstract int size();
// 새 메서드 구현
public void forEach(Consumer<? super E> action) { ... }
}
// 단점:
// - Collection 인터페이스의 본질 (다중 구현) 손상
// - 기존 코드 수정 필요
// - 다른 인터페이스 (예: Map) 도 같은 문제
// Java 8+ 의 해결:
public interface Collection<E> {
boolean add(E e);
boolean remove(Object o);
// ... 기존 추상 메서드
// ★ 새로 추가: default 메서드
default void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E t : this) {
action.accept(t);
}
}
default boolean removeIf(Predicate<? super E> filter) {
// 기본 구현
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
// 효과:
// - 기존 구현체 (ArrayList 등) 그대로 사용 가능
// - 새 메서드도 자동 사용 가능 (default 구현으로)
// - 필요시 오버라이드로 효율 최적화
// - 사용자 코드 깨짐 없음
이전 (Java 7):
List<String> list = new ArrayList<>();
// forEach 없음 → 직접 for-each
for (String s : list) {
System.out.println(s);
}
// stream() 없음 → 외부 라이브러리 (Guava 등)
Java 8+:
List<String> list = new ArrayList<>();
// forEach 사용 가능
list.forEach(System.out::println);
// stream() 사용 가능
list.stream()
.filter(s -> s.length() > 5)
.forEach(System.out::println);
1. 인터페이스의 진화 가능
- 기존 인터페이스에 새 메서드 추가
- 호환성 유지
- API 풍부화
2. 함수형 프로그래밍 진입
- Stream API 도입의 토대
- 람다와 자연스러운 결합
3. 인터페이스의 표현력 ↑
- 단순 명세 → 부분 구현 포함
- 추상클래스에 가까워짐
4. 코드 중복 ↓
- 공통 구현을 default 로
- 모든 구현체가 자동 보유
default 메서드가 Java 8 에 추가된 이유는?
답:
1. 하위 호환성 유지:
Stream API 등 함수형 기능 도입:
API 풍부화:
→ 하위 호환성 + 진화 가능성 의 균형.
public interface Greeter {
// 추상 메서드
String getName();
// default 메서드 — 본체 있음
default String greet() {
return "Hello, " + getName();
}
// default 메서드는 다른 추상/default 메서드 호출 가능
default String formal() {
return "Dear " + greet();
}
}
// 구현
public class Person implements Greeter {
private String name;
@Override
public String getName() {
return name;
}
// greet, formal 구현 안 해도 됨 — default 사용
}
// 사용
Person p = new Person("Alice");
p.greet(); // "Hello, Alice"
p.formal(); // "Dear Hello, Alice"
1. default 키워드 명시
- default void method() { ... }
- public default 도 가능 (public 자동)
2. 본체 작성 필수
- { } 또는 코드
- 추상 메서드와 다름
3. 자동으로 public
- public default void m() — public 자동
- private default 불가
4. final 가능 X
- default 와 final 조합 불가
- 구현체가 오버라이드 가능
public interface Greeter {
default String greet() {
return "Hello";
}
}
// 1. 그대로 사용
public class Person implements Greeter {
// greet() 그대로
}
// 2. 오버라이드
public class FormalPerson implements Greeter {
@Override
public String greet() {
return "Good day";
}
}
// 3. 추상으로 다시 만들기
public abstract class AbstractPerson implements Greeter {
@Override
public abstract String greet(); // 다시 추상으로
}
// 4. super 호출
public class CombinedPerson implements Greeter {
@Override
public String greet() {
return Greeter.super.greet() + ", everyone"; // ★ Greeter.super
}
}
Greeter.super.greet():
super 와 비슷한 의미public interface Animal {
default String sound() {
return "Some sound";
}
}
public class Dog implements Animal {
// sound() 그대로
}
Dog d = new Dog();
d.sound(); // "Some sound"
// 메서드 해결 흐름:
// 1. Dog 의 메서드 검색 → 없음
// 2. Dog 의 부모 클래스 검색 → Object 의 sound() 없음
// 3. Dog 가 구현한 인터페이스 검색
// → Animal 의 default sound() 발견
// 4. Animal.sound() 실행
public interface Calculator {
int add(int a, int b);
// 편의 default 메서드
default int addThree(int a, int b, int c) {
return add(add(a, b), c);
}
default int doubleAdd(int a, int b) {
return add(a, b) * 2;
}
}
public interface Loggable {
String getName();
// 기본 구현 (대부분 만족)
default void log(String message) {
System.out.println("[" + getName() + "] " + message);
}
default void logError(String message) {
log("ERROR: " + message);
}
}
public interface Predicate<T> {
boolean test(T t);
// 조합 default 메서드
default Predicate<T> and(Predicate<? super T> other) {
return t -> test(t) && other.test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
return t -> test(t) || other.test(t);
}
default Predicate<T> negate() {
return t -> !test(t);
}
}
// 사용
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> notTooLong = s -> s.length() < 100;
Predicate<String> valid = notEmpty.and(notTooLong);
public interface Swimmer {
default void move() {
System.out.println("Swimming");
}
}
public interface Walker {
default void move() {
System.out.println("Walking");
}
}
// ❌ 충돌
public class Duck implements Swimmer, Walker {
// 컴파일 에러:
// class Duck inherits unrelated defaults for move()
// from types Swimmer and Walker
}
// ✓ 해결: 명시적 오버라이드
public class Duck implements Swimmer, Walker {
@Override
public void move() {
Swimmer.super.move(); // Swimmer 의 default
Walker.super.move(); // Walker 의 default
}
}
→ Diamond Problem 의 부활 (다음 섹션).
default 메서드를 추가할 때 주의할 점은?
답:
1. 이름 충돌 가능성: 같은 시그니처가 여러 인터페이스에 있으면 충돌
2. 구현 가정: 모든 구현체가 default 로 만족하지 않을 수 있음
3. 테스트 부담: default 코드도 모든 구현체에서 동작 확인
4. 상태 의존 회피: default 는 인스턴스 필드 접근 X
5. 단순한 구현 권장: 복잡한 default 는 추상클래스 고려
클래스 다중 상속 (자바 X):
A.m()
/ \
B C
\ /
D
D 가 A 의 m() 을 어떻게 받나?
→ Diamond Problem
자바는 단일 상속으로 회피.
인터페이스 (Java 7 까지):
A
/ \
B C
\ /
D
모든 메서드가 abstract → 구현 X
→ 충돌 없음
인터페이스 (Java 8+):
default 메서드 등장
→ 구현 있음
→ Diamond Problem 부활!
public interface A {
default void m() {
System.out.println("A");
}
}
public interface B extends A {
@Override
default void m() {
System.out.println("B");
}
}
public interface C extends A {
@Override
default void m() {
System.out.println("C");
}
}
public class D implements B, C {
// D.m() 은 B 의 m()? C 의 m()? A 의 m()?
// 모호함 → 컴파일 에러
}
규칙 1: 클래스 우선 (Class Wins)
슈퍼클래스의 메서드 > 인터페이스의 default
규칙 2: 더 구체적인 인터페이스 우선 (More Specific Interface Wins)
B extends A 라면 B 의 default > A 의 default
규칙 3: 명시적 충돌 해결 (Explicit Override)
여러 default 가 충돌 시 컴파일 에러
→ 구현 클래스가 명시적으로 오버라이드해야
public interface Greeter {
default String greet() {
return "Hi from interface";
}
}
public class BaseClass {
public String greet() {
return "Hi from class";
}
}
public class Person extends BaseClass implements Greeter {
// Person.greet() 은?
}
Person p = new Person();
p.greet(); // "Hi from class" — 클래스 우선!
이유:
public interface A {
default void m() {
System.out.println("A");
}
}
public interface B extends A {
@Override
default void m() {
System.out.println("B");
}
}
public class D implements B {
// D.m() 은?
}
D d = new D();
d.m(); // "B" — 더 구체적인 B 우선
이유:
public interface Swimmer {
default void move() {
System.out.println("Swim");
}
}
public interface Walker {
default void move() {
System.out.println("Walk");
}
}
// 두 인터페이스 모두 default move()
// 상속 관계 없음 → 충돌
public class Duck implements Swimmer, Walker {
// ❌ 컴파일 에러
// class Duck inherits unrelated defaults for move()
}
// 해결 1: 명시적 오버라이드 + super 호출
public class Duck implements Swimmer, Walker {
@Override
public void move() {
Swimmer.super.move(); // 또는 Walker.super.move()
}
}
// 해결 2: 새 구현
public class Duck implements Swimmer, Walker {
@Override
public void move() {
System.out.println("Duck moves uniquely");
}
}
// 해결 3: 둘 다 호출
public class Duck implements Swimmer, Walker {
@Override
public void move() {
Swimmer.super.move();
Walker.super.move();
}
}
public interface A {
default void m() { System.out.println("A"); }
}
public interface B extends A {
@Override
default void m() { System.out.println("B"); }
}
public interface C extends A {
@Override
default void m() { System.out.println("C"); }
}
public class D implements B, C {
// B 와 C 가 모두 A 를 상속
// 둘 다 m() 오버라이드
// 충돌!
}
// 해결
public class D implements B, C {
@Override
public void m() {
B.super.m(); // 또는 C.super.m()
}
}
충돌 결정 우선순위:
1. 슈퍼클래스의 메서드 (있다면)
2. 더 구체적 인터페이스의 default (extends 관계)
3. 위가 모두 모호하면 → 명시적 오버라이드 필수
Diamond Problem 이 default 메서드로 부활하면서 자바가 어떻게 해결했나?
답:
부활 원인:
해결 규칙 3가지:
결과:
public interface Calculator {
// 추상 메서드
int add(int a, int b);
// ★ Java 8+: static 메서드
static Calculator createDefault() {
return (a, b) -> a + b;
}
static Calculator multiply() {
return (a, b) -> a * b;
}
}
// 사용
Calculator c1 = Calculator.createDefault(); // 인터페이스명.메서드
c1.add(2, 3); // 5
Calculator c2 = Calculator.multiply();
c2.add(2, 3); // 6 (이름은 add 지만 곱셈)
1. 인터페이스명으로 호출
Calculator.createDefault()
2. 구현 클래스로는 호출 X
public class MyCalc implements Calculator { ... }
MyCalc.createDefault(); // ❌ 컴파일 에러
3. 인스턴스로도 호출 X
Calculator c = new ...;
c.createDefault(); // ❌ 컴파일 에러
4. 자동으로 public
static Calculator createDefault() — public 자동
인터페이스 static:
- 인터페이스명으로만 호출
- 구현체에 상속 안 됨
- 인터페이스 전용 유틸리티
클래스 static:
- 클래스명 또는 인스턴스로 호출 (인스턴스로는 권장 X)
- 자식 클래스에 상속 (overriding 은 hiding)
- 일반 유틸리티
Java 7 까지:
인터페이스에 static 메서드 X
관련 유틸리티는 별도 클래스로:
Collection 인터페이스
Collections 유틸리티 클래스 (별도)
예: Collections.sort(list)
예: Collections.unmodifiableList(list)
Java 8+:
인터페이스에 직접 static 메서드 추가 가능
유틸리티가 인터페이스 안에:
Stream.of(1, 2, 3)
List.of("a", "b")
Map.of("k", "v")
→ 사용자가 별도 클래스 찾을 필요 X
// Comparator 의 static 메서드
Comparator.naturalOrder();
Comparator.reverseOrder();
Comparator.comparing(Shipment::getId);
Comparator.comparingInt(Shipment::getWeight);
// Stream 의 static 메서드
Stream.of(1, 2, 3);
Stream.empty();
Stream.generate(() -> 0);
Stream.iterate(0, n -> n + 1);
// List/Map/Set 의 of (Java 9+)
List.of("a", "b", "c");
Map.of("k1", "v1", "k2", "v2");
Set.of(1, 2, 3);
// Optional 의 static
Optional.of(value);
Optional.empty();
Optional.ofNullable(value);
// Path/Paths 의 of
Path.of("dir", "file.txt");
// Predicate 의 static
Predicate.isEqual(target);
Predicate.not(predicate); // Java 11+
public interface ShipmentValidator {
boolean validate(Shipment shipment);
// static 팩토리 메서드들
static ShipmentValidator alwaysTrue() {
return s -> true;
}
static ShipmentValidator weight(int min, int max) {
return s -> s.getWeight() >= min && s.getWeight() <= max;
}
static ShipmentValidator port(Set<String> allowedPorts) {
return s -> allowedPorts.contains(s.getOriginPort());
}
// default 조합 메서드
default ShipmentValidator and(ShipmentValidator other) {
return s -> validate(s) && other.validate(s);
}
}
// 사용
ShipmentValidator validator =
ShipmentValidator.weight(0, 10000)
.and(ShipmentValidator.port(Set.of("BUSAN", "INCHEON")));
Shipment s = ...;
boolean valid = validator.validate(s);
→ static + default 의 강력한 조합.
인터페이스의 static 메서드가 등장한 이유는?
답:
1. 유틸리티 클래스 분리 해소:
팩토리 메서드 제공:
관련 유틸리티 통합:
함수형 인터페이스와 결합:
Java 9+ 추가:
- private 메서드
- private static 메서드
목적:
- default/static 메서드 간 코드 공유
- 외부 노출 없는 내부 구현
// Java 8 의 문제 — default 메서드 간 중복
public interface Calculator {
default int sumPositives(List<Integer> nums) {
int total = 0;
for (int n : nums) {
if (n > 0) { // 중복 1
total += n;
}
}
return total;
}
default int countPositives(List<Integer> nums) {
int count = 0;
for (int n : nums) {
if (n > 0) { // 중복 2
count++;
}
}
return count;
}
// n > 0 검사 로직이 중복
// public default 메서드로 만들면 외부 노출
// 내부 헬퍼 메서드 필요 → Java 9+ private 도입
}
public interface Calculator {
default int sumPositives(List<Integer> nums) {
return nums.stream()
.filter(this::isPositive) // private 호출
.mapToInt(Integer::intValue)
.sum();
}
default int countPositives(List<Integer> nums) {
return (int) nums.stream()
.filter(this::isPositive) // private 호출
.count();
}
// ★ Java 9+: private 메서드
private boolean isPositive(int n) {
return n > 0;
}
}
1. private 키워드 필수
private void helper() { ... }
2. 본체 작성 필수
- 추상 X
- 구현 있어야
3. 외부 노출 X
- 인터페이스 안에서만 호출
- 구현체에서도 호출 X
- 자식 인터페이스에서도 호출 X
4. private static 도 가능
- static 메서드 간 공유
public interface MathUtil {
static int sumOfSquares(int a, int b) {
return square(a) + square(b);
}
static int diffOfSquares(int a, int b) {
return square(a) - square(b);
}
// ★ Java 9+: private static 메서드
private static int square(int n) {
return n * n;
}
}
// 사용
int s = MathUtil.sumOfSquares(3, 4);
// MathUtil.square(5); // ❌ 컴파일 에러 (private)
public interface Demo {
// private 인스턴스 메서드
// - default 메서드 안에서만 호출
// - this 사용 가능
private void instanceHelper() { ... }
// private static 메서드
// - default 와 static 메서드 양쪽에서 호출
// - this 사용 불가
private static void staticHelper() { ... }
default void method1() {
instanceHelper(); // ✓
staticHelper(); // ✓
}
static void method2() {
// instanceHelper(); // ❌ static 에서 instance 호출 불가
staticHelper(); // ✓
}
}
// java.util.stream.Collectors 같은 곳에 활용
public interface SomeInterface {
default List<String> filterAndSort(List<String> input) {
return input.stream()
.filter(this::isValid)
.sorted(getComparator())
.collect(Collectors.toList());
}
default List<String> filterAndCount(List<String> input) {
long count = input.stream()
.filter(this::isValid)
.count();
return List.of(String.valueOf(count));
}
private boolean isValid(String s) {
return s != null && !s.isEmpty();
}
private Comparator<String> getComparator() {
return Comparator.naturalOrder();
}
}
Java 9+ 인터페이스의 가능한 멤버:
추상 메서드:
abstract void m1(); // 또는 그냥 void m1();
default 메서드:
default void m2() { ... }
static 메서드:
static void m3() { ... }
private 메서드:
private void m4() { ... }
private static 메서드:
private static void m5() { ... }
상수 필드:
int CONSTANT = 100; // public static final 자동
중첩 타입:
interface Inner { ... }
class Helper { ... }
enum Type { ... }
→ 인터페이스가 추상클래스와 거의 동등한 표현력.
Java 9 의 private 메서드가 추가된 이유는?
답:
1. 코드 중복 제거:
외부 노출 회피:
API 응집도:
static 메서드 간 공유:
→ 인터페이스 표현력의 완성.
public interface Collection<E> extends Iterable<E> {
// 기존 추상 메서드들
boolean add(E e);
boolean remove(Object o);
int size();
// ...
// ★ Java 8+ 추가 default 메서드들
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
}
// 활용
List<Shipment> list = ...;
list.removeIf(s -> s.getStatus() == ShipmentStatus.CANCELLED);
list.stream().filter(...).forEach(...);
public interface Iterable<T> {
Iterator<T> iterator();
// ★ Java 8+
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
// 활용
List<String> list = ...;
list.forEach(System.out::println);
public interface Comparator<T> {
int compare(T o1, T o2);
// ★ Java 8+ default 메서드들
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor) {
return thenComparing(comparing(keyExtractor));
}
default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor) {
return thenComparing(comparingInt(keyExtractor));
}
// ★ Java 8+ static 메서드들
static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
static <T> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}
static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) { ... }
}
// 강력한 활용
List<Shipment> shipments = ...;
// 단일 정렬
shipments.sort(Comparator.comparing(Shipment::getWeight));
// 복합 정렬
shipments.sort(
Comparator.comparing(Shipment::getCreatedAt)
.thenComparing(Shipment::getWeight)
.thenComparing(Comparator.comparing(Shipment::getBlNo).reversed())
);
→ Comparator 가 함수형 인터페이스 + 풍부한 default 의 모범.
public interface Map<K, V> {
// 기존
V get(Object key);
V put(K key, V value);
// ★ Java 8+ default
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v : defaultValue;
}
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
V v;
if ((v = get(key)) == null) {
V newValue = mappingFunction.apply(key);
if (newValue != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
V oldValue = get(key);
V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
default void forEach(BiConsumer<? super K, ? super V> action) {
for (Map.Entry<K, V> entry : entrySet()) {
action.accept(entry.getKey(), entry.getValue());
}
}
// ... 더 많은 default
}
// 활용 — 캐싱 패턴
Map<Long, Shipment> cache = new HashMap<>();
Shipment s = cache.computeIfAbsent(id, this::loadFromDb);
// 빈도 카운팅
Map<String, Long> counts = new HashMap<>();
counts.merge(key, 1L, Long::sum);
public interface List<E> extends Collection<E> {
// ★ Java 8+
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
}
// 활용
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.replaceAll(String::toUpperCase);
// 결과: [A, B, C]
list.sort(Comparator.reverseOrder());
// 결과: [C, B, A]
public interface Stream<T> extends BaseStream<T, Stream<T>> {
// 추상
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// ...
// ★ static 팩토리
static <T> Stream<T> of(T... values) { ... }
static <T> Stream<T> empty() { ... }
static <T> Stream<T> generate(Supplier<? extends T> s) { ... }
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f) { ... }
}
// 사용
Stream.of(1, 2, 3, 4, 5)
.filter(n -> n > 2)
.map(n -> n * n)
.forEach(System.out::println);
public final class Optional<T> {
// (실제로는 class, 인터페이스 아님)
// 하지만 비슷한 패턴
public static <T> Optional<T> of(T value) { ... }
public static <T> Optional<T> empty() { ... }
public static <T> Optional<T> ofNullable(T value) { ... }
public boolean isPresent() { ... }
public T get() { ... }
public T orElse(T other) { ... }
public T orElseGet(Supplier<? extends T> supplier) { ... }
public T orElseThrow() { ... }
}
@Service
public class ShipmentService {
private final Map<Long, Shipment> cache = new ConcurrentHashMap<>();
public Shipment getOrLoad(Long id) {
// Map.computeIfAbsent — default 메서드 활용
return cache.computeIfAbsent(id, this::loadFromDb);
}
public List<Shipment> getActive() {
// Collection.stream + Stream.filter + Collectors.toList
return repository.findAll().stream()
.filter(s -> s.getStatus() == ShipmentStatus.ACTIVE)
.toList();
}
public void cleanupExpired() {
// Collection.removeIf — default
cache.values().removeIf(Shipment::isExpired);
}
public Map<String, Long> countByRoute(List<Shipment> shipments) {
Map<String, Long> counts = new HashMap<>();
shipments.forEach(s ->
counts.merge(s.getRoute(), 1L, Long::sum)
);
return counts;
}
public List<Shipment> sorted(List<Shipment> shipments) {
return shipments.stream()
.sorted(
Comparator.comparing(Shipment::getPriority).reversed()
.thenComparing(Shipment::getCreatedAt)
)
.toList();
}
}
→ default + static 메서드가 모든 자바 코드의 핵심.
Java 8+ 표준 라이브러리의 default 메서드 활용 5가지는?
답:
1. Collection.removeIf, stream, forEach — 람다 순회/필터
2. Comparator.reversed, thenComparing — 복합 정렬
3. Map.computeIfAbsent, merge, getOrDefault — 안전한 캐싱
4. List.replaceAll, sort — 컬렉션 조작
5. Stream.of, empty (static) — 팩토리 메서드
→ 거의 모든 자바 코드의 토대.
| 항목 | default 메서드 | 추상클래스 구현 메서드 |
|---|---|---|
| 위치 | 인터페이스 | 추상클래스 |
| 키워드 | default | (없음) |
| 다중 상속 | ✓ (인터페이스 다중 구현) | ❌ (단일 상속) |
| 인스턴스 필드 접근 | ❌ (필드 X) | ✓ |
| 생성자 | ❌ | ✓ |
| 접근 제어자 | public 자동 | 자유 (private/protected/public) |
| 상태 보유 | ❌ | ✓ |
| Diamond 충돌 | 가능 (명시적 해결) | 단일 상속이라 없음 |
// 인터페이스 + default
public interface Greeter {
String getName(); // 추상
default String greet() {
return "Hello, " + getName();
}
}
// 추상클래스 + 구현 메서드
public abstract class AbstractGreeter {
protected String name; // ← 인스턴스 필드
public AbstractGreeter(String name) { // ← 생성자
this.name = name;
}
public abstract String getTitle();
public String greet() {
return "Hello, " + getTitle() + " " + name;
}
}
차이:
// 동일한 동작을 인터페이스 vs 추상클래스로
// 1. 인터페이스 + default
public interface Animal {
String getName();
default void greet() {
System.out.println("Hi, I'm " + getName());
}
}
public class Dog implements Animal {
private String name;
public Dog(String name) { this.name = name; }
@Override
public String getName() { return name; }
// greet() 자동 사용
}
// 2. 추상클래스
public abstract class AnimalAbstract {
protected String name;
public AnimalAbstract(String name) { this.name = name; }
public void greet() {
System.out.println("Hi, I'm " + name);
}
}
public class Cat extends AnimalAbstract {
public Cat(String name) { super(name); }
// greet() 자동 사용
}
두 방식 모두 가능. 선택 기준:
// ❌ default 에 인스턴스 필드 추가 시도
public interface Counter {
int count; // 컴파일 에러 (public static final 강제)
default void increment() {
count++; // 불가
}
}
// 해결: 추상 메서드로 우회
public interface Counter {
int getCount();
void setCount(int n);
default void increment() {
setCount(getCount() + 1);
}
}
// 또는 추상클래스 선택
public abstract class AbstractCounter {
protected int count = 0;
public void increment() {
count++;
}
}
→ 상태가 필요하면 추상클래스가 자연스러움.
// 시나리오: Duck 은 Animal + Swimmer + Flyer
// 인터페이스 활용 (가능)
public interface Animal { String name(); }
public interface Swimmer { default void swim() { ... } }
public interface Flyer { default void fly() { ... } }
public class Duck implements Animal, Swimmer, Flyer {
private String name;
@Override public String name() { return name; }
// swim, fly 자동
}
// 추상클래스만으론 어려움
public abstract class Animal { ... }
public abstract class Swimmer { ... }
public abstract class Flyer { ... }
public class Duck extends Animal {
// Swimmer, Flyer 도 extends?
// ❌ 단일 상속!
// 우회: 컴포지션
private Swimmer swimmer = new BasicSwimmer();
private Flyer flyer = new BasicFlyer();
}
→ 능력 조합은 인터페이스 + default 가 자연스러움.
default 메서드 (인터페이스):
✓ 능력 표현 (can-do)
✓ 다중 구현 필요
✓ 상태 없음 (또는 추상 메서드로 우회)
✓ 기존 인터페이스에 추가
✓ 람다와 함께
추상클래스:
✓ "is-a" 강한 관계
✓ 공통 상태 (필드) 보유
✓ 생성자 필요
✓ protected/package 가시성 활용
✓ Template Method 패턴
✓ 모든 자식이 같은 토대
default 메서드와 추상클래스 구현 메서드의 가장 큰 차이는?
답:
1. 상태 보유:
상속 방식:
의도:
표현력:
→ 추상클래스가 더 풍부, default 는 더 유연.
public interface List<E> {
default void sort(Comparator<? super E> c) {
// 기본 구현
}
}
// 기존 코드
public class MyList<E> implements List<E> {
// sort 안 만들었음 → default 사용
}
// Java 8 업그레이드 후
MyList<String> list = ...;
list.sort(Comparator.naturalOrder());
// default 가 자동 호출됨
// 의도하지 않은 동작 가능 (예: 멀티스레드 안전 X)
문제:
// ❌ default 에서 상태 접근 시도
public interface Counter {
// 추상 메서드
int getValue();
void setValue(int v);
// default 메서드
default void increment() {
// 상태에 접근하려면 추상 메서드로 우회 필요
setValue(getValue() + 1);
// setValue, getValue 호출이 매번
// 추상클래스라면 count++ 한 줄
}
}
문제:
public interface A {
default void m() { System.out.println("A"); }
}
public interface B {
default void m() { System.out.println("B"); }
}
public class C implements A, B {
// ❌ 컴파일 에러
// class C inherits unrelated defaults for m() from types A and B
// 해결: 명시적 오버라이드
@Override
public void m() {
A.super.m();
}
}
문제:
public interface MyInterface {
default void process() {
helper1();
helper2();
}
default void helper1() { ... }
default void helper2() { ... }
}
public class MyClass implements MyInterface {
@Override
public void helper1() {
// 오버라이드
}
}
MyClass m = new MyClass();
m.process();
// 디버깅 시:
// - process() 는 어디서?
// - helper1() 는 오버라이드 됨
// - helper2() 는 인터페이스 default
// - 추적이 복잡
// Java 8 출시 시 Collection 에 추가된 default 들
// → 충분히 검토 후 추가
// 일반적 권장:
// - 정말 필요한 default 만 추가
// - 명확한 기본 동작
// - 모든 구현체에 안전한 동작
public interface Calculator {
int add(int a, int b);
default int multiply(int a, int b) {
// 기본 구현
int result = 0;
for (int i = 0; i < b; i++) {
result = add(result, a);
}
return result;
}
}
// 테스트:
// - add() 만 구현하는 클래스의 multiply() 도 테스트
// - default 가 모든 구현체에서 동작 확인
// - 테스트 부담 ↑
public interface ThreadUnsafeContainer<E> {
// 추상 메서드
void add(E e);
boolean remove(E e);
// default 메서드
default void addAll(Collection<? extends E> c) {
// ❌ 멀티스레드 안전 X
// 다른 스레드가 add 중에 새 요소 추가하면?
for (E e : c) {
add(e);
}
}
}
문제:
public interface MyInterface {
// 같은 시그니처 두 번 — 한 쪽은 추상, 한 쪽은 default
// ❌ 컴파일 에러
void m();
default void m() { } // 같은 시그니처
}
→ 같은 인터페이스에서 같은 시그니처 충돌.
권장:
✓ 편의 메서드 (실제 동작은 추상 메서드 활용)
✓ 기존 인터페이스의 호환 유지
✓ 함수형 인터페이스의 조합 메서드 (and, or, then)
✓ 명확하고 단순한 기본 구현
회피:
✗ 복잡한 비즈니스 로직
✗ 상태에 강하게 의존하는 로직
✗ 멀티스레드 안전 보장 필요 시
✗ Diamond 위험 높은 시그니처
✗ 추상클래스로 더 자연스러운 경우
→ default 는 도구, 만능 아님.
default 메서드 사용 시 주의 사항 3가지는?
답:
1. 상태 의존 회피:
Diamond Problem 검토:
멀티스레드 안전성:
추가:
| Q | 핵심 답변 |
|---|---|
| default 메서드의 등장 이유? | 하위 호환성 + Stream API 도입 |
| default 메서드 문법? | default 키워드 + 본체 |
| Diamond Problem 부활? | Java 8 default 메서드로 |
| Diamond 해결 규칙 3가지? | 클래스 우선, 구체적 우선, 명시적 |
| 인터페이스 static 메서드? | 인터페이스명으로 호출, 상속 안 됨 |
| Java 9 private 메서드 이유? | default 간 코드 공유, 외부 노출 회피 |
| default vs 추상클래스 구현 차이? | 상태 X, 다중 구현, 가시성 public |
| Collection.stream 가능 이유? | default 메서드로 추가 |
| Comparator.reversed/thenComparing? | default 메서드 조합 |
| Map.computeIfAbsent? | default 안전한 캐싱 |
| default 의 위험? | 의도하지 않은 동작, Diamond, 멀티스레드 |
답:
답:
답:
public interface MyInterface {
default void method1() {
method2(); // ✓ 가능
}
default void method2() {
System.out.println("2");
}
}
답:
public class C implements MyInterface, MyInterface {
// ❌ 컴파일 에러 (중복)
}
public interface A { default void m() {} }
public interface B extends A { }
public class C implements A, B { // OK, 간접 중복
// A 의 default 사용
}
답:
1. default 메서드 = 하위 호환성의 해결
2. Diamond Problem 의 부활과 해결
3. 인터페이스의 표현력 진화
이번 Unit에서 default 의 메커니즘을 봤다면, 다음은 선택의 결정적 기준.
🚀 Phase 4 — 추상화의 두 도구
✅ Unit 4.1 추상클래스의 특징
✅ Unit 4.2 인터페이스의 특징
✅ Unit 4.3 Java 8 default & static 메서드 ← 여기
⏭ Unit 4.4 추상클래스 vs 인터페이스 선택 기준 (Phase 4 완주)
✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
🚀 Phase 4 — 추상화의 두 도구 (3/4 진행)
총: 16/43 Unit 작성 (약 37%)