본문 바로가기
Programming

[SOLID] 2. OCP(Open Close Principle) - 개방 폐쇄 원칙

by jewook3617 2021. 10. 5.

이번 글에서는 SOLID 원칙의 O에 해당하는 OCP에 대해 정리해보겠습니다.

 

OCP(Open Close Principle)

OCP는 Open Close Principle의 줄임말입니다. 한글로는 개방 폐쇄 원칙이라고 부릅니다. 개방 폐쇄 원칙이란 확장에는 열려있고 변경에는 닫혀있다는 뜻입니다.

이렇게만 들으면 무슨 말인지 감이 잡히지 않습니다. 여기서 말하는 확장이란 새로운 기능이 추가됨을 의미합니다.

확장에는 열려있고 변경에는 닫혀있다는 말은 새로운 기능이 추가되어야 할 때 기존 코드의 변경 없이 새로운 코드를 쉽게 추가할 수 있어야 한다는 의미입니다.

OCP를 위반하는 typescript 코드를 살펴보겠습니다.

interface Excel {}

class StatisticsController {
  private excelFile: Excel;

  constructor(excelFile: Excel) {
    this.excelFile = excelFile;
  }

  public printStatistics() {
    // this.excelFile에서 데이터를 읽어와 통계표를 만들어 출력하는 메서드
  }
}

위 코드는 excel 파일로부터 데이터를 읽어와 통계표를 만들어주는 코드입니다.

StatisticsController 클래스에서 excel 파일 뿐만 아니라 docx 파일로부터 데이터를 읽어와 통계표를 만들어주는 기능이 추가되어야 한다고 가정해보겠습니다.

위 코드에 새로운 기능을 추가하려면 다음과 같이 변경되어야할 것입니다.

interface Excel {}
interface Docx {}

class StatisticsController {
  private excelFile: Excel;
  private docxFile: Docx;

  constructor(file: File) {
    if (file.getType() === "Excel") { 
    	this.excelFile = file;
    }
    else if (file.getType() === "Docx") {
    	this.docxFile = file;
    }
  }

  public printStatistics(type: string) {
    // type에 해당하는 파일에서 데이터를 읽어와 통계표를 만들어 출력하는 메서드
    
    if (type === "Excel") {
      // this.excelFile에서 데이터 불러오기
    }
    else if (type === "Docx") {
   	  // this.docx 파일에서 데이터 불러오기
    }

    // ...
  }
}

기능이 추가됨에 따라 constructor 내부에서 file의 type을 확인하고 printStatistics() 메서드에서 역시 type을 parameter로 받아 type 별로 다르게 동작하도록 변경해야 합니다.

docx 파일뿐만 아니라 더 다양한 타입의 파일을 지원해야 한다면 타입이 추가될 때마다 기존 코드 역시 변경되어야 할 것입니다.

기능이 추가되면서 기존의 코드를 수정해야 하기 때문에 위 코드는 OCP를 위반하는 코드입니다.

해결 방법

그렇다면 기존의 코드를 수정하지 않고 새 기능을 추가하려면 어떻게 해야 할까요?

아래 코드를 살펴보겠습니다.

interface Excel {}
interface Data {}
interface ImportableFile {
  getData(): Data;
}

class ExcelFile implements ImportableFile {
  private excelFile: Excel;

  constructor(excelFile: Excel) {
    this.excelFile = excelFile;
  }

  public getData(): Data {
    return {};
  }
}

class StatisticsController {
  private file: ImportableFile

  constructor(file: ImportableFile) {
    this.file = file;
  }

  public printStatistics() {
    // this.file에서 데이터를 읽어와 통계표를 만들어 출력하는 메서드
    // ...
    this.file.getData();
    // ...
  }
}

이번에는 StatisticsController의 constructor에서 받는 parameter의 타입이 Excel 타입이 아닌 ImportableFile 타입입니다. 그리고 ImportableFile 타입에는 getData() 메서드가 존재해 printStatistics() 메서드에서 파일의 데이터를 읽어올 때 ImportableFile의 getData() 메서드를 통해 데이터를 읽어옵니다.

ExcelFile 클래스가 ImportableFile 인터페이스를 구현하고 있으므로 Excel 타입의 파일에서 데이터를 읽어와 통계표를 만드는 기능을 구현하려면 다음과 같이 StatisticsController 인스턴스를 생성하면 됩니다.

const excelFile = new ExcelFile(file);
new StatisticsController(excelFile);

위 경우에 docx 파일로부터 데이터를 읽어와 통계표를 만들어주는 기능을 추가하려면 어떻게 하면 될까요?

아래 코드를 보겠습니다.

interface Docx {}

class DocxFile implements ImportableFile {
  private docxFile: Docx;

  constructor(docxFile: Docx) {
    this.docxFile = docxFile;
  }

  public getData(): Data {
    return {};
  }
}

const docxFile = new DocxFile(file);
new StatisticsController(docxFile);

ImportableFile 인터페이스를 구현하는 새로운 DocxFile 클래스를 만들고 StatisticsController에 주입했습니다.

이게 전부입니다.

StatisticsController 클래스에 주입되는 파일이 ImportableFile 인터페이스를 구현하고 있다면 파일의 타입에 상관없이 this.file.getData()를 통해 데이터를 불러올 수 있기 때문에 StatisticsController의 코드를 변경할 필요가 없습니다.

docx 파일뿐만 아니라 더 다양한 타입의 파일을 지원해야 한다면 ImportableFile 인터페이스를 구현하는 다양한 파일들의 클래스를 만들고 StatisticsController에 주입해주기만 하면 됩니다.

이렇게 OCP를 잘 준수하는 코드를 작성하면 변경사항에 쉽게 대응할 수 있는 코드를 만들 수 있습니다.