- 유지보수성은 모든 소프트웨어에서 중요한 가치이며, 코드를 이해하는데 걸리는 시간으로 측정할 수 있다.
- 코드를 이해하는데 시간이 오래걸리면 그만큼 유지보수성이 나빠지고 품질 또한 저하된다.
누군가가 나의 코드를 이해할 수 없다면 문제는 나에게 있다.
1-1 -er로 끝나는 이름을 사용하지 마세요
객체와 클래스의 차이는?
- 클래스는 객체를 생성하고 이 클래스를 통해 객체가 생성된다.(인스턴스화)
class Cash { public Cash(int dollars) { ~~~ } } Cash fine = new Cash(5);
- 클래스는 객체의 팩토리라고 할 수 있다. 그렇다고 해서 이 예제를 팩토리라고 할 수는 없다.
- 단순히 Java에서 제공하는 new 연산자가 충분히 강력하지 않기 때문이다
- 무슨 소리를 하는건가 싶지만 new 연산자는 “객체"라고 불리는 것을 클래스의 인스턴스를 생성하는 것 말고는 없다.
- 유사한 객체가 이미 존재하거나 재사용 가능한지 확인도 안하며 new가 동작하는 방식을 변경할 수 있는 어떤 매개변수도 제공해주지 않는다.
- new는 객체의 팩토리를 제어할 수 있는 원시적인 수단이다.
- c++은 팩토리에서 객체를 제거할 수 있는 delete 연산을 제공함
- Java는 delete 연산자를 제공해주지 않아요
class Cash { public : public Cash(int dollars); } Cash five = new Cash(5); cout << five; delete five; // 객체파괴
- new는 Cash 클래스의 정적 메서드이며 new 가 호출되면 Cash 클래스가 제어를 획득한 후 five 객체를 생성한다.
- 이 객체는 숫자 5를 캡슐화하고 정수처럼 행동한다.
class Cash def initialize(dollars) # ... end end Cash five = Cash.new(5);
- 개념적으로 팩토리 패턴과 new 연산자는 동일하다.
- 결론적으로 클래스는 객체의 “팩토리"이다.
- 클래스는 객체를 만들고, 추적하고, 적절한 시점에 파괴한다.
- 대부분의 언어에서 이런 기능이 클래스의 코드가 아니라 런타임 엔진에 의해 구현된다는 사실은 중요하지 않다.
- 겉으로는 요청에 응답해 객체를 제공하는 클래스만 보이기 때문이다.
- 그래서 어떤 객체도 생성하지 않는 유틸리티 클래스는 어떻게 바라봐야 할까?
- new 연산자가 실행되기 전에 부가적인 로직을 더할 수 있기 때문에 보다 유연하고 강력하게 만들 수 있다.
class Shape { public Shape make(String name) { if(name.equals("circle")) { return new Circle(); } if(name.equals("rectangle")) { return new Rectangle(); } throw new IllegalArgumentException("not found"); } }
- 팩토리에서도 객체를 생성하는 최종 단계에서는 여전히 new 연산자를 사용한다.
- 핵심은 개념상 팩토리 패턴과 new 연산자가 크게 다르지 않다는 점이다.
- 완벽한 객체지향 언어였다면 new 연산자 안에서 이런 타입명을 이용해서 생성하거나 하는 기능들을 제공했을 것이다.
- 우리는 객체를 필요할 때 꺼낼 수 있고 더 이상 필요하지 않은 객체를 반환할 수 있는 웨어하우스로 클래스를 바라보도록 하자.
여기서 하고자 하는 말은 무엇일까?
- 템플릿과 팩토리의 차이는 무엇일까?
- 템플릿은 표본? 정의한 대로 만들어 내는 것
- 팩토리는 ?
- 단순히 필요한 시점에 복사되는 수동적이고 멍청한 코드 덩어리로 클래스를 격하시킨다.
- 즉 클래스를 객체의 능동적인 관리자로 생각해야 한다는 것이다.
- 클래스는 객체를 꺼내거나 반환할 수 있는 위치이기 때문에 클래스를 저장소 or 웨어하우스라고 불러야 한다.
- 객체가 만약 살아있는 생명체였다면 클래스는 어머니라고 표현할 수 있다.
클래스 이름을 적절하게 짓는 방법
클래스의 잘못된 이름
class CashFormatter { private int dollars; CashFormat(int dlr) { this.dollars = dlr; } public String format() { return String.format("$ %d", this.dollars); } }
- CashFormatter 클래스의 객체는 dollar에 저장된 금액을 문자열로 포맷팅 하는 일을 수행한다.
- 이 부분에서 이름을 포맷터(Formatter)로 짓기로 한 것은 적절해 보인다.
- 하지만 CashFormatter 클래스는 존중할 만한 객체가 아니다. 이 객체는 의인화할 수도, 코드 안에서 존경받는 시민으로 대우할 수도 없다.
- 이런 이름은 아주 인기가 많은 편이다. 하지만 이 방식을 따르지 않기를 권장한다.
- 이유 : 클래스의 이름은 객체가 노출하고 있는 기능에 기반해서 안된다.
- 즉 클래스의 이름은 무엇을 하는지가 아니라 무엇인지에 기반해야 한다.
- Cash, USDCash, CashInUSD 라는 클래스 이름과 usd() 라는 메서드 이름으로 수정할 수 있다.
객체는 역량으로 특정지어라.
- 내가 어떤 사람인지는 키, 몸무게, 피부색과 같은 속성이 아니라 내가 할 수 있는 행위로 설명하라.
숨어있는 악마 -er
- -er로 끝나는 이름을 가진 수많은 클래스들이 존재하며 잘못 지어진 이름이다.
- Manager, Controller, Helper, Handler 등등..
- -er과 상반된 클래스
- Target, EncodedText, DecodedData, Content 등등..
- 하지만 이 규칙에도 예외는 존재한다 !
- 어떤 활동을 수행하는 대상을 가리키기 위해 접미사 -er이 붙지만 오랜 시간이 지나면서 정착된 경우도 존재한다.
- computer, user 가 대표적이다.
- user는 어떤 물건을 사용하는 사람을 지칭하지 않고 소프트웨어와 상호작용 하는 사람이라고 생각함.
- computer는 계산을 하는 무엇이 아니라 전자 기기인 컴퓨터 자체를 가리킨다.
- 객체는 객체의 외부 세계외 내부 세계를 이어주는 연결장치가 아니다.
- 객체는 내부에 캡슐화된 데이터를 다루기 위해 요청할 수 있는 절차의 집합이 아니다.
- 객체는 캡슐화된 데이터의 대표자이다.
- 연결장치는 존중받지 못한다. 정보를 수정하거나 스스로 어떤 일을 수행할 만큼 충분히 강력하지도 똑똑하지도 않기 때문에 단순히 정보를 전달하기만 한다.
- 개인적으로 이부분에서 스프링에서 사용하고 있는 Controller의 이름은 적절하지 않나..? 라는 생각이 드는 것 같다.
- 대표자는 스스로 결정을 내리고 행동할 수 있는 자립적인 엔티티이다. 객체는 연결장치가 아니라 대표자여야 한다.
결론
- 클래스에 이름을 붙일 때는 무엇을 하는지가 아니라 “무엇인지"를 생각하자.
- 내가 무엇을 하는 지와 내가 누구인지는 다르다.
1-2 생성자 하나를 주 생성자로 만들자
생성자란?
- 생성자(ctor)는 새로운 객체에 대한 진입점 이라고 할 수 있다.
- 생성자는 몇 개의 인자들을 전달받아, 어떤 일을 수행한 후, 임무를 수행할 수 있도록 객체를 준비시킨다.
class Cash { private int dollars; Cash(int dlr) { this.dollars = dlr; } }
- 위의 예제는 하나의 생성자가 존재하고 인자로 전달된 달러를 dollars 프로퍼티에 캡슐화하는 일을 수행한다.
- 책에서 권장하는 방식에 맞게 클래스를 설계하면 많은 수의 생성자와 적은 수의 메서드를 포함하게 될 것이다.
- 2~3개의 메서드와 5~10 개의 생성자를 포함하는 것이 적당하다.
- 생성자가 많으면 오히려 좋지 않다고 생각을 하고 있었는데 이 챕터를 읽고 생각이 바뀐 계기가 되었다.
- 핵심은 응집도가 높고 견고한 클래스에는 적은 수의 메서드와 상대적으로 많은 생성자가 존재한다는 점이다.
- 생성자의 개수가 많을수록 클래스는 개선되고, 사용자 입장에서 클래스를 더 편리하게 사용할 수 있게된다.
// 여러 방식으로 인스턴스를 생성하는 경우 new Cash(30); new Cash("$29.95"); new Cash(29.95d); new Cash(29.95f); new Cash(29.95, "USD");
- 메서드가 많아지면 클래스의 초점이 흐려진다.
- 단일 책임 원칙을 위반한다.
주 생성자 부 생성자
- 생성자의 주된 작업은 제공된 인자를 사용해 캡슐화하고 있는 프로퍼티를 초기화 하는 일을 한다.
- 이런 초기화 로직을 단 하나의 생성자에 위치시키고 주 생성자라고 부른다.
- 나머지는 부 생성자로 부르고 주 생성자를 호출하도록 한다.
new Cash(30); new Cash("$29.95"); new Cash(29.95d); new Cash(29.95,"USD"); public class Cash { private int dollars; public Cash(float dlr) { this((int) dlr); } public Cash(String dlr) { this(Cash.parse(dlr)); } public Cash(int dollars) { this.dollars = dollars; } static private int parse(String dlr) { return Integer.parseInt(dlr); } }
- 주 생성자를 모든 부 생성자 뒤에 위치시키면 찾는 시간도 단축되며 유지보수성도 좋아진다.
- 이 원칙을 따르지 않는다면 발생할 수 있는 문제점
- 요구사항에 대한 검증을 모두 작성해야 하는 번거로움이 생긴다.
결론
- 주 생성자와 부 생성자를 나눠서 코드를 작성하도록 하자 !
- 메서드의 개수가 많아지면 분리를 고려하자 !
- 항상 유지보수성을 생각하면서 코드를 작성하자 !
1-3 생성자에 코드를 넣지 마세요
- 주 생성자는 객체 초기화 프로세스를 시작하는 유일한 장소이기 때문에 제공되는 인자들은 완전해야 한다.
- 어떤 것도 누락하지 않고 중복되는 정보도 없다는 것을 의미한다.
- 인자들을 이용해서 할 수 있는 것은 무엇이고, 할 수 없는 일은 무엇일까?
인자에 손대지 말자
class Cash { private int dollars; Cash(String dlr) { this.dollars = Integer.parseInt(dlr); } }
- 클래스가 내부에 캡슐화 하고 있는 것은 정수형 이지만 인자가 문자열로 들어오고 있다.
- 따라서 생성 시 정수형으로 변환하고 있는데 이게 과연 올바른 코드일까?
- 객체 초기화에는 코드가 없어야 하고 인자를 건드려서는 안된다.
- 대신 인자들을 다른 타입의 객체로 감싸거나 가공하지 않은 형식으로 캡슐화 해야한다.
public class Cash { private int dollars; public Cash(String dlr) { this.dollars = this(new StringAsInteger(dlr)); } public Cash(Number dlr) { this.dollars = dlr; } } public class StringAsInteger extends Number { private String source; public StringAsInteger(String source) { this.source = source; } @Override public int intValue() { return Integer.parseInt(this.source); } }
생성자에 코드가 없을 시 가져오는 이점
- 생성자는 객체를 인스턴스화 하는 것이며
- 그 객체가 작업을 할 수 있도록 만들어주는 역할을 한다.
- 위의 두가지가 겹치면 안된다.
- 생성자는 어떤 일을 수행하는 곳이 아니기 때문이다.
- 따라서 생성자 안에서 인자에게 어떤 작업을 하도록 요청해서는 안된다.
- 즉 할당문만 있어야 한다.
- 생성자에 코드가 없다면 성능 최적화가 더 쉽기 때문에 코드의 실행속도가 빨라진다.
- 생성자에 코드가 없다면 사용자가 쉽게 제어할 수 있는 투명한 객체를 만들 수 있다.
- 객체를 이해하고 재사용하기도 쉬워진다.
- 파싱이 한번만 수행되는 경우에도 일관성을 생각해서 생성자에 코드를 넣지 않는 것을 권장한다.
- 클래스의 미래가 어떤일이 일어날 지 모르기 때문이다.
- 객체지향 사실과 오해에서 우리는 미래에 대해 알 수 없지만 대비는 할 수 있다는 내용이 생각났다.
결론
- 생성자에 인자를 건드리는 행위를 하지 말자.
- 다만 개인적으로 생성자에 인자를 검증하는 유효성 코드는 작성해도 좋다고 생각한다.