Backend
home
📘

[개념 정리] 리스코프 치환 원칙

생성 일시
2025/03/01 14:34
태그
Java
게시일
2025/03/01
최종 편집 일시
2025/03/02 05:44

1. 리스코프 치환 원칙이란?

리스코프 치환 원칙(LSP)란 ‘파생 타입은 반드시 기본 타입을 완벽하게 대체할 수 있어야 한다’는 의미이다. 또한 서브클래스의 객체는 슈퍼클래스의 객체와 반드시 같은 방식으로 동작해야 한다는 사실을 의미한다. 리스코프 치환 원칙은 타입 변환 후에 뒤따라오는 런타임 식별에 유용한 원칙이다.

2. 예제

‘프리미엄’, ‘VIP’, ‘무료’라는 세 가지 유형의 회원이 있는 체스 동호회가 있다고 가정한다. 여기에는 기본 클래스 역할을 하는 Member(회원) 추상 클래스와 PremiumMember(프리미엄 회원), VipMember(VIP 회원) 그리고 FreeMember(무료 회원)의 세 가지 서브클래스가 있다. 이 세 가지 서브클래스(회원)의 유형이 기본 클래스를 대체할 수 있는지 살펴본다.

리스코프 치환 원칙을 따르지 않는 경우

Member 클래스는 추상 클래스이며 체스 동호회의 모든 구성원을 나타내는 기본 클래스이다.
package bad; public abstract class Member { private final String name; public Member(String name) { this.name = name; } public abstract void joinTournament(); public abstract void organizeTournament(); }
Java
복사
PremiumMember 클래스는 체스 토너먼트에 참가하거나 그러한 토너먼트를 주최할 수 있다.
package bad; public class PremiumMember extends Member { public PremiumMember(String name) { super(name); } @Override public void joinTournament() { System.out.println("Premium member joins tournament ..."); } @Override public void organizeTournament() { System.out.println("Premium member organize tournament ..."); } }
Java
복사
VipMember 클래스
package bad; public class VipMember extends Member { public VipMember(String name) { super(name); } @Override public void joinTournament() { System.out.println("VIP member joins tournament ..."); } @Override public void organizeTournament() { System.out.println("VIP member organize tournament ..."); } }
Java
복사
FreeMember 클래스는 토너먼트에 참가할 수 있지만 토너먼트를 주최할 수는 없다. 이것은organizeTournament 메서드에서 다뤄야 하는 문제이다. organizeTourment 메서드에서 의미 있는 문구를 가진 예외를 발생시키거나 혹은 다음과 같이 문구를 그냥 표시할 수도 있다.
package bad; public class FreeMember extends Member { public FreeMember(String name) { super(name); } @Override public void joinTournament() { System.out.println("Classic Member joins tournament..."); } // 이 메서드는 무료 회원이 토너먼트를 개최할 수 없으므로 // 리스코프 치환 원칙에 맞지 않다. 즉, 기본 클래스를 대체할 수 없다. @Override public void organizeTournament() { System.out.println("A free member cannot organize tournaments"); } }
Java
복사
하지만 예외를 발생시키거나 문구를 표시한다고 리스코프 치환 원칙을 지키는 것은 아니다. 무료 회원은 토너먼트를 주최할 수 없으므로 기본 클래스 대체가 될 수 없으며 이는 리스코프 치환 원칙에 맞지 않는다. 다음과 같이 회원 리스트에 해당하는 members1을 생성해보자.
List<Member> members1 = List.of( new bad.PremiumMember("Jack Hores"), new bad.VipMember("Tom Johns"), new bad.FreeMember("Martin Vilop") );
Java
복사
그리고 다음 for문을 실행해보면 작성했던 코드가 리스코프 치환 원칙에 어긋난다는 것을 알 수 있다. FreeMember 클래스가 Member 클래스를 대체할 차례가 왔을 때 FreeMember 클래스는 체스 토너먼트를 주최할 수 없어서 필요한 작업을 할 수 없기 때문이다.
import bad.Member; import java.util.List; public class Main { public static void main(String[] args) { System.out.println("\nApproach that doesn't follow LSP:\n"); List<Member> members1 = List.of( new bad.PremiumMember("Jack Hores"), new bad.VipMember("Tom Johns"), new bad.FreeMember("Martin Vilop") ); // 이 코드는 리스코프 치환 원칙에 맞지 않습니다. 무료 회원은 토너먼트를 주최할 수 없습니다. for (Member member : members1) { member.organizeTournament(); } } }
Java
복사

리스코프 치환 원칙을 따르는 경우

체스 토너먼트에 참가하고 주최하는 두 가지 일을 분리하기 위해 두 가지 인터페이스를 정의하는 것으로 리팩터링을 시작한다.
TournamentJoiner - 토너먼트 참가
package good; public interface TournamentJoiner { public void joinTournament(); }
Java
복사
TournamentOrganizer - 토너먼트 주최
package good; public interface TournamentOrganizer { public void organizeTournament(); }
Java
복사
기본 추상 클래스인 Member에서 앞 두 가지 인터페이스를 다음과 같이 구현
package good; public abstract class Member implements TournamentJoiner, TournamentOrganizer { private final String name; public Member(String name) { this.name = name; } }
Java
복사
PremiumMember와 VipMember는 수정하지 않고 Member 기본 클래스를 그대로 상속받도록 한다. 그러나 FreeMember 클래스는 토너먼트를 주최할 수 없기 때문에 Member 기본 클래스를 상속받지 않을 것이다. 대신 TournamentJoiner 인터페이스만 구현해준다.
PremiumMember
package good; public class PremiumMember extends Member { public PremiumMember(String name) { super(name); } @Override public void joinTournament() { System.out.println("Premium member joins tournament ..."); } @Override public void organizeTournament() { System.out.println("Premium member organize tournament ..."); } }
Java
복사
VipMember
package good; public class VipMember extends Member { public VipMember(String name) { super(name); } @Override public void joinTournament() { System.out.println("VIP member joins tournament ..."); } @Override public void organizeTournament() { System.out.println("VIP member organize tournament ..."); } }
Java
복사
FreeMember
package good; public class FreeMember implements TournamentJoiner { private final String name; public FreeMember(String name) { this.name = name; } @Override public void joinTournament() { System.out.println("Free member joins tournament ..."); } }
Java
복사
이제 체스 토너먼트에 참가할 수 있는 회원 리스트 member2를 다음처럼 정의할 수 있다.
List<TournamentJoiner> members2 = List.of( new good.PremiumMember("Jack Hores"), new good.VipMember("Tom Johns"), new good.FreeMember("Martin Vilop") );
Java
복사
List 객체 members2에 반복문을 실행하면서 각 회원 유형을 TournamentJoiner 인터페이스로 대체해보면 기대한 방식으로 잘 동작하고 리스코프 치환 원칙도 준수한다는 것을 확인할 수 있다.
// 이 코드는 리스코프 치환 원칙을 준수한다. for (TournamentJoiner member : members2) { member.joinTournament(); }
Java
복사
같은 방법으로 체스 토너먼트를 주최할 수 있는 회원 리스트 member3을 다음과 같이 정의할 수 있다.
List<TournamentOrganizer> members3 = List.of( new good.PremiumMember("Jack Hores"), new good.VipMember("Tom Johns") );
Java
복사
FreeMember는 TournamentOrganizer 인터페이스를 구현하지 않기 때문에 리스트에 포함될 수 없다. List 객체 member3에 반복문을 실행하면서 TournamentOrganizer 인터페이스로 대체해보면 기대한 방식으로 잘 동작하고 리스코프 치환 원칙도 준수한다는 것을 확인할 수 있다.
// 이 코드는 리스코프 치환 원칙을 준수한다. for (TournamentOrganizer member : members3) { member.organizeTournament(); }
Java
복사