리스코프 치환 원칙(LSP)은 SOLID 원칙중에서 가장 이해하기가 어려웠습니다. 우선 리스코프라는 이름을 가진 분이 연구를 했다고 하는데 컴퓨터 과학 분야에 유명한 분인 것 같습니다. 튜링상을 받았다고 하니 대단하신 분이죠.
LSP 를 한 줄로 정리해서 말하면
서브 타입은 언제나 기반 타입으로 교체를 할 수 있어야 한다.
내용이 너무 어려워서 이걸 풀어서 쉽게 설명 드리면 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 사용 했을 때 코드가 원래의 의도대로 동작을 해야 한다는 의미입니다. 추상층에서 정의된 부모가 제공해준 메소드들을 재 정의를 하지 말고 사용을 하라는 의미 입니다.(부모가 준 정보를 훼손하지 말고 사용하라는 뜻 같습니다)
다형성의 원리도 클래스를 상속 및 합성하여 타입을 통합하고 업 캐스팅을 해도 메소드 동작에 문제 없이 설계를 잘 해야한다는걸 OOP 공부를 해보셨다면 다들 잘 아실겁니다.
예를 들어서 위 그림과 추상층에서 생물의 특성을 '숨을 쉰다'와 '다리를 움직인다' 두 가지로 정의했을 때, 사람, 고릴라, 고래, 앵무새가 이 추상화된 특성에 부합하는지를 표로 나타낸 것이다.
생물 | 숨을 쉰다, 두 다리로 걷는다 |
사람 | O |
고릴라 | O |
고래 | X |
앵무새 | X |
이 표에서 볼 수 있듯이, '숨을 쉰다'는 모든 생물에 적용되지만 '다리를 움직인다'는 일부 생물에게는 적용되지 않는다. 사실 생물 모두 다리가 달려있지는 않는다. 이는 추상층의 정의가 너무 구체적일 경우 발생할 수 있는 문제점을 잘 보여준다. 따라서 리스코프 치환 원칙을 적용하기 위해서는 추상층의 정의를 더 일반화하거나, 구체적인 특성은 하위 클래스에서 정의하는 것이 바람직하다는 원칙을 가지고 있습니다. 아래 표로 다시 분류를 해보겠습니다.
생물 -> 숨을 쉰다 | 이족보행 -> 두 다리로 걷는다 | |
사람 | O | O |
고릴라 | O | O |
고래 | O | X |
앵무새 | O | X |
표로 분리를 해놓은 것처럼 별도로 다리로 걷는다를 추상화 계층을 만들어서 최소한의 특징을 가지고 있을떄 사용되지않는 내용을 제거할수가 있습니다.여기서는고래와 앵무새가 이족 보행에 대한 특징이 없으니 별도로 구현이 하지 않습니다.
다른 예시를 가정해 보겠습니다. 파일을 생성하는 기능이 있고 Excel 과 PDF, 한글 파일을 생성하는 기능이 구현이 되어있고 생성된 문서 파일에 AI 요약 글 생성 기능 추가 요청이 온 상황이라고 가정을 해봅시다.
요구사항에 맞게 UML 을 요구사항 처럼 설계를 했습니다. 이렇게 설계를 하고 구현을 하다 보니 한글 파일에서는 AI 기능이 동작이 안된다는 걸 알고 예외 처리를 진행했습니다. (어디까지 예시를 가정 한것입니다. 한글에서도 AI 기능이 지원 될 수 있습니다.)
public abstract class FileGenerator {
// AI 요약
abstract void summaryAI(String token);
// 파일 생성
abstract void generatorFile();
}
public class ExcelFileGenerator extends FileGenerator {
@Override
public void summaryAI(String token) {
System.out.println(token+"토큰 인증 시도");
System.out.println("AI 요약 생성중 입니다.");
}
@Override
public void generatorFile() {
System.out.println("Excel 파일 생성 중입니다.");
}
}
public class PdfFileGenerator extends FileGenerator {
@Override
public void summaryAI(String token) {
System.out.println(token+"토큰 인증 시도");
System.out.println("AI 요약 생성중 입니다.");
}
@Override
public void generatorFile() {
System.out.println("Excel 파일 생성 중입니다.");
}
}
public class HangulFileGenerator extends FileGenerator {
@Override
public void summaryAI(String token) {
throw new Exception("한글파일은 AI 요약기능을 사용할수 없습니다.");
}
@Override
public void generatorFile() {
System.out.println("Excel 파일 생성 중입니다.");
}
}
한글 파일은 AI 요약이 지원 되지 않는 상황이다. 그렇다면 위에서 언급했던 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 사용 했을 때 동작이 문제 없이 될까 ? 라는 생각을 했을 때 한글 파일은 동작이 제대로 안 될 거고 이는 당연히 LSP 를 위반한다고 볼 수 있다. 클래스가 자신이 사용하지 않는 기능을 구현하려고 하여 위반된 예제인 것이다.
그렇다면 이럴 경우는 어떻게 해야 할까 ? 대안은 여러가지가 있지만 필자는 어떠한 클래스가 다른 클래스에 종속이 될 떄는 최소한의 인터페이스만 사용하도록 권장하는 것이다. 즉 최소 단위로 인터페이스를 만들어 사용되는 곳에만 구현을 진행을 해야 합니다.
개선한 UML 을 아래와 같이 그려보았습니다.
그리고 설계된 UML 을 보고 아래와 같이 구현하였습니다.
public interface AIAssistant {
//AI 요약
void summeryAI(String token);
}
public abstract class FileGenerator {
// 파일 생성
abstract void generatorFile();
}
public class ExcelFileGenerator extends FileGenerator implements AIAssistant {
@Override
public void generatorFile() {
System.out.println("Excel 파일 생성 중입니다.");
}
@Override
public void summeryAI(String token) {
System.out.println(token+"토큰 인증 시도");
System.out.println("AI 요약 생성중 입니다.");
}
}
public class PdfFileGenerator extends FileGenerator implements AIAssistant {
@Override
public void generatorFile() {
System.out.println("Excel 파일 생성 중입니다.");
}
@Override
public void summeryAI(String token) {
System.out.println(token+"토큰 인증 시도");
System.out.println("AI 요약 생성중 입니다.");
}
}
public class HangulFileGenerator extends FileGenerator {
@Override
public void generatorFile() {
System.out.println("Excel 파일 생성 중입니다.");
}
}
Client 클래스는 아래와 같이 구현을 하였습니다.
public class Client {
private FileGenerator fileGenerator;
public Client(FileGenerator fileGenerator) {
this.fileGenerator = fileGenerator;
}
void generatorFile() {
fileGenerator.generatorFile();
}
void useAISummary(String token) {
if (fileGenerator instanceof AIAssistant) {
((AIAssistant) fileGenerator).summeryAI(token);
} else {
System.out.println("AI 기능을 지원하지 않는 파일 생성기입니다.");
}
}
public static void main(String[] args) throws Exception {
Client excelClient = new Client(new ExcelFileGenerator());
excelClient.generatorFile(); // Excel 파일 생성중 입니다.
excelClient.useAISummary("AI_EXCEL"); //AI 요약 생성중 입니다."
Client pdfClient = new Client(new PdfFileGenerator());
pdfClient.generatorFile(); // PDF 파일 생성중 입니다.
excelClient.useAISummary("AI_EXCEL"); // AI 요약 생성중 입니다.
Client hangulClient = new Client(new HangulFileGenerator());
hangulClient.generatorFile(); // 한글 파일을 생성중 입니다.
hangulClient.useAISummary("AI_HANGUL"); //AI 기능을 지원하지 않는 파일 생성기입니다.
}
}
main 함수에 각 클라이언트가 사용할 FileGenerator 구현 클래스를 주입했을 때 이 클래스가 AI 요약 기능을 사용하는지를 확인하기 위해서 useAISummary 메소드 안에 instanceof 로 AIAssistant 사용 유무를 분기처리 하였습니다.
LSP 에서 말하고자 하는건 최소한의 추상층을 구성하여 필요한 내용을 구현하는것 같습니다. 사실 부모 타입으로 의도된 동작을 해야하는거고 이 부분이 충족 되면 OCP 에서 말했던것 처럼 변경에는 닫혀있고 확장에는 열려있는 구조가 성럽이 되는것 같습니다.
참고
https://www.youtube.com/watch?v=ebTdQrCGXvg
https://www.youtube.com/watch?v=E9NZ0YEZrYU&list=PLBNdLLaRx_rIRXCp9tKsg7qDQmAX19ocw