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