반응형

 

자바 방패를 들고 있는 아기 바다표범

 

개요

자바는 C, C++ 에 비해 메모리 관리 측면에서 안전하다. 또 자바로 작성한 클래스는 불변식이 지켜진다. 개발자가 원한다면 어떤 객체 안의 멤버필드를 특정 조건을 만족하도록 클래스를 설계하고, 불변성을 갖도록 할 수 있다.

하지만 아무런 노력 없이 이러한 클래스를 만들 수 있는건 아니다. 프로그래머의 실수로 인해 의도하지 않는 변경이 일어날 수 있다. 외부에 의해 불변식이 깨질 수도 있다는 뜻이다.

 

일반적으로 객체를 생성할 때 수정자와 같은 장치가 없다면 외부에서 내부를 수정하는 것을 막아놨음을 의미한다. 하지만 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.


불변식을 지키고 싶었던 클래스

반응형

Period 클래스를 통해 객체를 생성할 경우 end 값이 start 보다 느리도록 설정된다. 유효성 검사도 하기때문에 문제가 없어보이지만 start나 end 값을 변경하여 불변식을 깨트릴 수 있다.

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end){
        if(start.compareTo(end) > 0){
            throw new IllegalArgumentException(start +" 가 "+ end +" 보다 늦다.");
        }

        this.start = start;
        this.end = end;
    }

    public Date start(){
        return start;
    }

    public Date end(){
        return end;
    }
}

 

 

불변식을 헤치고 싶었던 외부 클래스

Date start = new Date();
Date end = new Date();

Period p = new Period(start, end);

// start 와 end 는 변경되....엔다
start.setTime(10);

 

외부 클래스에 의해 불변식이 깨져버렸다.

위와 같이 변경이 가능한 이유는 Date 클래스가 가변 객체이기 때문이다.

 

가변객체
클래스의 인스턴스가 생성된 이후 내부 상태 변경이 가능한 객체

 


Date 는 Instant or LocalDateTime 을 사용하자

이와 같은 문제는 자바 8에서 해결됐다. Date 대신 사용 가능한 불변 객체인 Instant나 LocalDateTime이 등장했기 때문이다. 가변적 성질을 가진 Date 사용은 지양하자.

그럼 모든 멤버필드를 불변객체로 사용하는건 어떨까??

 


모든 멤버필드를 불변객체로 사용하는 건 좀...

 위와 같이 불변식을 지키려고 모든 멤버필드를 불변객체로 사용하는 건 힘.들.다. 멤버필드는 충분히 가변 객체일 수도, 커스텀 클래스 타입일 수도 있는데, 이러한 객체를 모두 불변객체로 대체하거나 만드려면 너모 힘들기 때문이다.

그럼 Date 와 같은 가변객체를 사용하는 상황에서 Period 인스턴스의 내부를 보호하려면 어떻게 해야할까?

바로 방어적 복사, defensive copy 를 사용해야 한다.

 

다시금 자바 방패를 들고 있는 아기 바다표범

 

 


Defensive Copy 를 적용하여 인스턴스 내부 보호하기

 

Defensive Copy는 인스턴스 내부 필드를 설정할 때, 파라미터로 온 값을 그대로 할당하는 게 아닌 복사하는 방법이다.

아래와 같이 매개변수로 받은 가변객체에 대해 똑같은 타입의 새로운 객체 생성(복사)한 후 멤버필드에 할당하고 있다.

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end){

        this.start = new Date(start.getTime()); // defensive copy
        this.end = new Date(end.getTime()); // defensive copy

        if(start.compareTo(end) > 0){
            throw new IllegalArgumentException(start +" 가 "+ end +" 보다 늦다.");
        }
    }

    public Date start(){
        return start;
    }

    public Date end(){
        return end;
    }
}

 

 

불변식을 헤쳤던 코드를 적용해보니 아래의 start와 Period 내부에 생성된 start는 엄연히 다른 인스턴스이므로 불변식 지킬 수 있게 되었다.

public static void main(String[] args) {

    Date start = new Date();
    Date end = new Date();

    Period p = new Period(start, end);

    // start는 변경되지 않는다!!
    start.setTime(10);
}

 

 

 

라고 생각했지만 가변 객체는 가변 객체. 어떻게든 멤버필드를 외부에서 가져간다면, 불변식은 깨지게 된다. 아래처럼 말이다.

// start 는 변경된다.
p.start().setTime(10);

 

 

이런 상황에서 불변성을 지키려면 어떻게 해야할까? 멤버 필드에 할당할 때와 마찬가지로, 멤버 필드를 리턴할 때도  defensive copy 를 적용하면 된다. 이로써 Period 인스턴스는 불변식을 지키게 되었다.

public Date start(){
        return new Date(start.getTime());
    }

    public Date end(){
        return new Date(end.getTime());
    }
}

 


방어적 복사본을 유효성 검사 이전에 하자!

 

눈치챘을 지 모르지만 위 코드에서 특이한 부분이 하나 있다. 유효성 검사보다 방어적 복사본을 만드는 코드가 먼저 위치한다. 유효성 검사를 먼저하는게 당연하다 생각할 수 있지만 이유가 있다.

public Period(Date start, Date end){

    this.start = new Date(start.getTime()); // 방어적 복사 먼저
    this.end = new Date(end.getTime()); // 방어적 복사 먼저

    if(start.compareTo(end) > 0){ // 그 다음 유효성 검사
        throw new IllegalArgumentException(start +" 가 "+ end +" 보다 늦다.");
    }
}

 

 

 

이유?!

유효성 검사를 먼저 할 경우 멀티스레드 환경에서 불변식을 헤치는 상황이 발생할 수 있기 때문이다.

유효성 검사를 끝낸 직후 방어적 복사를 하기 전에 다른 스레드에서 파라미터로 들어온 가변객체의 값을 변경이라도 한다면 불변식이 깨져버린다. 

유효성 체크를 먼저하다가 불변식이 깨져버린 상황

 

 

만약 방어적 복사를 유효성 검사보다 선행한다면 어떨까? start.setTime(23) 이 호출되어도 인스턴스 내부에 영향을 미치지 않게 되는 것이다.


방어적 복사의 사용 시기

 

매개변수를 방어적으로 복사하는 목적은 외부로부터 들어오는 매개변수가 가변객체이고, 이를 인스턴스 내부에서 갖게 될때  인스턴스의 불변식을 지키기 위함이다. 인스턴스 내에 멤버필드를 추가할 때에는 '변경될 수 있는 멤버필드인가'를 항상 고려해야한다. 변경되도 상관 없다면 방어적 복사를 할 필요가 없지만, 불변식을 가져야 한다면 방어적 복사를 사용해야 한다. 멤버필드의 불변성을 확신할 수 없을 때도 방어적 복사를 사용하는 것이 좋다.

 


되도록 불변 객체를 조합해 객체를 구성하자

 

불변식을 지키기 위한 코드를 작성하려면 생각보다 많은 고민과 시간을 쏟아야 할것 같지 않은가? 또한 방어적 복사는 멤버필드를 새로 생성하기 때문에 성능 저하가 수반된다. 즉, 불변식을 지키는 클래스를 설계할 때에는 가변 객체보다는 불변 객체를 조합해 구성하는 것이 좋다.

 


정리

불변식을 지키고자 하는 클래스가 매개변수로 받거나 반환하는 멤버필드가 가변객체라면 반드시 방어적 복사(defensive copy)를 해야한다. 클래스 설계시 의도적으로 수정자 메서드를 생성하지 않았다면 '방어적 복사'를 떠올려야 할 때이다.

 

 

 

반응형
반응형

개요

 일반적으로 어플리케이션 내에서 사용되는 상수는 Enum을 통해 관리한다. Enum이 등장하기 전에는 어떤 방식으로 상수를 관리했고, Enum이 기존의 방식보다 어떤 장점을 갖고 있는지 알아보자.


Enum이 등장하기 전 상수 관리

public class Constant {

    public static final int PIZZA_S_DIAMETER = 14;
    public static final int PIZZA_M_DIAMETER = 16;
    public static final int PIZZA_L_DIAMETER = 18;
    
    public static final int TORTILLA_S_SIZE = 4;
    public static final int TORTILLA_M_SIZE = 6;
    public static final int TORTILLA_L_SIZE = 8;
}

 

위와 같이 상수만을 정의하는 클래스를 만들고 외부에서 바로 접근 가능하고, 변하지 않도록 public static final 키워드를 통해 정의한다. 이러한 구현 방식을 정수 열거 패턴이라고 한다.

 


정수 열거 패턴의 단점

1. 타입 안전을 보장할 방법이 없다.

 상수를 타입으로 구분하지 못하기 때문에 PIZZA, TORTILLA와 같은 접두어를 썼다. 하지만 이 값을 받는 메서드나 클래스에서는 타입으로 값을 받기 때문에 int 타입으로 받게 된다. 그 결과 PIZZA 관련 상수를 받아야할 메서드에 TORTILLA 관련 상수를 보내도 컴파일 에러가 발생하지 않지만 의도한 값을 받지 못했기 때문에 추후 로직에서 버그가 발생할 수 있다.

public class Pizza {

    private final int size;

    public Pizza(int size){
        this.size = size;
    }
}

 

Pizza firstPizza = new Pizza(Constant.PIZZA_S_DIAMETER); // s 사이즈 피자
Pizza secondPizza = new Pizza(30); // 30 사이즈 피자 >> 버그가 발생할 수도...
Pizza thirdPizza = new Pizza(Constant.TORTILLA_S_SIZE); // 또띠아 S 사이즈 피자 >> 버그가 발생할 수도...

 

 

2. 문자열로 출력했을 때 의미를 알 수 없다.

 상수 값을 출력하거나 이 값을 파라미터로 받은 메서드에서 이를 디버깅하면 값만 출력된다. 이 상태에서는 'S 사이즈 피자의 길이' 라는 의미는 알 수 없고 14라는 값만 알 수 있는 것이다. 14가 어떤 의미를 가지고 있는지도 함께 알려줄 수 있다면 디버깅 시 값의 의미를 파악하거나 출처를 알기 훨씬 쉬워질것이다.

 

3. 순회 방법이 까다롭다.

같은 열거그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅지 않다. 예를들어 현재 존재하는 모든 피자 사이즈를 출력하고 싶다면 개발자가 일일이 하드코딩하거나 리플렉션을 사용해야 한다.

Class<Constant> constant = Constant.class;

for(Field field : constant.getFields()){
    if(field.getName().startsWith("PIZZA")){
        System.out.println(field.getName());
    }
}

 

 


 

 

Enum을 통한 상수 관리

 Enum 타입 자체는 클래스이며, 상수 하나당 인스턴스를 만들어 public static final 필드로 공개하는 방식이다. 아래의 예제는 내부적으로 각각의 size를 가진 S 인스턴스, M 인스턴스, L 인스턴스를 PizzaSize 타입으로 생성하고 이를 public static final로 공개하는 것이다.

 

public enum PizzaSize {
    S(14), M(16), L(18);
    
    private final int size;
    PizzaSize(int size){
        this.size = size;
    }
}

 

public enum TortillaSize {
    S(4), M(6), L(8);

    private final int size;

    TortillaSize(int size){
        this.size = size;
    }
}

 

 


열거 패턴의 단점을 극복한 Enum 타입

1. 컴파일 타임에서의 타입 안전성을 제공한다.

 이제 상수를 타입으로 구분할 수 있으므로 Pizza 클래스의 생성자 메서드에서 int 가 아닌 PizzaSize 타입으로 값을 받을 수 있다. 이로써 PizzaSize 타입이 아닌 다른 타입이 들어올 경우 컴파일 에러가 발생하게 된다. 컴파일 타임에서의 타입 안정성을 제공받게 되었다.

 

public class Pizza {

    private final PizzaSize size; // PizzaSize로 수정

    public Pizza(PizzaSize size){ // PizzaSize로 수정
        this.size = size;
    }
}

 

Pizza firstPizza = new Pizza(PizzaSize.S); // s 사이즈 피자
Pizza secondPizza = new Pizza(30); // 30 사이즈 피자 >> 컴파일 에러!!
Pizza thirdPizza = new Pizza(TortillaSize.S); // 또띠아 S 사이즈 피자 >> 컴파일 에러!!

 

2. 문자열로 출력했을 때 의미를 알 수 있다.

Enum에는 값과 이름이 존재하기 때문에 문자열로 출력했을 때 이 값의 의미를 알 수 있다. 또한 메서드 레벨에서 Enum 타입으로 값을 받기 때문에 디버깅 시 값에 대한 의미를 파악하기 쉽다. 그냥 14가 들어왔을 때 보다 PizzaSize.size = 14, PizzaSize.name = "S"로 들어온다면 피자 사이즈 S는 14라는 것과, 이 값이 PizzaSize 라는 Enum에서 관리된다는 출처도 알 수 있다.

디버깅 시 size에 대한 의미를 알 수 있다.

 

 

3. 순회 방법이 간단하다

 Enum 타입은 정의된 상수 값을 배열에 담아 반환하는 정적 메서드인 values를 제공한다. 정의된 상수를 모두 출력하고 싶다면 values 메서드를 사용하면 된다. 리플렉션을 사용했던 방식에 비하면 매우 간단함을 알 수 있다.

Arrays.stream(PizzaSize.values()).forEach(System.out::println);

 

 

 

4. 메서드나 멤버 필드를 추가할 수 있다.

 enum도 결국은 클래스이다. 멤버 필드나 메서드를 추가하여 피자 사이즈 관련 책임을 부여할 수 있다. 예를들어 피자 사이즈별로 사용되어야 하는 밀가루 양이 정해져있고 이 공식이 size * 30g 이라고 가정해보자. 이를 외부에서 구할 수도 있지만 size에 따라 밀가루 양이 결정되므로 size를 관리하는 PizzaSize 에게 책임을 위임해도 된다.

 

public enum PizzaSize {
    S(14), M(16), L(18);

    private final int size;

    private static final int SIZE_PER_FLOUR = 30; // 가중치
    
    PizzaSize(int size){
        this.size = size;
    }

    public int amountOfFlour(){
        return size * SIZE_PER_FLOUR;
    }
}

 

System.out.println(PizzaSize.S.amountOfFlour()); // 420
System.out.println(PizzaSize.M.amountOfFlour()); // 480
System.out.println(PizzaSize.L.amountOfFlour()); // 540

 

 

amountOfFlour 라는 메서드를 통해 모든 사이즈의 피자가 필요한 밀가루 양을 구하고 있다. 사이즈에 따라 밀가루 양을 구하는 로직이 다르지 않기 때문에 가능한 것이었다.

 

그럼 반대로 인스턴스마다 처리해야하는 로직이 다를 경우는 어떻게 처리해야 할까?

 


값에 따라 분기하는 Enum 타입

 

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    
    public double apply(double x, double y){
        switch (this){
            case PLUS : return x+y;
            case MINUS : return x-y;
            case TIMES : return x*y;
            case DIVIDE: return x/y;
        }
        
        throw new AssertionError("알 수 없는 연산 : "+this);
    }
}

 

 위 코드는 동작하기는 하나 좋은 코드는 아니다. 새로운 상수를 추가하면 case 문도 추가해야 한다. 만약 이를 깜빡한다면 컴파일은 되지만 새로 추가한 연산을 수행하려 할 때 "알 수 없는 연산"이라는 런타임 에러가 발생한다.

 이를 구현하는 좋은 방법은 열거 타입에 apply 라는 추상 메서드를 선언하고 각 인스턴스에서 이를 재정의하는 방법이다.

 


추상화 메서드를 재정의하는 Enum 타입

public enum Operation {
    
    PLUS {public double apply(double x, double y){return x+y;}},
    MINUS {public double apply(double x, double y){return x+y;}},
    TIMES {public double apply(double x, double y){return x+y;}},
    DIVIDE {public double apply(double x, double y){return x+y;}};
    
    abstract double apply(double x, double y);
}

 

 이 경우 상수를 추가하게 되면 반드시 apply 메서드를 반드시 정의해야 한다. 정의하지 않을 경우 컴파일 에러가 발생한다. 또한 분기처리 로직이 사라져 코드가 훨씬 깔끔해졌다.


 

정리

 Enum 타입은 정수 열거 패턴 방식의 단점을 극복할 수 있으며, 상수를 단순 값이 아닌 상태와 책임을 갖는 싱글톤 인스턴스 형태로 동작하게 한다는 점에서 객체지향적으로 설계가 가능하고, 디버깅이나 출력 시 보다 의미있는 정보를 제공할 수 있다.

반응형
반응형
반응형

개요

 제네릭을 사용하면 많은 비검사 경고들을 마주한다. 필자의 경우 이러한 경고들은 IDE의 도움을 받아 수정했으며, 수정 자체에 큰 의의를 두진 않았다. 하지만 이러한 경고를 제거하는 것만으로 그 코드는 ClassCaseException이 발생할 일이 없는 타입 안전성을 보장하는 코드가 된다고 한다. 이제 비검사 경고를 제거하는 올바른 방법을 알아보자.

 


컴파일러의 도움을 받아 제거하라

 대부분의 경고는 컴파일러가 알려준 대로 수정하면 사라진다.

Set<Coin> coinSet = new HashSet();

 위 코드는 '매개변수화된 클래스 HashSet을 원시사용 했다.' 라는 경고가 발생한다. 매개변수화된 클래스는 제네릭 클래스를 나타낸다. 즉, 제네릭 클래스인데 타입 매개변수를 사용하지 않았다는 경고이다.

 

Set<Coin> coinSet = new HashSet<Coin>();
Set<Coin> coinSet = new HashSet<>(); // 컴파일러의 추론 기능 활용

 위와 같이 타입 매개변수를 명시하여 경고를 해결할 수도 있지만 다이아몬드 연산자(<>) 만으로 해결할 수 있다. 컴파일러가 올바른 실제 타입 매개변수를 추론해주기 때문이다.

 


경고를 제거할 수 없지만 타입 안전함이 확신된다면 경고를 숨겨라

 타입 관련 경고를 제거하려면 @SuppressWarnings("unchecked") 어노테이션 사용하면 된다. 이 어노테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있지만 가능한한 좁은 범위에 적용해야 한다. 보통 변수 선언, 아주 짧은 메서드, 생성자에 사용되며, 절대 클래스 전체에 적용해서는 안된다.

 

 

아래는 ArrayList의 toArray 메서드이다.

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

 

 이 중 아래 코드에서 '확인되지 않는 형변환' 경고가 발생한다.

return (T[]) Arrays.copyOf(elementData, size, a.getClass());

 

return 문에는 해당 어노테이션을 적용할 수 없으므로, 아래와 같이 변수를 선언하고 어노테이션을 적용해준다. 또한 경고를 무시해도 되는 안전한 이유를 항상 주석으로 남겨둔다.

public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        
        // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환이다.
        @SuppressWarnings("unchecked")
        T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass());
        return result;
    }
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

 

 


SuppressWarnings 옵션

옵션 내용
all 모든 경고
cast 캐스트 연산자 경고
dep-ann 사용하지 말아야할 주석 경고
deprecation 사용하지 말아야할 메서드 경고
fallthrough switch 문 break 누락 경고
finally 반환하지 않은 finally 블럭 경고
null null 경고
rawtypes 제네릭을 사용하는 클래스 매개변수가 불특정일때 경고
unchecked 검증되지 않은 연산자 경고
unused 사용하지 않은 코드 관련 경고

 


정리

 비검사 경고는 중요하니 무시하지 말자. 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있다. 경고를 없앨 방법을 찾지 못했다면, 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 @SuppressWarnings("unchecked") 어노테이션으로 경고를 숨기자. 그런 다음 경고를 숨긴 근거를 주석으로 남긴다.

반응형
반응형

개요

 '태그달린 클래스'라는 단어가 되게 생소하다. 이에 대한 의미를 이해하고, 이 클래스가 과연 어떤 단점을 갖길래 계층구조로 리팩토링 하라는지도 이해해보자.

 


태그달린 클래스란?

 태그달린 클래스란 멤버 필드와 관련있다. 멤버 필드가 클래스의 유형을 나타내는 경우 해당 멤버 필드를 태그 필드라고 한다. 그리고 태그 필드를 갖는 클래스를 태그달린 클래스라고 한다.


태그 달린 클래스의 예

 Figure 클래스는 shape 필드가 이 클래스의 유형을 나타낸다. 즉, 태그 필드이다. 유형마다 생성자가 따로 존재하며, area 메서드의 동작도 달라지는 것을 볼 수 있다. 이러한 클래스의 단점을 하나씩 짚어보자.

public class Figure {
    enum Shape { RECTANGLE, CIRCLE };

    // 태그 필드 - 현재 모양을 나타낸다.
    final Shape shape;

    // 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
    double length;
    double width;

    // 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다.
    double radius;

    // 원 생성자
    Figure(double radius){
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    Figure(double length, double width){
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area(){
        switch (shape){
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

태그달린 클래스의 단점

 

1. 쓸데없는 코드가 많다

 열거 타입 선언, 태그 필드, switch 등 쓸데없는 코드가 많다. 이런 코드가 많은 이유는 이 클래스로 생성되는 인스턴스가 여러 유형(태그)을 가질 수 있기 때문이다.

 

2. 가독성 저하

 한 클래스에 여러 유형에 대한 로직이 혼합돼어 있어 가독성이 저하된다.

 

3. 불필요한 초기화가 늘어난다.

 멤버 필드의 불변성을 명시하기 위해 필드를 final로 선언한다. 위와 같은 코드는 필드를 final로 선언하려면 해당 태그에 쓰이지 않는 필드들도 생성자에서 초기화해야한다. 불필요한 초기화 코드가 늘어나는 것이다.

 

4. 태그 추가 시 클래스 전체를 수정해야한다.

 또 다른 의미의 태그를 추가하려면 클래스 전체를 수정해야한다. 예를들어 삼각형이 추가될 경우 이에 대한 생성자가 추가되어야하고, area() 메서드도 수정이 되어야 한다.

 

5. 인스턴스 타입만으로는 어떤 태그인지 알 수 없다.

 Figure 이라는 타입만으로 이 태그가 원인지, 사각형인지 알 수 없다.

 

 

태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다. - 책에서
태그 달린 클래스

 


클래스 계층 구조로 변환하기

 많은 단점을 갖는 태그달린 클래스를 계층 구조로 리팩토링 해보자. 리팩토링이 끝나면 기존 단점들을 얼마나 극복했는지도 확인해보자. 가장 먼저 계층구조의 루트가 될 추상 클래스를 정의해야한다.

 

1. 추상 메서드 선언

 태그 값에 따라 동작이 달라지는 메서드를 추상 메서드로 선언해야한다. 그리고 각각의 하위 클래스에서 이 동작을 정의하도록 한다. area 메서드가 이에 해당한다.

 

2. 일반 메서드 선언

 태그 값에 상관 없이 동작이 일정한 메서드들을 추상 클래스의 일반 메서드로 추가한다. Figure 클래스에는 이러한 메서드가 없기 때문에 넘어간다.

 

3. 멤버 필드 선언

 모든 하위 클래스에서 공통으로 사용하는 필드들을 전부 추상 클래스의 필드에 추가한다. Figure 클래스에는 이러한 필드가 없기 때문에 넘어간다.

 

이를 토대로 추상 클래스를 작성하면 아래와 같다.

abstract class Figure {
    abstract double area();
}

 

4. 구체 클래스 설계

 이제 추상 클래스를 확장한 구체 클래스를 설계한다. Figure의 태그는 원(Circle)와 사각형(Rectangle)이 있으므로 이를 클래스로 분리한다. 이로써 계층구조로의 리팩토링이 끝났다. 이제 기존 단점들을 극복했는지 확인해보자.

 

public class Circle extends Figure{

    private final double radius;
    
    public Circle(double radius){
        this.radius = radius;
    }
    
    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

 

public class Rectangle extends Figure{

    private final double length;
    private final double width;

    public Rectangle(double length, double width){
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }
}

 


단점을 모두 날려버린 계층구조

 

1. 쓸데없는 코드가 많다

 > 열거 타입 선언, 태그 필드, switch 등 쓸데없는 코드가 모두 없어졌다.

 

2. 가독성 저하

 > 다른 유형의 로직이 혼합되어 있지 않다. 클래스는 자신에 대한 로직만을 관리하고 있다.

 

3. 불필요한 초기화가 늘어난다

 > 사용하지 않아 불필요하게 초기화 해야했던 필드들이 모두 없어졌다.

 

4. 태그 추가 시 클래스 전체를 수정해야한다

 > 이제 태그 추가가 아닌 클래스 추가로 변경되었다. 만약 삼각형이라는 클래스가 추가되어도 기존 클래스의 수정은 필요 없게 되었다.

 

5. 인스턴스 타입만으로는 어떤 태그인지 알 수 없다

 > 타입만으로도 원인지, 사각형인지 알 수 있다.

 


태그 필드가 있다면 무조건 계층구조로?

 

그럼 태그 필드가 있는 클래스는 모두 계층구조로 바꿔야할까? 그건 아닌것 같다. 분리될 태그들이 상위 클래스와 is-a 관계일 때만 효용성을 가진다고 생각하기 때문이다. 물론 이런 관계를 갖지 않았다면 애초에 태그 필드를 도입하지 않았을 확률이 높지만, 코드에는 정답이 없고 사람이 짜는 것이니 기존 클래스의 구조를 분석한 후 계층 구조로 리팩토링 하는 것이 바람직하다고 생각한다.

 


정리

 태그 달린 클래스를 써야 하는 상황은 거의 없다. 만약 태그 필드가 있다면 이를 없애고 계층 구조로 리팩터링 하는 것을 고려해야 한다.

 

반응형
반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 많은 클래스가 하나 이상의 자원에 의존한다. 가령 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스나 싱글톤 클래스 구현한 모습을 드물지 않게 볼 수 있다.

 

1) 정적 유틸리티 클래스로 구현한 예

public class SpellChecker {
    
    // 의존 객체 생성
    private static final KoreaDictionary dictionary = new KoreaDictionary();
    
    private SpellChecker(){} // 외부 객체 생성 방지
    
    public static boolean isValid(String word){
        ...
    }
    
    public static List<String> suggestions(String typo){
        ...
    }
}

 

2) 싱글톤 클래스로 구현한 예

public class SpellChecker {

	// 의존 객체 생성
    private final KoreaDictionary dictionary = new KoreaDictionary();
    
    // 싱글턴 패턴을 사용해 생성한 인스턴스
    public static SpellChecker INSTANCE = new SpellChecker(); 
    
    private SpellChecker(){
    }

    public static boolean isValid(String word){
        ...
    }

    public static List<String> suggestions(String typo){
       ...
    }

}

 


2. 위 방식의 문제

 두 방식 모두 한국어 사전만 사용한다면 큰 문제가 되지 않지만 다른 종류의 사전을 사용해야한다면 변경이 필요할 때마다 의존 객체 생성 부분 코드를 수정해야한다.

 


3. 수정자 메서드를 통한 문제 해결?

 수정자 메서드를 추가하여 의존객체를 변경하는 방법이 있다. 이를 사용하면 사전의 종류가 바뀌는 건 맞지만, 멀티쓰레드 환경에서는 변경되는 상태 값을 공유하면 안되므로 사용해선 안된다.

 결국 멀티쓰레드 환경에서는 의존객체가 변경되는 클래스를 싱글톤이나 정적 유틸리티 클래스로 구현하면 안된다.

그렇다면 이러한 클래스는 어떻게 구현해야할까? 바로 생성자를 통해 의존 객체를 주입해야한다.

 


4. 생성자를 통한 의존 객체 주입

public class SpellChecker {
    
    private final Dictionary dictionary;
    
    // 생성자를 통한 의존 객체 주입
    public SpellChecker(Dictionary dictionary){
        this.dictionary = dictionary;
    }

    public static boolean isValid(String word){
        ...
    }

    public static List<String> suggestions(String typo){
        ...
    }
}

 생성자를 통해 객체를 생성할 때만 의존 객체를 주입하고 있다. 의존 객체는 불변성을 갖게 되어 멀티 쓰레드 환경에서도 안심하고 사용할 수 있다.

 KoreaDictionary 클래스 대신 Dictionary 인터페이스를 사용하고, KoreaDictionary와 같은 여러 사전 클래스들이 Dictionary 인터페이스를 구현하도록 했다. 만약 영어사전에 대한 맞춤법 검사 기능을 제공해야한다면 Dictionary 인터페이스를 구현한 EnglishDictionary 클래스를 만들고, 이를 외부에서 생성자를 통해 주입하면 된다. 이로써 의존 객체가 바뀌더라도 SpellChecker의 코드는 변경할 필요가 없게 되었다.

 

 이렇게 의존성을 외부로부터 주입받는 패턴을 의존 객체 주입 패턴이라고 하며 이 패턴의 변형으로 자바 8에서 등장한 Supplier<T> 인터페이스가 있다. 한정적 와일드 카드 타입을 사용해 팩터리의 타입 매개변수를 제한하여 사용할 수 있으며, 클라이언트에서는 해당 타입의 하위 타입을 포함하여 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.

 

public class SpellChecker {

	...
    public SpellChecker(Supplier <? extends Dictionary> factory){
        this.dictionary = factory.get();
    }
    
    ...
}

 

SpellChecker spellChecker = new SpellChecker(() -> new KoreaDictionary());

 

※ 개방 폐쇄 원칙

 SpellChecker의 맞춤법 검사 기능이 영어까지 '확장' 되었지만, SpellChecker의 코드 '변경'은 일어나지 않는다. 이처럼 '확장'에는 열려있고, '변경'에는 닫혀있는 것을 '개방 폐쇄 원칙' 이라고 한다.


 

5. 정리

 클래스가 하나 이상의 객체에 의존한다면 확장성을 고려하여 싱글턴과 정적 유틸리티 클래스 형태로 사용하지 않는 것이 좋다. 의존 객체는 내부에서 직접 생성하지 말고 생성자 혹은 정적 팩터리 메서드를 통해 넘겨주는 것이 좋다. 의존 객체 주입은 변경에 대한 유연성, 재사용성, 테스트 용이성을 제공한다.

 

 

반응형

+ Recent posts