본문 바로가기
전문가를 위한 C++

2025/03/02 ch.06 재사용을 고려한 설계

by 딴짓거리 2025. 3. 2.

재사용 가능한 코드를 직접 설계하고 작성할 수 있어야 한다

 

6.1 재사용 철학

코드는 반드시 작성자나 다른 프로그래머가 재사용할 수 있게 설계해야 한다.

- 작성은 한번, 사용은 여러번

- 무슨 수를 쓰더라도 코드 중복은 피한다

- 같은 일은 반복하지 않는다

 

-> 코드를 한 프로그램에서만 사용하는 경우는 극히 드묾

언젠가 다른 곳에서 사용할 일이 있다. 처음부터 제대로 설계한다

재사용 가능한 코드를 직접 설계하고 작성할 수 있어야 한다

-> 재사용할 수 있게 설계하면 시간과 비용을 절약할 수 있음

-> 팀 내 다른 프로그래머도 활용할 수 있도록 작성

대부분 프로젝트는 팀 단위로 수행되므로 잘 설계된 라이브러리와 코드를 제공하면 팀원에게 도움이 된다

-> 재사용성이 낮으면 코드 중복이 늘어난다.

중복된 코드가 많으면 유지 보수하기 힘들어진다. 복사 붙여넣기 작업이 많다 싶으면 함수나 클래스로 따로 빼라

-> 첫 번째 수혜자는 바로 작성자 자신이다

경험이 풍부한 프로그래머는 작성한 코드를 함부로 버리지 않고 개인용 라이브러리 형태로 구축한다.

 

6.2 코드를 재사용할 수 있도록 설계하는 방법

1. 용도나 분야가 약간 달라도 충분히 사용할 수 있도록 볌용성을 갖춰야 함.

 특정 용도에 너무 특화된 프로그램 컴포넌트는 다른 프로그램에서 재사용하기 힘듦

2. 재사용 가능한 코드는 사용하기 쉽게 만들어야 하마. 인터페이스와 기능을 금방 이해해서 자신이 만드는 애플리케이션에 즉시 적용할 수 있어야 함

 

코드를 클라이언트에 전달하는 방식도 중요

프로젝트 코드에 즉시 통합할 수 있도록 소스 코드 상태로 전달할 수도 있고

애플리케이션에 링크할 수 있도록 정적 라이브러리로 제공

윈도우의 DLL이나 리눅스의 공유 객체와 같은 동적 라이브러리로 전달

- 전달 방식에 따라 설계 제약사항도 달라짐

# 클라이언트란 우리가 작성한 인터페이스를 사용하는 프로그래머를 말함. 유저가 아님!

 

재사용할 수 있는 코드를 설계하는데 가장 중요한 부분 -> 추상화

 

6.2.1 추상화 방법

인터페이스와 구현을 실질적으로 분리한다

구현 - 원하는 작업을 달성하기 위해 작성한 코드를 뜻함

인터페이스 - 이렇게 작성한 코드를 다른 사람들이 사용하기 위한 수단

 

C에선 라이브러리에 있는 함수를 담은 헤더 파일이 인터페이스 역할을 담당

OOP에선 외부에서 접근할 수 있는 클래스의 속성과 메서드로 구성

 

잘 정의된 인터페이스는 공용 메서드만 가지고 있다.

클래스의 속성은 절대로 공개하면 안됨

케터와 세터 공용메서드를 통해 접근하도록 구성해야 함

재사용 가능한 코드를 직접 설계하고 작성할 수 있어야 한다

 

인터페이스와 구현을 분리하는 이유는 클라이언트가 코드를 사용할 때 코드의 구체적인 작동방식은 공부하지 않아도 되기 때문. 또한 코드 작성자도 인터페이스를 변경하지 않고 쉽게 코드를 수정하거나 업그레이드 할 수 있다.

동적 링크 라이브러리를 활용하면 클라이언트가 실행 파일을 다시 빌드 할 필요도 없다.

# 동적 링크 라이브러리(DLL) 여러 프로그램에서 동시에 사용할 수 있는 재사용 가능한 코드와 데이터가 포함된 파일

해당 라이브러리의 주요 기능과 바람직한 사용법을 명시할 수 있다.

 

# 인터페이스를 설계할 때는 구현 세부사항을 클라이언트에 드러내지 않는다

 

핸들 - 일부 라이브러리에서 한 인터페이스에서 리턴한 정보를 다른 곳에 전달하기 위해 클라이언트 코드에서 보관하는 정보

라이브러리에서 여러 함수나 메서드를 호출하는 과정에서 특정 인스턴스의 상태를 유지하는 데 주로 사용함

라이브러리에서 핸들을 사용하도록 설계할 때는 내부 구현사랑을 드러내면 안됨 -> 불투명  클래스로 구현

불투명 클래스란 내부 데이터 멤버를 직접 접근하지 못하게 하거나 게터나 세터로 간접적으로 접근하게 만든 클래스

절대 핸들 내부의 변수를 클라이언트 코드가 직접 조작할 수 없게 만든다.

 

# C++는 public 인터페이스와 비public 데이터 멤버, 메서드를 모두 한 클래스 정의에서 작성할 수 밖에 없기 때문에 클래스 내부 구현사항이 어느정도 클라이언트에게 노출 될 수 밖에 없다

 

추상화 원칙은 설계 과정 전반에 적용해야 한다.

필요한 결정을 내릴 때마다 자신이 작성한 코드가 추상화 원칙을 따르는지 확인한다.

클라이언트 입장에서 이 인터페이스를 사용하려면 내부 구현사항을 알아야 하는지 검토해본다.

 

1. 코드를 적절하게 구성하기

 - 클래스 계층 구조, 템플릿 사용 여부, 서브 시스템으로 나누는 방식등을 고려

 - 우리가 작성한 라이브러리나 코드의 기능을 사용하는 데 진입점 역할을 하는 인터페이스를 설계한다.

 

6.2.2 재사용에 최적화된 코드 구조화

설계를 시작할 때부터 함수와 이를 구성한 클래스부터 라이브러리나 프레임워크 전체에 이르기까지 모든 계층을 전반적으로 고려해야 한다. 이러한 다양한 계층을 컴포넌트라고 부른다

 

- 서로 관련 없거나 논리적으로 구분되는 개념을 합치지 않기

응집도를 높여라(반드시 한 작업만 처리하거나 여러작업이어도 서로 성격이 같은 것들만 처리하도록 설계하라)

이를 단일 책입 원칙(SRP) 라고도 부른다. 

 

프로그램을 통째로 가져다 쓰는 경우는 드물다. 기능을 논리적으로 구분해서 별도의 컴포넌트로 구현해야 다른 프로그램에서 재사용하기 좋다

서로 호환되는 부품을 교체할 수 있게 만드는 것

 

프로그램을 서브시스템 단위로 논리적으로 나누기

반드시 독립적으로 재사용할 수 있는 컴포넌트로 만들어야 함. 최대한 결합도가 낮게 만든다

한 부분을 재사용할때 다른 부분까지 가져와야 하는 일이 없도록

 

클래스 계층을 사용해서 논리적으로 나누기

서브 시스템 관점뿐만 아니라 클래스 관점에서도 서로 관련없는 개념이 한데 섞이지 않도록 주의해아 함

 

집계방식으로 논리적 개념 나누기

집계란 has- a  관계를 말함

객체가 제공하는 기능의 일부분을 수행하는 객체를 따로 두는 것

서로 관련 없거나, 관련은 있지만 상속만으로 분리하기 힘든 기능을 구분할 수 있다.

 

사용자 인터페이스에 대한 종속성 제거하기

데이터를 관리하는 라이브러리는 데이터 조작 부분과 사용자 인터페이스 부분을 분리해야 함

이런 종류의 라이브러리는 특정한 사용자 인터페이스의 타입에 종속되면 안됨

 

범용 데이터 구조와 알고리즘을 템플릿으로 구현하기

C++에서 제공하는 템플릿을 이용하면 범용 구조체를 타입 또는 클래스 형태로 생성할 수 있다.

가능한 한 데이터 구조와 알고리즘을 프로그램에 특화된 방식으로 만들지 말고 범용적으로 설께한다.

 

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

템플릿 말고도 범용 데이터 구조를 얼마든지 만들 수 있다(void* 라던지)

하지만 이러한 방법은 타임 세이프하지 않다. 컨테이너가 저장된 원소의 타입을 검사하거나 특정한 타입의 원소만 받아들이게 만들 수 없다.

void*를 직접 사용하지 않고 c++17부터 제공하는 std::any 클래스를 사용할 수도 있다. 

객체의 타입이 무엇이든 any에 저장가능. 내부적으로 void* 이지만 원래 타입을 저장해두기 때문에 타입 안정성을 보장한다 (이럴 필요까지 있나..)

데이터 구조를 특정한 클래스에 맞게 정의하기. 다형성을 이용해 이 클래스를 상속한 모든 파생 클래스를 이 데이터 구조에 저장할 수 있도록 만들기. <- 타입 안정성 보장하지 않음

 

템플릿은 타입안정성을 보장한다 하나의 템플릿 인스턴스에는 항상 한가지 타입만 저장함

저장 공간에 할당하는 작업이 필요 없음. 성능 좋음!

 

템플릿의 단점

문법이 복잡함. 동형 데이터 구조만 지원(데이터 구조마다 한가지 타입으로 된 객체만 저장 가능)

타입 안정성을 보장하기위해 어쩔 수 없음

근데 c++17부턴 이걸 우회하는 방법을 제공함. std::any나 std::variant 쓰면 됨

 

또한 최종 바이너리 코드의 크기가 커지는 코드 비대 현상이 발생

각 템플릿 인스턴스에 고도로 특화된 코드는 그보다 느리지만 범용적인 코드보다 길어지는 경향이 있음

-> 요즘은 저장공간이 커서 큰 문제는 아님

 

템플릿과 상속

템플릿을 사용할지 상속을 사용할지?

- 동일한 기능을 다양한 타입에 제공할 때는 템플릿

- 특정 타입마다 동작을 다르게 제공할 때는 상속

 

물론 동시에 적용해도 됨

 

 

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

안전한 코드를 작성하기 위해 적용할 수 있는 스타일이 두가지 있다. 혼용하는게 가장 좋다

1. 계약에 따른 설계 - 함수나 클래스에 대한 문서는 클라이언트 코드에서 해야 할 일과 이 클래스나 함수가 제공할 것을 명시한 일종의 계약서와 같음. 

계약 설계 방식의 세 가지 관점

사전 조건 - 함수나 메서드를 호출하기 전에 클라이언트 코드에서 반드시 만족해야 할 조건

사후 조건(후행조건) - 함수나 메서드의 실행이 끝날 때 반드시 만족해야 할 조건 

불변 조건 - 함수나 메서드의 전체 실행 과정에 항상 만족해야 할 조건

 

2. 함수나 클래스를 최대한 안전하게 설계하는 것

에러검사 수행!

 

에러코드, false nullptr optioinal 익셉션 등등 c++에서 제공하는 다양한 기능을 사용해 안전한 코드를 작성합니다

 

 

확장성을 고려한 설계

항상 다른 클래스가 상속해서 확장할 수 있도록 개방적인 동시에, 수정할 수 없게 폐쇄적이어야 함

개방/폐쇄 원픽(OCP) - 구현을 수정하기 않고도 동작을 확장할 수 있어야 함

상속을 이용하자

 

6.2.3 사용성 높은 인터페이스 설계

프로그래머가 다루는 인터페이스를 신경써야 함

 

사용자 고려하기

용도 고려하기

 

유틸맅티 클래스와 라이브러리

 

서브시스템 인터페이스

모형성 - 테스트할때 인터페이스 구현에서 특정 부분을 동일한 인터페이스의 다른 구현으로 교체할 수 있어야 함

유연성 - 테스트하는 경우가 아니더라도 특정 인터페이스에 대해 다양한 구현 코드를 제공해서 서로 교체할 수 있게 만들어야 할 때가 있다

# 서브시스템의 핵심 목적을 분명히 하기

 

컴포넌트 인터페이스

 

인터페이스를 사용하기 쉽게 설계하기

 

C++에서 제공하는 연산자 오버로딩을 사용하면 객체에 대해 인터페이스를 사용하기 쉽게 만들 수 있다.

 

필요한 기능 빼먹지 않기

 

군더더기 없는 인터페이스 제공하기

 

문서와 주석 제공하기

 

범용 인터페이스 설계

 

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

- 커스터마이즈 지원하기

 

범용성과 사용성의 조화

 

솔리드원칙

 

 

 

~326p