바뀔 가능성이 있는 역할의 경계를 정하고 구현을 갈아 끼웁니다.
인터페이스는 서로 다른 코드가 구현 세부를 몰라도 함께 일하게 만드는 약속입니다. 예를 들어 강좌 완료 서비스는 알림이 콘솔인지 이메일인지 알 필요 없이 send만 호출합니다. 운영에서는 이메일 구현을, 로컬에서는 콘솔 구현을, 테스트에서는 기록만 남기는 가짜 구현을 넣을 수 있으므로 서비스 코드를 고치지 않고 실행 환경과 요구에 맞게 선택할 수 있습니다.
예제에서 interface Notifier { void send(String message); } 부분부터 찾으세요. 선언 타입, 실제 객체와 반환값을 한 줄씩 따라가면서 컴파일 단계에서 잡히는 문제와 실행 중 생기는 문제를 구분하세요.
인터페이스 타입으로 필드를 선언하고 생성자에서 구현 객체를 받으면 호출하는 쪽과 구현하는 쪽의 변경 이유가 나뉩니다. 결제 수단, 파일 저장소, 외부 API처럼 구현이 둘 이상이거나 테스트에서 대체해야 하는 경계에 특히 잘 맞습니다. 한 클래스 안에서만 쓰고 대체할 이유도 없는 작은 로직까지 무조건 인터페이스로 만들면 파일과 이름만 늘어나므로 먼저 실제 변화 지점이 있는지 확인하세요.
계약은 구현 클래스가 가진 모든 메서드가 아니라 호출자에게 필요한 최소 동작만 담습니다. 반환값, 예외와 부수 효과까지 계약으로 문서화하고, 새 구현에는 같은 계약 테스트를 적용합니다. default 메서드는 기존 구현을 깨지 않고 동작을 보태는 데 쓸 수 있지만 인터페이스가 여러 책임을 떠안는 신호는 아닌지 함께 살펴봅니다.
현재 코드를 먼저 실행해 출력과 타입 흐름을 확인한 뒤 EmailNotifier를 추가해 같은 CourseService에 전달하고, 테스트에서는 RecordingNotifier의 lastMessage를 검사하세요.
구현 메서드는 interface 메서드보다 접근 범위를 좁힐 수 없어 public이어야 합니다. 인터페이스 이름을 구현 클래스 이름에서 기계적으로 따기보다 Notifier처럼 역할과 의도가 드러나게 지으세요.
직접 바꿔보세요EmailNotifier를 추가해 같은 CourseService에 전달하고, 테스트에서는 RecordingNotifier의 lastMessage를 검사하세요.
실행 버튼을 누르면 결과가 여기에 표시됩니다.