Rule 08 - equals를 재정의할 때는 일반규약을 따르라

equals 메서드를 재정의 하지 않아도 되는 경우

  • 각각의 객체가 고유하다. (ex: Thread)
  • 클래스에 “논리적 동일성(logical equality)” 검사 방법이 있건 없건 상관없다. (ex: 유틸성 객체)
  • 상의 클래스에서 재정의한 equals가 하위 클래스에서 사용하기에도 적당하다. (ex: AbstractList - List)
  • 클래스가 private 또는 default로 선언되었고(외부미노출), equals 메서드를 호출할 일이 없다.
    • 가능하다면 이런 경우에는 아래와 같이 방어코드로 재정의한다.
    • public Boolean equals(Object o) { throw new AssertError(); }

반대로, 위 조건을 만족하지 못한 경우 equals의 재정의가 요구된다.

  • 일반적으로 값 클래스(Value Class)는 대체로 그 조건에 부합한다 (ex: Integer, String 등)
  • 하지만 enum이나 Singleton 기반 객체는 equals가 필요없음

equals 메서드 재정의 일반 규약

equals 메서드는 동치 관계(equivalence relation) 를 만족해야함

  • 반사성(reflexive) : x.equals(x) = true
  • 대칭성(symmetric) : x.equals(y) = true -> y.equals(x) = true
  • 추이성(transitive) : x.equals(y) = true && y.equals(z) = true -> x.equals(z) = true
  • 일관성(consistent) : x.equals(y) 을 여러 번 해도 -> all true
  • Null에 대한 비동치성(Non-nullity) : x.equals(null) = false

하지만 애석하게도 객체 생성 가능(instantiable) 클래스를 상속하여 새로운 속성을 추가하면서 equals 규약을 어기지 않을 방법은 없다. 상속을 이용할 경우 동치관계는 깨짐 그 외에도 여러가지 우회법이 있지만 다 문제가 생긴다 (ex: instanceof 대신 getClass를 이용하는 경우)

일반적으로 대칭성(symmetric)을 만족시키지 못한 문제가 생긴다. : 자식.equals(부모) = true 이나 부모.equals(자식) = false 가 나올 확률이 큼

하지만 위 문제를 해결할 수 있는 방법이 존재함. -> 상속 대신 구성(Composit)하라

// equals 규약을 위반하지 않으면서 속성 추가
public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        if (color == null)
            throw new NullPointerException();
        point = new Point(x, y);
        this.color = color;
    }

    /**
     * ColorPoint의 Point뷰 반환
     */
    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

신뢰성이 보장되지 않는 자원(unreliable resource)들을 비교하는 equals를 구현하는 것은 삼가라

  • ex) URL#equals

Null에 대한 비동치성(Non-nullity)

public Boolean equals(Object o) { if (o == null) return false; … } 굳이 위와 같이 null 여부를 비교할 필요가 없다. 아래와 같이 instanceof 만으로도 false가 반환된다. public Boolean equals(Object o) { if (!(o instance MyType)) return false; … }

훌륭한 equals메서드 구현지침

  1. == 연산자를 사용하여 equals 인자가 자기 자신인지 검사하라.
  2. instanceof 연산자를 사용하여 인자의 자료형이 정확한지 검사하라.
  3. equals의 인자를 정확한 자료형으로 변환하라. : casting
  4. “중요” 필드 각각이 인자로 주어진 객체의 해당 필드와 일치하는지 검사하라.
    • 기본자료형은 == 비교 (float, double는 제외)
    • 객체(참조)는 equals 재귀적 호출로 비교
      • 객체는 null인 경우가 많으므로, 아래 2가지 방식으로 비교 : 아래의 경우는 객체가 같은 경우가 많을 경우 추천
      • filed == null ? o.field == null : field.equals(o.field)
      • field == o.field || (field != null && field.equals(o.field))
    • floatFloat.compare, doubleDouble.compare로 비교 : NaN, -0.0 때문에 특별취급
    • 배열은 Arrays.equals 로 비교
    • 추가적으로 필드의 비교 순서도 생각하면 좋다 : 불일치할 확률이 크거나 비교 비용이 낮은 필드부터 먼저 한다.
    • “중요” 필드의 의미는 동치성과 관계가 있는 필드만 비교한다는 것이다. : 중복필드나 논리적 상태와 관계없는 필드(Lock, Thread 등 제어필드)는 제외
  5. equals 메서드 구현을 끝냈다면 대칭성, 추이성, 일관성이 만족되는지 검토하라.
    • 적절한 단위테스트(unit test)를 이용

주의사항 몇가지 더

  • equals를 구현할 때는 hashCode도 재정의하라 : 다음 장
  • 너무 머리 쓰지 마라
  • equals 메서드의 인자타입을 Object에서 다른 것으로 바꾸지마라 : override가 되지 않고 overloading

Rule 09 - equals를 재정의할 때는 반드시 hashCode도 재정의하라

hashCode 일반규약

  • hashCode를 여러 번 호출해도 값은 동일하다. 멱등성
  • equals 가 같다고 판정한 두 객체의 hashCode 값은 같아야한다. : hashCode를 재정의 하지 않을 경우 위반
  • equals 가 다르다고 판정한 두 객체의 hashCode 값은 꼭 다를 필요는 없다

만약 필요할 때(Hash함수를 이용하는 경우) hashCode를 구현하지 않으면 아래와 같이 Hash함수를 사용하는 Collection에서 정상적으로 조회가 안 될 수 있다.

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");

String s = m.get(new PhoneNumber(707, 867, 5309));
System.out.println("s = " + s);     // s = null

기본적으로 equalshashCode를 재정의하는 것이 귀찮다면 Lombok(https://projectlombok.org/features/EqualsAndHashCode.html)을 사용하는 것을 추천한다.

hashCode구현 예시

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}

추가로 성능을 개선하려고 객체의 중요 부분을 해시 코드 계산 과정에서 생략하면 안 된다는 것이다.

Rule 10 - toString은 항상 재정의하라.

  • toString을 잘만들어 놓으면 클래스를 좀 더 쾌적하게 사용할 수 있다.
  • 가능하다면 toString 메서드는 객체 내의 중요정보를 전부 담아 반환해야한다.
  • toString이 반환하는 문자열의 형식을 명시하건 그렇지 않건 간에, 어떤 의도인지 문서에 분명하게 남겨야한다.
  • toString이 반환하는 문자열에 포함되는 정보들은 전부 프로그래밍을 통해서 가져올 수 있도록(programmatic access)하라.
    • toString을 파싱하지 않고 각 속성을 조회할 수 있는 getter를 제공하라는 것이다.
    • 역으로 말하면 toString을 통해서 반환된 정보를 파싱해서 사용하는 것은 매우 위험한 행위이다. toString 문자열 형식은 언제든 변할 수 있기 때문이다.

toString를 재정의 하는 가장 큰 이유는 객체를 문자열로 모두 표현해서 한 번에 확인할 수 있다는 것이다 - Debugging에 큰 도움이 된다.

기본적으로 toString을 재정의하는 것이 귀찮다면 Lombok(https://projectlombok.org/features/ToString.html)을 사용하는 것을 추천한다.

Rule 11 - clone을 재정의할 때는 신중하라

Cloneable clone를 허용(구현된)한다는 사실을 알리려고 고안된 믹스인(mixin) 인터페이스이다. - 어떤 선택적 기능을 제공한다는 사실을 선언하는 인터페이스(여기서는 복제) 위에 설명했다시피 특이하게도 구현할 메소드는 존재하지 않는다. 그저 protected로 선언된 Objectclone 메서드가 어떻게 동작할지 정한다.

Cloneable의 경우 인터페이스를 굉장히 괴상하게 이용한 사례로, 따라하면 곤란하다.

clone(복사) 메서드 일반규약

  • x.clone() != x
  • x.clone().getClass() == x.getClass()
  • x.clone.equals(x)
  • 어떤 생성자도 호출하지 않는다.
  • super.clone()를 반드시 호출한다.

Cloneable인터페이스의 책임 : public clone 메서드를 제공한다.

@Override
// Object 대신 PhoneNumber을 써도 된다 - 아래 공변반환형 참고
public PhoneNumber clone() {
    try {
        // super.clone() 라는 라이브러가 제공하는 기능을 적극 사용한다.
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); //수행될 리 없음.
    }
}

공변반환형(convariant return type) : 재정의 메서드의 반환값 자료형은 재정의 되는 메서드의 반환값 자료형의 하위클래스가 될 수 있다.

clone의 shallow copy와 deep copy

객체의 복사 https://en.wikipedia.org/wiki/Object_copying

실질적으로 clone는 deep copy를 지원해야한다. shallow copy(단순하게 super.clone())가 될 경우 일부 상태(속성)를 공유하게 되므로 문제가 생긴다. A라는 객체를 복사해서 B라는 객체를 만들었는데 A의 특정속성 (call by reference 기반 속성)을 수정하면 B도 수정되는 이슈

즉, 복사본의 불변식(invariant)이 깨진다.

복잡한 clone를 재정의 하는 것 보다는 복사 생성자(copy constructor)나 복사 팩터리(copy factory)를 제공하는 것 이 더 낫다.

  • 복사 생성자 : public Yum(Yum yum);
  • 복사 팩터리 : public static Yum newInstance(Yum yum);

또한 불변객체의 경우 실질적으로 복제를 허용하는 것 논리적으로 오류가 생긴다 : 복사본과 원본을 논리적으로 구별할 수 없음. - 불변객체는 일반적으로 값객체들인데 어짜피 논리적으로 같아도 괜찮지 않는가? Deep Copy면 괜찮을 거 같음.

복사 생성자, 복사 팩터리의 장점

  1. clone를 재정의 하지 않아도 됨
  2. final필드(불변객체)와 충돌없음
  3. 인터페이스 활용가능 : TreeSet.clone() VS new TreeSet(Set set)

결론

  • Cloneable을 계승하는 인터페이스는 만들지 않는다.
  • 계승을 목적으로 설계하는 클래스는 Cloneble(규칙 17)을 구현하지 말아야한다.
  • 계승 목적으로 클래스를 설계할 때는 올바르게 동작하는 protected clone 메서드를 제공하지 않으면 하위 클래스에서 Cloneable을 구현할 수 없다.
  • 즉, clone를 쓰는 경우는 배열을 복사할 때 빼고 사용할 일이 거의 없을 것이다. - 너무나도 단점이 많다.

Rule 12 - Compareable 구현을 고려하라.

public interface Comparable<T> {
    /*
    `this`가 인자(t)보다 작으면 음수
    `this`가 인자(t)와 같으면 0
    `this`가 인자(t)보다 크면 양수
    */
    int compareTo(T t);
}

Comparable 인터페이스

  • 말 그대로 비교를 통한 객체간 검색, 정렬, 최대/최소 계산
  • Object 메서드에 대한 재정의가 아니지만 그래도 중요한 이야기다.
  • 사용예시
    • TreeSet, TreeMap - final int compare(Object k1, Object k2)
    • Arrays, Collections - public static <T extends Comparable<? super T>> void sort(List<T> list)

알파벳 순서나 값의 크기, 또는 시간적 선후관계처럼 명확한 자연적 순서를 따르는 값 클래스를 구현할 때는 Comparable 인터페이스를 구현한 것을 반디시 고려해 봐야 한다.

compareTo 구현 일반규약

  • 반사성
  • 대칭성 : 모든 x와 y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
    • 만약 sgn(x.compareTo(y))이 예외를 발생시킨다면 sgn(y.compareTo(x))도 예외가 발생해야한다.
  • 추이성 : (x.compareTo(y)) > 0 && y.compareTo(z) > 0) -> x.compareTo(z) > 0
  • x.compareTo(y) == 0이면 sgn(x.xompareTo(z)) == sgn(y.compareTo(z))
  • 동치성(강력한권고) : (x.compareTo(y) == 0) == (x.equals(y))
    • 만약 위 조건이 만족하지 않을 경우 반드시 javadoc에 명시해야한다.

compareTo의 문제점

  • equals 재정의와 마찬가지로 계승을 이용하면 이슈가 발생한다. 고로 구성을 사용해서 해결하는 것을 추천

Example code

public int compareTo(PhoneNumber pn) {
    // 지역번호비교
    if (areaCode < pn.areaCode)
        return -1;
    if (areaCode > pn.areaCode)
        return 1;
    // 지역번호가 같으니 국번비교
    if (prefix < pn.prefix)
        return -1;
    if (prefix > pn.prefix)
        return 1;
    // 지역번호와 국번이 같으므로 회선번호 비교
    if (lineNumber < pn.lineNumber)
        return -1;
    if (lineNumber > pn.lineNumber)
        return 1;

    return 0;   // 모든 필드가 일치
}

Rule 01 - 생성자 대신 정적 팩터리 메서드를 사용할 수 없는지 생각해보라

장점

  • 첫번째 장점은, 생성자와는 달리 정적 팩터리 메서드에는 이름(name)이 있다.
    • 이름을 기반으로 가독성 확보 가능
    • (생성자와는 다르게) 시그네쳐에 의존적이지 않고 다양하게 제공 생성메소드 제공
  • 두번째 장점은, 생성자와는 달리 호출할 때마다 새로운 객체를 생성할 필요는 없다는 것이다.
    • ex) Boolean.valueOf(boolean)
  • 세번째 장점은, 생성자와는 달리 반환값 타입의 하위 타입 객체를 반환할 수 있다는 것이다.
    • 중요한 건 구현클래스가 아니라 인터페이스만 반환하면 된다.
    • 구상체가 아니라 인터페이스에 의존하기 때문에 더 유연해진다.
  • 네번째 장점은, 형인자 자료형(parameterized type) 객체를 만들 때 편하다는 점이다.
    • 하지만 JDK 1.7 이상 부터는 생성자에서도 Parameterized type 유추가 가능해졌다
    • ex) Map<String, List<String>> map = new HashMap<>();

정적 팩터리 메서드만 있는 클래스를 만들면 생기는 단점

  • public이나 protected로 선언된 생성자가 없으므로 하위클래스를 만들 수 없다.
  • 정적 팩터리 메서드가 다른 정적 메서드와 확연히 구분되지 않는다.
    • 하지만 개선안이 있다. 정적 메서드의 명칭을 관례에 따른다
      • valueOf : 형변환
      • of : valueOf 줄임
      • getInstance : 객체 반환 (같은 객체일 수도 있음)
      • newInstance : 항상 다른 객체 반환
      • getType : getInstance와 같으나, 해당 클래스의 서브타입이 아니라 다른 타입을 반환할 경우 사용한다. 여기에서 Type는 그 다른 타입을 말한다.
      • newType : newInstance와 같으나, 해당 클래스의 서브타입이 아니라 다른 타입을 반환할 경우 사용한다. 여기에서 Type는 그 다른 타입을 말한다.
/**
 * 서비스 인터페이스
 */
public interface Service {
    // 서비스에 대한 고유한 메서드가 이 자리에 온다
}

/**
 * 서비스 제공자 인터페이스
 */
interface Provider {
    Service newservice();
}

/**
 * 서비스 등록과 접근에 사용되는 객체 생성 불가능 클래스
 */
class Services {

    // 객체 생성 방지 (규칙 4)
    private Services() { }

    // 서비스 이름과 서비스 간 대응관계 보관
    private static final Map<String, Provider> providers = new ConcurrentHashMap<>();

    public static final String DEFAULT_PROVIDER_NAME = "<dev>";

    /**
     * 제공자 등록 API
     * @param p
     */
    public static void registerDefaultProvider(Provider p) {
        registerProvider(DEFAULT_PROVIDER_NAME, p);
    }

    private static void registerProvider(String name, Provider p) {
        providers.put(name, p);
    }

    /**
     * 서비스 접근 API
     * @return
     */
    public static Service newInstance() {
        return newInstance(DEFAULT_PROVIDER_NAME);
    }

    private static Service newInstance(String name) {
        Provider p = providers.get(name);
        if (p == null) {
            throw new IllegalArgumentException(
                    "No provider registered with name : " + name);
        }
        return p.newservice();
    }
}

정적 팩터리 메서드와 public 생성자는 용도가 다르며, 그 차이와 장단점을 이해하는 것이 중요하다. 정적 팩터리 메서드가 효과적인 경우가 많으니, 정적 팩터리 메서드를 고려해 보지도 않고 무조건 public 생성자를 만드는 것은 삼가기 바란다.

Rule 02 - 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라.

생성자 인자가 많을 경우 점층적 생성자 패턴(Telescoping constructor pattern)은 잘 동작하지만 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려워지고, 무엇보다 읽기 어려운 코드가 되고 만다.

생성자 인자가 많을 경우 자바빈 패턴을 사용할 경우 단점

  • 1회의 함수 호출로 객체 생성을 끝낼 수 없으므로, 객체 일관성(consistency)이 일시적으로 깨질 수 있다. - 방어할려면 모든 setter에 유효성 체크를 걸어야한다.
  • 불변(immutable)객체를 만들 수 없다.
  • 스레드 안정성(Thread-safety)을 제공하기 위해 할 일도 더 많아진다.

결론은 빌더패턴 (Builder pattern)

AdaPython언어는 선택적 인자에 이름을 붙일 수 있도록 허용하는데, 그거과 비슷한 코드를 작성할 수 있기 때문이다.

Class.newInstance는 컴파일 시점에 예외 검사가 가능해야 한다는 규칙을 깨트린다.

  • Class.newInstance는 항상 무인자 생성자를 호출하는데, 해당 생성자가 존재하지 않기 때문에 문제가 생긴다
/**
 * p.19 규칙 02 빌더패턴 고려하라
 * 빌더패턴!!
 */
public class NutritionFacts3 {
    private final int servingSize;      // (mL)                 *Required
    private final int servings;         // (per container)      *Required
    private final int calories;         //                      Optional
    private final int fat;              // (g)                  Optional
    private final int sodium;           // (mg)                 Optional
    private final int carbohydrate;     // (g)                  Optional

    public static class Builder {

        // 필수인자
        private final int servingSize;
        private final int servings;
        // 선택인자
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val; return this;
        }

        public Builder fat(int val) {
            fat = val; return this;
        }

        public Builder carbohydrate(int val) {
            carbohydrate = val; return this;
        }

        public Builder sodium(int val) {
            sodium = val; return this;
        }

        public NutritionFacts3 build() {
            return new NutritionFacts3(this);
        }
    }

    private NutritionFacts3(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        carbohydrate = builder.carbohydrate;
        sodium = builder.sodium;
    }

    public static void main(String[] args) {
        NutritionFacts3 cocaCola = new Builder(240, 8)
                .calories(100)
                .sodium(35)
                .carbohydrate(27).build();
    }
}

빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 특히 대부분의 인자가 선택적 인자인 상황에 유용하다.

빌더를 구현하는 것이 귀찮다면 Lombok(https://projectlombok.org/features/Builder.html)을 사용하는 것을 추천한다.

Rule 03 - private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라

해당 규칙은 잘 이해가 안됨. 본인이 이해한 내용은 싱글턴 패턴을 구현 시에는 private 생성자나 enum자료형으로 설계하라 로 이해함.

클래스를 싱글턴으로 만들면 클라이언트를 테스트하기가 어려워질 수가 있다.

  • public final 필드를 이용한 구현
    • public static final Elvis INSATNCE = new Elvis();
  • 정적 팩터리 메소드(getInstance)를 이용한 구현
    • public static Elvis getInstance() { return INSTANCE; }
  • Enum 단일 항목을 이용한 구현
    • public Enum Elvis{ INSTANCE; ...}

원소가 하나뿐인 enum 자료형이야말로 싱글턴을 구현하는 가장 좋은 방법이다.

  • 직렬화 이슈 해결
  • 간단한 구현
  • reflection 방어

Rule 04 - 객체 생성을 막을 때는 private 생성자를 사용하라

  • 정적 메서드나 필드만 필요한 경우 (하지만 대부분의 경우 객체지향적이지 않으므로 필요할 때만 사용한다.)
  • 일종의 유틸리티 클래스 (ex: java.lang.Math, java.util.Arrays)
public class Utility {
    private Utility() {
        throw new AssertionError();
    }
}

Role 05 - 불필요한 객체는 만들지 말라

적절한 예시 1.

String s1 = new String("stringette");   // 메모리 낭비
String s2 = "stringette";               // 문자열풀 재사용

Boolean b1 = new Boolean("true");       // 메모리 낭비
Boolean b2 = Boolean.valueOf("true");   // 정적 팩터리 메서드를 통한 재사용

적절한 예시 2.

public class Person {
    private final Date birthDate;

    // 생성자 생략

    // 비용이 큰 객체를 재사용하기 위해 객체 속성 선언
    private static final Date BOOM_START;   // 베이비붐 시작
    private static final Date BOOM_END;     // 베이비붐 끝

    // 정적 초기화 블록(static initializer)로 개선
    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 &&
                birthDate.compareTo(BOOM_END) < 0;
    }
}

위 코드 개선사항 : Lazy loading

Autoboxing 으로 인한 primitive과 Wrapper 클래스 객체 간 성능이슈

  • 필요에 따라서 primitive와 Wrapper 클래스 간 Autoboxing 이슈가 없게 해야한다.

Wrapper 클래스 객체 대신 primitive타입을 사용하고, 생각지도 못한 자동 객체화가 발생하지 않도록 유의한다.

Pooling 기법도 객체의 생성 비용이 너무 크지 않다면 사용하지 않는 것이 낫다. 최신의 JVM은 성능이 매우 좋아서 왠만한 생성 비용의 객체는 Pooling 기법을 사용하지 않아도 빠르다. 생성비용이 높은 객체

  • DB커넥션
  • 각종 I/O 접근 (File, Socket, Stream 등)

다른관점으로 접근하면

  • 재사용이 가능하다면 새로운 객체를 만들지 말라
  • 새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 말라
  • 방어적 복사 가 요구되는 상황에서 재사용하게 되면 그냥 새로 객체를 생성하는 것보다 더 큰 비용(복잡도)가 발생하니 유의하자.

추가로 무작정 객체를 재사용할 것이 아니라, 조건에 따라서 새로운 객체를 사용할 때를 구분해야한다.

Rule 06 - 유효기간이 지난 객체 참조는 폐기하라

메모리 누수가 어디서 생기는가?

// 메모리 누수(memory leak)가 어디서 생기는가?
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;  // 만기 참조 제거
        return result;
    }

    /**
     * 적어도 하나 이상의 원소를 담을 공간을 보장한다.
     * 배열의 길이를 늘려야 할 때마다 대략 두 배씩 늘인다.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

위 elements 배열처럼, 자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야한다.

그런다고 모든 객체를 항상 null로 처리할 필요는 없다. 예를 들어 지역변수 객체라면 해당 스코프 (메소드 또는 코드블럭)이 끝날 때 해당 지역변수 객체는 GC로 처리된다. 객체 참조를 null 처리하는 것은 규범(norm - 해야만 하는 것)이라기 보단 예외적인 경우에 사용하는 조치가 되어야한다. - 예를 들면 위 Stack 처럼 예외적으로 자체 메모리 관리하는 경우

캐시(cache)도 메모리 누수가 흔희 발생하는 장소다.

  • 객체 참조를 캐시에 넣어두고 고아객체로 만드는 경우가 많다.
  • 해결 방안은 WeakHashMap와 같은 자료구조를 사용하면 된다. : WeakHashMap의 경우 값항목의 수명이 키에 대한 외부 참조에 따라 결정된다.

메모리 누수가 흔희 발견되는 또 한 곳은 Callback이다. 이해가 명확히 안 됨

  • Callback를 등록하고 사용하는 클라이언트가 명시적으로 Callback을 명시적으로 제거하지 않는 경우가 있다.
  • 해결 방안은 Callback 참조를 WeakReference로 가지는 것이다. (위 WeakHashMap을 이용하는 것과 같은 방식이다.)

Rule 07 - 종료자 사용을 피하라

종료자(finalizer)는 예측 불가능하며(GC알고리즘대로), 대체로 위험하고, 일반적으로 불필요하다.

  • C++ 의 소멸자(Destructor)와는 다르다 : Java는 GC가 메모리를 관리하고 C++은 사용자가 소멸자 등을 이용해서 직접 제어한다.

긴급한 작업(time-critical) 작업을 종료자 안에서 처리하면 안 된다.

  • 종료자는 즉시 실행되지 않는다 (GC알고리즘대로) - 종료자의 더딘 실행(tardy finalization)
  • 만약 긴급 작업이 요구되면 try-finally 절을 이용한다.

System.gcSystem.runFinalization 을 실행하는 것은 일반적으로 절대 호출하면 안된다.

그러면 자원 반환은 어떻게 하지?

명시적인 종료 메서드(termination method)를 하나 정의해서 사용한다.

  • 예를 들면 Connection#Close
  • 그리고 위와 같은 종료메소드는 try-finally 문과 함께 쓰인다.
Foo foo = new Foo();
try {
    // TODO anything
} finally {
    foo.terminate();    // 명시적 종료메소드 호출
}

종료 보호자 : 상속 받는 자식 클래스 객체가 부모 클래스 객체의 명시적 종료자를 호출하지 않을 경우 강제 호출을 위한 방어기법

/**
 * 종료 보호자 숙어 (Finalizer guardian idiom)
 */
public class Foo {
    // 이 객체는 바깥 객체(Foo)를 종료 시키는 역할만 한다.
    private final Object finalizerGuardian = new Object() {
        @Override
        protected void finalize() throws Throwable {
            // 바깥 객체를 종료시키는 코드
        }
    };
    // ...
}

가능한 종료자를 사용하지 않도록 하고, 자원 반환에 대한 안전장치 (ex: 명시적 종료 메서드)를 구현하자.

대칭성1

p.42

때로 대칭성을 찾아서 표현하면 코드의 중복을 제거할 수 있다. 코드 곳곳에서 비슷한 아이디어가 사용되었다면 대칭성에 따라 아이디어를 일관된 방식으로 표현해야 한다. 이렇게 되면 하나의 구현으로 통합할 수 있어서 중복되는 구현을 제거하기 쉬워진다.

이해됨

추상클래스

p.60

클래스 계층에서 최상위에 위치한 클래스를 인스턴스화 해서 사용할 가능성이 조금이라도 있다면 그렇게 하라. 추상화를 진행하다 보면, 쓸데없이 추상클래스를 너무 많이 만들게 되는 경우가 생긴다, 최상위 클래스를 인스턴스화 가능한 클래스로 만들면 이런 쓸데없는 추상 클래스 계층을 제거할 수 있다.

이해됨

외재 상태

p.90

때로 프로그램의 일부에서만 객체의 특정 상태를 필요로 하는 경우가 있다. 예를들어 객체가 디스크의 어느 부분에 저장되어 있느냐 하는 정보는 데이터 입력을 담당하는 부분을 제외한 프로그램의 다른 부분에는 의미가 없다. 필드를 통해 이러한 데이터를 저정하면 대칭성의 원리를 위배하게 된다. 다른 필드는 시스템 전체에 모두 유용하게 사용되기 때문이다.

어떤 객체와 관련된 특수 목적 정보는 객체가 아니라 그 객체를 필요로 하는 부분에 저장하는 편이 낫다. 앞에서 언급한 예의 경우, 데이터 입출력을 담당하는 부분에 IdentityMap이라는 맵을 만들고, 객체를 키로 디스크 상에 저장된 위치를 데이터로 사용하면 된다.

외재 상태를 사용하면 객체의 복사가 어려워진다. 외재 상태를 사용한 객체를 복사하는 것은 필드를 사용한 객체를 복사하는 것에 비해 복잡하다. 모든 외지 상태를 올바르게 복사하려면 상태를 어떻게 사용하는지 알아야한다. 또한 외재 상태를 사용하면 디버깅도 어려워진다. 일반적인 디버거는 객체의 외재 상태를 보여주지 않는다. 이런 문제 때문에 외재 상태를 사용하는 경우는 많지 않다. 하지만 적절하게 사용할 경우 외재 상태는 매우 유용하게 사용될 수 있다.

이해안됨

역할 제시형 작명

p.102~103

요컨대 나는 변수 이름을 통해 변수의 역할을 전달한다. 변수에 관한 다른 중요한 정보-생명기간, 범위, 타입-는 일반적으로 문맥(Syntax)을 통해 전달할 수 있다.

이해안됨 : 범위(Scope)는 변수의 접근제어자와 static 등으로로, 타입(Type)은 변수의 선언타입으로 알 수 있는데, 생명기간(LifeCycle)은 어떻게 전달할 수 있는 것인가? 혹시 final로 알 수 있다는 것인가?

도우미 메소드

p.138

도우미 메소드의 마지막 목적은 공용 구문(common sub-expression)을 제거하는 것이다. 조그마한 특정 연산이 필요할 경우마다 도우미 메소드를 호출하면 해당 구문의 수정은 어렵지 않다. 하지만 같은 2-3줄의 코드가 필요한 경우마다 반복된다면, 잘 선택한 이름을 통해 프로그래머의 의도를 부각시킬 수 없을 뿐 아니라 코드 수정도 어려워진다.

이해됨

호환성을 유지하는 업그레이드

p.182

한 가지 결정해야 할 사항은 어떤 종류의 호환성을 제공하는가 하는 것이다. 후방 호환성(backward compatibility) 업그레이드를 통해 프레임워크에 구형 메소드 호출과 구형 객체 전달을 지원할 것인가? 아니면 전방 호환성(forward compatibility) 업그레이드를 통해 신형 스타일의 객체를 클라이언트에 전달해도 동작하도록 할 것인가?

이해안됨

본 포스트는 도메인 주도 설계 라는 책을 참조하였습니다. 가능하면 본 책을 직접 보시고 DDD를 이해하는 것을 추천드립니다. http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12174216

도메인객체_라이프사이클

그림 1) 도메인 객체 생명주기


도메인 객체 관리의 문제

  1. 생명주기 동안의 무결성 유지하기
  2. 생명주기 관리의 복잡성으로 모델이 난해해지는 것을 방지하기

해결을 위한 3가지 패턴

1. Aggregate(집합체) : Domain 캡슐화. 접근 Root

2. Factory : Domain 객체(또는 Aggregate) 생성 캡슐화

3. Repository : 인프라스트럭쳐 캡슐화 및 객체지향적 접근 제공


Aggregate(집합체)

객체들이 밀접한 연관관계를 맺는 객체 집합 에는 불변식이 적용돼야 한다. - 일관성 있는 객체의 상태를 유지하기 위해서.

불변식 : 데이터가 변경될 때마다 유지돼야 하는 일관성 규칙

모델 내의 참조에 대한 캡슐화의 추상화가 필요하고 그것이 바로 Aggregate(데이터 변경의 단위로 다루는 연관 객체의 묶음)이다.

  • 루트(Root) 와 경계(Boundary)가 있다.

지역 및 전역 식별성과 객체 참조

그림 2) 지역 및 전역 식별성과 객체참조

참조 : http://www.moshesty.com/category/ddd/

Aggregate 구현 규칙

  • 루트 Entity는 전역 식별성을 지니며, 궁극적으로 불변식을 검사할 책임이 있다.
  • 각 루트 Entity는 전역 식별성을 지닌다. 경계 안의 Entity는 지역 식별성을 지니며, 이러한 지역 식별성은 해당 Aggregate 안에서만 유일하다.
  • Aggregate 경계 밖에서는 루트 Entity를 제외한 Aggregate 내부의 구성요소를 참조할 수 없다.
  • 데이터베이스 질의(Query)를 이용하면 Aggregate의 루트만 직접적으로 획득할 수 있다.
  • Aggregate 안의 객체는 다른 Aggregate의 루트만 참조할 수 있다.
  • 삭제 연산은 Aggregate 경계 안의 모든 요소를 한 번에 제거해야한다.
  • Aggregate 경계 안의 어떤 객체를 변경하더라도 전체 Aggregate의 불변식은 모두 지켜져야 한다.

다시 정리하면

Entity와 Value Object를 Aggregate로 모으고 각각에 대해 경계를 정의하라. 한 Entity를 골라 Aggregate의 루트로 만들고 Aggregate 경계 내부의 객체에 대해서는 루트를 거쳐 접근할 수 있게 하라. Aggregate 밖의 객체는 루트만 참조할 수 있게 하라. 내부 구성요소에 대한 일시적인 참조는 단일 연산에서만 사용할 목적에 한해 외부로 전달될 수 있다. 루트를 경유하지 않고 Aggregate의 내부를 변경할 수 없다. 이런 식으로 Aggregate의 각 요소를 배치하면 Aggregate 안의 객체와 전체로서의 Aggregate의 상태를 변경할 때 모든 불변식을 효과적으로 이행할 수 있다.

Factory

도메인 객체 생성과 Aggregate에 Root 캡슐화 + 불변식 확보

복잡한 객체 생성의 책임을 갖는다.

Factory와 Factory의 위치 선정

  • 객체 내인 경우 팩토리 메소드 패턴
  • 객체 외인 경우 추상 팩토리 패턴

생성자 만으로 충분한 경우

도메인 객체 생성이 복잡하지 않은 경우

인터페이스 설계

팩토리 내의 도메인 객체 메소드 서명(Signature)를 정하는 두가지 사항

  • 각 연산은 원자적이여야한다.
    • 복잡한 도메인 객체를 생성하는 데 필요한 것들은 모두 한 번에 Facotry로 전달해야 한다.
    • 예외가 발생할 시 실패에 대한 표준이 있어야 한다. (null or Exceptoin 등)
  • Factory는 자신에게 전달된 인자와 결합될 것이다.
    • Factory는 당연히 생상할 도메인 객체와 필요한 파라메터와 강결합은 당연하다. 그로 인해 Factory를 이용하는 객체들의 의존성은 약해진다.

불변식 로직의 위치

Facotry의 책임 중 하나는 생성된 객체(Aggregate 포함) 불변식을 충족되도록 보장

고로 Factory가 가지고 있어야한다. 또한 이로 인한 도메인 객체는 본연의 도메인 로직에 응집력을 더 가진다.

Entity Factory와 Value Object Facotry

Value Object

  • 모든 구성요소가 필요

Entity

  • 모든 세부사항이 요구되지 않는다.
  • 하지만 식별자 할당에 대해서 생각해야한다.
    • 사용자로 부터 입력 (ex:자연키)
    • 인프라스트럭쳐 제공 (ex:DB sequence)
    • 다른 객체로 부터 제공

저장된 객체의 재구성

  1. 재구성에 사용된 Entity Factory는 새로운 ID를 사용하지 않는다.
  2. 객체를 재구성하는 Factory는 불변식 위반을 다른 방식으로 처리할 것이다. ??

Repository

기존에는 DB와 객체간의 패러다임 불일치 해결을 위한 기술적인 이슈 때문에 많은 에너지를 소비하게 됬었고, 그로 인해 도메인 자체에는 집중력이 떨어지고 도메인에 관한 내용도 객체-DB 매핑 전에 들어가기 시작했었다.

그러한 이슈를 해결하기 위해서 인프라스터럭쳐에 접근하는 행위도 캡슐화 하고 DB에 의존적이지 않고 순수한 객체로 핸들링 할 수 있는 수단이 필요했다. 그것을 Repository 가 해결해준다.

  • 객체 재구성 의 책임
  • 영속화 객체 접근의 캡슐화

클라이언트는 Repository를 사용하므로써 각 객체 타입에 대해 메모리상에 해당 타입의 객체로 구성된 컬랙션이 있다고 착각을 불러 일으키는 객체를 만든다.

Aggregate 루트에 대해서만 Repository를 제공하고, 모든 객체 저장과 접근은 Repository에 위임해서 클라이언트가 모델에 집중하게 하라.

Repository의 이점

  • Repository는 영속화된 객체를 획득하고 해당 객체의 생명주기를 관리하기 위한 단순한 모델을 클라이언트에게 제시한다.
  • Repository는 영속화 기술과 다수의 데이터베이스 전략, 또는 심지어 다수의 데이터 소스로부터 애플리케이션과 도메인 설계를 분리한다.
  • Repository는 객체 접근에 관한 설계 결정을 전해준다.
  • Repository를 이용하면 테스트에서 사용할 가짜 구현을 손쉽게 대처할 수 있다.

Repository에 질의하기

명세(Specification) 패턴 이용을 추천

클라이언트 코드가 Repository 구현을 무시한다. (개발자는 그렇지 않지만)

의도하지 않는 행위가 발생하지 않도록 해야한다. 세부 구현까지는 아니더라도 원리는 파악해야한다.

  • 코스트가 높아져서 성능에 이슈가 생김
  • 예외 발생
  • 인프라스트럭쳐 특성에 따른 사이드이펙트

Repository 구현

  • 도메인 객체 타입을 추상화 한다. (OCP)
  • 클라이언트와 분리를 활용한다. (DIP)
  • 트랜젝션 제어를 클라이언트에 둔다.

프레임워크의 활용

직접 구현은 힘들다. 최근의 언어는 Repository를 구현하는 많은 수단을 제공한다.

ORM 기술이 대표적이다.

Factory와의 관계

객체 생성이나 재구성이 필요할 시 Factory에 해당 책임을 위임하는 것이 좋다.

RDBMS를 위한 객체 설계

데이터 모델과 객체 모델이 서로 갈라지게 해서는 안된다.

위 조건을 만족하기 위해서 필요하다면 일부 객체 관계의 풍부함을 희생해서 관계 모델에 밀접하게 해야한다.

반대로 객체 매핑을 단순화 하는데 도움이 된다면 DB 반정규화를 이용해서 절충한다.

가능하면 원칙을 지키는데 노력을 하고 더 나은 도메인 설계를 위한 경우에만 타협한다. 하지만 원칙없이 단순한 패턴에 의한 절충 오히려 독으로 돌아온다.

본 포스트는 도메인 주도 설계 라는 책을 참조하였습니다. 가능하면 본 책을 직접 보시고 DDD를 이해하는 것을 추천드립니다. http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12174216

Service

도메인의 개념 중 사물(도메인객체)이 아닌 활동이나 행동(연산)로 표현되는 모델링 대상

종종 이러한 연산은 여러 도메인 객체(Entity, Value Object)를 모아 그것들을 조율하는 행위

Service는 모델에서 독립적인 인터페이스로 제공되는 연산으로서 Entity나 Value Object와 달리 상태를 캡슐화 하지 않는다. Service는 흔히 사용되는 패턴이지만 Service는 도메인 계층에도 마찬가지로 적용될 수 있다.

  • 모델에서 독립적인 인터페이스로 제공되는 연산
    • 클라이언트에 무엇을 제공할 수 있느냐?
  • 상태를 캡슐화 하지 않음 : 연산 자체가 상태를 가질 필요가 없으므로 캡슐화 할 상태도 없는 것이다.

Service의 3가지 특징

  • 연산이 원래부터 Entity나 Value Object의 일부를 구성하는 것이 아니라 도메인 개념과 관련돼 있다.
  • 인터페이스가 도메인 모델의 외적 요소의 측면에서 정의된다. : Actor 즉, Use case diagram의 그것와 비슷??
  • 연산이 상태를 갖지 않는다. : 연산에 영향을 주는 상태를 갖지 않는다 로 이해하면 편하다.
    • 하지만 특정 도메인의 경우 전역정보로 인한 사이드 이펙트는 있을 수 있다

요약

도메인의 중대한 프로세스나 변환 과정이 Entity나 Value Object의 고유한 책임이 아니라면 연산을 Service로 선언되는 독립적인 인터페이스로 모델에 추가하라. 모델의 언어라는 측면에서 인터페이스를 정의하고 연산의 이름을 Ubiquitous Language의 일부가 되게끔 구성하라. Service는 상태를 갖지 않게 만들어라.

Service와 격리된 도메인 계층

응용, 도메인, 인프라스트럭처 간 격리된 서비스

구성 단위

??

Service에 접근하기

??

Module

모듈, 패키지라고도 함

  • 모듈화의 가장 큰 이유는 인지 과부하(cognitive overload) 방지
  • 모듈도 낮은 결합도와 높은 응집도를 가져야한다.

모델 > 모듈 > 개념(도메인객체)

시스템의 내력을 말해주는 Module을 골라 일련의 응집력 있는 개념들을 해당 Module에 담아라. 이렇게 하면 종종 모듈 간의 결합도가 낮아지기도 하는데, 그렇게 되지 않는다면 모델을 변경해서 얽혀 있는 개념을 풀어낼 방법을 찾아보거나, 아니면 의미 있는 방식으로 모델의 각 요소를 맺어줄, Module의 기줄이 될 법한 것 중 미처 못보고 지나친 개념을 찾아보라. 서로 독립적으로 이해하고 논리적으로 추론할 수 있다는 의미에서 낮은 결합도가 달성되도록 노력하라. 높은 수준의 도메인 개념에 따라 모델이 분리되고 그것에 대응되는 코드도 분리될 때까지 모델을 정제하라.

Ubiquitous Language를 구성하는 것으로 Module의 이름을 부여하라. Module과 Module의 이름은 도메인에 통찰력을 줄 수 있어야 한다.

기민한 Module

Moduel를 리팩터링 하는 것은 부담이 큰 작업이므로 최소화해야하고 애초에 설계에서 부터 잘해야한다.

인프라스트럭처 주도 패키지화의 함정

패키지화는 모델에 따라야하지 기술이나 프레임워크에 따라가면 안된다. 즉 Layer(또는 Tier) 기준으로 패키징 하는 것은 피해야한다.

  • 도메인모델의 응집력이 흩어진다. 즉 도메인 모델이 패키지별로 분리
  • Layer와 패키지로 도메인모델 분리에 따른 복잡성이 증가

여러 서버에 코드를 분산 하는 것이 실제로 의도했던 바가 아니라면 동일한 객체는 아니더라도 하나의 개념적 객체를 구현하는 코드는 모두 같은 Module에 둬야한다.

패키지화를 바탕으로 다른 코드로부터 도메인 계층을 분리하라. 그렇게 할 수 없다면 가능한 도메인 개발자가 자신의 모델과 설계 의사 결정을 지원하는 형태로 도메인 객체를 자유로이 패키지화 할 수 있게 하라

모델링 패러다임

객체지향!!

객체지향 패러다임이 지배적인 이유

  • 인간에게 직관적인 개념을 바탕으로 한 쉬운 접근
  • 복잡함과 단순함의 조화
  • 대중적인 개발자 커뮤니티와 높은 성숙도

지금은 객체지향 패러다임이 지배하지면, 영원하지는 않을 것이다.

객체 세계에서 객체가 아닌 것들

DDD에서는 도메인 모델을 바탕으로한 OOP 패러다임을 기반으로 하지만, 다른 요소들로 인해서 패러다임이 혼재할 수 있다.

ex 1) 인프라스트럭쳐 기술 중 관계형 패러다임을 가지는 RDB에 의존하는 경우 ex 2) 룰 패러다임

패러다임이 혼재할 때 Model-Driven Design 고수하기

확고한 Ubiquitous Languag를 바탕으로 패러다임 간 틈을 메꾼다

Model-Driven Design은 객체지향적일 필요는 없지만 모델의 구성물(객체, 규칙 ,워크플로우)은 OOP에 분명 의존한다.

객체가 아닌 요소를 객체지향 시스템에 혼합하는 4가지 법칙

  • 구현 패러다임을 도메인에 억지로 맞추지 않는다.
  • Ubiquitous Language에 의지한다.
  • UML에 심취하지 않는다.
  • 회의적이여야 한다.