반응형

ITEM 18 "상속보다는 컴포지션을 사용하라"

 

ITEM 17 과 마찬가지로 OCP(Open Close Priciple) 원칙에 대한 내용이다.

 

 

이 ITEM 을 확인하기 전에 위 우아한 테크코스 - OCP 와 전략패턴 영상을 보는 것을 추천한다.

OCP 원칙을 설명하면서 상속은 is-a 관계, 합성은 has-a 관계일 때 사용하여야 한 된다는 점과 템플릿 메서드 패턴과 전략 패턴의 확실한 차이도 확실히 배워갈 수 있다.

 

우아한 테크코스 영상은 초보 프로그래머가 알아야 되는 내용들을 잘 설명해준다.

 

이번 장에서는 상속과 비교하면서 합성 연관관계를 권장하고 있다.

 

상속을 사용하면 상위 클래스와 하위 클래스 개념이 생긴다.

상위 클래스가 버전이 릴리즈 될 때마다 내부 구현이 달라지면 그 여파로 하위 클래스가 오동작할 수 있다.

그래서 상위 클래스가 충분히 확장을 고려하지 않은 상태로 구현되면 캡슐화가 깨질 수 있다.

 

상속이 잘 되려면...

1 . 상위 클래스와 하위 클래스가 한 프로그래머가 통제하여야 상위 및 하위 클래스가 변경될 시 그 side-effect 를 잘 알고 같이 수정할 수 있다.

2 . 확장할 목적으로 설계되어 있어야 하고 문서화도 잘 되어 있어야 한다.

3 . 상위 클래스와 하위 클래스가 완벽히 IS-A 관계에 있어야 한다.

 

public class InstrumentedHashSet<E> extends HashSet<E> {
	private int addCount = 0;
    
    public InstrumentedHashSet() {}
    
    public InstrumentedHashSet(int initCap, float loadFactor) {
    	super(initCap, loadFactor);
    }

	@Override public boolean add(E e) {
    	addCount++;
        return super.add(e);
    }
    
    @Override public boolean addAll(Collection<? extends E> c) {
    	addCount += c.size();
        return super.addAll(c);
    }
    
    public int getAddCount() {
    	return addCount;
    }
}

 

위와 같이 HashSet 이 처음 생성된 이후로 아이템이 몇 개가 더해졌는지 알기 위해 상속을 사용해서 addAll 과 add 메서드를 Override 했다고 생각해보자.

 

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));

 

재정의한 addAll 메서드를 호출하면 3을 반환하는 것이 아니라 6을 반환한다.

원인은 addAll 메서드는 add 메서드를 사용해서 구현하기 때문이다. add 함수에도 addCount 변수를 1증가시키고 있으니 6이 반환되는 것이다.

이런 내부 구현 방식은 HashSet 문서에 나와 있지 않다. HashSet 클래스를 만든 프로그래머가 이런 사실을 공유하지 않는다면 잘못된 방식으로 구현할 수 있다는 것을 보여준다. 심지어 HashSet 클래스의 addAll 메서드가 다음 릴리즈 때에도 구조가 유지될 것이라는 보장이 없다.

 

다시 메서드를 재정의한다면 되지 않을까?...

상위 클래스의 private 변수를 써야하는 상황이라면 아예 불가능하기도 하고, 상위 클래스의 메서드를 다시 재정의 해야 될 수도 있다. 성능, 상속을 한 이유가 사라짐, 오류 발생할 가능성 등 다양한 문제점이 생길 수 있다.

또 메서드를 재정의했는데 상위 클래스에서 추가한 메서드의 이름과 반환 타입이 같다면 컴파일조차 되지 않는다.

 

 

합성을 사용하자

새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조한다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 점에서 이러한 설계를 합성(composition)이라고 한다.

그리고 새로운 클래스의 메서드에서 기존 클래스에 대응되는 메서드를 호출하여 그 결과를 받는다.

 

public class InstrumentedSet<E> extends ForwardingSet<E> {
	private int addCount = 0;
    
    public InstrumentedSet(Set<E> s) {
    	super(s);
    }

	@Override public boolean add(E e) {
    	addCount++;
        return super.add(e);
    }
    
    @Override public boolean addAll(Collection<? extends E> c) {
    	addCount += c.size();
        return super.addAll(c);
    }
    
    public int getAddCount() {
    	return addCount;
    }
}

public class ForwardingSet<E> implements Set<E> {
	private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    
    ... (생략) ...
    
    public boolean add(E e) { return s.add(e); }
    public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
    
}

 

위와 같이 Set 을 클래스 내 변수로 생성해서 Set 의 메서드를 호출하는 전달 메서드를 정의하면 상위 클래스의 메서드 변경에도 유연하게 대처할 수 있다.

 

"상속은 강력하지만 캡슐화를 해칠 수 있다."

반응형
반응형

ITEM 17 "변경 가능성을 최소화하라"

 

제목을 보자마자 들었던 생각은 OCP(Open Close Priciple) 원칙이다.

"확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다"라는 뜻인데 이 원칙을 잘 지켜야 구조가 좋아지고 코드 품질이 훌륭해진다. 협업을 위해 반드시 지켜야 되는 개념이기도 하다.

 

이번 장에서는 불변 클래스로 만드는 것을 권장하고 있다.

 

불변 클래스

= 객체가 파괴되는 순간까지 인스턴스의 내부 값을 수정할 수 없는 클래스

 

불변 클래스 특징

이러한 불변 클래스가 되려면 아래와 같은 특징을 가져야 한다.

 

1 . 객체의 상태를 변경하는 메서드를 제공하지 않는다.

2 . 클래스를 확장할 수 없도록 한다. 하위 클래스가 상속하여 객체의 상태를 바꿀 수 있으므로 상속을 막기 위함이다.

3 . 모든 필드를 final 로 선언한다.

4 . 모든 필드를 private 로 선언한다. 필드가 참조하는 가변 객체를 다른 쪽에서 수정한다면 side effect 가 발생한다.

5 . 자신 외에는 내부의 가변 컴보넌트에 접근할 수 없도록 한다.

TIP . 메서드들이 인스턴스 자신을 수정하지 않고 새로운 인스턴스를 반환하게 작성하면 안전하게 사용될 수 있다.

(이런 패턴을 함수형 프로그래밍이라고 한다. 메서드가 호출되어도 객체의 상태를 변경하지 않는 것을 의미한다.)

 

불변 객체는 가변 객체와 다르게 근본적으로 Thread-Safe 하며 따로 동기화할 필요가 없다.

또한 만약 가변 객체였다면 임의의 복잡한 상태에 놓일 수 있다. 끔찍하게 도미노처럼 side effect 가 연쇄적으로 발생할 수도 있다. 그래서 상태를 변경하는 메서드들의 상태 전이들을 정밀하게 문서화하지 않는다면 믿고 쓰기가 어렵다.

불변 객체는 실패 원자성을 제공한다. 불변 객체를 사용하는 임의의 메서드에서 예외가 발생한 후에도 불변 객체는 내부 상태를 바꾸지 않으니 실패하더라도 원자성을 제공한다.

 

안심하고 공유할 수 있기에 생성된 인스턴스를 최대한 재활용하는 것을 권장하고 있다. 아이템 1에서 소개한 정적 팩토리 메서드를 제공하는 것도 하나의 방법이다.

 

불변 클래스 단점

장점이 많아 보이는 불변 객체도 단점은 존재한다. 값이 다르다면 반드시 독립된 객체로 만들어야 한다.

불변 객체는 상수와 유사하기 때문에 값이 조금이라도 틀려진다면 새롭게 객체를 하나 생성해야 한다.

 

심지어 객체를 완성하는데 무거운 작업들이 수행되거나 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 이슈도 생기게 된다. 이 문제점을 해결하기 위해 2 가지가 있다.

 

1 . 다단계 연산을 제공하여 에측하여 기본 기능으로 제공한다.

2 . 다단계 연산이 예측이 되지 않는다면 가변 동반 클래스를 public 으로 제공한다.

 

불변 클래스를 만드는 방법

자신의 클래스를 상속하지 못하게 final 클래스로 선언하거나,

모든 생성자를 private 혹은 package-private 로 만들고 public 정적 팩토리를 제공하자.

 

성능을 위해서 불변 클래스 규칙을 완화

정책을 "어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다"로 변경한다.

외부 상태 값이 아니라 내부 상태 값만 변경해야 한다.

반응형
반응형

ITEM 2 "생성자에 매개변수가 많다면 빌더를 고려하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 2편 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 생성자에 매개변수가 많다면 빌더 사용을 권장하고 있다.

 

public class Pizza {
    private final String name;
    private final String size;
    private final String[] toppings;

    // 이름만 가지는 생성자
    public Pizza(String name) {
        this.name = name;
    }

    // 이름과 크기를 가지는 생성자
    public Pizza(String name, String size) {
        this.name = name;
        this.size = size;
    }

    // 모든 필드를 가지고 있는 생성자
    public Pizza(String name, String size, String[] toppings) {
        this.name = name;
        this.size = size;
        this.topings = toppings;
    }
}

 

여기 피자에 대한 간략한 정보를 가지는 클래스가 있다.

피자라는 객체를 만드려고 할 때, 이름 매개변수만 필요한 경우가 있을 수 있고, 이름과 크기를 모두 가지는 경우, 필드 모두가 필요한 경우가 있을 수 있다. 그 때마다 보통 위 코드와 같이 점층적으로 생성자를 만들곤 한다.

 

그런데 이런 점층적 생성자 패턴은 아래와 같은 단점을 가지고 있다.

1 . 매개변수 개수가 많아지면 클라이언트 코드 부분에서 작성하다가 실수할 수도 있고 읽기도 어렵다.

Pizza("치즈", "L", "오이, 불고기"); 이렇게 작성했을 때 각각이 가지고 있는 필드가 어떤 의미를 나타내는지 정확히 알 수 없다.

2 . 사용자가 설정하길 원하지 않는 매개변수도 강제로 포함해서 값을 지정해주어야 한다.

예를 들어 사이즈는 상관없는 피자를 만들고 싶은데 강제로 사이즈 값을 넣어주어야 한다. 선택적인 매개변수가 중간에 위치해 있는 경우, 필수 매개변수가 되어버린다.

 

그렇다면 어떻게 해야 필수 매개 변수와 선택적인 매개 변수를 적재적소에 넣어 셋팅할 수 있을까?

 

대안 1 자바 빈즈 패턴

매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 호출해서 원하는 매개변수의 값을 설정하는 방식이다.

 

Pizza myPizza = new Pizza();
myPizza.setName("컴비네이션");
myPizza.setToppings("치즈, 올리브");

 

이렇게 지정하면 중간에 원하지 않은 매개변수는 피해서 셋팅할 수 있고 읽기 쉬운 것처럼 보인다.

하지만, 객체 하나를 만드려고 메서드를 여러 개 호출해야 되고 객체가 완성이 되기 전까지는 일관성이 무너진다. 중간에 다른 코드에서 setter 함수가 불릴 수 있으며 불변 객체로 만들 수 없다. 또 이런 코드는 Thread-Safe 하지 않다.

이런 문제점을 해결하고자 자바스크립트 같은 언어에서는 freeze 라는 메서드를 지원하고 있지만, 모든 언어 체계에서 지원하는 것은 아니다. 설령 이 방법을 쓴다고 하더라도 프로그래머가 이 객체에 freeze 하는 구간이 어디 있는지 소스코드를 보며 찾아야 한다는 단점이 있다.

 

대안 2 빌더 패턴

필수 매개 변수만으로 생성자를 호출해 빌더 객체를 얻고 그 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수들을 설정하는 방법이다. 마지막에는 build() 라는 메서드를 호출해 하나의 체인 형태로 구성한다.

 

Pizza myPizza = new Pizza.Builder("콤비네이션")
	.size("L")
    .toppings("불고기, 치즈, 감자")
    .build();

 

빌더의 생성자나 메서드에서 여러 매개 변수들을 혼합해서 유효성 검사도 할 수 있다는 장점이 있으니 잘못된 매개변수를 검증하는 코드도 추가하자. 보통 매개변수를 객체로 복사해온 다음 확인하고, 검증에 실패하면 IllegalArgumentException 에러 메세지를 던져 어떤 매개변수가 잘못 되었는지 확인한다.

 

이 빌더패턴을 활용하면 클래스 계층 구조를 잘 활용할 수 있다. 아래와 같이 추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서 추상 클래스를 상속받으면, 각 하위 클래스에서 추상 빌더를 상속받아 만들 수 있다.

 

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, SAUSAGE
    }

    final Set<Topping> toppings;

    abstract static class Builder<T extends  Builder<T>> { // `재귀적인 타입 매개변수`
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }

}

 

추상 빌더에서 재귀적인 타입 매개변수를 사용하고 self 라는 메서드를 통해 자기 자신을 호출하는 부분이 흥미롭다.

하위 클래스에서 build 메서드의 리턴 타입으로 자기 자신을 리턴하는 Covariant 리턴 타이핑을 사용하면 클라이언트 코드에서 타입 캐스팅을 하지 않아도 된다.

 

public class NyPizza extends Pizza {

    public enum Size {
        SMALL, MEDIUM, LARGE
    }

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }


        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

 

이렇게 설계해두면 가변 인자 매개변수를 여러 개 사용할 수 있다는 장점도 있고, addTopping 메서드를 여러 번 호출해서 전달받은 매개변수들을 하나의 필드에 모아두는 것도 가능하다. 매번 생성하는 객체를 조금씩 다르게 변화를 줄 수도 있다.

 

이 책에서는 빌더 패턴은 생성자 이전에 빌더 객체를 만들어야 하므로 성능 이슈를 가져올 수도 있다고 하는데 그렇게 크지 않아 보인다. 점차 요구사항이 늘어나면 매개변수가 많아질 가능성이 많으니 빌더 패턴을 잘 학습하고 알아두었다가 앞으로 늘어날 가능성이 있는 클래스 생성자에 적극적으로 활용해보자.

 

"생성자와 팩토리 메서드에 매개 변수가 많을 경우, 빌더 패턴을 고려하자"

반응형
반응형

ITEM 1 "생성자 대신 정적 팩토리 메서드를 고려하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 1편 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 생성자 대신 정적 팩토리 메서드를 사용하는 것을 권장하고 있다.

 

public class Point {
	double x, y;
    
    // 생성자
    public Point(double x, double y) {
    	this.x = x;
        this.y = y;
    }
    
    // 정적 팩토리 메서드
    public static Point asPolar(double rho, double phi) {
    	double x = rho * Math.cos(phi);
        double y = rho * Math.sin(phi);
        return new Point(x, y);
    }
    
    // 이렇게 생성할 수 없음. 위 생성자와 타입과 인자의 갯수가 같으므로
    public static Point(double rho, double phi) {
    	this.x = rho * Math.cos(phi);
        this.y = rho * Math.sin(phi);
    }
}

 

여기 위와 같이 직교좌표나 극좌표에서 하나의 점을 표시하는 클래스가 있다.

클래스 이름과 같은 함수를 생성자라고 하고, "public static" 키워드가 붙고 클래스 객체를 생성하는 함수를 정적 팩터리 메서드라고 한다. 생김새만 다를 뿐 생성자와 같은 역할을 하는데 정적 팩터리 메서드를 권장하고 있다.

 

정적 팩터리 메서드는 아래와 같은 장단점을 가지고 있다.

 

장점 1 이름을 가질 수 있다.

생성자는 클래스 이름과 동일하게 함수 이름을 작성해야 한다는 규칙이 있다. 그래서 매개변수와 생성자의 이름만으로 반환될 객체의 특성을 제대로 설명하지 못 한다는 특징이 있다. 하지만 정적 팩토리 메서드는 위 예제와 같이 극좌표계에서 쓰일 객체를 "asPolar" 라는 이름으로 잘 설명해주고 있다.

 

또 타입의 순서와 갯수가 같은 동일한 생성자를 만들 수 없다. 자바에서는 오버로딩을 지원하고 있지만 어디까지나 매개변수 타입의 순서와 갯수가 같아야 한다. 위 코드에서 극좌표계 생성자를 만들다가 실패한 코드가 그 예제이다. 

 

장점 2 새로운 객체를 생성할 필요는 없다.

생성자는 이름 그대로 반드시 하나의 객체를 생성해야 한다. 그렇지만 정적 팩토리 메서드는 프로그래머가 임의로 작성한 메서드이기 때문에 제약이 없다. 불변한 객체를 미리 만들어 놓고 그 객체를 재사용하는 식으로 코드를 작성할 수 있다. 객체가 불변함이 보장된다면 굳이 하나의 객체를 생성해야 하는 생성자는 메모리 비용이 많이 드는 나쁜 선택이다.

 

public class Point {
	
    private static Point pointInstance;
	private static final Point SINGLE_POINT_OBJECT = new Point();

    public static Point getPoint() {
        return SINGLE_POINT_OBJECT;
    }

    public static Point getPointWithSingleTon() {
        if (pointInstance == null) {
        	pointInstance = new Point();
        }
        return pointInstance;
    }
}

 

이렇게 객체 생성을 컨트롤 할 수 있어 인스턴스 통제 클래스라고 부르며 언제, 어디까지 인스턴스를 살게 할 수 있을지 결정할 수 있다. 싱글톤 패턴을 만들 때에도 유용하고 불변 클래스를 만들 때에도 사용된다.

 

장점 3 반환 타입의 하위 타입 객체를 반환할 수 있다.

말 그대로 상속받은 자식 클래스의 객체를 반환할 수 있다는 뜻이다. 자식 클래스의 객체를 반환할 수 있게 해준다면 그 자식 클래스를 공개하지 않고도 객체를 사용할 수 있다는 것이다. 명시한 인터페이스만 다른 시스템에 노출시키고 구현체는 숨김으로써 객체지향의 OCP 원칙을 잘 지키며 설계할 수 있다. 이를 인터페이스 기반 프레임워크라고 부른다.

 

다른 시스템에 노출 시키려면 모두 public 으로 제한자를 풀어야 되는데 구현체는 private 로 숨기고 API 만 public 으로 만들고 그 인터페이스 정적 팩토리 메서드에서 구현체들을 반환만 해주는 것이다.

 

자바 8 이전에는 인터페이스에서 정적 메서드를 만들 수가 없었다고 한다. 그래서 인스턴스화가 불가한 동반 클래스로 만들어 그 안에 정의했다고 한다.

 

장점 4 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

장점 3의 연장선 상의 개념이다. 조건에 따라 하위 타입의 객체를 반환할 수 있게 설계가 가능하다.

이 책에서는 EnumSet 클래스를 예로 들며 이 클래스는 public 생성자가 없고 원소의 갯수에 따라 객체를 반환하는 메서드만 있다고 한다. 원소가 64개 이하면 RegularEnumSet 객체를, 65개 이상이라면 JumboEnumSet 객체를 반환한다.

Intellij 에서 직접 확인해봤다.

 

 

맨 마지막 EnumSet 생성자 보면 접근 제한자가 없다. 같은 패키지 내에서만 호출될 수 있다.

 

 

그리고, noneOf 이라는 정적 팩토리 메서드에서 원소의 개수에 따라 하위 클래스의 객체를 반환하는 것을 볼 수 있다.

이 클래스를 사용하는 프로그래머 입장에서는 저런 세세한 구현을 알지 못하더라도 공통적인 기능의 EnumSet 을 사용할 수 있게 된다. 장점 3에서 이야기 하였듯이 실제 구현은 공개하지 않았는데도 명세서만 보고 개발이 가능하다.

 

장점 5 정적 팩토리 메서드를 작성할 때 반환할 객체의 클래스가 존재하지 않아도 된다.

책에서는 서비스 프로바이더 프레임워크를 예시로 들면서 설명하고 있는데 그 개념을 모르는 사람은 어렵기도 하고 이해가 가지 않을 수 있다. 쉽게 설명하면 "구현""정의" 그리고 "사용" 관점에서 나누어 설계되어 있는 프레임워크라고 보면 된다. 정의부는 이 시스템에서 제공하고자 하는 명세서를 작성하는 담당을 하고, 구현부는 실제 그 기능을 만드는 구현체이다. 사용부는 사용자가 이 시스템에 접근해서 서비스를 요청하는 담당을 한다. 여기서 사용부는 어떻게 구현되었는지 알 필요가 없다. 무엇을 요청할지만 알면 된다. 그래서 사용부의 API 는 정의 인터페이스만 반환할 뿐이다.

정의 인터페이스와 연결만 되어 있으니 반환할 객체의 클래스가 존재하지 않아도 개발이 가능하다.

 

단점 1 상속이 불가능하다. == 하위 클래스를 만들지 못 한다.

상속을 하려면 public 이나 protected 생성자가 필요한데 정적 팩토리 메서드가 구현된 클래스는 생성자가 불필요하기 때문에 private 으로 보통 선언한다. 따라서 상속이 불가능하며 하위 클래스를 만들지 못한다.

나중에 "상속보다 합성", "불변 클래스" 라는 말을 많이 듣게 될텐데 상속이 불가능하다는 것은 단점이라기 보다는 장점으로 보일 수 있다.

 

단점 2 프로그래머가 정적 팩터리 메서드를 찾기가 어렵다.

단점이라기 보다는 불친절에 가깝다. 자바독을 보면, 생성자는 설명을 잘 해두지만 정적 팩터리 메서드는 직접 개발자가 소스코드를 뒤지며 찾아야 한다. 안 써있다. 앞으로 개발자들이 생성자와 같은 역할을 하는 정적 팩터리 메서드도 잘 문서화하면 문제가 없을 것이다. 그 이전에 작성해두었던 정적 팩토리 메서드는 어떻게 찾아야 될까?

흔히 사용되는 정적 팩터리 메서드 명명 방식들을 눈에 익혀두고 생각이 안 나면 이 접두사들을 보고 찾자.

 

from 매개변수가 하나일 때 사용
of 여러 매개변수를 받을 때 사용
valueOf from 과 of 의 더 자세한 버전
getInstance / instance 매개변수로 명시한 인스턴스 반환
newInstance / create getInstance/instance 와 기능은 같지만, 매번 새로운 인스턴스를 생성해서 반환
getType 여기서 Type 은 다른 클래스 이름. 다른 클래스의 객체를 반환
newType newInstance 와 같지만, 다른 클래스의 객체를 반환
type getType 과 newType 의 간결한 버전

 

"정적 팩토리 메서드를 고려하고 항상 문서화하자."

반응형

+ Recent posts