21. 인터페이스는 구현하는 쪽을 생각해 설계하라
1. 핵심 내용
인터페이스 변경 시 기존 구현체에 미치는 영향을 반드시 고려해야 한다.
특히,
default method
를 추가할 때 기존 구현체에서 예상치 못한 동작이 발생할 수 있음을 염두에 둬야 한다.기능 추가뿐만 아니라, 기존 코드와의 호환성을 유지하면서 확장할 방법을 고민하는 것이 인터페이스 설계의 핵심이다.
2. 목차
인터페이스를 변경하면 발생하는 문제
default method 추가가 기존 구현체에 미치는 영향
실제 사례: Collection 인터페이스의 removeIf 메서드
결론: 인터페이스 설계 시 기존 구현체를 반드시 고려해야 한다
1. 인터페이스를 변경하면 발생하는 문제
1) 기존 인터페이스 확장이 어려운 이유
인터페이스는 구현해야 하는 규칙(계약, Contract)을 정의하는 역할을 한다.
기존 인터페이스에 메서드를 추가하면, 기존 구현체들은 이 메서드를 모르고 존재하게 된다.
컴파일러는 기존 클래스들이 인터페이스의 계약을 완전히 따르지 않는다고 판단하여 컴파일 오류를 발생시킨다.
결과적으로, 모든 구현체가 새로운 메서드를 오버라이드해야 하는 유지보수 부담이 생긴다.
📌 기존 인터페이스 변경 시 발생하는 문제 예제
public interface Vehicle {
void drive();
}
// 기존 클래스
public class Car implements Vehicle {
@Override
public void drive() {
System.out.println("자동차가 주행합니다.");
}
}
🚨 이제 Vehicle 인터페이스에 새로운 메서드를 추가하면?
public interface Vehicle {
void drive();
void honk(); // 새로운 메서드 추가
}
⛔ 컴파일 오류 발생!
"Car 클래스가 honk() 메서드를 구현하지 않았기 때문"
✅ 해결 방법:
"default method를 사용하여 기본 구현 제공"
2) default method가 도입되면서 뭐가 달라졌을까?
자바 8부터 인터페이스에서 기본 구현을 제공할 수 있는
default method
가 추가됨.default method
를 사용하면 기존 구현체들이 새로운 메서드를 몰라도 자동으로 기본 동작을 가지게 됨.즉, 컴파일 오류 없이도 인터페이스를 확장할 수 있는 방법이 생겼다.
📌 default method를 사용한 예제
public interface Vehicle {
void drive();
// 새로운 기능을 default method로 추가
default void honk() {
System.out.println("경적을 울립니다!");
}
}
public class Car implements Vehicle {
@Override
public void drive() {
System.out.println("자동차가 주행합니다.");
}
}
✅ Car 클래스는 honk()를 구현하지 않았지만, 자동으로 추가된 메서드를 사용할 수 있음.
📌 실행 코드
public class Main {
public static void main(String[] args) {
Vehicle car = new Car();
car.drive(); // "자동차가 주행합니다."
car.honk(); // "경적을 울립니다!" (default method 실행)
}
}
2. default method 추가가 기존 구현체에 미치는 영향
1) default method는 기존 구현체에 자동으로 추가됨
기존 구현체들이 새로운 기능을 직접 구현하지 않아도 자동으로 추가됨.
하지만, 모든 기존 구현체가 이를 필요로 하거나 안전하게 사용할 수 있다고 보장할 수 없음.
인터페이스를 설계할 때는 기존 구현체들이 새로운 기능을 올바르게 다룰 준비가 되어 있는지 고려해야 함.
📌 예제: 기존 인터페이스에 default method 추가하기
public interface Printer {
void print();
// 새로운 기능 추가
default void scan() {
System.out.println("스캔 기능을 실행합니다.");
}
}
// 기존 클래스
public class BasicPrinter implements Printer {
@Override
public void print() {
System.out.println("문서를 인쇄합니다.");
}
}
✅ "BasicPrinter는 scan()을 구현하지 않았지만, scan()을 사용할 수 있음."
public class Main {
public static void main(String[] args) {
Printer printer = new BasicPrinter();
printer.print(); // "문서를 인쇄합니다."
printer.scan(); // "스캔 기능을 실행합니다." (자동 추가된 기능)
}
}
🚨 문제점:
"BasicPrinter는 기본적인 프린트 기능만 제공하는데, scan() 기능이 자동으로 추가됨."
"scan()이 필요하지 않은 프린터에서도 scan() 메서드가 존재하는 것이 타당할까?"
"이처럼 default method는 기존 구현체에 예상치 못한 동작을 추가할 가능성이 있음."
2) 문제 발생 가능성
(1) 필요 없는 기능이 자동 추가됨
모든 클래스가 새로운 기능을 원하지 않을 수 있음.
"BasicPrinter"
예제처럼"기능이 필요하지 않은 클래스에도 default method가 추가됨."
"이 기능이 반드시 필요한 것이 아니라면 인터페이스가 과도하게 확장될 위험이 있음."
(2) 설계 원칙과 충돌 가능
인터페이스 설계 시, 각 구현체의 동작 원칙을 고려해야 함.
그러나, default method는 기존 원칙을 무시하고 추가될 가능성이 있음.
📌 예제: 강제로 추가된 default method가 기존 원칙을 깨뜨리는 경우
public interface SecureStorage {
void storeData(String data);
// 기본적으로 모든 저장소에서 데이터를 암호화한다고 가정
default void encryptData(String data) {
System.out.println("데이터를 암호화하여 저장합니다.");
}
}
// 기존 클래스 (암호화 필요 없음)
public class SimpleStorage implements SecureStorage {
@Override
public void storeData(String data) {
System.out.println("데이터를 암호화 없이 저장합니다.");
}
}
public class Main {
public static void main(String[] args) {
SecureStorage storage = new SimpleStorage();
storage.storeData("비밀번호123"); // "데이터를 암호화 없이 저장합니다."
storage.encryptData("비밀번호123"); // "데이터를 암호화하여 저장합니다." (기존 원칙과 충돌)
}
}
🚨 문제점:
"SimpleStorage는 원래 암호화를 하지 않는데, encryptData()가 자동 추가됨."
"이로 인해 인터페이스가 기존 구현체의 원칙을 깨뜨릴 수 있음."
✅ 해결 방법
기존 원칙을 지키면서 확장할 수 있는 방식으로 설계해야 함.
필요하지 않은 기능을 강제로 추가하기보다는, 새로운 인터페이스로 분리하는 것이 더 적절할 수도 있음.
(3) 기존 구현체가 새로운 기능을 예상하지 못했을 수 있음
기존 코드가 새로운 기능을 올바르게 다룰 준비가 되어 있지 않다면, 예상치 못한 동작을 유발할 가능성이 있음.
"특히, default method가 기존 코드와 충돌할 경우, 런타임 오류를 발생시킬 수도 있음."
📌 예제: 멀티스레드 환경에서 예상치 못한 동작 발생
자바의 Collections.synchronizedCollection()을 사용하면,
컬렉션을 동기화된(Thread-Safe) 형태로 감쌀 수 있다.
즉, 컬렉션의 add(), remove() 등의 메서드는 동기화된 상태에서 실행된다.
하지만 removeIf()는 default method로 추가된 메서드이므로, 동기화를 고려하지 않고 구현됨.
이로 인해 멀티스레드 환경에서 예기치 못한 동작이 발생할 수 있다.
import java.util.*;
public class SynchronizedCollectionExample {
public static void main(String[] args) {
// 동기화된 컬렉션 생성
Collection<Integer> syncNumbers = Collections.synchronizedCollection(
new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5))
);
// 여러 스레드가 동시에 removeIf 실행 시 충돌 가능성 있음
syncNumbers.removeIf(n -> n % 2 == 0);
System.out.println(syncNumbers); // [1, 3, 5]
}
}
🚨 문제점:
"SynchronizedCollection은 멀티스레드 환경에서 안전해야 하지만, removeIf는 동기화 없이 실행됨."
"여러 스레드가 동시에 removeIf를 실행하면 데이터 충돌 가능성이 발생할 수 있음."
3. 실제 사례: Collection 인터페이스의 removeIf 메서드
1) removeIf는 자바 8에서 default method로 추가됨
Collection
인터페이스에removeIf
가 추가되면서 요소를 특정 조건에 따라 쉽게 삭제할 수 있게 됨.기존 구현체는 별도의 구현 없이
removeIf
를 사용할 수 있음.하지만, 모든 기존 컬렉션이 이를 안전하게 사용할 수 있는 것은 아님.
2) 일반적인 removeIf 동작 (문제 없음)
import java.util.*;
public class RemoveIfExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 짝수만 제거
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
}
}
✅ ArrayList에서는 removeIf가 정상적으로 동작함. ✅ 리스트 내부 요소를 순회하며 주어진 조건(짝수)을 만족하는 요소를 삭제함.
3) SynchronizedCollection에서 발생할 수 있는 문제
📌 SynchronizedCollection이란?
멀티스레드 환경에서 안전한 컬렉션.
Collections.synchronizedCollection()
을 사용하면 컬렉션의 메서드가 동기화됨.하지만
removeIf
는default method
로 제공되면서 동기화를 고려하지 않고 추가됨.따라서,
SynchronizedCollection
에서도 동기화되지 않은 상태에서 실행될 가능성이 있음.
📌 SynchronizedCollection에서 removeIf 실행 (문제 발생 가능)
import java.util.*;
public class SynchronizedCollectionExample {
public static void main(String[] args) {
Collection<Integer> syncNumbers = Collections.synchronizedCollection(
new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5))
);
syncNumbers.removeIf(n -> n % 2 == 0);
System.out.println(syncNumbers); // [1, 3, 5]
}
}
문제점
"SynchronizedCollection은 모든 메서드가 동기화되어야 하지만, removeIf는 그렇지 않음."
"멀티스레드 환경에서 데이터 충돌 가능성이 발생함."
✅ 해결 방법: "removeIf 실행 전 동기화 적용"
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (this) { // 동기화 적용
return super.removeIf(filter);
}
}
4. 결론: 인터페이스 설계 시 기존 구현체를 반드시 고려해야 한다
새로운 기능 추가는 기존 코드에 영향을 줄 수 있다.
기존 코드가 안전하게 동작할지 예측해야 한다.
기존 원칙을 깨지 않으면서 확장하는 방식으로 설계해야 한다.
📌 즉, 인터페이스 설계는 "기능 추가"가 아닌, "기능 확장"의 관점에서 접근해야 한다. 🚀
Last updated