반응형

개요

코드 분석중...

 

 누군가 작성한 코드를 분석하기 위해 100줄 남짓한 메서드를 보고있다. 최상위에 지역변수들이 초기화되어 있고, 아래로 비지니스 로직이 주욱 구현되어 있다. 코드를 분석해나가는 도중 어떤 지역변수가 사용되었지만, 이 값이 어떤 값을 갖고있는지를 잊어버려 최상위에 적힌 지역변수를 다시 확인했다. 어떤 변수는 반복문에서도, try 문 안에서도, 다른 변수에 값을 할당하는 부분에도 사용됐다. 변수의 유효 범위가 너무 넓은 것이다. 변수를 체크하다보니 로직의 흐름을 까먹어 다시 분석하기도 한다. 이런 과정을 반복하면서 코드 분석을 마무리했다.

 


지역변수가 최상위에 선언되어 있지 않았다면?

 지역변수들이 최상위에 선언되어 있지 않고, 쓰일 때 초기화되어 있거나, 현재 보이는 코드라인에 초기화 된 값이 보인다면 어땠을까? 다시 확인할 필요도 없고, 코드 분석에 대한 집중력도 유지할 수 있다. 즉, 지역변수의 범위를 최소화한다면 유지보수성과 가독성을 향상시킬 수 있다.

 


지역변수의 범위를 최소화하는 방법

 가장 강력한 방법은 '가장 처음 쓰일 때 선언하는 방법'이다. 앞서 개요에서 말했던 방법이다. 사용하려면 멀었는데 미리 선언부터 해두면 이를 다시 확인하거나 잘못 사용하게 되는 상황이 발생할 수 있다. 사실 거의 모든 지역변수는 선언과 동시에 초기화해야한다. 여기서 거의라고 말한 이유는 그렇지 않은 예외 케이스가 존재하기 때문인데 바로 try-catch 문이다. 예외 처리를 해야하는 경우 선언은 try 문 밖에서, 초기화는 try 문 안에서 해야 catch나 finally 에서 이에 맞게 핸들링을 하거나 리소스를 제거하는 행위를 할 수 있다.

 

아래와 같이 FileInputStream 과 같은 타입의 값을 초기화할 때 예외처리를 하지 않을 경우 아래와 같이 컴파일 타임에 에러가 발생한다. 이를 처리하기 위해 예외를 외부로 던질수도 있지만, 이는 예외에 대한 책임을 전가하기에 메서드 내에서 처리를 하려는 사람도 있을 것이다.

예외 처리를 하지 않아 컴파일 에러 발생

 

 

그 경우 inputStream을 try 외부에서 먼저 선언하고 내부에서 초기화하게 된다.

public void myMethod(){
    File file = new File("");

    FileInputStream inputStream = null;
            
    try{
        inputStream = new FileInputStream(file);
    }catch (FileNotFoundException e){
        e.printStackTrace();
    }finally {
        try{
            inputStream.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

 

물론 InputStream은 autoClose 가 구현되어 있으니 try-resource를 사용한다면 try 괄호 안에서 초기화가 되겠지만, 그게 아닐 경우 위처럼 선언과 동시에 초기화되지 않을 수도 있다. 어쨌든 중요한건 대부분의 지역변수는 사용할때, 선언과 동시에 초기화해야 한다는 것이다.

 


반복문의 반복변수

 지역변수는 반복문에서도 사용된다. 바로 '반복변수'이다. 일반적으로 for나 while 과 같은 반복문에서 사용하는데 만약 반복 변수의 값을 반복문이 종료된 뒤에도 써야 하는 상황이 아니라면 while 보다 for 문을 쓰는 게 낫다.

 

예를들어 컬렉션을 순회하는 코드를 작성할 경우 for문은 아래와 같이 for-each나 전통적인 for문을 사용해서 구현할 수 있다.

List<String> list = new ArrayList<>();
//...


// for-each
for(String e : list){
    //...
}

// 전통 for
for(Iterator<String> i = list.iterator(); i.hasNext();){
	//...
}

 

 

while의 경우는 아래와 같이 구현되는데, while 구문 안에 조건을 넣어야하므로 while 구문 전에 반복 변수를 초기화를 해야한다. 그런데 아래와 같이 while 반복문이 두번 사용될 경우 복붙 과정에서 실수로 조건부의 i를 i2로 수정하지 않아 버그가 발생할 수 있다. 이 경우 두번째 반복문이 실행되지 않을 것이다.

Iterator<String> i = list.iterator();
while(i.hasNext()){
    //...
}

Iterator<String> i2 = list.iterator();
while(i.hasNext()){ // 버그유발
    //...
}

 

 

for문의 경우는 어떨까? 반복 변수의 유효범위가 for문 안으로 한정되어 있기 때문에 위와 같은 버그가 발생하지 않는다. 심지어 변수명을 다르게 설정할 필요도 없다.

for(Iterator<String> i = list.iterator(); i.hasNext();){
    //...
}
for(Iterator<String> i = list.iterator(); i.hasNext();){
    //...
}

 

 

여기서 키포인트는 초기화를 내부에서 했냐, 외부에서 했냐가 아니다. while에 사용되는 반복변수는 for문에 사용되는 반복변수보다 유효 범위가 훨씬 넓다는 것이다. while의 반복변수는 메서드 전체인데 반해, for문의 반복변수는 for문 안으로 한정되어 있다. 즉, 지역변수의 유효범위가 넓을수록 버그를 발생시킬 확률이 높고, 가독성과 유지보수성을 헤친다는 것을 말하고 있다.

 


정리

 지역변수의 범위를 최소화하는 목적은 가독성과 유지보수성을 높이는 것이다. 이를 위해 지역변수를 선언과 동시에 초기화하거나, 실제로 쓰일 때 초기화 하거나, while 보다는 for문을 쓴것인데, 만약 이를 다 지킨다고 하더라도, 코드가 길면 어떨까? 긴 코드 안에는 여러 책임들이 얽혀있고, 여러 기능들을 하고, 여러 예외들을 처리한다면 지역변수의 범위를 최소화한다 한들 가독성과 유지보수성을 높인다는 목적을 이루지 못한다.

 

때문에 지역변수의 범위를 최소화하기에 앞서 선행되어야 할 가장 중요한 것은 단일 책임원칙에 맞게 코드를 구현하여 메서드를 작게 만드는 것이다. 이게 선행됐을 때 비로소 지역변수의 범위를 최소화하는 것이 의미있는 행위가 될것이다.

반응형
반응형

1. 개요

 자바 8 이전에는 빈 값에 대한 처리 선택지가 '예외' 혹은 'null 반환'이었다. 이 둘 모두 허점이 있다.

예외는 정말 예외적인 상황에서만 사용해야한다는 것과(빈 값이라고 예외를 날려서는 안될 케이스도 있다. ex. 검색결과)

예외를 생성할 때 스택 추적을 하므로 이에 대한 비용 문제가 있다. null을 반환하면 위 문제가 생기지 않지만 언젠가, 그리고 어디선가 NullPointException 이 발생하여 시스템 버그를 초래할 수 있다. 그런데 자바 8 이후 또 하나의 선택지가 생겼다. 그게 바로 Optional<T> 이다. 

 


2. Optional<T> 이란?

 옵셔널은 1개의 null이 아닌 T 타입 객체를 담거나, 아무것도 담지 않을 수 있는 불변 컬렉션이다. 옵셔널을 사용하지 않는다면 보통 T를 반환할테지만 상황에 따라 아무것도 반환하지 않아야 할때가 있다면 T 대신 Optional<T> 를 반환하도록 선언하면 된다. 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 낮다.

 


3. max 값을 구하는 예제

 

최댓값을 구하는 메서드이다. 파라미터로 들어온 Collection의 요소가 없을 경우 예외를 발생시키고 있다.

public static <E extends Comparable<E>> E max(Collection<E> c){
    if(c.isEmpty())
        throw new IllegalArgumentException("빈 컬렉션"); // 클래스 내부에서 예외를 던지고 있다.

    E result = null;
    for(E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);
    
    return result;
}

 

 

이처럼 예외 처리가 메서드 내에 강하게 결합되어 있기 때문에 여러가지 한계를 맞이하게 된다. 이를 호출하는 클래스의 상황에 따라 예외 메시지를 다르게 하고싶을 수도, 예외를 발생시키지 않을 수도 있지 않은가? 하지만 이 방식은 그게 쉽지않다.

예외 메시지를 다르게 하고싶다면 클라이언트는 이 런타임 예외가 발생한다는 것을 알고 있어야하고, 이 예외에 대해 외부에서 catch 문에서 예외 전환 로직을 작성해야한다. 원치않는 결합도가 생겨버렸다.

예외를 발생시키고 싶지 않다면 마찬가지로 catch 문에서 예외를 잡고 아무것도 수행하지 않도록 설정해야한다. 이상하다.

 

이를 Optional 로 구현해보면 어떨까?

public static <E extends Comparable<E>> Optional<E> max2(Collection<E> c){
    if(c.isEmpty())
        return Optional.empty(); // 빈 옵셔널을 반환함으로써 클래스 외부에서 예외를 발생시킬 수도, 시키지 않을 수도 있다. Null도 아니다!

    E result = null;
    for(E e: c)
        if(result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);

    return Optional.of(result); // result 를 참조하는 Optional 타입을 반환한다.
}

 

 

빈 옵셔널은 Optional.empty(), 값이 든 옵셔널은 Optional.of(value)로 처리한게 끝이다. 이 방식은 위 방식과는 다르게 호출한 클라이언트에서 Optional 값에 대한 처리가 가능하다. 원하는 예외메시지를 발생시킬수도, 발생시키지 않을수도 있고, 더 나아가 기본 값을 넣을 수도 있다. 앞선 코드보다 훨씬 유연하고 깔끔한 것을 볼 수 있다.

Integer maxValue = max2(list).orElse(0); // 빈 옵셔널일 경우 0으로 설정

//클라이언트에 따른 예외 메시지 변경
Integer maxValue2 = max2(list).orElseThrow(() -> new IllegalArgumentException("요청 리스트가 비어있습니다."));

 

 

참고로 Optional.of(value)에 null을 넣으면 NPE가 발생하므로 주의해야한다. 또한 옵셔널을 반환하는 메서드는 절대 null을 반환하면 안된다. 옵셔널을 도입한 취지를 저버리는 행위이기 때문이다.

 


4. Optional 을 사용해야하는 기준

그렇다면 null, 예외를 던지는 대신 옵셔널을 선택하는 기준은 뭘까? 옵셔널은 검사 예외와 취지가 비슷하다. 즉, 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다. 반환 값이 없을 경우에 대한 처리를 사용자가 작성해야하므로 null 보다 안전하고 깔끔하게 처리할 수 있다.

 

클라이언트 입장에서 이를 사용한다면 클라이언트는 옵셔널에 대한 처리를 클라이언트 코드에서 선택하면 된다.

1) 기본 값 설정

int maxValue = max2(list).orElse(0);  // 기본 값 설정

 

2) 원하는 예외 설정

// 원하는 예외 설정
String maxString = max2(wordList).orElseThrow(() -> new RuntimeException("단어 리스트가 비어있습니다. 리스트를 확인해주세요"));

 

3) 항상 값이 유효하다고 가정할때 설정

int value = max2(list).get(); // 항상 값이 있다고 가정하고 get!

 

 

이 외에도 filter, map, ifPresent 등 다양한 메서드들을 지원하고 있다. 앞선 기본 메서드로 처리가 힘들다면 API 문서를 참조해 문제를 해결해줄 수 있는 메서드가 있는지 찾아보자. 만약 적합한 메서드를 찾지 못했다면 isPresent 메서드를 활용할 수 있다. isPresent 메서드는 가스레인지의 안전벨브 역할로, 옵셔널 값이 채워져있다면 true, 비어있다면 false를 리턴한다. 

 

자바 9 버전부터 지원하는 Optional.map 메서드를 사용해 원하는 타입을 갖는 Optional 객체로 변환할 수도 있다. 아래는 Integer 타입이 들어있는 최대값을 받아 String 타입으로 변환한다. 값이 없을 경우에 대해 "N/A" 값이 반환되도록 orElse 체인 메서드로 처리했다.

String res = max2(list).map(val -> Integer.toString(val)).orElse("N/A");

 

 

참고로 이를 지원하지 않는 자바 8 버전의 경우 아래와 같이 작성할 수 있다. isPresent로 필터링 후 get으로 빼온다.

streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)

 

 

자바 9에서는 Optional 에 stream() 메서드가 추가되었다. Optional을 stream으로 변환해주는 어댑터다. 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로 한단계 벗겨주는 것이다. 어찌됐든 이런 저런 메서드들을 많이 지원하니 찾아보는 것을 권장한다.

 


5. 반환값으로 옵셔널을 사용했을때의 단점

결합도를 낮추고 유연성과 코드의 깔끔함을 더해주지만 구조상 단점이 있다. 객체를 박싱하는 옵셔널 특성 상 박싱하고자 하는 객체가 많을수록 Optional 객체를 생성하고 박싱하는 비용이 발생한다. 언박싱할때도 비용이 발생하는 건 마찬가지이다. 때문에 리스트, 스트림, 배열 등에 사용할 요소들에 대해 별 생각없이 Optional 을 사용한다면 성능 측면에서 문제가 발생할 수 있다. 성능이 중요한 상황에서는 옵셔널을 사용하지 않는 것이 좋다.

 


6. 기본 타입을 담는 옵셔널도 있다.

int, long, double 과 같은 기본 타입에 대해 Optional 을 사용할 땐 Integer, Long, Double 과 같은 참조 타입으로 박싱하여 사용하지 말고 OptionalInt, OptionalLong, OptionalDouble 과 같은 옵셔널 타입을 사용하자. 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 해야한다.

 


7. 정리

반환 값이 없을 수도 있는 메서드라면 옵셔널 사용을 고려해야한다. 하지만 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.

반응형
반응형

개요

메서드 시그니처를 신중히 설계하라. 개별 아이템으로 두기 애매한 API 설계 요령들을 정리한 아이템이다.


1. 메서드 이름을 신중히 짓자

메서드 명은 표준 명명 규칙을 따라야 한다. 아래는 점프 투 자바에서 제공한 명명 규칙이다.

https://wikidocs.net/1936

 

02-03 이름 짓는 규칙

자바 코드를 작성하면서 클래스, 메서드, 변수 등의 이름을 지을 때 개발자들이 가장 고민한다. 하지만 규칙을 알아 두면 부담을 크게 줄일 수 있다. 여러 사람이 프로그래밍할 때 …

wikidocs.net

 

첫째, 메서드 명은 동사로 한다.

둘째, 메서드 명은 소문자로 시작한다.

셋째, 카멜케이스 방식을 사용한다.

 

이 사항을 기본적으로 지키면서 이해하기 쉽고 일관되게 네이밍을 지어야 한다.

실제로 협업 간에는 변수, 클래스, 메서드 등에 대한 네이밍 컨벤션을 미리 정리해놓고 사용하기도 한다. 


2. 편의 메서드를 너무 많이 만들지 말자

편의메서드
편의를 위한 메서드로 클래스의 책임에 충실한 클래스와는 거리가 멀다는 특징이 있다.
예를들어 게시글을 CRUD 하는 BoardService 클래스에 max, min, extractString 과 같이 편의를 위한 메서드가 있다면 편의 메서드로 볼 수 있다.

 

메서드가 너무 많은 클래스는 유지보수가 어렵다. 인터페이스도 마찬가지이다. 아주 자주 쓰일 경우에만 별도의 약칭 메서드를 만들고, 그 외에는 편의 메서드를 만들지 않는게 좋다.

 


3. 매개변수 목록은 짧게 유지하자

매개변수는 4개 이하가 적당하다. 만약 긴 매개변수 목록을 유지해야한다면 아래 내용 적용을 고려해야한다.

 

첫째, 여러 메서드로 쪼갠다.

둘째, 매개변수 여러 개를 묶어주는 정적 멤버 클래스를 만들어 활용한다.

셋째, 빌더 패턴과 정적 멤버클래스를 함께 사용한다. 매개변수가 많거나 일부를 생략해도 될때 좋다.

 


4. 매개변수의 타입으로는 클래스보다 인터페이스를 사용하자

예를들어 HashMap을 매개변수로 사용하는 대신 Map을 사용할 경우 HashMap 뿐만 아니라 TreeMap, ConcurrentHashMap 등 다양한 타입의 구현체 클래스를 전달할 수 있다.

인터페이스 대신 클래스를 사용하면 클라이언트에게 특정 구현체만 사용하도록 제한하는 꼴이다.

 


5. boolean 보다는 원소 2개짜리 열거 타입이 낫다.

* 단, 메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.

 

열거 타입을 사용하면 코드를 읽고 쓰기가 쉽고, 확장하기도 용이하다.

다음은 화씨온도와 섭씨온도를 원소로 정의한 열거타입이다.

public enum TemperatureScale { FAHRENHEIT, CELSIUS }

 

온도계 클래스의 정적 팩터리 메서드가 이 열거 타입을 입력받아 적합한 온도 인스턴스를 생성해준다고 생각해보자. Thermometer.newInstance(true) 보다는 Thermometer.newInstance(TemperatureScale.CELSIUS) 가 명확하다. 만약, 나중에 캘빈 온도도 지원해야 한다면 열거타입에 추가만 하면 된다.

반응형
반응형

 

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

 

개요

자바는 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