지난 글에서는 클래스, 객체, 메서드, 생성자 주입, IoC(제어의 역전) 개념까지 알아보았습니다...(알아 봤죠??)
이번에는 스프링 구조에서 당연하게 사용되는 인터페이스가 왜 필요한지를 알아보겠습니다...(알아 볼꺼죠?)
"UserService에 알림 기능을 넣고 싶은데...
이메일도 있고, 문자도 있고, 슬릭도 있고, 뭐 이것저것 있고...
아! 나중엔 카카오톡을 쓸 수도 있어요!!"
클라이언트가 이렇게 말한다면??
지금 당장 선빵을 갈...기는건 아니고요,
우리는 인터페이스 기반 설계로 이미 준비되어 있으니 걱정 없습니다!
처음에는 이렇게 단순할 수 있습니다.
public class UserService {
private final EmailService emailService = new EmailService();
public void process() {
emailService.send("가입 완료!");
}
}
하지만 이런 구조는 유연하지 않습니다.
→ 새로운 알림 방식을 도입하려면 이 코드를 직접 수정해야 하니까요!
public class SmsService { ... }
public class SlackService { ... }
위와 같은 서비스가 추가될 때마다 UserService 코드는 조건문으로 점점 복잡해지고…
if (type.equals("email")) {
emailService.send();
} else if (type.equals("sms")) {
smsService.send();
} ...
이제 서비스를 확장하는 건 ‘수정의 연속’이 됩니다.
이럴 때 등장하는 게 바로 인터페이스입니다.
public interface NotificationService {
void send(String message);
}
그리고 다양한 구현체를 만들어보죠:
public class EmailNotificationService implements NotificationService {
public void send(String message) {
System.out.println("이메일 발송: " + message);
}
}
public class SmsNotificationService implements NotificationService {
public void send(String message) {
System.out.println("SMS 발송: " + message);
}
}
public class UserService {
private final NotificationService notificationService;
public UserService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void process() {
notificationService.send("가입 완료!");
}
}
구현체가 무엇이든 관계없습니다.
인터페이스만 맞으면 교체도, 테스트도 자유롭습니다!
NotificationService service = new SmsNotificationService();
service.send("어서오세요!");
OCP (Open-Closed Principle)
기존 코드는 수정하지 않고 (Closed)
새로운 기능은 확장 (Open) 할 수 있도록!
UserService는 절대 바꾸지 않고도
SlackNotificationService를 만들어 넣기만 하면 OK!
public class FakeNotificationService implements NotificationService {
public void send(String message) {
System.out.println("[테스트용] 알림: " + message);
}
}
→ 테스트에서는 진짜 이메일/문자 보내지 않고
가짜 구현체(Fake) 로 대체 가능 = 테스트 안정성 증가!
@Service
public class EmailNotificationService implements NotificationService { ... }
@RequiredArgsConstructor
@RestController
public class UserController {
private final NotificationService notificationService;
@PostMapping("/notify")
public void send() {
notificationService.send("API 호출됨");
}
}
NotificationService를 구현한 클래스가 자동으로 주입됨| 항목 | 직접 의존 방식 | 인터페이스 기반 |
|---|---|---|
| 구현체 교체 | 코드 수정 필요 | 객체만 바꾸면 됨 |
| 테스트 | 실제 서비스 사용 | Fake/MOCK 가능 |
| 유지보수 | 결합도 높음 | 유연하고 확장 쉬움 |
| 스프링 호환성 | 낮음 | DI 구조와 찰떡 |
| 개념 | 설명 |
|---|---|
| 인터페이스 | 기능 명세서, 계약서 |
| 구현체 | 실제 동작을 담당하는 클래스 |
| 다형성 | 같은 인터페이스로 다양한 행동 |
| OCP | 기존 코드 수정 없이 기능 확장 |
| 테스트 이점 | 가짜 구현체로도 동작 가능 |
| 스프링과 연결 | 인터페이스 기반 DI 구조가 핵심 |
와.. 도움 많이 되었습니다 감사합니다