맨땅에 헤딩하는 개바른자

리스코프 치환 원칙 (LSP) Liskov substitytion principle 본문

JAVA

리스코프 치환 원칙 (LSP) Liskov substitytion principle

앵낄낄 2022. 5. 26. 19:52
반응형

클래스 상속과 인터페이스 구현을 올바르게 사용하도록 도와준다.

“형식”과 “하위형식" 이라는 용어가 있다고 한다면 부모와 자식의 관계를 이루었다는 걸 유추 할 수 있다.

자식은 부모를 상속하거나 구현할 수 있으며 부모의 기능을 그대로 물려 받는 행동을 유지해야한다고 가정하면 이해가 쉬우려나?…

아무튼 부족한 짱구로 글로 이해하려니 쉽지가 않다

간단하게 말하면 부모의 Type의 기능은 자식 타입에서 유지가 되고 그 기능을 사용할 수 있다.

이해한 기반으로 코드로 설명해보겠습니다.

Child라는 상속클래스에는 "String 나는_X째_입니다();" 하는 메소드가 정의되어있고
각 하위 클래스 FirstChild, SecondChild에는 extends Child를 상속하여 구현되어있다.
ThirdChild는 상속도 없고 기능도 없다.

FirstChild에는 아무 기능을 선언하지 않았다.
SecondChild에는 "String 나는_X째_입니다();"를 @Override하여 메소드 출력 값을 직접 구현하였다

Child child = new FirstChild();
child.나는 X째 입니다();
결과 : 나는 X째 입니다.
--------------------------------
Child child = new SecondChild();
child.나는 X째 입니다();
결과 : 나는 둘째 입니다.
--------------------------------
Child child = new ThirdChild();
컴파일 오류!!

결과를 보았듯이 첫째는 아무기능도 구현하지 않아도 부모의 기능을 사용할 수 있었고
둘째는 직접구현한걸 오버라이딩하여 출력을 바꾸었다. 
셋째는 컴파일 오류가 날 것이다. 상속을 안했기 때문에

이 과정에서 Child 상속클래스로 부모의 정해진 기능을 수행하되 어떤 하위클래스를 주입받느냐에 따라 결과물이 다르게 나올 수 있다.

또 다른 예시 코드로 설명해보겠습니다.

Type을 주입받아서 클래스가 동작하는게 있다고 가정해보겠습니다.
class Parent<T extends Child> {
		String 몇째니(T t);
}

첫째 소환
Parent parent1 = new Parent<FirstChild>();
parent1.몇째니(new FirstChild());

둘째 소환
Parent parent2 = new Parent<SecondChild>();
parent2.몇째니(new SecondChild());

셋째 소환
Parent parent3 = new Parent<ThirdChild>(); << 컴파일 오류
parent3.몇째니(new ThirdChild());
요렇게 인스턴스를 선언하고

웃기지만 상황극을 해보겠다 눈가리고 부모가 아이를 찾고있다고 해보자
첫째와 둘째 셋째를 확인하기위해 몇째니라는 질문의 기능이 있다고 치면
첫째는 "나는 X째 입니다."
둘째는 "나는 둘째 입니다." (아주 예의바른놈)
셋째는 아무런 말을 듣지 못할 것이다... (오류 발생)
으로결과가 나올 것이다.

부모과 자식간의 상속 구현관계에 따라서 인스턴스화 할 때 기능 호출을 할 수 있거나, 선언자체를 강하게 명시할 수도있습니다.

Parent<>구간을 보시면 Type으로 주입되는 클래스가 아주 명확하게 Child를 받은 객체만 받는다고 선언되어있습니다.

Child라는 부모를 상속받지 않으면 사용할 수도 없는 구조로 되버립니다. 그래서 셋째는 컴파일오류가 발생할 것입니다.

그러면.. 적절한 예시를 통해서 어떨 때 이점있게 사용할 수 있는지 알아보겠습니다.

파일을 import하여 파싱한 결과를 저장하는 기능이 있다고 합니다.

여러 확장자가 존재하며 그 중 파싱이 지원되는 확장자만 파싱을 수행하고 이외에는 오류를 반환하는 코드 입니다.

public class DocumentManagementSystem {
    private final List<Document> documents = new ArrayList<>();
    private final List<Document> documentsView = unmodifiableList(documents);
    // tag::importer_lookup[]
    private final Map<String, Importer> extensionToImporter = new HashMap<>();

    public DocumentManagementSystem() {
        extensionToImporter.put("letter", new LetterImporter());
        extensionToImporter.put("report", new ReportImporter());
        extensionToImporter.put("jpg", new ImageImporter());
    }
    // end::importer_lookup[]
    {
        extensionToImporter.put("invoice", new InvoiceImporter());
    }

    // tag::importFile[]
    public void importFile(final String path) throws IOException {
        final File file = new File(path);
        if (!file.exists()) {
            throw new FileNotFoundException(path);
        }

        final int separatorIndex = path.lastIndexOf('.');
        if (separatorIndex != -1) {
            if (separatorIndex == path.length()) {
                throw new UnknownFileTypeException("No extension found For file: " + path);
            }
            final String extension = path.substring(separatorIndex + 1);
            final Importer importer = extensionToImporter.get(extension);
            if (importer == null) {
                throw new UnknownFileTypeException("For file: " + path);
            }

            final Document document = importer.importFile(file);
            documents.add(document);
        } else {
            throw new UnknownFileTypeException("No extension found For file: " + path);
        }
    }
    // end::importFile[]

    public List<Document> contents() {
        return documentsView;
    }

    public List<Document> search(final String query) {
        return documents.stream()
                        .filter(Query.parse(query))
                        .collect(Collectors.toList());
    }
}

extensionToImporter 맵에는 Importer 를 구현한 객체들의 인스턴스화 된 객체를 확장자별로 등록해두었습니다.

파일이 들어오는 확장자에 따라서 extensionToImporter 매칭되는 확장자가 있으면 importer()기능을 사용할 수 있고, extensionToImporter 매칭되는 확장자가 없으면 오류를 반환하는 코드가 됩니다.

파일을 읽고 파싱하는 과정의 로직은 한 구간으로 되어있고, 확장자에 따라서 다르게 파싱구간만 각각 다르게 구현해야할 때 LSP원칙을 이용하게되면 코드가 간결하고, 유지보수가 용이할 수 있는 코드가 나올 것입니다.

확장자가 추가될 때 중심의 코드를 수정하는것이 아닌 Importer 의 구현체를 추가하기만하면 기존에 검증 된 로직을 그대로 사용 할 수 있습니다.

반응형