본문 바로가기
IT/Java

이펙티브자바 2장

by 봉즙 2019. 11. 23.

생성자 대신 정적 팩터리 메서드(static)를 고려

정적 팩토리 메서드 : 객체를 생성하는 메서드를 만들고 static으로 선언하는 기법
   그 클래스의 인스턴스를 반환하는 단순한 정적 메서드

전통적으로 클래스의 인스턴스를 얻는 방법은 public 생성자이다. 하지만 정적팩터리 메서드를 제공할 수 있다.

팩터리 메서드 이점

1) 이름을 가질 수 있다. 생성자에는 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 설명하지 못한다.

2) 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.(싱글톤) 더욱 유연해지기에 immutable class는 인스턴스를 미리 만들어 두거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. ex) Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다. 플라이웨이트패턴도 비슷한 기법이다.
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지 통제할 수 있다. 이런 클래스를 인스턴스통제 클래스라고 한다. 인스턴스를 통제하면 클래스를 싱클턴으로 만들수도 있으며, 인스턴스화 불가로 만들수 있다. 또한 immutable class에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.

3) 반환타입의 하위타입 객체르르 반환할 수 있는 능력이 있다. API를 만들 때 유연성을 응용하면 구현클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작제 유지할 수 있다. 이는 인터페이스를 정적 팩토리 메서드의 반환타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.
  주의사항 : 인터페이스가 정적 메서드를 가질수 없다는 제한이 자바8 부터 풀렸기에 인스턴스화 불가 동반 클래스를 둘 이유가 별로 없다. 동반 클래스에 두었던 public 정적 멤버들 상당수를 그냥 인터페이스 자체에 두면되기 때문이다. 하지만 정적메서드들을 구현하기 위한 코드 중 많은 부분은 여전히 별도의 pagkage-private 클래스에 두어야 할 수 있다. 정적필드와 정적 멤머 클래스는 public 이여야 한다.

4) 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. 있반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다.

5) 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
  서비스 제공자 프레임 워크는 3개의 핵심 컴포넌트로 이루어진다. 1)서비스 인터페이스, 2) 제공자 등록 API, 3) 서비스 접근 API, 종종 추가적으로 제공자 인터페이스가 쓰이기도 한다.

 

팩토리 메서드 단점

1) 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.

2) 정적 팩토리메서드는 프로그래머가 찾기 어렵다.

 

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

생성자의 경우 수가 많아져서 관리가 용이하지 못해진다 => 빌더 패턴

점층적 생성자 패턴 : 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수를 1개 받는 생성자, 선택매개변수를 2개받는 생성자 이러한 식으로 선택매개변수를 전부 다 받는 생성자까지 늘려가는 형식, 매개변수가 많아지면 클ㄹ라이언트 코드를 작성하거나 읽기 어렵다.

자바빈즈 패턴 : 객체 하나를 만들려면 메서드를 여러 개 호출해야하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.

빌더 패턴 : 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자 (혹은 정적 패고리)를 호출해 빌더 객체를 얻는다. 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는게 보통이다.

package effectivejava.chapter2.item2.builder;

// 코드 2-3 빌더 패턴 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취했다. (17~18쪽)
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    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 sodium(int val)
        { sodium = val;        return this; }
        public Builder carbohydrate(int val)
        { carbohydrate = val;  return this; }

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

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

    public static void main(String[] args) {
    	//빌더 패턴 선언
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

  빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다. 빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수를 흉내낸 것이다. build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하여 불변식을 보장하려면 빌더로 부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다 => 추상클래스에서 빌더 만들조 자식에서 사용하게끔

불변 : String 객체는 한번 만들어 지면 절대 값을 바꿀수 없는 불변 객체이다. 불변식은 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 말한다.

package effectivejava.chapter2.item2.hierarchicalbuilder;
import java.util.*;

// 코드 2-4 계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴 (19쪽)

// 참고: 여기서 사용한 '시뮬레이트한 셀프 타입(simulated self-type)' 관용구는
// 빌더뿐 아니라 임의의 유동적인 계층구조를 허용한다.

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
	//Topping 값을 고융하지 않는다.
	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));
            //self는 this보다 유연성이 좋다.
            return self();
        }

        abstract Pizza build();

        // 하위 클래스는 이 메서드를 재정의(overriding)하여
        // "this"를 반환하도록 해야 한다.
        protected abstract T self();
    }
    
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // 아이템 50 참조
    }
}

 self를 더해 하위클래스에서는 형변환  하지 않고 메서드 연쇄를 지원할 수 있다.

 

공변반환 타이핑 : 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능, 이를 이용하면 클라이언트가 형변환에 신경쓰지도 않고 빌더를 사용할 수 있다.

 

빌더패턴 단점 : 객체를 만들려면 빌더부터 만들어야하며 성능새 민감한 상황에서는 문제가 될수 있고, 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. 생성자나 정적 팩토리 처리해야할 매개 변수가 많다면 빌더 패턴을 선택하는 것이 좋다.

 

private 생성자나 열거 타입으로 싱글턴임을 보증하라

싱클턴 : 인스턴스를 오직하나만 생성할 수있는 클래스, 클래스를 싱글턴으로 만드는 경우 이를 사용하는 클라이언트를 테스트하기가 어려워 질 수 있다.

 

싱클턴을 만드는 방식 3가지

두방식 모두 생성자는 private으로 감춰두고 유일한 인스턴스에 접근할 수 있는수단으로 public static 멤버를 하나 마련해둔다.

package effectivejava.chapter2.item3.field;

// 코드 3-1 public static final 필드 방식의 싱글턴 (23쪽)
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

1) public static final 필드인 Elvis.INSTANCE를 초기화 할때만 생성자가 호출되며 생성자를 수정하여 두번째 객체가 생성되려 할 때 예외를 던지도록하여 싱글턴을 만든다.

2) 정적 팩토리 메서드를 public static 멤버로 제공한다. 주로 두번째 방법을 사용

package effectivejava.chapter2.item3.staticfactory;

// 코드 3-2 정적 팩터리 방식의 싱글턴 (24쪽)
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { 
    //return INSTANCE 메서드 통해서 좀더 유연
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
    public static void main(String[] args) {
        Elvis elvis = Elvis.getInstance();
        elvis.leaveTheBuilding();
    }
}

  항상 같은 객체 참조를 반환하기에 제2의 인스턴스는 만들어지지않는다.

 정적 팩토리 방식 장점

1. API를 바꾸지 않고도 싱클턴이 아니도록 변경할 수 있다.
2. 정적팩토리를 제네릭 싱글턴 팩토리로 만들 수 있다.
3.정적팩토리의 메서드 참조를 공급자로 사용 할 수 있다.

 

3) 원소가 하나인 열거 타입을 선언 (좋은 방법)

package effectivejava.chapter2.item3.enumtype;

// 열거 타입 방식의 싱글턴 - 바람직한 방법 (25쪽)
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() {
        System.out.println("기다려 자기야, 지금 나갈께!");
    }

    // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

  대부분의 상황에서는 원소가 하나뿐인 열거타입이 싱글턴을 만드는 가장 좋은 방법이다.

 

인스턴스화를 막으려면 private 생성자를 사용해라

 정적멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 것이 아니다.

추상 클래스로 마드는 것으로는 인스턴스화를 막을 수 없다. 하위클래스를 만들어서 인스턴스화 할 수 있기에 막을 수 없다. 컴파일러가 기본생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

싱글톤의 장점 : 모든 생성자는 상위클래스의 생성자를 호출하게 되는데, 이를 private으로 선언하면 하위클래스가 상위클래서의 생성자에 접근하는 길이 막히게 된다.

 

자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

  사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방시기이 적합하지 않으며, 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식 생성자의 매개변수를 초기화하여 사용자가 입력한 것을 사용하게 한다.

팩토리 : 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어 주는 객체

의존 객체 주입이 유연성과 테스트 용이성을 개선하지만, 의존성이 많아지면 코드를 어지럽게해준다. 의존객체 주입 프레임워크를 사용하면 간단히 해결할 수 있다.

 

불필요한 객체 생성을 피하라

String s = new String("abc");

String s = "abc";

위의 코드는 실행될때마다 String 인스턴스를 생성하기에 인스턴스가 반복해서 만들어 지게된다.

아래의 코드는 인스턴스를 매번 만드는 것이 아닌 하나의 인스턴스를 사용한다. 같은 가상머진안에서 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩토리 메소드를 사용하는 것이 좋다. 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리메소드는 그렇지 않다.

String.matches는 정규표현직으로 문자열 상태를 확이하는 가장 쉬운 방법이지만 성능상으로 반복해서 사용하기엔 적합하지 ㅇ낳다. 성능을 개선하기 위해서는 정규표현식을 표현하는 불편 Patter 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고 나중에 메서드가 호출될 때마다 인스턴스를 재사용하는 것이 좋다.

불필요한 객체를 만들어 내는 예로 오토박싱이있다. 기본타입과 그에 대응하는 박싱된 기본타입의 구분을 흐려주지만 완전히 없애주는 것은 아니기에 섞어쓰는 것을 권장하지 않는다. 기본타읍을 사용하는 것이 성능적으로 뛰어나며, 의도치않은 오토박싱이 숨어들지 않도록 해야한다.

데이터 베이스 연결을 제외하고는 객체풀을 만들지 않는 것이 성능상으로 유리하다.

방어적 복사에 대해서는 객체를 재사용했을 경우와 성능차이를 비교해보는 것이 좋다. 객체를 새로 만들어서 사용하는 것이 데이터 사용에 안전하나 성능의 저하를 불러 올 수 있기 때문이다.

 

다 쓴 객체 참조를 해제해라

메모리 누수는 스택이 커졌다가 줄어들었들 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않아서 생기기도 한다. 해당 참조를 다 썻을때 null처리를 통해 참조 해제를 함으로써 메모리 처리에 이득이 생긴다. 하지만 객체 참조를 null 처리하는 일은 예외적인 경우여야한다. 일반적으로는 scope밖으로 밀어내는 것이 가장 좋다.

null처리를 해야하는 경우

1) 스택이 자기 메모리를 직접 관리하기 때문에 Stack클래스는 메모리 누수에 취약하다. 일반적으로 자기 메모리를 직접 관리하는 클래스라면 메모리 누수에 주의해야 한다

2) 캐시 또한 메모리 누수를 일으킨다. WeakHashMap은 캐시를 구현할 때만 유용하다. 쓰지 않는 엔트리를 정리하기 위해서는 백그라운드 쓰레드를 활용하거나 캐시에 새 엔트리를 추가하는 경우도 있다. LinkedHashMap은 removeEldestEntry 메서드를 사용하여 후자의 방식으로 처리한다. 이안에는 캐시에도 최근, 오래된것의 순서가 존재한다.

3)리스너 혹은 콜백에 의해 생긴다. 콜백을 weak reference로 저장하면 가비지 컬렉터에 의해 정리된다.

* callback : 함수 포인터, 이벤트처리, 컬렉션에서 comparator, axjax

 

finalizer와 cleaner 사용을 피해라

finalizer는 기본적으로 쓰지 않는 것이 원칙이며 그 대안이 cleaner이나 느리고 불필요하기에 사용하지 않는 것이 좋다. C++의 파괴자는 비메모리 자원을 회수하는 용도로 쓰인다. 자바에서는 try-with-resources와 try-finally를 통해 해결한다. 

finalizer와 cleaner는 실행까지 얼마나 걸리는지 알 수 없기에 제때 실행되야 하는 작업은 절대 할 수 없다. 이는 가비지 컬렉터의 알고리즘에 달려있어 종류마다 다르다.

상태를 영구적으로 수정하는 작업에서는 절대 사용하서는 안된다.

보안문제, 속도문제 또한 가지고 있다. 보안적으로 방어하기 위해서는 finalize메서드를 만들고 final로 선언하여 자손에서 새성하지 못하도록 만들 수 있다.

이를 대체하기 위해서는 AutoCloseable을 구현해주고 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하여 종료하는 것을 통해 대체할 수 있다.

finalizer와 cleaner 사용용도

1) 자원의 소육자가 close메서드를 호출하지 않는 것에 대비한 안전망의 역할

2) 네이티브 피어와 연결된 객체, 네이티브 피어는 자바 객체가 아니기에 GC가 존재를 알지 못한다. 이를 회수하기 위해서 사용한다.

네이티브 피어 : 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체

import java.lang.ref.Cleaner;

// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스 (44쪽)
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
    private static class State implements Runnable {
        int numJunkPiles; // Number of junk piles in this room

        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        // close 메서드나 cleaner가 호출한다.
        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    // 방의 상태. cleanable과 공유한다.
    private final State state;

    // cleanable 객체. 수거 대상이 되면 방을 청소한다.
    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override public void close() {
        cleanable.clean();
    }
}

State 인스턴스는 Room인스턴스를 호출해서는 안된다. 데드락처럼 순한참조가 생기게 되어 가지비걸렉터가 Romm 인스턴스를 회수해갈 기회가 오지 않는다.

 

try-finally 보다는 try-with-resources를 사용하라

try-with-resources를 사용하면 중첩된 try-finally에서 두번째 예외가 첫번째 finally를 집어 삼키는 겨우가 생기는 것을 방지한다. 이 구조를 사용하기 위해서는 AutoCloseable이라는 인터페이스를 구현해야 한다.

'IT > Java' 카테고리의 다른 글

이펙티브 자바 4장 - 1  (0) 2019.11.27
이펙티브 자바 3장  (0) 2019.11.26
1장 이펙티브 자바  (0) 2019.11.22
null의 활용  (0) 2019.07.29
Interface  (0) 2019.07.24

댓글