1. 불변 객체로 만드는 것을 고려하라가변 클래스는 오용하기 쉽다해결책 : 객체를 생성할 때만 값을 할당하라해결책 : 불변성에 대한 디자인 패턴을 사용하라빌더 패턴쓰기 시 복사 패턴2. 객체를 깊은 수준까지 불변적으로 만드는 것을 고려하라깊은 가변성은 오용을 초래할 수 있다해결책 : 방어적으로 복사하라해결책: 불변적 자료구조를 사용하라3. 지나치게 일반적인 데이터 유형을 피하라지나치게 일반적인 유형은 오용될 수 있다패러다임은 퍼지기 쉽다페어 유형은 오용하기 쉽다해결책 : 전용 유형 사용4. 시간 처리정수로 시간을 나타내는 것은 문제가 될 수 있다한 순간의 시간인가, 아니면 시간의 양인가일치하지 않는 단위시간대 처리 오류해결책 : 적절한 자료구조를 사용하라양으로서의 시간과 순간으로서의 시간 구분더 이상 단위에 대한 혼동이 없다시간대 처리 개선5. 데이터에 대해 진실의 원천을 하나만 가져야 한다또 다른 진실의 원천은 유효하지 않은 상태를 초래할 수 있다해결책 : 기본 데이터를 유일한 진실의 원천으로 사용하라데이터 계산에 비용이 많이 드는 경우6. 논리에 대한 진실의 원천을 하나만 가져야 한다논리에 대한 진실의 원천이 여러 개 있으면 버그를 유발할 수 있다해결책 : 진실의 원천은 단 하나만 있어야 한다
1. 불변 객체로 만드는 것을 고려하라
가변 객체의 문제점
- 3장에서는 설정 함수를 갖는 가변 클래스에서 어떻게 잘못된 설정이 쉽게 이루어지고 이로 인해 잘못된 상태가 되는지 살펴봄
- 6장에서는 입력 매개변수를 변경하는 함수가 어떻게 예상을 벗어나는 동작을 초래하는지 살펴봄
- 가변 객체는 추론하기 어렵다 : 불변 객체라면 객체를 여기저기 전달하더라도 그 객체가 변경되거나 추가되지 않았다는 것을 확신할 수 있다
- 가변 객체는 다중 스레드에서 문제가 발생할 수 있다 : 객체가 가변적이면 해당 객체를 사용하는 다중 스레드 코드가 특히 취약할 수 있다. 한 스레드가 객체를 읽는 동안 다른 스레드가 그 객체를 수정하는 경우 오류발생
가변 클래스는 오용하기 쉽다
- TextOptions 인스턴스를 자유롭게 전달하더라도 변경되지 않는 것이 훨씬 더 좋을 것임
해결책 : 객체를 생성할 때만 값을 할당하라
- TextOptions를 만들 때 모든 필드값이 반드시 필요한 것이 아니라면 빌더 패턴이나 쓰기 시 복사 패턴을 사용하는 것이 좋음.
- named argument 나, Optional parameter 기능을 사용하는 것도 좋은 접근
해결책 : 불변성에 대한 디자인 패턴을 사용하라
클래스에서 세터 함수를 제거하고 멤버 변수를 파이널로 표시하면 클래스가 불변적이 되고 버그를 방지할 수 있다.
그러나 모든 필드값이 필요하지 않을 때에는 그렇게 만들어진 클래스가 별로 쓸모 없을 수 있음. 일부 값이 반드시 필요하지 않거나 불변적인 클래스의 가변적 버전을 만들어야 하는 경우, 유용한 두 가지 패턴이 있다
빌더 패턴
- 필수 값은 생성자를 통해, 필수적이지 않은 값은 세터 함수를 통해 제공
- 생성 후 클래스의 인스턴스 복사본을 약간 수정해야 하는 경우 역시 빌더 패턴을 사용할 수는 있는데 이 때는 클래스에서 미리 값이 채워진 빌더를 만드는 함수를 제공해야 한다. 이러한 작업에는
쓰기 시 복사 패턴
이 더 적절하다.
쓰기 시 복사 패턴
- 위의 용례를 지원하며 동시에 TextOptions를 변경할 수 없도록 하는 방법은 copy-on-write 패턴임
- 위와 같이 작성하면 값을 변경할때 클래스의 새 인스턴스가 생성되고, 새로운 인스턴스에는 원하는 변경사항이 반영되지만 기존 인스턴스는 수정되지 않음
클래스를 변경할 수 없게 하는 것은 클래스가 오용될 가능성을 최소화하는 좋은 방법이다.
이것은 세터함수를 제거하고 인스턴스를 생성할 때에만 값을 제공하면 간단하게 할 수 있다
다른 상황에서는 그에 맞는 적절한 설계 패턴을 사용해야 할 수도 있다.
2. 객체를 깊은 수준까지 불변적으로 만드는 것을 고려하라
- 클래스가 실수로 가변적으로 될 수 있는 일반적인 경우는 깊은 가변성(deep mutability) 때문
깊은 가변성은 오용을 초래할 수 있다
- 인스턴스에 넘겨주고 난 후 해당 참조의 값을 삭제. 인스턴스 내부도 동일한 리스트를 갖게됨
- getter를 통해 리스트를 갖고와서 수정해버림
해결책 : 방어적으로 복사하라
- 클래스가 생성될 때, 그리고 게터함수를 통해 객체가 반환될 때 객체의 복사본을 만들라
- 방어적 복사의 단점
- 복사하는 데 비용이 많이 들 수 있다
- 클래스 내부에서 발생하는 변경을 막지는 못한다.
해결책: 불변적 자료구조를 사용하라
- 방어적 복사본을 만들 필요 없이 객체를 전달할 수 있다
- Java : Guava 라이브러리의 ImmutableList
- 불변적인 자료구조를 사용하는 것은 클래스가 깊은 불변성을 갖도록 보장하기 위한 좋은 방법 중 하나임
3. 지나치게 일반적인 데이터 유형을 피하라
지나치게 일반적인 유형은 오용될 수 있다
- 특정 정보를 표현하려면 종종 둘 이상의 값이 필요할 수 있다. 예를 들어 2D 지도의 위치는 위도와 경도에 대한 두 가지 값이 모두 필요함
- 리스트는 너무 일반적인 데이터유형. 이런 식으로 리스트를 사용하면 코드를 오용하기 쉬움
단점
- 위 유형(List<List<Double>>) 자체로는 아무것도 설명해주지 않는다. 개발자가 해당 변수를 사용하는 메서드의 주석을 읽어보지 않으면 이 리스트가 무엇인지, 어떻게 이해해야 하는지 알지 못함
- 개발자가 리스트에서 어떤 항목이 위도고 어떤항목이 경도인지 알 수 없다. 혼동하기 쉬움
- 형식 안정성이 거의 없다. 컴파일러가 목록 내에 몇 개의 요소가 있는지 보장할 수 없다.
요약 하자면 코드 계약의 세부 조항에 대한 자세한 지식없이 해당 변수를 사용하는 함수를 올바르게 호출하는 것은 거의 불가능하다.
패러다임은 퍼지기 쉽다
위도와 경도를 나타내기 위해 List<Double>을 사용했다면 다른 곳에서 그렇게 또 사용할 가능성이 매우 높아진다. (임시 변통으로 작성된 코드는 다른 코드 전반에 퍼지는 경향이 있다)
페어 유형은 오용하기 쉽다
- 리스트 대신 Pair<Double> 을 사용하면 정확히 두 개의 값을 포함해야 하므로 호출하는 쪽에서 실수로 너무 많거나 적은 값을 제공하는 것은 방지할 수 있다.
- 그러나 다른 문제는 여전히 해결되지 않음
해결책 : 전용 유형 사용
- 일반적이고 바로 가져다 쓸 수 있는 데이터 유형을 사용하는 것이 때로는 빠르고 쉬운 방법처럼 보일 수 있지만 무언가 구체적인 것을 나타낼 필요가 있을 때, 적은 노력을 들여 전용 유형을 정의하는 것이 더 나을 때가 많다.
4. 시간 처리
시간은 단순한 것처럼 보일지 모르지만, 실제로 시간을 나타내는 것은 다음과 같은 점에서 상당히 까다롭다
- 어떤 때는 1969년 7월 21일 02:57 UTC 같이 절대적인 시간을 지칭하지만, 또 다른 때는 5분 내와 같은 상대적인 시간으로 표현
- ‘오븐에서 30분 굽기’ 와 같은 시간의 양을 언급하는 경우도 있다. 시간은 분, 초, 밀리초 등 다양한 단위 중 하나로 표시할 수 있다
- 표준 시간대, 일광 절약 시간, 윤년, 심지어 윤초와 같은 개념도 있어서 상황이 훨씬 더 복잡하다
정수로 시간을 나타내는 것은 문제가 될 수 있다
- 정수는 어느 한 순간을 의미하는 시각과 시간의 양 두 가지를 모두 나타낼 수 있다
한 순간의 시간인가, 아니면 시간의 양인가
- 매개변수 deadline이 시간의 절대 순간인지, 시간의 양을 나타내는지 알 수 없다
일치하지 않는 단위
정수 유형은 값이 어떤 단위에 있는지 나타내는 데 전혀 도움이 되지 않는다. 함수 이름, 매개변수 이름, 주석문을 사용하여 단위를 나타낼 수 있지만, 여전히 코드를 오용하기가 상대적으로 쉽다
시간대 처리 오류
- 사용자가 날짜(생일 등)를 입력하고 이를 로컬 표준시 내의 날짜 및 시간으로 해석하면 다른 표준시 사용자가 정보에 액세스할 때 다른 날짜가 표시될 수 있다
- 서버가 서로 다른 위치에서 실행되고 시스템을 다른 표준 시간대로 설정한 경우 서버단의 논리만으로도 다른날짜가 표시되는 경우가 발생할 수 있다.
순간으로서의 시간, 양으로서의 시간, 날짜와 같은 시간에 기초한 개념은 최상의 경우라 할지라도 사용하기에 까다롭다.
하물며 정수와 같은 매우 일반적인 유형을 사용해 그러한 것들을 표현하려고 한다면 자신뿐 아니라 다른 개발자까지도 어렵게 만드는 것이다.
해결책 : 적절한 자료구조를 사용하라
- Java : java.time 패키지 클래스
- C# : Noda 시간 라이브러리
- C++ : 크로노 라이브러리
양으로서의 시간과 순간으로서의 시간 구분
java.time 패키지의 Instant와 Duration 클래스 이용하기
더 이상 단위에 대한 혼동이 없다
- Instant나 Duration 같은 유형이 제공하는 또 다른 이점은 단위가 유형 내에 캡슐화 되어 있다는 점이다.
- 따라서 어떤 단위가 사용되어야 하는지 설명하기 위한 계약의 세부조항이 필요하지 않고, 실수로 잘못된 단위를 제공하는 것이 불가능
시간대 처리 개선
생일을 나타내는 예에서 생일이 실제로 어느 시간대인지는 신경쓰지 않는다.
하지만 생일을 타임스탬프를 사용해서 정확히 순간과 연결해서 표현하고 싶다면, 시간대에 대해 신중하게 생각할 수 밖에 없다.
다행히도 시간을 다루는 라이브러리는 종종 이렇게 정확한 순간과 연결하지 않고도 날짜(및 시간)를 나타낼 수 있는 방법을 제공 → LocalDateTime
[참고]
java.time 패키지 핵심 클래스
LocalDate : 날짜
LocalTime : 시간
LocalDateTime : 날짜와 시간
ZonedDateTime : 날짜와 시간, 시간대
Period : 날짜의 차이
Duration : 시간의 차이
5. 데이터에 대해 진실의 원천을 하나만 가져야 한다
- 기본 데이터 : 코드에 제공해야 할 데이터. 코드에 이 데이터를 알려주지 않고는 코드가 처리할 방법이 없다.
- 파생 데이터 : 주어진 기본 데이터에 기반해 코드가 계산할 수 있는 데이터
또 다른 진실의 원천은 유효하지 않은 상태를 초래할 수 있다
- balance 는 credit - debit 으로 계산할 수 있기에 , 해당 정보는 중복 저장된 값임
해결책 : 기본 데이터를 유일한 진실의 원천으로 사용하라
데이터 계산에 비용이 많이 드는 경우
파생된 값을 계산하는데 비용이 많이 들 때는, 그 값을 지연 계산한 후에 결과를 캐싱하는 것이 좋다.
무언가를 지연 계산한다는 것은 그 값이 정말로 필요할 때 까지 계산을 미룬다는 것을 의미함
- 클래스가 불변적이 아니라면 상황은 훨씬 복잡해짐. 클래스가 변경될 때마다 cachedDebit 과 cachedCredit을 null로 재설정해야 함. 이것은 매우 번거롭고 오류를 일으키기 쉬우므로, 이 경우 또한 객체를 불변적으로 만들어야 한다는 것을 강력하게 지지하는 또 다른 일례임
6. 논리에 대한 진실의 원천을 하나만 가져야 한다
논리에 대한 진실의 원천이 여러 개 있으면 버그를 유발할 수 있다
정수값을 기록한 후에 파일로 저장하는 코드 샘플의 예시를 보자
- 위 시나리오에서 값이 파일에 저장되는 형식은 논리의 중요한 부분이지만, 이 형식이 무엇인지에 대해서는 진실의 원천이 두개 존재함. 이 형식을 지정하는 논리가 DataLogger 및 DataLoader 클래스에 독립적으로 포함되어 있음
- 클래스가 모두 동일한 논리를 포함하면 모든것이 잘 작동하지만, 한 클래스가 수정되고 다른 클래스가 수정되지 않으면 문제가 발생
해결책 : 진실의 원천은 단 하나만 있어야 한다
중요한 논리에 대해 진실의 원천이 하나만 존재하도록 하면 코드가 훨씬 더 견고해진다.