🌈 프로그래밍/C++

[C++] 재사용을 고려한 디자인이란?

반응형

 

안녕하세요? 수구리입니다.

 

저번 포스팅에서는 vector와 관련되어서 많은 내용들을 알아보았습니다.

 

 

[C++] vector 컨테이너 부수기

안녕하세요? 수구리입니다. 이번 포스팅에서는 vector에 대한 좀 더 자세히 알아보기 위해서 정리를 해보았습니다. 알고 있었던 부분도 있었지만, 더 나아가 자세한 내용을 살펴보니 제가 모르던

tasddc.tistory.com

 

이번 포스팅에서는 코드의 재사용과 관련된 철학에 대하여 정리를 해보려고 합니다.

 

재사용이란 말 그대로 다시 사용하도록 하는 것으로 코드를 재사용하는 것은 한 프로젝트에만 국한되는 것이 아니라 다른 프로젝트에서도 사용할 수 있도록 하는 것입니다.

 

코드를 간결하게, 자주 반복되어지는 부분은 함수로 따로 빼는 작업을 잘해둔다면 재사용하는데 더 편리해지겠죠??

 

바로 시작해보겠습니다!

 

 

6.1 재사용 철학

재사용 철학에 대한 좌우명

  1. 작성은 한 번, 사용은 여러번
  2. 무슨 수를 쓰더라도 코드 중복은 피하자
  3. 같은 일을 반복하지 않는다.

근거

  • 코드를 한 프로그램에서만 사용하는 경우는 극히 드묾
  • 재사용을 고려해서 디자인을 하면 시간과 비용 절약
  • 팀 내 다른 프로그래머도 활용할 수 있어야 함
  • 재사용성이 낮으면 중복된 코드가 늘어남
  • 재사용하기 좋은 코드의 첫 번째 수혜자는 바로 나!

 

6.2 코드를 재사용 하기 위한 디자인

1. 용도나 분야가 달라도 사용할 수 있는 범용성을 갖춰라!

  • 특정 분야에 너무 특화되어 있다면 다른 프로그램에서 사용이 힘들다.

2. 사용하기 쉽게 만들어라!

  • 인터페이스와 그 기능을 바로바로 이해할 수 있도록 하여 즉시 적용이 가능해야 한다.

 

이 장에서 설명하는 클라이언트는 내가 작성한 인터페이스를 사용하는 프로그래머이고, 클라이언트 코드는 내가 작성한 인터페이스를 사용하도록 작성된 코드임.


코드 구조화에 대한 4가지 Tip!

1. 서로 관련이 없거나 논리적으로 구분되는 개념은 합치지 않기

  • 한 컴포넌트는 반드시 하나의 동작만 하도록 응집도(Cohesion)를 높이는데 주력해야 한다.
  • 단일 책임성 원칙(Single Responsibility Principle, SRP)라고도 부른다.
  • 재사용을 고려하지 않더라도 이 원칙을 따르면 좋다.
  • 프로그램을 디자인할 때에는 기능을 논리적으로 구분 -> 별도의 컴포넌트로 구현 -> 다른 프로그램에서 재사용 용이
    • 프로그램을 서브시스템 단위로 나누기
      • 서브시스템을 디자인할 때 반드시 독립적으로 재사용할 수 있는 컴포넌트로 만들어야 함.
      • 즉, 결합도(Coupling)를 낮게 만든다.
    • 클래스 계층을 사용해서 논리적으로 나누기
      • 프로그램을 논리적으로 나눌 때, 서브시스템 관점뿐만 아니라 클래스의 관점에서도 서로 관련 없는 개념이 엮이지 않도록 주의
    • 집합 관계를 사용해서 논리적으로 나누기
      • has-a 관계처럼 객체가 제공하는 기능의 일부분을 수행하는 객체를 따로 둠.
    • 사용자 인터페이스에 대한 종속성 제거
      • 데이터를 관리하는 라이브러리에서 데이터 조작 부분과 사용자 인터페이스 부분을 분리해야 함.
      • 즉, 이런 라이브러리는 특정한 사용자 인터페이스의 타입에 종속되면 안 된다.
      • 분리하는 좋은 대표적인 패턴으로는 MVC 패턴이 있다.

2. 제네릭 데이터 구조와 알고리즘을 템플릿으로 구현하기

  • 템플릿을 이용하면 제네릭 구조체를 타입 또는 클래스 형태로 생성 가능
  • 템플릿은 원하는 타입을 매개변수로 지정해서 만들기 때문에 한 코드를 모든 타입에 적용 가능
  • 구조와 알고리즘을 모든 타입에 적용 가능
  • 즉, 현 프로그램에 특화된 형태로 만들지 말고 템플릿을 사용하여 범용적으로 만든다.

 

- 템플릿이 다른 제네릭 프로그래밍 테크닉보다 나은 이유

템플릿을 사용하지 않고 void * 포인터를 사용하는 방법이 있음.
하지만 type-safe하지는 않는다.
반면 템플릿을 제대로 알고 사용한다면 타입에 안전하다.
템플릿 인스턴스는 항상 한 가지 타입만 저장.

 

- 템플릿의 단점

문법이 복잡한 단점이 있음
동형(동종-homogeneous) 데이터 구조(하나의 데이터 구조에 같은 타입)만 지원한다
타입의 안정성 보장을 위함.

 

- 템플릿과 상속

어떤 경우에 템플릿, 상속을 이용할까?
동일한 기능을 다양한 타입에 적용하려면.. 템플릿 사용!
구체적인 타입마다 동작을 다르게 제공하려면.. 상속 사용!

 

3. 적절한 검사 기능과 안전장치 제공하기

  • 안전한 코드를 작성하기 위한 2가지 스타일
    • 계약에 따른 디자인(design by contract)으로, 다음 3가지 조건이라는 관점에서 보아야 함.
      • 사전(선행 조건) : 함수나 메서드를 호출하기 전에 클라이언트 코드에서 반드시 만족해야 할 조건
      • 사후(후행 조건) : 함수나 메서드의 실행이 끝날 때 반드시 만족해야 할 조건
      • 불변 조건 : 함수나 메서드의 전체 실행 과정에 항상 만족
    • 함수나 클래스를 최대한 안전하게 디자인하는 것이다.
      • 코드에서 에러 검사를 수행하는 데 있다.
      • 클라이언트 코드에 에러를 알려줄 때 에러 코드 or false or nullptr와 같은 별도의 값 리턴
      • 익셉션을 던지는 방식으로 에러 발생 유무 리턴
      • 메모리 관련해서 안전한 코드를 작성하려면 스마트 포인터 활용

 

4. 확장성을 고려한 디자인

  • 개방/폐쇄 원칙(Open/Closed Principle, OCP) : 클래스는 다른 클래스가 상속해서 확장하는 데에는 개방적, 구현을 수정하는 데는 폐쇄적 방식으로 동작을 확장할 수 있게 디자인.
  • 드로잉 app 예시를 통해서 설명
// 초기 버전 : 사각형만 지원하는 드로잉 app

class Square{
  // 구체적인 내용 생략
};

// 사각형을 실제로 그리는 담당
class Renderer{
  public:
    void render(const vector<Square>& squares);
};

void Renderer::render(const vector<Square>& squares){
  for (auto& square : squares){
    // square 객체 렌더링
  }
}
  • 원 그리기 기능 추가를 위해 Circle 클래스 정의
// 원 그리기 기능 추가
class Circle{
  // 구체적인 내용 생략
};

// render() 메서드 수정
void Renderer::render(const vector<Square>& squares, const vector<Circle>& circles){
  for (auto& square : squares){
    // square 객체 렌더링
  }
  for (auto& circle : circles){
    // circle 객체 렌더링
  }
}
  • 위처럼 수정했을 때, 수정에 폐쇄적인 방식이라고 볼 수 없다.
  • why? 원 그리기를 추가하기 위해 클래스를 확장하려면 render() 메서드의 기존 구현 코드를 수정했기 때문이다.
  • 따라서,,, 수정에 폐쇄적인 방식으로 클래스를 확장하려면 상속을 이용해야 한다.
// Shape 클래스로부터 Square라는 파생 클래스를 만든다.
class Square : public Shape {};

// 상속 적용한 디자인
class Shape{
  public:
    virtual void render () = 0;
};

class Square : public Shape{
  public:
    virtual void render() override { /* 사각형 렌더링 */ }
  // 멤버 생략
};

class Circle : public Shape {
  public:
    virtual void render() override { /* 원 렌더링 */ }
  // 다른 멤버 생략
};

class Renderer{
  public:
    boid render(const vector<shared_ptr<Shape>>& objects);
};

void Renderer::render(const vector<shared_ptr<Shape>>& objects){
  for (auto& object : objects){
    object -> render();
  }
}
  • 위처럼 상속을 사용하면 또 다른 도형을 추가하더라도 render () 메서드를 수정할 필요가 없다.
  • 즉, 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적임.

 

6.3 사용성 높은 인터페이스 디자인

  • 세련되고 효율적으로 구현했다고 하더라도, 인터페이스가 형편없으면 소용이 없다.
  • 인터페이스의 핵심 기능은 코드를 쉽게 사용하기 위함.

인터페이스 디자인에 대한 6가지 Tip!

1. 익숙한 방식을 따르자

  • 사용자에게 익숙한 표준 방식을 따르자.
  • 연산자 오버 로딩이라는 기능을 제공해준다.
  • 이 기능을 통해 객체에 대한 인터페이스를 사용하기 쉽게 만들어 줌.
  • 연산자 오버 로딩에 대한 내용은 이후 더 자세하게 알아보자.

2. 필요한 기능을 빼먹지 말자

  1. 클라이언트가 필요로 하는 동작을 모두 인터페이스에 추가한다.
    • 물론 모든 가능한 경우를 완벽히 지원하는 라이브러리를 만든다는 것은 거의 불가능.
    • 하지만, 최대한으로 고민하여 결정 범위를 지원하면 충분.
  2. 인터페이스에 최대한 많은 기능을 구현한다.
  3. 라이브러리의 결과를 합치는 데 필요한 일을 클라이언트에 떠넘기는 안 된다.

3. 군더더기 없는 인터페이스를 제공하자

  • 쓸데없는 기능은 인터페이스에서 빼고 최대한 간결, 깔끔하게 구성
  • 라이브러리를 작게 만들수록 유지 보수하기가 편하다.
  • 이 원칙은 근본적으로 주관적이므로 인터페이스를 만드는 이가 필요한 부분인지 아닌지 판단해야 한다.

4. 문서와 주석을 제공하자

  • 문서를 제공하는 방법
    1. 인터페이스 코드 안에 주석을 다는 것
    2. 별도로 문서를 제공하는 것
  • 라이브러리에 대한 문서를 작성할 때에는 구현이 아닌 동작에 초점을 맞춰서 설명해야 한다.
  • 인터페이스 주석에 대한 흔한 실수
    • 구현에 대한 세부사항을 너무 많이 담는 것!!

5. 하나의 기능을 다양한 방식으로 실행하게 만들기

  • 자동차에서 리모컨에 달린 버튼으로 차문을 잠그는 기능을 제공하지만, 수동으로 문을 잠그거나 열 수 있도록 기존 방식도 제공해야 함.
  • std::vector에서 원소에 접근하기 위해 두 가지 메서드를 제공하는데
  • 경곗값 검사를 해주는 at() 메서드와 경계값 검사를 하지 않아 속도가 빠른 operator []를 제공함

6. 커스터마이즈 지원하기

  • 프로그래머가 원하는 형태로 커스터마이즈 하는 기능을 제공하면 인터페이스의 유연성을 높일 수 있음.
  • 클라이언트의 능력에 따라 변형해서 사용할 수 있는 선택권을 제공

 

범용성과 사용성을 잘 조합하기 위한 2가지 Tip!

  • 범용성과 사용성은 서로 충돌할 때가 있다.
  • 흔히 범용성을 높이면 인터페이스가 복잡해지게 된다.
  • 다음은 범용성과 사용성을 조합하기 위한 팁을 소개한다.

1. 여러 가지 인터페이스를 제공하자

  • 인터페이스 분리 원칙을 적용하여 기능을 충분히 제공함과 동시에 복잡도를 낮출 수 있다.

2. 자주 사용하는 기능을 쉽게 만들자

  • 범용 인터페이스 안에서도 다른 것보다 특히 자주 쓰이는 것이 있다.
  • 예로 지도 서비스에서 영어를 기본 언어로 설정하도록 제공하고, 다른 값으로 변경할 수 있도록 한다.

 

6.4 SOLID 원칙

S [SRP(Single Responsibility Principle), 단일 책임성 원칙]

  • 컴포넌트마다 하나의 잘 정의된 책임을 가지며 관련 없는 기능을 합치지 아니함.

O [OCP(Open/Closed Principle), 개방 폐쇄의 원칙]

  • 클래스는 확장에 개방적, 수정에는 폐쇄적이어야 함

L [LSP(Liskov Substitution Principle), 리스 코프 치환 원칙]

  • 어떤 객체의 자리를 그 객체의 서브타입 인스턴스로 치환이 가능해야 한다.

I [ISP(Interface Segregation Principle), 인터페이스 분리 원칙]

  • 인터페이스는 깔끔하고 간결해야 함

D [DIP(Dependency Inversion Principle), 의존성 뒤집기/역전 원칙]

  • 의존성 주입은 의존성 역전 원칙을 구현하기 위한 방법 중 하나

 

6.5 요약

  • 이번 장에서는 코드의 구조화에 대한 네 가지 팁과 인터페이스 디자인에 대한 여섯 가지 팁 그리고 서로 충돌하기 쉬운 범용성과 사용성을 잘 조합하기 위한 두 가지 팁을 배워보았다. 그리고 객체 지향 디자인 원칙을 기억하기 좋은 SOLID 원칙에 대해서도 알아보았다. 이렇게 6장을 마지막으로 PART 2를 마치고 다음 7장에서부터는 PART 3로 넘어가서 소프트웨어 공학 프로세스의 구현 단계와 관련된 주제를 살펴보도록 하자!
반응형