Bertrand Meyer은 1988년에 그의 논문에서 개방 폐쇄 원칙을 아래와 같이 설명했다.
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
즉 소프트웨어를 설계할 때 클래스를 포함한 엔티티들은 확장에 열려있고 변경에는 닫혀있도록 설계해야 함을 뜻한다.
구현체에 의존하는 것이 아닌 인터페이스에 의존하고, 인터페이스에서 제공하는 퍼블릭 인터페이스 메서드를 사용하게끔 구성한다면(즉, 추상화에 의존한다면) 이러한 OCP 원칙을 준수할 수 있다. 다시 말하자면, 컴파일타임 의존성보다는 런타임 의존성을 갖게끔 구성하는 것이 좋다.
뱅킹서비스가 있고 2가지 계정 타입이 있다고 해보자. 아래의 그림과 같다.
(출처: https://www.baeldung.com/java-liskov-substitution-principle#1-the-root-cause)
뱅킹서비스에서 계좌를 사용하기 위해서는 아래와 같은 형태로 코드를 짤 것이다.
public class BankingAppWithdrawalService {
private String accountType;
public BankingAppWithdrawalService(String accountType) {
this.accountType = accountType;
}
public void withdraw(BigDecimal amount) {
if (accountType == "current"){
CurrentAccount currentAccount = new CurrentAccount();
currentAccount.withdraw(amount);
} else if (accountType == "saving") {
SavingAccount savingAccount = new SavingAccount();
savingAccount.withdraw(amount);
}
}
}
클라이언트 코드인 뱅킹서비스에서 위의 코드와 같이 구성하게 된다면 OCP 원칙을 쉽게 깨드릴 수 있는 구조가 된다.
왜냐하면, 만약 Account 타입이 CurrentAccount, SavingAccount 외에 하나가 더 늘어나게 됐을 경우 뱅킹서비스 코드를 '변경'해야 하기 때문이다.
즉 변경에 닫혀 있는 구조가 아니란 소리다.
public class BankingAppWithdrawalService {
private String accountType;
public BankingAppWithdrawalService(String accountType) {
this.accountType = accountType;
}
public void withdraw(BigDecimal amount) {
if (accountType == "current"){
CurrentAccount currentAccount = new CurrentAccount();
currentAccount.withdraw(amount);
} else if (accountType == "saving") {
SavingAccount savingAccount = new SavingAccount();
savingAccount.withdraw(amount);
} else if (accountType == "fixed") { // FixedTermDepositAccount 타입이 하나 더 추가되었다.
FixedTermDepositAccount fixedAccount = new FixedTermDepositAccount();
fixedAccount.withdraw(1000);
}
}
}
위의 코드는 구체 클래스에 의존하고 있다.
CurrentAccount currentAccount = new CurrentAccount();
SavingAccount savingAccount = new SavingAccount();
FixedTermDepositAccount fixedAccount = new FixedTermDepositAccount();
이와 같이 구체 클래스에 의존하게 되면 Account 타입이 확장될 때 마다 클라이언트 코드가 변경되어야 한다.
클라이언트 코드에 인터페이스를 제공하고 해당 인터페이스의 퍼블릭 메서드를 사용하게끔 구성한다면 클라이언트 코드의 변경없이 확장에 유연한 구조로 바꿀 수 있다.
(출처: https://www.baeldung.com/java-liskov-substitution-principle#1-the-root-cause)
public interface Account {
void deposit(BigDecimal amount);
void withdraw(BigDecimal amount);
}
public class CurrentAccount implements Account {
// constructor, getters and setters
@Override
public void deposit(BigDecimal amount) {
System.out.println("deposit into CurrentAccount");
}
@Override
public void withdraw(BigDecimal amount) {
System.out.println("withdraw from CurrentAccount");
}
}
public class SavingAccount implements Account {
// constructor, getters and setters
@Override
public void deposit(BigDecimal amount) {
System.out.println("deposit into SavingAccount");
}
@Override
public void withdraw(BigDecimal amount) {
System.out.println("withdraw from SavingAccount");
}
}
public class BankingAppWithdrawalService {
private Account account;
public BankingAppWithdrawalService(Account account) {
this.account = account;
}
public void withdraw(BigDecimal amount) {
account.withdraw(amount);
}
}
클라이언트 코드인 BankingAppWithdrawalService에서는 Account 인터페이스의 퍼블릭 인터페이스 메서드 withdraw를 통해 객체와 통신하게 된다. 만약 계좌의 타입이 2개가 아닌 여러개로 늘어나게 되더라도 Account의 구현체들이 Account 인터페이스 명세를 잘 지켜 구현하게 된다면 클라이언트의 코드 변경 없이 확장이 가능하게 된다.