본문 바로가기

[OOP] 배려가 담긴 코드 (객체 지향과 디자인 패턴)

 

 

들어가며

스타트업의 개발자로서 좋은 설계가 우선인가, 아니면 빠른 기능 개발이 우선인가 항상 고민하곤 했습니다. 제가 다녔던 스타트업은 빠르게 기능 개발을 해서, 시장에서 인정받아야 했기에, 빠른 시간 안에 많은 기능을 개발해야 했습니다. 그러다 문득 추가 기능을 개발하거나, 기존 기능을 수정해야 할 때, 많은 어려움을 겪곤 했습니다. 그렇게 일을 마치고, 더 나은 개발자가 되기 위한 공부를 하면서 제가 작성한 코드는 배려가 부족한 코드였다는 것을 깨달았습니다.

 

제가 개발한 코드는 당장의 기능 개발은 빠르게 할 수 있더라도, 재사용성이 대단히 떨어졌고, 가독성 또한 대단히 떨어졌습니다. 또한 기능을 수정하거나 추가할 때도 광범위한 코드를 건드려야만 했기에 효율성도 떨어졌습니다. 빠르게 기능을 개발하기 위해 적었던 코드가 언젠가는 발목을 잡을 수 있는 코드가 될 수 있다는 것을 깨달았습니다. 그렇다면 비즈니스에 도움이 되고, 다른 팀원들에게도 도움이 될 수 있는, 배려가 담긴 코드를 작성하기 위해서는 어떻게 해야 할까 고민하기 시작했습니다. 배려가 담긴 코드를 작성하기 위해 공부한 내용에 대해 작성해보고자 합니다.

 

 

 

 

 


 

 

 

 

 

 

 

출처: 인프런 '객체 지향 프로그래밍 입문'

 

 

디자인된 코드

인프런 CTO 이동욱 님의 객체지향(Object Oriented) 디자인(Design) 글에서 특정 누군가만 손댈 수 있고, 그 사람의 머릿속에만 모든 애플리케이션 구조가 담겨 있다면 안 좋은 디자인의 신호라는 표현을 보면서, 내가 그동안 작성했던 코드는 디자인이 좋지 못한 코드였다는 생각을 했습니다. 그렇다면 잘 디자인된 코드를 작성하기 위해서는 어떤 것을 고려해야 할지, 객체 지향과 디자인 패턴 책의 예시를 타입 스크립트 코드로 변경하여 살펴보겠습니다. 

 

 

 

 

 

 

영 좋지 못한 코드

예를 들어 다음과 같은 UI를 갖는 클라이언트 프로그램을 개발한다면, 메뉴 영역에서 메뉴 1과 메뉴 2를 누르면 화면 영역에 내용이 출력됩니다. 모든 화면은 공통 버튼을 한 개 가지며, 그 버튼이 눌릴 때마다 화면 영역의 데이터가 변경됩니다. 위의 로직을 만들기 위해, 설계가 안 좋은 코드에 대해 먼저 살펴보겠습니다.

 

 

 

interface OnClickListener {}

...

class Apllication implements OnClickListener {
  private menu1 = new Menu('menu1');
  private menu2 = new Menu('menu2');
  private button1 = new Button('button1');

  private currentMenu = null;

  public Application() {
    this.menu1.setOnClickListener(this);
    this.menu2.setOnClickListener(this);
    this.button1.setOnClickListener(this);
  }

  public clicked(eventSource): void {
    if (eventSource.getId().equals('menu1')) {
      this.changeUIToMenu1();
    } else if (eventSource.getId().equals('menu2')) {
      this.changeUIToMenu2();
    } else if (eventSource.getId().equals('button1')) {
      if (this.currentMenu === null) return;
      if (this.currentMenu.equals('menu1')) this.processButton1WhenMenu1();
      else if (this.currentMenu.equals('menu2')) this.processButton1WhenMenu2();
    }
  }

  private changeUIToMenu1(): void {
    this.currentMenu = 'menu1';
    console.log('메뉴1 화면으로 전환');
  }

  private changeUIToMenu2(): void {
    this.currentMenu = 'menu2';
    console.log('메뉴2 화면으로 전환');
  }

  private processButton1WhenMenu1(): void {
    console.log('메뉴1 화면의 버튼1 처리');
  }

  private processButton1WhenMenu2(): void {
    console.log('메뉴2 화면의 버튼1 처리');
  }
}

 

 

완벽한 구현은 하지 않았지만, 위 코드는 단순하게 두 개의 메뉴와 한 개의 버튼에서 이벤트가 발생하면 그 이벤트를 clicked() 메서드에서 처리한다고 봐주시면 됩니다. clicked() 메서드는 이벤트를 누가 발생시켰는지에 따라 if-else 블록을 이용해서 이벤트를 처리합니다.

 

menu1이 눌리면 메뉴1 화면으로 전환하고, menu2가 늘리면 메뉴 2 화면으로 전환하고, button1이 눌렸을 때 메뉴 1 화면이냐, 메뉴 2 화면이냐에 따라 다르게 동작해야 하기 때문에 화면 전환이 일어날 때 currentMenu 필드에 현재 화면 이름을 저장하도록 했습니다. 

 

그때 여기에 추가적으로 button2가 필요하다는 요구가 추가로 들어왔다면, 위 코드의 로직은 아래처럼 바뀔 것입니다. 

 

 

 

interface OnClickListener {}

class Apllication implements OnClickListener {
  private menu1 = new Menu('menu1');
  private menu2 = new Menu('menu2');
  private button1 = new Button('button1');
  private button2 = new Button('button2');

  private currentMenu = null;

  public Application() {
    this.menu1.setOnClickListener(this);
    this.menu2.setOnClickListener(this);
    this.button1.setOnClickListener(this);
    this.button2.setOnClickListener(this);
  }

  public clicked(eventSource): void {
    if (eventSource.getId().equals('menu1')) {
      this.changeUIToMenu1();
    } else if (eventSource.getId().equals('menu2')) {
      this.changeUIToMenu2();
    } else if (eventSource.getId().equals('button1')) {
      if (this.currentMenu === null) return;
      if (this.currentMenu.equals('menu1')) this.processButton1WhenMenu1();
      else if (this.currentMenu.equals('menu2')) this.processButton1WhenMenu2();
    } else if (eventSource.getId().equals('button2')) {
      if (this.currentMenu === null) return;
      if (this.currentMenu.equals('menu1')) this.processButton1WhenMenu1();
      else if (this.currentMenu.equals('menu2')) this.processButton1WhenMenu2();
    }
  }

  private processButton2WhenMenu1(): void {
    console.log('메뉴1 화면의 버튼2 처리');
  }

  private processButton2WhenMenu2(): void {
    console.log('메뉴2 화면의 버튼2 처리');
  }
}

 

clicked() 메서드의 button1 처리 코드와 button2 처리 코드는 currentMenu의 값에 따라 호출하는 메서드만 다를 뿐 구조는 완전히 동일합니다. 이 상태에서 만약 메뉴 3이 추가되면 위 코드는 if-else 블록이 추가될 것입니다. 만약 메뉴가 100개가 늘어난다면, 아마 끔찍한 코드가 탄생할 것입니다. 

 

처음에는 if-else를 활용하면 빠르게 구현할 수 있습니다. 하지만 if-else가 추가될수록 코드의 양은 많아지며, 수정해야 할 코드 또한 상당히 많아집니다. 그럼 이 좋지 못한 코드를 객체 지향 방식을 활용해서 수정해보도록 하겠습니다. 

 

 

 

 

보다 좋은 코드

먼저 메뉴1, 메뉴 2, 그리고 버튼 1이 존재하는 상태에서 다시 출발해 보겠습니다. 먼저 메뉴 1을 선택했을 때와 메뉴 2를 선택했을 때 비슷하게 동작하는 것들이 있는데, 이를 정의해보겠습니다.

 

  • 메뉴가 선택되면 해당 화면을 보여준다.
  • 버튼 1을 클릭하면 선택된 메뉴 화면에서 알맞은 처리를 한다.

 

위 동작은 메뉴가 늘어나더라도 동일하게 동작하는 것들입니다. 이런 공통 동작을 표현하기 위해 ScreenUI 타입을 정의했습니다. 

 

 

interface ScreenUI {
  show(): void;
  handleButton1Click(): void;
}

 

 

ScreenUI의 show() 메서드는 어떤 메뉴 버튼이 클릭될 때 실행되는 메서드로서, 메뉴에 해당하는 알맞은 화면을 보여주기 위해 사용됩니다. ScreenUI의 handleButton1Click() 메서드는 버튼 1이 눌렸을 때 실행됩니다.

 

 

메뉴 별로 실제 화면에 보이는 구성 요소와 버튼 1 클릭을 처리하는 코드가 다르므로 아래와 같이 각 메뉴 별로 ScreenUI 인터페이스를 구현한 클래스를 작성해 줍니다. 

 

 

interface ScreenUI {
  show(): void;
  handleButton1Click(): void;
}

class Menu1ScreenUI implements ScreenUI {
  show(): void {
    console.log('메뉴1 화면으로 전환');
  }

  handleButton1Click(): void {
    console.log('메뉴1 화면의 버튼1 처리');
  }
}

class Menu2ScreenUI implements ScreenUI {
  show(): void {
    console.log('메뉴2 화면으로 전환');
  }

  handleButton1Click(): void {
    console.log('메뉴2 화면의 버튼1 처리');
  }
}

 

이제 Application 클래스는 ScreenUI 인터페이스와 Menu1ScreenUI 클래스 및 Menu2ScreenUI 클래스를 이용해서 구현할 수 있습니다. 

 

 

interface ScreenUI {
  show(): void;
  handleButton1Click(): void;
}

class Menu1ScreenUI implements ScreenUI {
  show(): void {
    console.log('메뉴1 화면으로 전환');
  }

  handleButton1Click(): void {
    console.log('메뉴1 화면의 버튼1 처리');
  }
}

class Menu2ScreenUI implements ScreenUI {
  show(): void {
    console.log('메뉴2 화면으로 전환');
  }

  handleButton1Click(): void {
    console.log('메뉴2 화면의 버튼1 처리');
  }
}

interface OnClickListener {}

class Apllication implements OnClickListener {
  private menu1 = new Menu('menu1');
  private menu2 = new Menu('menu2');
  private button1 = new Button('button1');

  private currentScreen: ScreenUI = null;

  public Application() {
    this.menu1.setOnClickListener(this);
    this.menu2.setOnClickListener(this);
    this.button1.setOnClickListener(this);
  }

  public clicked(eventSource): void {
    if (eventSource.getId().equals('menu1')) {
      this.currentScreen = new Menu1ScreenUI();
      this.currentScreen.show();
    } else if (eventSource.getId().equals('menu2')) {
      this.currentScreen = new Menu2ScreenUI();
      this.currentScreen.show();
    } else if (eventSource.getId().equals('button1')) {
      if (this.currentScreen === null) return;
      this.currentScreen.handleButton1Click();
    }
  }
}

 

 

Application의 clicked 메서드에서는 menu1이나 menu2를 클릭하면 각각 Menu1ScreenUI 클래스나 Menu2ScreenUI 클래스의 인스턴스를 생성해서 currentScreen 필드에 할당한 뒤, currentScreen.show() 메서드를 호출합니다. 그리고 button1을 클릭하면 currentScreen의 handleButton1Click() 메서드를 호출합니다.

 

 

예를 들어, menu1을 클릭한 뒤에 button1을 클릭했다고 하면, menu1을 클릭하면 currentScreen에 Menu1ScreenUI 객체가 할당될 것입니다. 이 상태에서 button1을 클릭하면, currentScreen의 handleButton1Click() 메서드를 호출하므로 Menu1ScreenUI 객체의 handleButton1Click() 메서드가 호출됩니다. 비슷하게 menu2 버튼을 클릭한 뒤에 button1을 클릭하면 Menu2ScreenUI 객체의 handleButton1Click() 메서드가 호출될 것입니다. 여기서 중요한 점은 button1 클릭을 처리하는 코드는 현재 화면이 메뉴 1 화면인지 메뉴 2 화면인지에 상관없이 currentScreen.handleButton1Click()을 실행한다는 점입니다.  

 

 

 

 

조금 더 수정한 코드

여기서, 메뉴와 버튼 클릭을 처리하는 코드를 정리할 필요가 있습니다. 메뉴 클릭 처리 코드는 화면을 변경하는데 반해, 버튼 클릭 처리 코드는 변경된 화면에 버튼 클릭 결과를 반영하기 위해 사용됩니다. 두 종류의 버튼 처리 코드는 목적이 다르며 서로 다른 이유로 변경이 됩니다. 

 

예를 들어, 메뉴 관련 처리 코드는 메뉴가 추가되거나 삭제될 때 변경되고, 버튼 처리 코드는 버튼이 추가되거나 삭제될 때 변경됩니다. 이렇게 서로 다른 이유로 변경되는 코드가 한 메서드에 섞여 있으면 향후에 유지 보수를 하기 어려워질 수 있으니, 메뉴 클릭 처리 코드와 버튼 클릭 처리 코드를 분리했습니다. 

 

 

 

 

interface ScreenUI {
  show(): void;
  handleButton1Click(): void;
}

class Menu1ScreenUI implements ScreenUI {
  show(): void {
    console.log('메뉴1 화면으로 전환');
  }

  handleButton1Click(): void {
    console.log('메뉴1 화면의 버튼1 처리');
  }
}

class Menu2ScreenUI implements ScreenUI {
  show(): void {
    console.log('메뉴2 화면으로 전환');
  }

  handleButton1Click(): void {
    console.log('메뉴2 화면의 버튼1 처리');
  }
}

interface OnClickListener {}

class Apllication implements OnClickListener {
  private menu1 = new Menu('menu1');
  private menu2 = new Menu('menu2');
  private button1 = new Button('button1');

  private currentScreen: ScreenUI = null;

  public Application() {
    this.menu1.setOnClickListener(this);
    this.menu2.setOnClickListener(this);
    this.button1.setOnClickListener(this);
  }

  private menuListener:OnClickListener = new OnClickListener {
    new OnclickListener() {
      clicked(eventSource): void {
        if(eventSource.getId().equals('menu1')) {
          this.currentScreen = new Menu1ScreenUI()
        } else if(eventSource.getId().equals('menu2')) {
          this.currentScreen = new Menu2ScreenUI();
        }
        this.currentScreen.show();
      }
    }
  }

  private buttonListener:OnClickListener = new OnClickListener {
    new OnclickListener() {
      clicked(eventSource): void {
        if(this.currentScreen === null) return;
        if(eventSource.getId().equals('button1')) {
          this.currentScreen.handleButton1Click()
        }
      }
    }
  } 
  
}

 

 

메뉴 클릭 처리 코드와 버튼 클릭 처리 코드를 분리했더니, 두 종류의 코드가 섞여 있을 때에 비해서 두 작업을 더 잘 구분할 수 있게 됐습니다. 

 

 

지금까지 작성한 코드를 처음 코드와 비교하면, 큰 차이가 있음을 알 수 있습니다. Application 클래스에 모든 코드를 작성했었던 방식에서는 메뉴 1 코드와 메뉴 2 코드가 한 소스 코드에 있었습니다. 따라서 메뉴 1 코드를 수정하려면 메뉴 1 코드만 있는 경우와 비교해서 Application 소스 코드의 이곳저곳을 다 분석하고, 살펴봐야 했습니다. 메뉴 개수가 많다면 이런 불필요한 작업은 더 많아질 것입니다. 하지만 두 번째 수정한 방식은 클래스 개수가 다소 증가했지만, 메뉴 관련 코드들이 알맞게 분리됐습니다. 이를 통해 불필요한 노력을 줄일 수 있습니다.

 

두 번째는 버튼 클릭을 처리하는 코드가 단순화되었습니다. 처음 구현한 방식은 버튼 종류가 추가될 때마다 if-else 블록이 추가되었는데, 두 번째 방식의 경우엔 버튼 종류가 추가된 부분에 대해서만 로직을 단순하게 추가하면 되기 때문에 조금 더 유연한 코드가 됐습니다. 

 

이런 코드 변화를 통해 요구 사항이 바뀔 때 변화를 좀 더 수월하게 적용할 수 있다는 장점을 얻었습니다. 이런 장점을 얻기 위해 사용된 것이 바로 객체 지향 기법입니다. 아직 객체 지향 코드에 대해 잘 알지 못하지만, 지금부터라도 조금씩 이에 대해 공부해서 조금 더 변화에 수월하게 적용할 수 있는 코드를 작성해보고 싶습니다. 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

마치며

객체지향 프로그래밍의 필요성을 점점 느끼고 있습니다. 언젠가, 코드에 대한 역할과 책임을 명확하게 보여줄 수 있는 코드를 작성할 수 있다면 동료들이, 일을 더 수월하게 할 수 있지 않을까 생각합니다. 배려가 담긴 코드를 작성하기 위해 꾸준하게 노력하고 싶습니다. 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

출처

 

객체지향 (Object Oriented) 디자인 (Design)

여기서 이야기하는 디자인은 코드 설계와 동일하게 봐도 무방하다. 디자인이 왜 중요한가? 요즘의 웹 애플리케이션 개발에서는 디자인에 대한 지식이 없더라도, 원하는 바대로 작동하는 웹 애

jojoldu.tistory.com

 

좋은 코드란 무엇인가

많은 프로그래머들이 좋은 코드에 대해 얘기한다. 최근 코딩 경험이 적은 한 주니어가 나에게 좋은 코드는 유지보수 비용을 낮춘다고 말했다. 반면 잘 훈련되고 경험 많은 프로그래머들로부터

gyuwon.github.io

 

객체 지향과 디자인 패턴 - YES24

객체 지향 안내서. 객체, 책임, 의존, 캡슐화 등 객체 지향의 주요 개념들을 쉬운 예제와 그림을 통해 이해하기 쉽게 설명한다.

www.yes24.com