Java

[객체지향설계원칙] LSP

shininghyunho 2024. 5. 29. 21:50

LSP

Liskov Substitution Principle

이전 SRP, OCP 와 달리 이름으로 어떤 의미인지 파악하기가 힘듭니다.

하지만 핵심은 간단합니다.

 

하위 클래스가 상위 클래스를 완벽히 대체해야한다.

= 상위 클래스의 명세를 하위 클래스가 정확히 이행해야한다.

= 상위 클래스를 하위 클래스로 변경해도 정상 동작해야한다.

 

즉 클래스를 상속했을때 상위 클래스의 동작을 잘 수행해야한다는 말입니다.

 

List 예제

우리는 List를 사용할때 타입은 List로 선언하고

구현체는 ArrayList or LinkedList 를 사용합니다. (Vector는 단일스레드 환경에서 잘 사용x)

List 인터페이스와 구현체들

 

그래서 다음과 같이 특정 모듈을 만들때

List 타입으로 선언하여 사용하면

실제로 호출할때 ArrayList 인지 LinkedList인지 상관이 없습니다.

import java.util.*;

class ListModule {
    List<Integer> list;

    public void initList(List<Integer> list) {
        this.list=list;
    }

    public void addToList(List<Integer> other) {
        for(int i=0;i<list.size()&&i<other.size();i++) {
            list.set(i,list.get(i)+other.get(i));
        }
    }

    public void printAll() {
        StringBuilder sb=new StringBuilder();
        for(int v:list) sb.append(v).append(' ');
        System.out.print(sb);
    }
}

public class Example {
    public static void main(String[] args) {
        ListModule listModule=new ListModule();
        listModule.initList(new ArrayList<>(Arrays.asList(1,2,3,4)));
        listModule.addToList(new LinkedList<>(Arrays.asList(5,6,7,8)));
        listModule.printAll();
    }
}
/*출력결과
6 8 10 12
*/

 

위 예제에서 보면 ArrayList 와 LinkedList를 같이 사용했는데도 정상적으로 동작합니다.

당연히 Java 공식 라이브러리를 사용했기에 LSP도 잘 준수했습니다.

 

List에서 명세한 내용들을 ArrayList와 LinkedList가 제대로 이행하고 있습니다.

더 구체적으로 말하면 명세에서 의도한 동작을 수행했습니다.

  • 명세에서 의도한 기능을 수행
  • 명세에서 의도한 반환값을 리턴
  • 명세에서 의도한 익셉션을 호출

그래서 구현체가 어떻든간에 list.add, list.set, list.size 가 의도대로 동작합니다.

 

만약에 LSP를 준수하지 않았다면

list.size를 호출했는데 size가 아닌 capacity가 호출된다던가

아니면 음수가 출력될것입니다.

 

단순히 오버라이드만해서 사용한다고 OOP를 준수한게 아니라는 의미입니다.

명세를 정확히 파악하고 그대로 이행해야합니다.

로그인 예제

반대로 LSP를 지키지 못하면 어떻게 될까요?

대표적인 예시가 클래스마다 instanceof 로 타입을 확인하는 경우입니다.

 

다음은 NormalUser와 Admin의 로그인 과정을 다루는 보여줍니다.

class User {}
class NormalUser extends User {}
class Admin extends User {}
class LoginService {
    public void login(User user) {
        if(user instanceof Admin) {
            // 뭔가 다른 과정
        }
        else {
            // 일반 로그인 로직
        }
    }
}

 

Admin은 로그인 과정이 달라서 다르게 처리하려다보니 이런 결과가 나왔습니다.

LSP 원칙은 하위 클래스가 상위 클래스를 완벽 대체해야하는데,

인스턴스의 타입을 확인한다면 대체를 한게 아니게됩니다.

이러면 OCP 원칙도 위배하게 되어

새로운 User가 나타날때마다 새로운 로그인 로직을 추가해줘야합니다.

 

LSP를 준수하지 못한 경우는 설계에 문제가 있을 확률이 높습니다.

설계 단계에서 Admin인지 확인을 한다면 해결할 수 있을거같습니다.

class User {
    public boolean isAdmin() {
        return false;
    }
}
class NormalUser extends User {}
class Admin extends User {
    @Override
    public boolean isAdmin() {
        return true;
    }
}
class LoginService {
    public void login(User user) {
        if(user.isAdmin()) {
            // 뭔가 다른 과정
        }
        else {
            // 일반 로그인 로직
        }
    }
}

 

다음과 같이 Admin 클래스에서 특정 함수를 오버라이드하여 변경해준다면

login 메서드에서 더 일관적으로 관리가 가능해집니다.

 

해당 예시에서는 큰 차이가 없어보이지만

instance type이 아닌 로직을 통해 클래스를 구분하면

모듈이 더욱 커졌을때 차이가 생깁니다.

 

예를들어 admin과 비슷한 로직을 수행하는 다른 user가 생긴다면

그대로 isAdmin을 통해 구분이 가능해지겠죠.

 

'Java' 카테고리의 다른 글

[객체지향설계원칙] DIP  (0) 2024.05.31
[객체지향설계원칙] ISP  (0) 2024.05.31
[객체지향설계원칙] OCP  (0) 2024.05.23
추상클래스, 인터페이스의 문법적,의미적 차이  (0) 2024.05.22
[객체지향설계원칙] SRP  (0) 2024.05.14