본문 바로가기
CS/Code Complete

part2-chapter 6, 7

by 아찌방 2025. 1. 12.

 

▣ 6장: 클래스 다루기 - Working Classes 

 

프로그래머의 관점

70, 80 년대 : 루틴 => 21세기 : 클래스

 

유능한 프로그래머가 되기 위해서는 작업하는 동안 안전한 부분을 최대한으로 늘리는 것이다. 클래스는 이러한 목표를 이루기 위한 기본 도구

 

클래스란?

응집력있고 잘 정의되어 있는 데이터 및 루틴의 모음.

 


6.1 클래스의 토대: 추상 데이터형(ADT)

 

추상 데이터(ADT)란?

데이터와 해당 데이터에 대해 작동하는 연산의 모음

 

ADT의 필요성

  • 데이터 멤버를 직접 조작해야 하므로 유지보수성이 낮고 재사용성이 떨어짐.
  • 프로그램 내에서 비슷한 코드를 반복적으로 작성해야 하므로 효율성 저하.

ADT의 장점

1. 구현 세부사항을 감출 수 있음

데이터 타입 변경 시 프로그램 전체를 수정할 필요 없이 ADT 내부만 수정하면 끝

 

2. 변경 영향 최소화

데이터에 새로운 기능을 추가해도 다른 부분에 영향을 주지 않음.

 

3. 코드 가독성 향상

명확한 명칭을 지정함으로 혼동을 방지

코드의 의미 전달 용이


4. 성능 향상 용이

전체 프로그램을 다시 작성할 필요 없이 특정 부분만 수정

 

5. 신뢰성, 정확성 증가

명확한 메소드 호출이 가능 => 오류 감소

 

6. 데이터 전달 불필요

데이터를 전역으로 만들 필요나 매번 전달할 필요가 없음

또한 ADT 내부에서만 데이터 접근이 가능하므로 관리 부담 감소

 

7. 고수준 작업

컴퓨터적(배열, 구조체) 개념이 아닌, 현실적 개념(자동차, 글꼴 등)을 기준으로 작업 가능

 

ADT 구현 예시

currentFont.SetSizeInPoints(sizeInPoints)
currentFont.SetSizeInPixels(sizeInPixels)
currentFont.SetBoldOn()
currentFont.SetBoldOff()
currentFont.SetItalicOn()
currentFont.SetItalicOff()
currentFont.SetTypeFace(faceName)

# 안 좋은 예시
currentFont.size = 16  # (16이 픽셀인지 포인트인지 불분명)  
currentFont.attribute or 0x02  # 의미 불분명  
currentFont.attribute = currentFont.attribute or 0x02 # 복잡함

 

 

클래스는? = ADT + 상속, 다형성

 

즉, ADT를 활용하면 데이터와 관련된 조작 메서드를 묶어 일관성 추상화를 제공하며, 코드의 유지보수성과 재사용성을 향상시킬 수 있음. 또한 코드를 체계적으로 정리, 유지보수 및 확장성을 높일 수 있음


6.2 좋은 클래스 인터페이스

 

인터페이스 설계란?

추상화를 통해 복잡성을 숨기고, 일관성 있는 데이터와 메서드 제공

 

추상화란?

  • 복잡한 작업을 단순화하여 클래스 인터페이스를 설계.
  • 세부 구현은 감추고, 사용자에게는 관련된 데이터와 메서드만 제공.

 

좋은 인터페이스란?

  • 세부 구현 감추기: 외부에서는 사용하지 않을 내부 구현을 숨김.
  • 일관된 추상화 유지: 클래스가 명확한 목적과 책임을 갖도록 설계.
  • 유지보수성 향상: 기능 확장 시 추상화를 깨지 않도록 설계.

 

추상화? 캡슐화?

  • 추상화(Abstraction) : 복잡성을 관리하기 위해 세부 구현을 감추는 개념.
  • 캡슐화(Encapsulation) : 세부 구현에 접근하지 못하도록 강제하는 개념.

=> 추상화와 캡슐화는 상호 보완적이며, 둘 중 하나가 부족하면 전체 구조가 약화됨.

 

캡슐화 원칙

1. 클래스와 멤버의 접근성 최소화 => private, protected 사용이 기본

2. 멤버 데이터를 public으로 노출하지 않기. => 외부의 조작 가능성

3. 구현 세부사항을 인터페이스에 포함하지 않기

4. 친구 클래스(Friend Class) 사용 자제

5. 루틴을 공개 인터페이스에 추가하기 전에 검토 => 일관성 유지 확인

6. 가독성 중시

7. 의미적 캡슐화 위반 방지 => 코드를 구현 세부사항을 기반으로 작성하면 의존성 높아지고 유지보수 어려워짐

 

캡슐화 핵심

1. 결합도 최소화 : 단일 책임 원칙

2. 정보 은닉 : 데이터는 private로 설정하고, 필요한 경우만 메서드를 통해 접근

3. 데메테르 법칙 준수 : 한 객체는 자신이 직접 접근할 수 있는 객체와만 상호작용

4. 추상화 보호 : 블랙 박스



6.3 설계와 구현 문제

 

포함 관계("has a")와 상속("is a"):

  • has a: 클래스 멤버로 포함하여 관계 모델링.
  • is a: 상속을 사용하되, 공통 동작 및 데이터를 기반으로 설계.

포함 관계 구현

1. 포함 관계는 멤버 데이터를 통해 구현하는 것이 일반적

2. 포함 관계를 구현 할 수 없는 경우 비공개 상속 사용 => 최후의 수단(캡슐화를 깸, 클래스간 결합도를 높임) => 설계 검토 필요할 수 있음

3. 데이터 멤버는 클래스 당 7(± 2)개 정도가 적정

  • 원시 데이터 타입(예: int, string)은 최대 9개까지 허용 가능.
  • 복잡한 객체는 5개 이하로 제한하는 것이 바람직.

상속의 원칙과 설계 지침

1. 하위 클래스가 기본 클래스의 인터페이스를 준수 못하면 상속하지 말아야 됨. => LSP 준수

2. 문서화와 상속 금지 명시하기 => ex(C++ : non-virtual, JAVA : final, etc : non-overridable)

3. 상속의 유형

  • 추상 오버라이드 가능 메서드: 인터페이스만 상속, 구현 없음.
  • 오버라이드 가능 메서드: 기본 구현과 함께 상속, 오버라이드 가능.
  • 오버라이드 불가능 메서드: 기본 구현과 함께 상속, 오버라이드 불가능.

4. 중복 최소화 : 공통 인터페이스, 데이터, 동작은 상속 계층에서 가능한 한 높은 수준에 위치

5. 단일 인스턴스, 단일 하위 클래스 고려

6. 루틴 오버라이드 주의점 : 내용이 존재하는지, 없으면 오류 => 포함 관계가 더 나음

7. 깊은 상속 계층 피하기 : 6단계 이상 시 디버깅, 유지보수 어려움

  • 권장: 상속 계층을 2~3단계로 제한하고, 총 하위 클래스 수는 7±2로 제한.

다중 상속

  • 다중 상속은 신중하게 설계하고, 꼭 필요한 경우에만 사용해야 함.
  • Mixin 클래스는 다중 상속의 효과적인 활용 사례로, 독립적이고 간단한 속성을 추가하는 데 유용.
  • 복잡한 상속 구조는 피하고, 단일 상속과 인터페이스를 적절히 활용하여 설계를 간소화해야 함.

상속의 규칙이 많은 이유? 상속은 복잡성을 증가시킬 위험이 크기 때문

 

멤버 함수와 데이터

  • 설계 : 복잡성과 결함 발생 가능성을 최소화
  • 클래스는 간단하고 응집력 있게 유지.
  • Fan-out을 제한하여 클래스 간의 결합도를 낮춤.
  • 데메테르 법칙을 따라 간접 호출을 피하고, 클래스 간 협업을 단순화.

생성자

  • 멤버 데이터 초기화: 모든 생성자에서 멤버 데이터를 초기화하여 클래스 안정성을 확보.
  • 싱글톤 구현: 생성자를 private으로 설정하고, 정적 메서드로 단일 객체 관리.
  • 깊은 복사 우선: 복잡성을 낮추고 유지보수성을 높이기 위해 초기에는 깊은 복사를 선택.
  • 얕은 복사는 신중히 사용: 성능 문제가 확실히 증명될 때만 얕은 복사를 고려.

 


6.4 클래스를 작성하는 이유

=> 원칙에 따라 클래스를 설계하면 더 간결하고 유지보수 가능한 소프트웨어를 구축할 수 있음

 

1. 복잡성 감소 : 클래스는 복잡한 알고리즘과 데이터 조작을 숨기고 관리 용이성을 제공.

2. 변경에 대한 영향 최소화 : 변경이 필요한 부분을 한정해 유지보수를 간소화.

3. 중앙 집중식 제어 : 특정 기능과 데이터 접근을 클래스 내부로 한정.

4. 코드 재사용 가능성 증대 : 잘 설계된 클래스는 다른 프로젝트에서도 활용 가능.

 

이외

  • 실제 세계의 객체 모델링
  • 추상적인 객체 모델링
  • 복잡성 격리
  • 구현 세부 사항 숨기기
  • 전역 데이터 숨기기
  • 파라미터 전달 간소화
  • 프로그램 계층 구조 계획
  • 관련된 작업을 패키징
  • 특정 리팩토링 작업 수행


6.5 프로그래밍 언어와 관련된 이슈

  • 각 프로그래밍 언어는 클래스 정의, 캡슐화, 상속, 다형성을 다루는 방식이 다름.
  • 언어 특유의 규칙과 제약 사항을 준수해야 효과적인 클래스 설계 가능.
  • 예: C++의 다중 상속, Java의 단일 상속 제한, Python의 동적 타이핑.

6.6 클래스를 넘어서: 패키지

 

패키지?

객체 그룹을 집합체로 묶어 추상화, 캡슐화를 강화시킴

 

클래스 품질 체크리스트:

  • 추상 데이터 타입 : 클래스의 인터페이스를 평가하고 추상화가 일관되게 제공되는지 확인
  • 캡슐화 : 클래스가 내부 데이터를 노출하지 않고, 멤버 접근을 최소화하는지 점검
  • 상속 : "is a" 관계에 해당하는 상속만 사용하고, 문서화된 상속 전략을 따르는지 확인
  • 구현 문제 : 데이터 멤버가 7개 이하이고, 클래스 간 협력이 최소화되었는지 검토
  • 언어별 문제 : 사용하는 언어의 클래스와 관련된 특수한 문제를 확인

▣ 7장: 고급(고품질) 루틴

 

루틴? 특정 작업을 수행하는 메소드, 함수 또는 절차

루틴은 코드 중복을 줄이고 유지보수를 용이하게 하지만, 잘못 사용하면 오히려 코드 품질을 떨어뜨릴 수 있습니다.


7.1 루틴을 작성하는 이유

 

루틴의 역할:

1. 반복되는 코드를 줄임 => 재사용성 높음, 유지보수 용이, 복잡도 감소

2. 코드를 루틴으로 감춤 => 가독성 향상, 오류 발생 방지, (순서)의존성 낮춤, 이식성 향상

 

간단한 작업을 루틴으로 만드는 이유 => 코드의 명확성과 유지보수성, 확장성을 향상시킬 수 있음

 


7.2 루틴 수준의 설계

 

루틴을 설계의 목표 : 기능적 응집도 높이기

=> 가능한 하나의 작업만 수행

 

응집도?

루틴 설계 상 응집도는 루틴 내 작업들이 얼마나 밀접하게 관련되어 있는지를 나타냄.

높은 응집도 = 한 작업 수행 (기능적 응집도) => 코드 안정성, 유지 보수성 향상

 

응집도 종류

기능 > 순차 > 통신 > 시간 > 절차 > 논리 > 우연


7.3 좋은 루틴 이름

 

좋은 루틴 이름 => 가독성을 높임 => 유지보수 용이

 

예시

1. 수행하는 작업을 이름에 모두 표기

2. 의미 없고 모호한 동사 피하기

3. 숫자만으로 구분하지 말기

4. 이해하기 쉽게, 필요한 만큼 길게 짓기

5. 반환값 중심으로 작성

6. 목적을 반영하기

7. 반대 개념의 이름을 일관되게 사용

8. 공통 작업에 규칙을 따라 일관된 이름 사용


7.4 루틴의 길이에 대한 문제

 

과거 : 한 화면(50 - 150줄),  IBM(50줄 제한), TRW(두 페이지 재한)

현재 : 경우에 따라 4,000 - 12,000줄

 

객체지향 프로그래밍에서 루틴 길이

  • 일반적으로 짧음
  • 복잡할 경우 100 -200줄까지 확장 가능
  • 200줄 이상일 시, 이해 가능성에 한계 발생 가능

 

단순히 길이를 제한하기 보다는 응집력에 초점을 둬야 함.

=> 루틴이 수행하는 작업의 논리적 단위가 명확하고 이해하기 쉬운지

즉, 루틴 길이는 고정된 기준이 아니라 코드의 맥락과 복잡성에 따라 결정.

짧고 응집력 있는 루틴 작성을 기본 원칙으로 삼되, 필요할 경우 길이를 늘리는 것도 허용


7.5 루틴 매개변수 처리

 

1. 매개변수 순서 지키기 : 입력 > 수정(입출력) > 출력

2. in, out 키워드 만들기 : 직접 IN, OUT 키워드를 정의하여 사용하기

3. 비슷한 파라미터는 일관된 순서로 배치

4. 파라미터를 모두 사용 : 안 쓰면 제거

5. 상태 변수나 오류 변수는 마지막에 배치 : 루틴의 주요 목적과 거리가 멈

6. 루틴 파라미터를 작업 변수로 사용하지 않기 : 로컬 변수 선언해서 사용하기

7. 파라미터 가정 문서화 : 데이터가 특정 특성을 가진다면 assert등을 통해 명시

8. 루틴 파라미터 개수 제한 : 7개 이하 추천

9. 파라미터 이름 규칙 : 입력, 수정, 출력을 구분하는 접두사("i_", "m_", "o_") 등을 사용하기

10. 인터페이스 추상화 유지

11. 이름 붙이기

 


7.6 함수를 사용할 때 특별히 고려해야 할 사항

 

함수(Function)

  • 주된 목적이 단일 값을 계산하고 반환하는 경우 사용. 수학적 함수처럼 동작해야 함(예: sin(), CustomerID(), ScreenHeight()).
  • 함수는 일반적으로 입력 파라미터만 받고, 계산된 값을 반환하는 형태로 작성.

프로시저(Procedure)

  • 입력값을 수정하거나 출력값을 생성하는 경우 사용.
  • 다수의 입력, 수정, 출력 파라미터를 가질 수 있으며, 반환값이 없는 것이 일반적임.

=> 함수는 값을 반환하는 루틴. 프로시저는 그렇지 않은 루틴

 

함수를 사용할 때 고려 사항

 

1. 반환값 설정:

  • 함수에서 반환값을 잘못 설정하는 것을 방지하려면 모든 실행 경로에서 반환값을 설정해야 함.
  • 함수 시작 시 기본값을 초기화해 안전망 역할을 제공.

2. 로컬 데이터에 대한 참조 또는 포인터 반환 금지

  • 함수 종료 후 로컬 데이터가 범위를 벗어나면 참조 또는 포인터가 무효화됨.
  • 클래스 내부 데이터를 반환해야 하는 경우, 해당 정보를 멤버 변수에 저장하고 이를 반환하는 접근자 함수를 제공.

7.7 매크로 루틴과 인라인 루틴

 

매크로 사용은 잠재적인 위험, 가독성 저하를 초래함. 필요할 때만 사용 권장

대신 인라인 함수를 사용 추천

 

메크로 사용 시 주의점

1. 표현식 괄호로 묶기

#define Cube( a ) a*a*a => 연산자 우선순위로 의도치 않은 결과 반환 가능
#define Cube( a ) (a)*(a)*(a) => (x + 1) / Cube(y) 같은 표현식에 적용 어려움
#define Cube( a ) ((a)*(a)*(a)) => Good

 

2. 다중 문장 중괄호로 묶기

3. 규칙 따르기

4. ++, -- 연산자 주의

#define IncrementSquare( x ) ((x)*(x+1))
int a = 3;
IncrementSquare(++a);

=> ((++a)*(++a+1))
위와 같이 a가 두 번 증가

 

 

728x90