1. 리스코프 치환의 원칙

“자식 클래스는 부모 클래스에서 기대되는 행동을 깨뜨리지 않고 부모 클래스를 대체할 수 있어야 한다.”

  • 즉 부모 클래스의 객체를 사용하는 모든 코드에서 자식 클래스의 객체로 대체해도 프로그램의 동작에 문제가 없어야 한다는 뜻
  • 자식 클래스는 부모 클래스의 규약을 반드시 준수해야 한다.
    • 자식클래스는 부모 클래스의 내용을 삭제하거나 수정하면 안된다.

2. 예시

2-1 안좋은 예시

상황

  • 부모 클래스는 “메시지를 발송한다”는 기능을 제공
  • 자식 클래스는 메시지를 저장하는 것이 아니라 메시지를 저장한다는 기능으로 바꾼다.
// 부모 클래스: 메시지 발송 서비스
@Component
public class MessageService {
    public void sendMessage(String message) {
        System.out.println("메시지 발송: " + message);
    }
}
 
// 자식 클래스: 메시지를 저장하는 기능으로 변경 (계약 위반)
@Component
public class SaveMessageService extends MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("메시지 저장: " + message); // 메시지 발송을 하지 않음
    }
}
 
// 클라이언트 코드
@Service
public class NotificationService {
 
    private final MessageService messageService;
 
    @Autowired
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }
 
    public void notify(String message) {
        messageService.sendMessage(message); // 메시지가 발송되길 기대
    }
}
 
// 메인 클래스
@SpringBootApplication
public class LspViolationApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(LspViolationApplication.class, args);
 
        NotificationService notificationService = context.getBean(NotificationService.class);
        notificationService.notify("Hello, LSP!");
    }
}
 
결과
  • NotificationService는 메시지가 발송되길 기대하지만, SaveMessageService를 주입받으면 메시지가 발송되지 않고 저장만 되는 문제
  • 부모 클래스의 동작 규칙을 자식 클래스가 어겼기 때문에 예상치 못한 문제가 발생
  • 개인 혼자라면 이를 기억할수 있지만 팀원이 있을 경우 이를 파악하기 어려움

2-2 좋은 예시

상황

  • 부모 클래스는 “메시지를 발송한다”는 기능을 제공
  • 자식 클래스는 메시지를 저장하는 것이 아니라 메시지를 저장한다는 기능으로 바꾸는 것이 아니라 추가한다.
  • 부모 클래스의 “메시지를 발송한다.” 를 유지한 채로 여러 방법으로 “메시지를 발송하는 기능 추가”
// 부모 클래스: 메시지 발송 서비스
@Component
public class MessageService {
    public void sendMessage(String message) {
        System.out.println("메시지 발송: " + message);
    }
}
 
// 자식 클래스: 메시지를 저장하는 메서드를 추가 (계약 준수)
@Component
public class SaveAndSendMessageService extends MessageService {
    public void saveMessage(String message) {
        System.out.println("메시지 저장: " + message);
    }
}
 
// 클라이언트 코드
@Service
public class NotificationService {
 
    private final MessageService messageService;
 
    @Autowired
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }
 
    public void notify(String message) {
        if (messageService instanceof SaveAndSendMessageService) {
            ((SaveAndSendMessageService) messageService).saveMessage(message); // 메시지를 저장
        }
        messageService.sendMessage(message); // 메시지를 발송
    }
}
 
// 메인 클래스
@SpringBootApplication
public class LspComplianceApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(LspComplianceApplication.class, args);
 
        NotificationService notificationService = context.getBean(NotificationService.class);
        notificationService.notify("Hello, LSP with Contract Compliance!");
    }
}
 
 

결과

  • SaveAndSendMessageService는 부모 클래스의 기능(메시지 발송)을 유지하면서 메시지 저장 기능을 추가한다.
  • NotificationService는 부모 클래스 타입(MessageService)을 사용할 수 있으며, 자식 클래스(SaveAndSendMessageService)를 주입받더라도 예상대로 동작한다.
  • 하지만 전에 배웠던 단일 책임원칙을 위반하는것 같다… 아래에 수정해서 알려주겠다.
@Component
public class MessageSaver {
    public void saveMessage(String message) {
        System.out.println("메시지 저장: " + message);
    }
}
 
@Component
public class MessageSender {
    public void sendMessage(String message) {
        System.out.println("메시지 발송: " + message);
    }
}
 
 
@Service
public class NotificationService {
 
    private final MessageSaver messageSaver;
    private final MessageSender messageSender;
 
    @Autowired
    public NotificationService(MessageSaver messageSaver, MessageSender messageSender) {
        this.messageSaver = messageSaver;
        this.messageSender = messageSender;
    }
 
    public void notify(String message) {
        messageSaver.saveMessage(message);  // 메시지 저장
        messageSender.sendMessage(message); // 메시지 발송
    }
}