지난 실용주의 프로그래머 책을 읽고 나서 후기를 작성했었습니다.
실무 팀 투입 후 2주가 넘어선 시점에서 책 2권을 받았는데, 실용주의 프로그래머와 클린 코드 책이었습니다.
실용주의 프로그래머 후기:
[실용주의 프로그래머 정리와 개인적인 후기
실무 팀 투입 후 2주 정도가 되었을 때, 팀장님으로부터 책을 추천 받아 읽게되었습니다.책을 읽고 책에 나오는 팁들에 대한 정리와 저의 견해를 남겨두었습니다.
실용주의 프로그래머 정리와 개인적인 후기
실무 팀 투입 후 2주 정도가 되었을 때, 팀장님으로부터 책을 추천 받아 읽게되었습니다.책을 읽고 책에 나오는 팁들에 대한 정리와 저의 견해를 남겨두었습니다.1장. 실용주의 철학Tip1. 자신의
curihus.tistory.com
위 글을 적으면서 책 읽는 시간 보다 검색하고 생각 정리하고 블로그에 글 쓰는 시간이 더 오래걸렸기 때문에, 이번 후기는 정리보단 요약과 저의 생각과 찾아본 관점들, 그리고 실무에선 어떻게 사용하는지 등을 위주로 작성해보려 합니다.
들어가기 전

저는 클린 코드 책을 보기 전에 유튜브로 클린 코드에 대한 후기를 남겨둔 영상을 먼저 봤었습니다.
이 영상에선 클린 코드에 대해 부정적인 의견 위주로 남겨두었기 때문에 어느 정도 선입견 비판적인 태도로 보았고 필요한 부분이나 인상 깊었던 부분을 위주로 내용 정리 및 후기를 남기려 합니다.
이 책에서는 java와 java convention을 기준으로 설명하고 있습니다.
저는 C++을 주력 언어로 사용하고 있기 때문에, java 표준이 아닌 C++ 표준에 맞지 않는 부분은 생략했습니다.

이전에 java 언어에 대한 기초를 알아보고 클린 코드와 웹 백엔드 서비스에 관심을 가졌을 때 우아한테크코스 프리코스에 참여했었습니다. 이때, 간단한 미션이지만 어떻게 코드를 추상화하고 깨끗하게 작성할지 고민을 많이 했던 것 같습니다.
woowacourse-docs/cleancode/pr_checklist.md at main · woowacourse/woowacourse-docs
우아한테크코스 문서를 관리하는 저장소. Contribute to woowacourse/woowacourse-docs development by creating an account on GitHub.
github.com
책을 읽으면서 보니 우아한테크도 클린 코드 책에 영향을 많이 받았다는 것을 느낄 수 있었습니다.
2장 의미 있는 이름
클래스 이름은 명사나 명사구가 적합하다.
ex) Customer, WikiPage, AddressParser (Manager,Processor, Data, Info 등과 같은 단어는 피하고 동사는 사용하지 않는다.)
메서드 이름은 동사나 동사구가 적합하다.
ex) postPayment, deletePage 등
실무에서 이해하기 힘든 변수명 (ex. i, a, kk 등)을 사용하는 경우가 있습니다. 이럴 때, 유지 보수를 위해 해당 변수가 어떤 역할을 하는지 직관적으로 알기 힘들어 생산성이 떨어지는 경우가 있으니 주의해야 합니다.
3장 함수
함수를 만드는 첫째 규칙은 작게 만드는 것이다.
모니터 크기에 맞춰서 가로 150자를 넘어서면 안 된다, 20줄도 길다.
함수는 한 가지 일만 해야 한다.
-> 여기서 말하는 한 가지는 추상화 수준이 하나인 것입니다.(쉽게 설명하면 함수 내부에 단순 다른 표현이 아닌 의미 있는 이름으로 다른 함수를 추출할 수 있다면 여러 작업을 하는 것입니다)
Switch 문
Money calculatePay(Employee* e) {
switch (e->type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw InvalidEmployeeType(e->type);
}
}
위와 같은 함수가 존재한다고 할 때, 다음과 같은 문제가 있습니다.
1. 함수가 길다.(새 직원 유형을 추가하면 더 길어집니다.)
2. '한 가지' 작업만 하지 않는다.
3. SRP를 위반한다.
4. OCP를 위반한다.(새 직원 유형을 추가할 때마다 코드를 변경하기 때문입니다.)
위 함수를 수정한 버전은 아래와 같습니다.
enum EmployeeType { COMMISSIONED, HOURLY, SALARIED };
struct EmployeeRecord {
EmployeeType type;
};
class Employee {
public:
virtual ~Employee() = default;
virtual bool isPayday() = 0;
virtual Money calculatePay() = 0;
virtual void deliverPay(Money pay) = 0;
};
// 추상 팩토리 인터페이스
class EmployeeFactory {
public:
virtual ~EmployeeFactory() = default;
virtual std::unique_ptr<Employee> makeEmployee(const EmployeeRecord& r) = 0;
};
// 구체적인 팩토리 구현부: 여기서만 switch 문을 사용해 책임을 한 곳으로 격리
class EmployeeFactoryImpl : public EmployeeFactory {
public:
std::unique_ptr<Employee> makeEmployee(const EmployeeRecord& r) override {
switch (r.type) {
case COMMISSIONED:
return std::make_unique<CommissionedEmployee>(r);
case HOURLY:
return std::make_unique<HourlyEmployee>(r);
case SALARIED:
return std::make_unique<SalariedEmployee>(r);
default:
throw std::runtime_error("Invalid Employee Type");
}
}
};
위 부분에서도 switch 문을 사용했지만 다형적 객체를 생성하는 코드 안에서는 허용합니다.
서술적인 이름을 사용하라!
함수의 이름이 길어도 이름이 짧고 어려운 이름보다 좋습니다.
이때 이름을 붙일 때는 일관성이 있어야 하며 동일한 모듈 내에서는 같은 문구, 명사, 동사를 사용합니다.
함수 인수
인수(parameter)가 3개인 함수는 가능한 피하는 편이 좋고 4개 이상인 경우는 특별한 이유가 있어야 합니다.
인수가 2~3개 이상이라면, 일부를 독자적인 클래스 변수로 만드는 것을 고려해봐야 합니다.
Circle makeCircle(double x, double y, double radius); // before
Circle makeCircle(Point center, double radius); // after
함수의 의도와 인수의 순서와 의도를 잘 나타내기 위해 좋은 함수 이름을 지어야 합니다.
단항 함수는 동사와 명사 쌍을 이루어야 합니다. ex) writeField(name)
혹은 함수명에 키워드를 넣는 것입니다. 함수 이름에 인수 이름을 넣습니다.
ex) assertEquals 보단 assertExpectedEqualsActual(expected, actual)이 더 좋습니다. 이러면 인수 순서를 외울 필요가 없어집니다.
이외에도 부수 효과를 일으키지 않기, 명령 조회 분리, 오류 코드 보단 예외 사용하기, 반복하지 않기 등의 원칙이 존재합니다.
함수를 작게 만드는 것은 물론 중요하지만, 함수 하나에서 여러 작업을 하는 경우가 여럿 존재합니다. 특히 게임 개발은 일반 웹에서보다 상호작용이 더욱 많이 발생하여 만연하게 일어나는 일입니다. 물론 나눌 수는 있겠으나 너무 작게 나눈다면, 아래 사진처럼 하나의 함수를 읽기 위해 더 많은 함수를 읽기 위해 생산성이 떨어질 수 있겠습니다.

4장. 주석
저희 팀에서는 주석을 최대한 지양하고 있어서 다른 프로그래머들은 어떻게 주석을 활용하고 있는지 reddit 등의 커뮤니티를 보며 확인해봤습니다.
요즘 트렌드가 주석을 최대한 지양하는 것이 맞지만 그렇지 않은 프로그래머도 많고 필요한 부분에서는 주석을 쓰는 것을 지향한다는 의견이 많았습니다.
책에서는 좋은 주석은 다음과 같이 설명합니다.
1. 법적인 주석
2. 의도를 설명하는 주석
3. 의도 명료화
void testCompareTo() {
WikiPagePath a = PathParser::parse("PageA");
WikiPagePath b = PathParser::parse("PageB");
assertTrue(a.compareTo(b) == -1); // a < b
assertTrue(b.compareTo(a) == 1); // b > a
}
위 코드처럼 읽기 난해한 코드의 경우 주석처리를 하여 가독성을 좋게 합니다.
4. 결과를 경고하는 주석
5. TODO 주석
6. 중요성 강조 주석
좋은 주석이 있지만, 주석은 코드의 실패(코드로 명료하게 의도를 나타내지 못한 경우)를 보완하는 수단으로 사용해야 합니다.
10장 클래스
책은 Java Convention을 참고하여 저는 Google C++ Style Guide을 참고하여 작성했습니다.
선언 순서
public -> protected -> private 섹션 별로 나누어 선언합니다.
이때, 하나의 섹션 내에서는 아래의 순서로 선언합니다.
- 타입 및 타입 별칭 (typedef, using, enum, 중첩된 struct와 class, 그리고 friend 타입)
- (선택 사항, struct에 한함) 비정적(non-static) 데이터 멤버
- 정적 상수
- 팩토리 함수
- 생성자 및 대입 연산자
- 소멸자
- 그 외 모든 함수 (정적 및 비정적 멤버 함수, 그리고 friend 함수)
- 그 외 모든 데이터 멤버 (정적 및 비정적)
클래스를 작게 유지하라!
-> 책에서는 다섯 개의 메서드 정도가 괜찮다고 합니다.
SRP(Single Responsibiity Principle)은 클래스나 모듈을 변경할 이유가 하나뿐이어야 한다는 원칙입니다.
응집도(Cohesion)
클래스는 인스턴스 변수 수가 작아야 합니다. 각 클래스 메소드는 클래스 인스턴스 변수를 하나 이상 사용해야 합니다.
일반적으로 메소드가 변수를 더 많이 사용할수록 메소드와 클래스는 응집도가 더 높습니다.
(응집도가 높다는 말은 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미입니다.)
-> 응집도를 유지하면 작은 클래스가 여럿 나온다.
변경하기 쉬운 클래스
class Sql {
public:
Sql(std::string table, std::vector<Column> columns);
std::string create();
std::string insert(std::vector<std::string> fields);
std::string selectAll();
std::string findByKey(std::string keyColumn, std::string keyValue);
std::string select(Column column, std::string pattern);
std::string select(Criteria criteria);
std::string preparedInsert();
private:
std::string columnList(std::vector<Column> columns);
std::string valuesList(std::vector<std::string> fields, const std::vector<Column>& columns);
std::string selectWithCriteria(std::string criteria);
std::string placeholderList(std::vector<Column> columns);
};
위 클래스는 새로운 SQL 문을 지원하려면 반드시 Sql 클래스에 손대야 합니다. 또한 기존 SQL 문 하나를 수정할 때도 반드시 Sql 클래스에 손대야 합니다. -> SRP 원칙을 위반
이럴 때는 공개 인터페이스를 각각 Sql 클래스에서 파생하는 클래스로 변경하면 SRP와 OCP를 지킬 수 있습니다.
class Sql {
public:
Sql(std::string table, std::vector<Column> columns)
: table(table), columns(columns) {}
virtual std::string generate() = 0;
};
// 각 책임을 분리한 구체 클래스들 (SRP 준수)
class CreateSql : public Sql {
public:
using Sql::Sql;
std::string generate() override;
};
class SelectSql : public Sql {
public:
using Sql::Sql;
std::string generate() override;
};
class InsertSql : public Sql {
public:
InsertSql(std::string table, std::vector<Column> columns, std::vector<std::string> fields)
: Sql(table, columns), fields(fields) {}
std::string generate() override;
};
class Where {
public:
Where(std::string criteria);
std::string generate();
}
class ColumnList {
public:
ColumnList(std::vector<Column> columns);
std::string generate();
}
위처럼 공개 인터페이스는 Sql 클래스에서 파생하는 클래스로 만들고, 모든 파생 클래스가 공통으로 사용하는 비공개 메소드는 Where, ColumnList라는 두 유틸리티 클래스에 넣었다.
각 클래스는 극도로 단순하며 함수 하나를 수정했다고 다른 함수가 망가질 위험도 사라졌습니다. update 문을 새로 추가할 때 기존 클래스를 변경할 필요가 전혀 없습니다. 그저, UpdateSql 클래스를 새로 끼워넣으면 됩니다.
11장 시스템
시스템 제작과 시스템 사용을 분리하라
대다수 애플리케이션은 시작 단계라는 관심사(concern)를 분리하지 않습니다. 준비 과정 코드를 주먹구구식으로 구현할 뿐만 아니라 런타임 로직과 마구 뒤섞습니다.
Service GetService() {
if (service == null)
service = new MyServiceImpl(...);
return service;
}
위는 초기화 지연(Lazy Initialization) 혹은 계산 지연(Lazy Evaluation)이라는 기법입니다.
위 기법의 장점은 여러가지가 있습니다.
1. 실제로 필요할 때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다. 따라서 앱을 시작하는 시간이 그만큼 빨라진다.
2. 어떤 경우에도 null 포인터를 반환하지 않는다.
그러나, GetService() 메소드가 MyServiceImpl과 생성자 인수에 명시적으로 의존합니다. 런타임 로직에서 MyServiceImpl 객체를 전혀 사용하지 않더라도 의존성을 해결하지 않으면 컴파일이 되지 않습니다.
결정적으로, MyServiceImpl이 모든 상황에 적합한 객체인지 모른다는 사실이 가장 큰 우려 사항입니다.
Main 분리 기법
시스템 생성과 사용을 분리하는 한 가지 방법으로, 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정합니다.

main 함수에서 시스템에 필요한 객체를 생성한 후 애플리케이션에 넘기고 애플리케이션은 객체를 사용합니다. 애플리케이션은 생성 과정을 전혀 모른채로 모든 객체가 적절히 생성되었다고 가정하고 사용합니다.
팩토리 기법

때로는 객체 생성 시점을 애플리케이션이 결정할 필요도 생깁니다. 예를 들어, 주문 처리 시스템에서 애플리케이션은 LineItem 인스턴스를 생성해서 Order에 추가합니다. 이때 추상 팩토리 패턴을 사용합니다. 그러면 LineItem을 생성하는 시점은 애플리케이션이 결정하지만 LineItem을 생성하는 코드는 LineItem이 모릅니다.
의존성 주입
사용과 제작을 분리하는 강력한 메커니즘 하나가 의존성 주입(Dependency Injection)입니다. 의존정 주입은 제어 역전(Inversion of Control, IoC) 기법을 의존성 관리에 적용한 메커니즘입니다. 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 떠넘깁니다. 새로운 객체는 넘겨받은 책임만 맡으므로 SRP를 지키게 됩니다.
// 1. 추상화된 인터페이스 (의존 대상)
class Service {
public:
virtual void execute() = 0;
};
// 2. 의존성을 주입받는 클래스
class Client {
private:
Service* service; // 구체적인 구현이 아닌 인터페이스에 의존 [2]
public:
// 생성자를 통해 외부에서 의존성을 주입받음 (Constructor Injection) [1]
Client(Service* s) : service(s) {}
void doSomething() {
service->execute();
}
};
위 코드를 통해 다음을 알 수 있습니다.
- Client 내부 어디에도 new ServiceImpl() 같은 직접 생성 코드가 없습니다.
- Client는 주입받는 객체가 정확히 무엇인지 몰라도 되며, 오직 인터페이스에만 의존하는 수동적인 상태가 됩니다.
- 객체를 생성하고 연결(Wiring)하는 책임은 외부(예: main 함수 등)로 넘어갑니다.
팩토리 패턴의 경우에는 특히, 객체 생성이 많은 게임에서 많이 사용됩니다.
https://www.youtube.com/watch?v=qhtL9EYtB3Q
https://refactoring.guru/ko/design-patterns/abstract-factory
추상 팩토리 패턴
/ 디자인 패턴들 / 생성 패턴 추상 팩토리 패턴 다음 이름으로도 불립니다: Abstract Factory 의도 추상 팩토리는 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있
refactoring.guru
잼민이의 요약

개인적인 후기
인터넷을 보니 클린 코드 책에 대한 의견이 많았습니다. 클린 코드를 너무 신봉자와 신봉하면 안된다는 의견이 분분했습니다. 특히, 그로 인한 기술 부채가 발생하고 오히려 생산성이 떨어진다는 의견이 많았습니다.
가독성이 좋은 코드는 이후 개발자가 유지보수 하고 생산성을 높일 수 있지만, 팀과 프로젝트의 일정에 맞는 클린 코드와 일정 내 개발의 trade-off와 팀의 ground-rule에 따라 코드 작성 원칙을 정하고 따르는 것이 더 중요하다고 느꼈습니다.
물론, 너무 읽기 힘든 코드는 지양하고 주기적인 리팩토링을 통한 생산성 향상은 중요할 것입니다.
Appendix
이 책을 읽으며 추가적으로 찾아본 자료들을 첨부했습니다.
https://www.youtube.com/watch?v=AiPMVeULhMQ
https://www.youtube.com/watch?v=1Vn8et-MdaI
Toss Tech 블로그에서만 진입할 수 있는 영상이라 같이 첨부했습니다.
'CS > 소프트웨어 책 후기' 카테고리의 다른 글
| 실용주의 프로그래머 정리와 개인적인 후기 (0) | 2026.02.19 |
|---|