Justin의 개발 로그
article thumbnail

리스코프 치환 원칙(영어: Liskov substitution principle, LSP)은 바바라 리스코프 자료 추상화와 계층 (Data abstraction and hierarchy)이라는 제목으로 기조연설을 한 1987년 컨퍼런스에서 처음 소개한 내용

 

자료형 {S}가 자료형 {T}의 하위형이라면, 프로그램에서 자료형 {T}의 객체는 프로그램의 속성을 변경하지 않고 자료형 {S}의 객체로 교체할 수 있다.

 

public interface Animal {

  public void cry();
  public void move();
  public void MoveFast();  
}

public class Dog implements Animal {
  private String sould = "bowwow";

  public void cry() {
    System.out.println(sould);
  }
  
  public void move() {
    this.walk();
  } 

  public void MoveFast() {
    this.run();
  }
  
  private void walk() {
    System.out.println("Walking");
  }
  
  private void run() {
    System.out.println("Running");
  }
}
  
public class HoundDog extends Dog {
  private String sould = "growl";

  public void cry() {
    System.out.println(sould);
  }  
}

public class Duck implements Animal {
  private String sould = "Quack";

  public void cry() {
    System.out.println(sould);
  }  

  public void move() {
    this.swim();
  }  

  public void moveFast() {
    this.fly();
  }  
  
  private void swim() {
    System.out.println("Swimming Duck");
  }

  private void fly() {
    System.out.println("Flying Duck");
  }
}

public class TestAnimal {
  public void main(string[] args) {
  
  Animal animal = new HoudDog();
  animal.cry();
  animal.move();
  animal.moveFast();
  
  animal = new Duck();
  animal.cry();
  animal.move();
  animal.moveFast();
  }
}

 

표준적인 요구사항

 

하위형에서 메서드 인수(파라미터)는 하위호환만 허용한다.

하위형에서 메서드의 인수(파라미터)는 하위호환(확장한 객체로 변경) 가능하다.

  • 메서드 인수(파라미터)는 상위형에 선언된 인수 또는 해당 인수를 확장(Extends)한 경우에만 허용된다.
    • Animal > Dog > HoundDog
    • 상위 인수가 Dog 인 경우 하위 인수인 Animal을 사용할 수 없다.
    • 왜? 모든 Animal이 Dog이 될 수 없으니까 부모에서 정의한 메소드의 오바리이딩이 안됨
public abstract class Animal {

    protected String barkSound = "No Sound";

    public void bark(Animal animal){
        System.out.println(this.barkSound);
    }
}

//동물을 상속받은 클래스 개
public class Dog extends Animal {

    public Dog() {
        this.barkSound = "Bow Wow";
    }

    public void getSound(Dog dog)
    {
        System.out.println("Dog : " + dog.barkSound);
    }
}

 

메서드 인수(파라미터)의 반공변성(하위호환) 위반 사례 케이스

개(Dog) 클래스를 확장해서 사냥개(HoudDog)을 만드는데, getSound의 파라미터를 Dog 보다 상위의 Animal로 인자를 변경해서 오버라이드 시도하면, 아래와 같이 super.getSound(dog) 부분에서 부모 메소드를 찾을 수 없어 에러가 발생함.


메서드 인수(파라미터)는 상위형에 선언된 인수 또는 해당 인수를 확장(Extends)한 경우에만 허용한다는 규칙의 위반

 

 

 

아래와 같이 하위클래스로 선언하거나, 동일한 레벨로 선언하면 오류가 사라진다.

 

 

하위형에서 반환형(return)은 하위호완은 허용된다.

  • 하위형에서 메소드의 반환형은 상위형으로만 변경 가능하다. ???
    • Dog를 반환해야 하는 메소드에서 Animal을 반환하는 것도 불가. 모든 동물이 개는 아니니까
    • Dog를 반환해야 하는 메소드에서 사냥개를 반환하는 것은 허용됨


  • 하위형에서 메서드는 상위형 메서드에서 던져진 예외의 하위형을 제외하고 새로운 예외를 던지면 안된다.
public interface WhoAmI {
    public Dog call();
}

 

public class Dog extends Animal implements WhoAmI {

    public Dog() {
        this.barkSound = "Bow Wow";
    }

    public void getSound(Dog dog)
    {
        System.out.println("Dog : " + dog.barkSound);
    }


	// 1번. 허용 or 불가
    public Dog call()
    {
        return this;
    }
    
    // 2번. 허용 or 불가
    public Dog call()
    {
        return new HoundDog();
    }
    
    // 3번 허용 or 불가
    public HoundDog call()
    {
        return new HoundDog();
    }
    
    // 4번 허용 or 불가
    public HoundDog call()
    {
        return new Dog();
    }
    
    // 5번 허용 or 불가
    public Animal call()
    {
        return new Animal();
    }

}

 

  • 하위형에서 선행조건은 강화될 수 없다.
    상위형에서 정의된 인스턴스의 초기화 조건 등을 하위형에서 더 강력하게 제제하면 초기화에 실패하거나, 상위형에 정의된 기능을 사용할 때 정상 동작하지 않음
  • 하위형에서 후행조건은 약화될 수 없다.
    메모리 정리 등 후행 기능을 없애는 등의 행위는 객체(또는 로직)의 정상 동작을 방해한다.
  • 하위형에서 상위형의 불변조건은 반드시 유지되어야 한다.
    동물로 상속받은 객체에서 동물의 필수 조건을 변경하면 안됨
  • 상위형에서 예외를 던지는 경우 하위형에서는 상위형에서 던지는 예외보다 하위형의 예외만 던져야 한다.
    • 상위형에서 NumberFormatException을 던지를 로직인데, 하위형에서는 상위형에서 던지지 않던 NullPointException을 던지면 안됨
    • 상위형에서 IOException을 던지는 경우, 하위형에서 보다 보다 구체적인 FileIOException을 던지는 것은 OK

 

 


/*
정답
1. 허용 - 인터페이스 정의와 동일한 선언
2. 허용 - 인터페이스 정의보다 하위형의 객체 반환 가능
3. 허용 - 인터페이스 선언보다 하위형 반환으로 선언 가능
4. 불가 - 반환형 보다 상위형은 반환 불가
5. 불가 - 인터페이스 선언보다 상위형으로 선언 불가
*/

 

 

 

리스코프 치환 법칙을 지키지 않은 사례 - 직사각형과 정사각형

import org.springframework.stereotype.Service;

// 직사각형
//  가로, 세로를 입력받는다.
//  면적을 계산한다.
@Service
public class Rectangle {
    int width = 0;
    int height = 0;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    //  면적
    public int getArea() {
        return width * height;
    }

}

import org.springframework.stereotype.Service;

/** 정사각형
 직사각형을 확장해서 폭과 높이가 같도록 메소드를 오버라이드 한다.
 */
@Service
public class Square extends Rectangle {

    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static junit.framework.TestCase.assertEquals;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RectangleTest {

    @After
    public void cleanup ()
    {
        return;
    }
    
    @Test
    public void 직사각형_정사각형_면적테스트 () throws Exception {
        //given
        Rectangle rect = new Rectangle();

        //선언은 직사각형이지만, 하위 클래스인 정사각형으로 생성
        Rectangle rect2 = new Square();

        //when
        rect.setWidth(10);
        rect.setHeight(20);

        rect2.setWidth(10);
        rect2.setHeight(20);

        System.out.println("Rect area : " + rect.getArea());
        System.out.println("Rect2 area : " + rect2.getArea());

        //then
        assertEquals(200, rect.getArea());
        assertEquals(200, rect2.getArea());	//실패 직사각형으로 선언했지만, 가로,세로가 같은 정사각형이됨
    }
}

 

그럼 어떻게 이 문제를 해결할 것인가?

// 직사각형
//  가로, 세로를 입력받는다.
//  면적을 계산한다.
@Service
public abstract class Tetragon {
    int width = 0;
    int height = 0;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    // 면적
    public int getArea() {
        return width * height;
    }
}

// 직사각형
@Service
public class Rectangle extends Tetragon {

}

// 정사각형
@Service
public class Square extends Tetragon {

    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

테스트 케이스 작성

Square(정사각형)은 Rectangle(직사각형)으로 형변환이 불가능하게 된다.

 

테스트 케이스 수정

@SpringBootTest
public class NewRectangleTest {

    @After
    public void cleanup ()
    {
        return;
    }

    @Test
    public void 직사각형_정사각형_면적테스트 () throws Exception {
        //given
        Rectangle rect = new Rectangle();
        Square newSquare = new Square();

        //when
        rect.setWidth(10);
        rect.setHeight(20);

        newSquare.setWidth(10);
        newSquare.setHeight(20);

        System.out.println("Rect area : " + rect.getArea());
        System.out.println("Rect2 area : " + newSquare.getArea());

        //then
        assertEquals(200, rect.getArea());
        assertEquals(400, newSquare.getArea());
    }
}

정사각형 객체를 직사각형으로 형변환해서 발생하는 Side Effect을 방지할 수 있다.

전체적인 코드의 복잡도는 거의 증가하지 않았다.

 

 

 

 

[참고 - 뭘 표현하려는 걸까?]
한국이나 외국이나 기술자는 용어를 어렵게 쓰고, 설명도 어렵게 하는게 국룰인가보다~

profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!