- 코드의 유지보수성을 향상시키기 위한 방안들을 소개함
- 코드의 길이가 짧아지고, 이해하기 쉬워지고, 모듈성이 향상되고, 응집도가 높아진 코드를 생산해야 된다.
- birth - 객체는 살아있는 유기체이다. 객체와 객체의 역할을 이해하여 유지보수성을 높히자
- -er로 끝나는 이름을 사용하지 마세요
- 객체는 클래스의 팩토리이다. 결정을 스스로 내리고, 행동 가능한 자립적인 엔티티, 대표자이다.
- 클래스는 객체의 능동적인 매니저이다.
- 클래스가 무엇인지(what he is)에 기반한 네이밍을 해야한다.
- 클래스가 무슨 일을 하는지(what he does)으로 네이밍 해서는 안된다.
- 생성자 하나를 주 생성자로 만드세요
- ctor를 더 많이, 메서드를 적게 선언해라
- 메서드가 많아질 수록 SRP를 위반할 수 있다.
- 생성자가 많아질 수록 유연성이 향상된다.
- 메서드 오버로딩을 이용해서 부ctor를 모두 선언 뒤 마지막에 주ctor를 선언하자.
- 초기화 작업은 오로지 주ctor에서만 하고, 나머지 오버로딩된 생성자들은 인자를 준비하고, 포맷팅하고, 파싱하고, 변환만 해서 주ctor에 넘겨준다.
- 생성자에 코드를 넣지 마세요
- 객체 초기화를 하는 주ctor는 code-free해야하고, 인자를 건드려서는 안된다.
- -er로 끝나는 이름을 사용하지 마세요
- Education - 객체는 작아야한다
- 가능하면 적게 캡슐화하세요
- 4개 이하의 객체에 대해서 캡슐화 하자
- 자바에서 equals()를 오버라이드하자
- 최소한 뭔가는 캡슐화하세요
- 어떤 것도 캡슐화 하지 않고 메서드만 있는 객체는 바람직 하지 않다.
- 정적메서드가 존재하지 않고, 인스턴스 생성-실행을 엄격하게 분리하는 순수한 OOP에서는 기술적으로 프로퍼티가 없는 클래스를 만들 수 없다.
- 항상 인터페이스를 사용하세요
- 인터페이스를 이용한 느슨한 결합도를 통해 객체 분리가 가능해야한다. 상호작용하는 다른 객체를 수정하지 않고도 해당 객체를 수정할 수 있어야한다.
- 인터페이스는 객체 간 의사소통 하기 위한 계약constract이다.
- 메서드 이름을 신중하게 생각하세요
- 뭔가를 만들고 새로운 객체를 반환하는 빌더 메서드는 명사로 네이밍한다.
- 빌더는 항상 객체를 반환하기 때문에 반환타입이 void일 수 없다.
- 객체로 추상화한 엔티티를 수정하는 조정자 메서드는 동사로 네이밍한다. 조정자의 반환타입은 항상 void여야 한다.
- 빌더와 조정자 혼합하기
- Boolean값을 결과로 반환하는 경우 메소드는 빌더에 속하지만, 가독성 측면에서 이름을 형용사로 하는 것이 좋다 ex) isEmpty() 대신 empty(). 읽을 때는 is 접두어를 붙여서 읽는다.
- 퍼블릭 상수(public constant)를 사용하지 마세요
- 퍼블릭 상수, 열거형을 사용하면 클래스간 결합도는 커지고, 응집도는 낮아져서 쓰면 좋지 않다.
- 퍼블릭 상수, 열거형에 대해서 되도록 계약의 의미를 캡슐화하는 새로운 클래스를 만들어서 쓰는 것이 좋다
- 불변 객체로 만드세요
- 식별자 가변성: 두 객체를 비교 한 후 한 객체의 상태를 변경할 때 두 객체가 다름에도 동일하다고 생각할 수 있다. 상태 변경이 불가능한 불변 객체의 경우 이러한 문제가 애초에 생기지 않는다.
- 실패 원자성: 객체의 원자성(완전한 객체이거나 선언-초기화에 실패하거나)를 지킬 수 있게 된다.
- 시간적 결합: 생성자를 통한 인스턴스화 - 초기화를 분리시킬 수 없기 때문에 setter 사이의 시간적인 결합을 제거할 수 있다.
- 부수효과 제거: 객체의 상태를 불변으로 만들면 의도하지 않은 side effect를 찾을 필요도 없다
- NULL참조 없애기: 설정되지 않은 프로퍼티는 이해하기 어렵다. 만약 불변객체로 만든다면 애초에 프로퍼티가 NULL일 수도 없다.
- 스레드 안전성: 스레드 안전성을 위해 불변객체를 쓰자. 명시적인 동기화는 비용이 들고, 데드락이 발생할 수 있다.
- 더 작고 더 단순한 객체: 자바에서 250줄 이상의 클래스는 리팩토링이 필요하다. 불변객체는 생성자 안에서만 상태를 초기화 할 수 있기 때문에 더 작게 만들 수 있다. 불변성은 클래스를 더 깔끔하고 짧게 만든다. 가변 객체를 절대 쓰지 말자!
- 문서를 작성하는 대신 테스트를 만드세요
- 코드를 읽게 될 사람이 비지니스 도메인, 언어, 디자인패턴, 알고리즘을 모르는 주니어 프로그래머라고 가정해야 된다. 멍청한 사람들을 위한 단순한 코드를 짜자
- 단위테스트는 클래스의 일부이지 독립적인 개체가 아니다. 단위테스트는 클래스의 사용방법을 보여줘야 한다. 메인 코드만큼의 관심을 기울이자.
- 모의객체(mock) 대신 페이크 객체(Fake)를 사용하세요
- 모킹은 테스트가 장황해지고 이해하고 리팩토링하기 어려워진다. 인터페이스에 대해 페이크 클래스를 만들어서 사용하자
- 모킹은 가정을 사실로 전환시키기 때문에 단위테스트를 유지보수하기 어렵게 만든다.
- 인터페이스를 짧게 유지하고 스마트(smart)를 사용하세요
- 인터페이스가 커질 수록 클래스를 구현하기 위해 더 많은 것을 요구하게 되므로 클래스의 응집도와 견고함이 손상될 수 있다. 스마트 클래스를 인터페이스와 함께 배포함으로써 공통기능을 추출하고, 코드중복을 피할 수 있다.
- 가능하면 적게 캡슐화하세요
- Employment - 거대한 객체, 정적메서드, NULL참조, getter, setter new 연산자는 좋지 않다
- 5개 이하의 public 메서드만 노출하세요
- 생성자와 private 메서드는 많아도 상관없다
- 클래스 크기의 기준은 public, protected 메서드의 갯수이다. 5개를 기준으로 줄여보자
- 클래스를 작게 만들면 우아함, 유지보수성, 응집도, 테스트 용이성이 향상된다.
- 각각의 메서드가 객체의 진입점이고 진입점의 수가 적다면 문제를 더 쉽게 고립시킬 수 있다.
- 클래스가 작으면 메서드와 프로퍼티가 더 가까이 있을 수 있어서 응집도가 높아진다.
- 정적 메서드를 사용하지 마세요
- 정적메서드 대신 객체를 사용하는게 좋다. 정적메서드는 유지보수를 힘들게 한다.
- 객체 vs 컴퓨터 사고(Object vs computer thinking)
- 객체지향 프로그래밍은 절차적 프로그래밍과 다른 점이 있다. 그저 A is a B 라고 정의만 할 뿐 어떤 방식으로 계산하고 언제 계산하는 지는 통제하지 않는다.
- 따라서 OOP형 프로그래밍과 매우 유사하다. 반면 OOP의 정적메서드는 C와 어셈블리어의 서브루틴과 동일하다.
- 선언형 스타일 vs 명령형 스타일(declarative vs imperative style)
- 명령형 프로그래밍은 프로그램의 상태를 변경하는 문장statement를 사용해서 계산 방식을 서술한다
- 선언형 프로그래밍은 제어 흐름을 서술하지 않고 계산 로직을 서술한다. 엔티티 간의 관계로 구성되는 자연스러운 사고 패러다임에 더 가깝다. "제어를 서술하지 않고 로직만 표현한다" CPU에게 결과가 실제로 필요한 시점과 위치를 결정하도록 위임하고 요청이 있을 때만 계산을 실행하기 때문에 더 빠르다.
- 선언형은 다형성(코드 블록 간의 의존성을 끊을 수 있는 능력)때문에 더 좋은 방식이다.
- 객체지향에서 객체는 일급 시민이지만 정적메서드는 그렇지 않다. 생성자의 인자로 객체를 전달 할 수 있찌만 정적메서드는 전달할 수 없다.
- 선언형은 명령형보다 표현력이 더 좋기 때문에 더 직관적이다. 알고리즘과 실행 대신 객체와 행동의 관점에서 사고하는 것이 좋다.
- 선언형은 코드 응집도가 높기 때문에 더 좋다. OOP에서 선언형 스타일을 쓰는것은 좋다.
- 유틸리티 클래스
- 제거할 엄두가 안나는 정적메서드들은 우리의 코드가 객체를 직접 처리할 수 있또록 정적 메서드를 감싸는 클래스를 만들어서 고립시키는 방식이 있다.
- java.lang.math 같은 유틸리티 클래스는 어떤 것의 팩토리가 아니기 때문에 진짜 클래스라 할 수 없다. 유틸리티 클래스를 구현할 때는 클래스의 인스턴스가 생성되는 것을 방지하기 위해 priate ctor를 추가하는 것이 좋다. 이렇게 해야 어느 누구도 클래스의 인스턴스를 생성할 수 없다.
- 싱글톤 패턴
- 정적 메서드 대신 사용할 수 있는 개념이다. 사실은 끔찍한 안티패턴이다. 전역변수 그 이상도 이하도 아니다. 전역변수는 캡슐화를 완벽하게 위반한다. 싱글톤은 객체지향 패러다임을 잘못 사용한 예이다. 정적메서드가 있었기 떄문에 탄생할 수 있었다.
- 유틸리티 클래스와 싱글턴 클래스의 차이: 싱글톤은 분리 가능한 의존성으로 연결되어 있지만, 유틸리티 클래스는 분리가 불가능한 하드코딩된 결합도를 가진다. 싱글톤의 장점은 getInstance()와 함께 setInstance()를 추가할 수 있다는 것이다. → 캡슐화된 객체를 변경할 수 있기 때문에 싱글톤이 유틸리티 클래스보다 더 좋다. 유틸리티 클래스 안에는 객체가 존재하지 않기 떄문에 어떤 것도 변경, 분리할 수 없는 하드코딩된 의존성이 있다.
- 함수형 프로그래밍
- FP보다 OOP가 표현력이 더 뛰어나고 강력하다. FP에서는 함수만 사용할 수 있지만 OOP에서는 객체와 메서드를 조합할 수 있다.
- 조합 가능한 데코레이터
- 데코레이터 객체를 다중계층 구조로 구성해서 문장statement를 포함하지 않는 선언형으로 짤 수 있다.
- 객체 vs 컴퓨터 사고(Object vs computer thinking)
- 정적메서드 대신 객체를 사용하는게 좋다. 정적메서드는 유지보수를 힘들게 한다.
- 인자의 값으로 NULL을 절대 허용하지 마세요.
- A == NULL 같은 비교문은 객체 내에서 담당해야할 로직이다. 인자가 객체인지 NULL인지를 확인하는 짐을 메소드 구현자에게 떠넘겨서는 안된다. 항상 객체를 전달하되 전달된 객체에게 무리한 요청을 할 시 응답을 거부하도록 객체를 구현해야한다.
- OOP에서 "존재하지 않는 인자" 문제는 null object를 이용해서 해결해야 한다. 전달할 것이 없다면 비어있는 것처럼 행동하는 객체를 전달하면 된다.
- 메서드가 인자의 값으로 NULL을 허용하지 않기로 했는데 클라이언트가 여전히 NULL을 전달한다면? 1. NULL을 체크한 후 예외를 던진다. 2. 아예 NULL을 무시하고 어떠한 대비도 하지 않는다 → 이렇게 하면 메서드를 실행하는 도중 NPE가 발생해서 메서드 호출자는 자신이 실수했다는 것을 인지할 수 있게 됨
- 올바른 설계의 소프트웨어는 NULL참조가 존재해서는 안된다. 방어적으로 대응하지말고 무시함으로써 JVM에 정의된 표준방식으로 처리해야한다.
- 충성스러우면서 불변이거나, 아니면 상수이거나
- 상태state ↔ 데이터data 간의 차이를 이해하면 불변 객체의 장점에 대해 이해할 수 있다
- 객체란 파일, 웹페이지, 바이트 배열 등 실제 엔티티의 대표자이다.
- 객체는 식별자, 상태, 행동을 포함한다. 식별자를 통해 객체 f와 다른 객체를 구별한다. 상태는 f가 디스크 상의 파일에 대해 알고 있는 것이다. 행동은 요청을 수신했을 때 f가 할 수 있는 작업을 나타낸다.
- 불변 객체에는 식별자가 존재하지 않으며 절대로 상태를 변경할 수 없다. 불변 객체의 식별자는 객체의 상태와 완전히 동일하다.
- 절대 getter와 setter를 사용하지 마세요
- 객체가 자료구조보다 우월하다.
- 자료구조는 OOP에 해로운 존재이다. 자료구조는 어떤 personality도 지니지 않는 단순한 data bag일 뿐이다.
- 클래스는 어떤 식으로든 멤버에게 접근하는 것을 허용하지 않는 캡슐화가 존재한다. OOP가 지향하는 중요한 설계원칙이다.
- 자료구조는 glass box, 수동적이지만 객체는 blackbox, 능동적이다
- 모든 프로그래밍 스타일의 핵심 목표는 가시성의 범위를 축소해서 사물을 단순화시키는 것이다.
- 객체는 일급 시민이고, 생성자를 통한 객체 초기화가 곧 소프트웨어이다. 소프트웨어는 연산자나 구문이 아닌 생성자를 통해서 구성된다.
- 좋은 의도, 나쁜 결과
- getter, setter는 캡슐화 원칙을 위반하기 위해 설계되었다.
- 접두사에 대한 모든 것
- get, set 접두어의 메서드는 데이터를 노출하기 떄문에 클래스의 모든 사용자가 이 데이터를 볼 수 있다.
- 객체가 자료구조보다 우월하다.
- 부 ctor 밖에서는 new를 사용하지 마세요
- 객체가 필요한 의존성을 직접 생성하는 대신, 우리가 ctor을 통해 의존성을 주입하는 것은 좋은 프랙티스이다. 이를 위해 부 ctor을 제외한 어떤 곳에서도 new 연산자를 사용하지 않는다. 이렇게 해야 하드코딩된 의존성을 피할 수 있다. - DI, IoC개념
- 인스트로스펙션과 캐스팅을 피하세요
- type introspection, casting을 절대로 사용해서는 안된다. - instanceof, Class.cast()...
- reflection은 강력한 기법이지만 코드를 유지보수하기 어렵게 만드는 기법이다. 코드가 런타임에 다른 코드에 의해 수정된다는 사실을 기억해야 한다면 코드를 읽기 매우 어렵다.
- 타입에 따라 객체를 차별하기 때문에 OOP의 기본 사상을 훼손한다.
- 런타임에 객체의 타입을 introspect하는 것은 클래스 사이의 결합도가 높아지기 때문에 기술적으로 좋지 않다. 이를 메소드 오버로딩을 통해서 해결하자
- 클래스 캐스팅은 방문한 객체에 대한 기대expectation을 문서에 명시적으로 기록하지 않은 채로 외부에 노출해버린 것과 같다. 클라이언트와 객체 사이의 불명확하고, 은폐되고, 암시적인 관계는 유지보수성에 심각한 영향을 끼친다.
- 5개 이하의 public 메서드만 노출하세요
- Retirement - 부정확한 예외 처리는 유지보수에 나쁜 영향을 준다.
- 절대 NULL을 반환하지 마세요
- NULL 반환을 하는 객체를 신뢰할 수 없게 된다. 유지보수성이 떨어지게 된다.
- 객체라는 사상에는 우리가 신뢰하는 엔티티라는 개념이 담겨져 있다. 객체는 자신만의 생명주기, 자신만의 행동, 자신만의 상태를 가지는 살아있는 유기체이다. 존재하면 살아있고, 존재하지 않으면 죽어있는 것이다. 반환값을 검사하는 방식은 애플리케이션에 대한 신뢰가 부족하다는 신호이다.
- NULL 대신 예외처리를 통해서 빠르게 실패하는 방식을 채택하자.
- 빠르게 실패하기 vs 안전하게 실패하기
- 문제가 발생하면 빠르게 실행을 중단하고 예외를 던지는 방식이 소프트웨어가 계속 실행될 수 있도록 NULL을 반환하는 안전하게 실패하기 방식보다 낫다.
- 실패를 눈에 잘 띄게 만들고 추적하기 쉽게 만든다. 상황을 구조하지 않는 대신 실패를 분명하게 만들기 때문에 더 빠르게 문제를 찾을 수록 더 빠르게 실패하고, 결과적으로 품질이 향상된다.
- NULL의 대안
- 메서드를 두 개로 나누기 : 객체의 존재를 확인하는 메서드(boolean), 객체를 반환하는 메서드(반환타입 객체, 여기서 예외를 던진다)
- NULL, 예외를 던지는 대신 객체 컬렉션을 반환하는 방식 ex) 사용자를 데이터베이스에서 발견하지 못했다면 return new ArrayList<>(0); 이런 식으로 빈 컬렉션을 반환
- java.util.Optional은 컬렉션과 동일하지만 오직 하나의 요소만 포함할 수 있따. 의미론적으로 부정확하기 때문에 OOP와 대립되며 사용을 권장하지 않는다
- null object 디자인 패턴. 널 객체 패턴은 원하는 객체를 발견하지 못할 경우, 겉으로 보기에는 원래 객체처럼 보이지만, 실제로는 다르게 행동하는 객체를 반환하는 것이다.
- 빠르게 실패하기 vs 안전하게 실패하기
- 체크 예외(checked exception)만 던지세요
- Checked exceiption vs unchecked exception→ NPE, IllegalArgumentException...
- → IOException, SQLException
- 꼭 필요한 경우가 아니라면 예외를 잡지 마세요
- 메서드를 설계할 때 모든 예외를 잡아서 메서드를 안전하게 만들기보다는 상위로 문제를 전파하는 방식이 좋다. 문제가 발생한 장소에서 어떻게든 문제를 해결해서 소프트웨어를 견고하게 만드는 철학은 코드 전체를 유지보수하기 어렵고 매우 불안정하게 만든다.
- 항상 예외를 체이닝 하세요
- 예외를 체이닝하고 절대로 원래 예외를 무시하지 않는다
- 이 방식은 낮은 수준의 root cause를 소프트웨어의 더 높은 수준으로 이동시킨다.
- 문제와 관련된 문맥을 풍부하게 만들기 위해 필요하다.
public int length(File file) throws Exception { try { return content(file).length(); } catch (IOException ex) { throw new Exception("길이를 계산할 수 없다.", ex); } }
- 단 한번만 복구하세요
- 예외 후 복구는 흐름 제어를 위한 예외 사용으로 알려진 안티 패턴의 또 다른 이름이다.
- 모든 예외는 애플리케이션의 가장 높은 곳 까지 전파되어야 한다.
- 복구에 적합한 몇개의 장소(대체로 main 메서드 같이 애플리케이션의 가장 높은 레벨) 이외에 장소에서는 예외를 잡고 다시 던지거나, 또는 절대로 예외를 잡지 말아야 한다.
- 예외를 잡고, 체이닝하고 다시 던진다. 가장 최상위 수준에서 오직 한번만 복구한다
- 관점 지향 프로그래밍을 사용하세요
- AOP(aspect-oriented). 전형적인 연산을 크게 단순화 시키고 OOP의 장황함을 제거할 수 있는 기본적인 기법이다.
- 핵심 클래스로부터 덜 중요한 기술과 메커니즘을 분리해서 코드 중복을 제거할 수 있다. ex) HTTP요청을 3번 재시도하고 실패시 에러를 던지는 메서드에서 @RetryOnFailure 같은 메서드를 사용해서 "실패재시도" 코드블럭이라는 apect로 content()메서드를 둘러싼다. 일종의 adapter라고 생각하고 구현한다.
- 하나의 예외 타입만으로도 충분합니다
- 절대 복구하지 않기, 항상 체이닝하기를 접목시킨다면 예외 타입은 항상 중복기능인 이유를 이해할 수 있다. 따라서 어떠한 예외라도 담을 수 있는 예외 객체만 있으면 된다.
- 따러서 흐름 제어를 위해서는 절대로 예외를 사용하지 않는다. 예외의 타입 정보도 필요없다. 예외를 잡는 단 한가지 목적은 예외를 체이닝한 후 다시 던지기 위해서이다.
- final이나 abstract이거나
- 상속은 자식 클래스가 부모 클래스의 코드를 계승받는 하향식 프로세스이다. 자식이 부모의 유산에 접근하는 일반적인 상속과 달리, 메서드 오버라이딩은 부모가 자식의 코드에 접근하는 것을 가능하게 한다. 이 지점에서 상속이 OOP의 유지보수성을 해친다.
- 클래스와 메서드를 final, abstract 둘 중 하나로만 제한한다면 문제가 발생할 수 있는 가능성을 없앨 수 있다.
- 클래스의 종류: final, abstract, 일반 클래스
- final클래스는 사용자 관점에서 블랙박스이다. 상속으로 수정할 수 없다. 어떠한 메서드도 오버라이딩 할 수 없다.
- abstract 클래스는 glassbox이고 불완전하다. 스스로 행동할 수 없어서 도움이 필요하며 일부 요소가 누락되어 있다. 제대로 된 클래스를 생성하기 위한 원재료이다.
- 이 둘에 속하지 않는 클래스는 좋지 않다.
- RAII를 사용하세요
- resource acquisition is initialization : 리소스 획득이 초기화이다
- 가비지 컬렉션을 이용해서 객체를 제거하는 자바에는 사라진 개념이다.
- 파일, 스트림, 데이터베이스 커넥션 등 실제 리소스를 사용하는 모든 곳에서 try-with-resources기법을 사용해서 구현하는 것이 가능하다. 객체의 close() 메서드를 호출한다. 이렇게 사용하려면 객체가 closable 인터페이스를 구현해야한다. (AutoCloseable)
- 절대 NULL을 반환하지 마세요
후기: 어떤 부분은 100% 동의하지만 또 어떤 부분은 너무 과한게 아닌가? 싶을 정도로 이상적인 객체지향적 코드 방식을 제안하는 책이다.
특히 getter, setter를 사용하지 말라는 단원을 읽다보면 그럼 개발을 어떻게 하라는거야? 라는 소리가 나올 법 하다. 혼자보기에는 좀 아쉬운 책이다. 본인이 소속된 개발팀 사람들이 다같이 한 단원씩 읽어보면서 자신의 의견을 주고 받는 식으로 대화해본다면 정말 재밌을 것 같다.