개방 폐쇄 원칙(OCP)

박시시·2022년 10월 20일
0

SOLID

목록 보기
3/5

Bertrand Meyer은 1988년에 그의 논문에서 개방 폐쇄 원칙을 아래와 같이 설명했다.

"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”

즉 소프트웨어를 설계할 때 클래스를 포함한 엔티티들은 확장에 열려있고 변경에는 닫혀있도록 설계해야 함을 뜻한다.

  • 확장에 열리다: 애플리케이션 요구사항이 변경되면 새로운 동작을 추가하여 기능을 확장할 수 있어야 함을 뜻한다.
  • 변경에 닫히다: 기존 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있어야 한다.

구현체에 의존하는 것이 아닌 인터페이스에 의존하고, 인터페이스에서 제공하는 퍼블릭 인터페이스 메서드를 사용하게끔 구성한다면(즉, 추상화에 의존한다면) 이러한 OCP 원칙을 준수할 수 있다. 다시 말하자면, 컴파일타임 의존성보다는 런타임 의존성을 갖게끔 구성하는 것이 좋다.

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);
		} 
	}

}

OCP를 준수한 설계

위의 코드는 구체 클래스에 의존하고 있다.

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 인터페이스 명세를 잘 지켜 구현하게 된다면 클라이언트의 코드 변경 없이 확장이 가능하게 된다.

참조

https://www.baeldung.com/java-open-closed-principle

0개의 댓글