클린 코드 #10 클래스
코드 레벨, 함수 레벨까지 제대로 작성하는 것도 중요하지만, 더 차원 높은 단계까지 신경써야 한다.
이 챕터에서는 깨끗한 클래스에 대해 다룬다.
클래스 체계
표준 자바 컨벤션에 따르면
- 변수 목록이 가장 먼저 나온다.
- 그 중 상수( = 정적 공개 상수)가 맨 처음에 나온다.
- 그 다음으로 정적 비공개 변수가 나온다.
- 그 다음에 비공개 인스턴스 변수가 나온다.
- 공개 변수가 필요한 경우는 거의 없다.
- 변수 목록 다음에는 공개 함수가 나온다.
- 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다. 추상화 단계가 순차적으로 내려가기 위함이다.
캡슐화
변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 할 필요는 없다. protected로 만들고 테스트 코드에서 접근을 허용하기도 한다.
같은 패키지 안에서 테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 그 함수나 변수를 protected로 선언하거나 패키지 전체로 공개한다. 하지만 캡슐화를 푸는 것은 최후의 수단이다. 공개하지 않을 방법을 최대한 찾다가 안되면 protected를 쓰자.
클래스는 작아야 한다!
클래스를 만드는 규칙 중 첫 번째는 클래스의 크기는 작아야 한다는 것이다.
그렇다면 얼마나 작아야 할까?
- 개발자들은 알겠지만 많은 클래스들의 크기가 크다.
- 메서드의 수가 적더라도 책임이 많으면 안 된다.
클래스 이름은 클래스 책임을 기술해야 한다. 작명으로부터 클래스의 크기를 줄일 수 있다. 클래스 이름이 떠오르지 않는다면 영어를 못하거나 클래스 책임이 너무 많기 때문이다. Processor, Manager과 같은 모호한 단어가 있다면 여러 책임을 떠앉는지 점검하자.
클래스 설명은 if, and, or, but을 사용하지 않고, 25자 내외로 가능해야 한다.
클래스 명을 설명해보자. “~하며 ~기능을 제공한다” 형태라면, 클래스에 책임이 너무 많은 것이다.
단일 책임 원칙(SRP, Single Responsibility Principle)
SRP는 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙이다. 그런데 SRP가 갑자기 왜 나왔을까?
클래스는 ‘책임(=변경할 이유)’ 이라는 개념을 정의하여 적절한 클래스의 크기를 제시한다. 클래스는 변경할 이유가 하나여야, 클래스의 크기도 적절해진다.
에를 들어 하나의 클래스가 버전 정보와 View를 관리한다고 해보자. 버전 정보가 바뀌거나 View가 바뀐다면 이 클래스의 변경할 이유는 두 가지인 것이다.
또한, 변경할 이유를 파악하다보면 코드 추상화도 쉬워진다. 아까 예시를 가져와보면, 버전 정보를 다루는 메서드를 빼내서 독자적인 Version이라는 클래스를 만들 수 있게 된다. 재사용도 가능해진다.
SRP는 객체 지향 설계에서 더욱 중요한 개념이다. 지키기 쉽지만 클래스 설계자가 많이 무시하는 규칙 중 한 가지기도 하다.
우리가 깨끗한 소프트웨어를 만들기보단 돌아가는 소프트웨어에 초점을 맞추기 때문이다. 둘은 별개의 활동이고 관심사를 분리하는 건 올바른 태도이다. 하지만 프로그램이 돌아가면 일이 끝났다고 생각하고, 깨끗한 소프트웨어 만드는 활동을 진행하지 않는다.
어떤 개발자들은 자잘한 단일 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려한다. 하지만, 클래스가 몇 개든 돌아가는 부품 수는 비슷하다. 결국 익혀야 하는 양은 비슷하다. 많은 도구를 어떻게 관리하고 나눠 넣을지 고민하면 된다.
복잡한 대규모 시스템을 다루려면 체계적인 정리가 필수적이다. 무엇이 어디에 있는지 찾아야 하기 때문이다. 저자는 이런 시스템에서, 변경을 가했을 때 영향이 미치는 컴포넌트만 이해해도 충분하다고 한다. 거대한 책임의 클래스로 구성된 시스템은 당장 알 필요 없는 사실까지 독자에게 들이밀어 독자를 방해한다.
큰 클래스 몇 개 대신, 작은 클래스 여럿으로 이뤄지는 시스템을 만들자. 그리고 작은 클래스들간의 협력 관계를 만들어 동작하자.
응집도
클래스는 인스턴스 변수 수가 작아야 한다. 그리고 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
일반적으로 메서드가 변수를 더 많이 사용할 수록 메서드와 클래스는 응집도가 높다.
가장 응집도가 높은 클래스는 모든 인스턴스 변수를 메서드마다 사용하는 클래스이다. 하지만 이렇게 응집도가 가장 높은 클래스는 가능하지 않고 바람직하지 않다. 하지만 우리는 응집도 높은 클래스를 선호한다.
함수를 작게, 매개변수 목록을 짧게 만들다 보면 몇 개의 메서드만이 사용하는 인스턴스 변수가 아주 많아진다.
클래스를 쪼개야 한다.
응집도가 높아지도록 변수와 메서드를 분리하여 새로운 클래스 두세 개로 쪼개준다.
응집도를 유지하면 작은 클래스 여럿이 나온다.
큰 함수를 작은 함수로 나누기만 해도 클래스가 많아진다.
예를 들어보자. 변수가 아주 많은 큰 함수가 있다. 큰 함수 일부를 작은 함수로 빼내려고 하는데, 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다면 변수 네개를 인수로 넘겨야 할까?
전혀 아니다. 네 변수를 클래스 인스턴스 변수로 승격하면 새 함수는 인수가 필요 없다. 함수를 쪼개기 쉬워진다.
하지만 이렇게 하면 클래스가 응집력을 잃는다. 몇몇 함수만 사용하는 인스턴스 변수가 늘어나기 때문이다.
어? 몇몇 함수가 몇몇 변수만 사용한다면? 독자적인 클래스로 분리하자! 클래스가 응집력을 잃으면 쪼개면 된다.
큰 함수를 작은 함수로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.
변경하기 쉬운 클래스
대다수 시스템은 지속적인 변경이 가해진다. 변경을 하면 시스템이 의도대로 동작하지 않을 위험이 생긴다. 깨끗한 시스템은 클래스를 체계적으로 정리하기 때문에 변경에 수반하는 위험을 낮춘다.
클래스 일부에서만 사용되는 비공개 메서드는 코드를 개선할 여지를 시사한다. 하지만 해당 클래스가 논리적으로 완성되어 수정될 일이 없다면 책임을 분리하려고 시도할 필요가 없다. 클래스를 손대는 순간 설계 개선을 고민하자.
public class SQL {
public String create();
public String insert();
public String selectAll();
public String findByKey();
// ...
private String selectWithCriteria(String criteria);
// ...
만약 상속 구조로 개선한다면, 특정 메서드만 사용하는 비공개 메서드는 파생 클래스로 옮기고, 모든 파생 클래스가 공통으로 사용하는 비공개 메서드는 유틸리티 클래스에 넣을 수 있다.
abstract public class Sql {
// 생략
// 추상 메서드
abstract public String generate();
}
public class CreateSql extends Sql {
// override
}
public class SelectSql extends Sql {
// override
}
public class InsertSql extends Sql {
// override
}
클래스를 분리하였기 때문에, update문을 추가하더라도 기존 클래스를 변경할 필요가 전혀 없다. UpdateSql 클래스를 만들고 상속받으면 끝이다. 기능을 추가해도 다른 코드가 망가지지 않는다.
위 클래스는 SRP 뿐만 아니라 OCP도 지원한다. (확장에 개방적, 수정에 폐쇠적)
우리가 재구성한 Sql 클래스는 파생 클래스를 생성하는 방식이므로 새 기능에 개방적이고, 다른 클래스를 닫아놓는 방식으로 수정에 폐쇠적이다. 끼어넣기만 하면 된다.
새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소인 시스템 구조가 바람직하다.
변경으로부터 격리
요구사항은 변하기 마련이고, 따라서 코드도 변하게 된다. 상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 따라서 우리는 인터페이스, 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.
상세한 구현에 의존하는 코드는 테스트가 어렵다. 예를 들어, Portfolio 클래스가 TokyoStockExchangeAPI를 사용하여 포트폴리오 값을 계산한다고 해보자. 테스트 코드는 시세에 영향을 받게 된다.
API를 직접 호출하는 대신, StockExchange라는 인터페이스를 생성하고 메서드를 선언한다. 그리고 이 인터페이스를 구현하는 TokyoStockExchange 클래스를 구현한다. Portfolio 클래스는 인터페이스에 의존하도록 수정한다.
이렇게 되면, TokyoStockExchange를 흉내내는 테스트용 클래스를 만들 수 있다. 이와 같이 테스트가 가능할 정도로 시스템 결합도를 낮추면 유연성과 재사용성이 높아진다. 결합도가 낮다는 것은 시스템 요소가 다른 요소, 변경으로부터 잘 격리되어 있다는 의미다.
그리고 결합도를 최소로 줄이면, DIP를 따르는 클래스가 나온다.
'Software Engineering > Clean Code' 카테고리의 다른 글
[클린 코드] #9 단위 테스트 (0) | 2025.09.03 |
---|---|
[클린 코드] #8 경계 (0) | 2025.09.03 |
[클린 코드] #7 오류 처리 (0) | 2025.09.03 |
[클린 코드] #6 객체와 자료구조 (0) | 2025.09.03 |
[클린 코드] #5 형식 맞추기 (0) | 2025.09.03 |