반응형

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말함

클래스를 싱글턴으로 만들면 이를 사용하는 클라이언튼를 테스트하기가 어려워 질 수 있음
타입이 인터페이스이면 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 가자 구현으로 대체할 수 없음

public static final 필드 방식의 싱글턴

public class Elvis{
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }
}

위와 같은 방식으 싱글턴 생성은 리플렉션 API를 사용할 경우 싱글톤임을 보장하지 못함
이러한 공격(?)을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면됨

장점

1) 해당 클래스가 싱글텀임이 API에 명백히 드러남, public static 필드가 final이니 절대로 다른 객체를 참조할 수 없음
2) 간결함

정적 팩터리 방식의 싱글턴

public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() {return INSTANCE;}

    public void leaveTheBuilding() { ... }
}

Elvis.getInstance는 리플렉션을 통한 예외에서도 제2의 Elvis 인스턴스를 만들지 않음

장점

1) API를 바꾸지 않고도 싱글턴이 아니게 변경 할 수 있다. -> 스레드별 다른 인스턴스
2) 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있음
3) 원한다면 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있음

싱글톤 클래스를 직렬화하는 경우

  • 모든 인스턴스 필드를 일시적이라고 선언하고, readResolve 메서드를 제공해야함
  • 이렇게 하지 않으면 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어짐
  • 싱글턴임을 보장해주는 readResolve 메서드
  • private Object readResolve(){ return INSTANCE; }

열거 타입 방식의 싱글턴 - 바람직한 방법

public enum Elvis {
    INSTANCE; 

    public void leaveTheBuilding(){ ... }
}
  • 복잡한 직렬화 상황이나 리플렉션 공경에서도 제2의 인스턴스가 생기는 일을 완벽히 막아줌
  • 대부분 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법임
728x90
반응형

1) 점층적 생서자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

점층적 생성자 패턴 - 확장하기 어렵다!

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 NutritionFacts(int servingSize, int servings){
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, inst calories){
        this(servingSize, servings, calories, 0);
    }
}

점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

자바빈즈 패턴 - 일관성이 깨지고 불변으로 만들 수 없다.

public class NutritionFacts {
    private int servingSize = -1;
    private int servings = -1;
    private final int calories = 0;
    private final int fat = 0;
    private final int sodium = 0;
    private final int carbohydrate = 0;

    public NutritionFacts(){}

    public void setServingSize(int val){servingSize = val;}
    public void setServings(int val){servings = val;}
    public void setCalories(int val){calories = val;}
    public void setFat(int val){fat = val;}
    public void setSodium(int val){sodium = val;}
    public void setCarbohydrate(int val){ carbohydrate = val;}

}

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

자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없음(아이템 17_변경 가능성을 최소화하라)

final 등 불변을 보장, 스레드 안정하도록 구현이 필요함

위 두가지를 아우리는게 세번째 대안인 빌터 패턴이다

빌더 패턴 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취함

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 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);
        }

        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;
    }

}

//클라이언트 후출시
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100).sodium(35).carbohydrate(27).build();

메서드 호출이 흐르듯이 연결된다는 뜻으로 플루언트 API 혹은 메서드 연쇄라 한다.

계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴

public abstract class Pizza{
    public enum Topping{HAM, MUSHROOM, ONION, PEPPER, 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.requiredNonNull(topping));
            return self();
        }

        abstract Pizza build();

        protected abstract T self();
    }

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

재귀적 타입 하전을 이용하는 제네릭 타입
추상 메서드인 self를 더해 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 지원

뉴욕피자

public class NyPizza extends Pizza {
    public enum Size {SMALL, MEDIUM, LARGE}
    private final Size size; 

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

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

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

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

    }

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

}

@Override public NyPizza build() {
return new NyPizza(this);
}
다음 구체 하위 클래스는 하위 클래스 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공병환 타이핑이라 한다.
클라이언트가 형변환에 신경 쓰지 않고도 빌더를 사용할 수 있음

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE).addTopping(ONION).build();

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.

실무에서는 롬복에서 제공하는 Builder 어노테이션을 잘 활용하면 될 듯하다.

728x90
반응형

정적 팩터리 메서드

class ExampleItem() {

    public static Boolean valueOf(boolean b) {
        return b ? Boolean.TRUE : Boolean.FALSE;
    }

}

정적 팩터리 메서드 장점

1) 이름을 가질 수 있다.

BitInteger(int, int, Random)과 정적 팩터리 메서드인 BigInteger.probablePrime 비교

[값이 소수인 BigInteger를 반환한다]는 의미를 더 잘 설명함

2) 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체를 아예 생성을 피할 수 있음

이는 디자인 패턴 중에서 플라이웨이트 패턴*(Flyweight pattern)과 비슷한 기법

  • 플라이웨이트 패턴이란?
    어떤 클래스의 인스턴스 한 개만 가지고 여러 개의 "가상 인스턴스"를 제공하고 싶을 때 사용하는 패턴
    즉, 인스턴스를 가능한 대로 공유시켜 쓸데없이 new 연산자를 통한 메모릴 낭비를 줄이는 방식
    자주 사용하는 속성과 자주 사용하지 않는 속성을 분리해서 재사용하는 방식

public class Font {
    final String family;

    final int size;

    public Font(String family, int size) {
        this.family = family;
        this.size = size;
    }

    public int getSize() {
        return size;
    }

    public String getFamily() {
        return family;
    }
}
public class FontFactory {

    private Map<String, Font> cache = new HashMap<>();

    public Font getFont(String font) {
        if (cache.containsKey(font)) {
            return cache.get(font);
        } else {
            String[] split = font.split(":");
            Font newFont = new Font(split[0], Integer.parseInt(split[1]));
            cache.put(font, newFont);
            return newFont;
        }

    }
}
public class Client {

    public static void main(String[] args) {
        FontFactory fontFactory = new FontFactory();
        Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));
        Character c2 = new Character('e', "white", fontFactory.getFont("nanum:12"));
        Character c3 = new Character('l', "white", fontFactory.getFont("nanum:12"));
    }
}

3) 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

API를 만들때 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스를 사용하면 API를 작게 유지함
아이템 20. 추상 클래스보다는 인터페이스를 우선하라

public interface Singer{
    AudioClip sing(Song song);
}

public interface Songwriter{
    Song compose(int chartPosition);
}
public interface SingerSongwriter extends Singer, Songwriter{
    AudioClip strum();

    void actSensitive();
}

자바 8 이전에는 인터페이스에 정적 메서드를 선언할 수 없어서 java.util.Collections에서 정적 팩터리 메서드를 통해 얻도록 했음
하지만 이후에 인터페이스가 제공되면서 컬렉션 프레임워크는 인터페이스만으로 다루게 되어 API가 작아지고 개념적인 무게와 개념의 수와 난이도가 낮아짐
인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 없음

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

EnumSet 클래스
원소가 64개 이하면 원소들을 long 변수로 관리하는 RegularEnumSet의 인스턴스
원소가 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환
하지만 클라이언트는 두 클래스의 존재를 모름

내부적으로 위 기능이 필요 없어서 변경하더라도 클라이언트의 코드는 변화 없음
(일종의 캡슐화, OCP개념을 설명)

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

서비스 제공자 프레임워크를 만드는 근간 ex) JDBC
구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제, 클라이언트 구현체로 부터 분리
(IoC 개념에 대한 설명과 같음)

리플렉션

  • 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
  • 컴파일 시간이 아닌 실행 시간(런타임)에 동적으로 특정 클래스의 정보를 추출 할 수 있는 프로그래밍 기법
  • Intellij 자동완성, 스프링 어노테이션
Class clazz = Child.class;
System.out.println("Class name: " + clazz.getName());
//출력
Class name: test.Child
Class clazz2 = Class.forName("test.Child");
System.out.println("Class name: " + clazz2.getName());

//출력
Class name: test.Child
Class clazz = Class.forName("test.Child");
Constructor constructors2[] = clazz.getConstructors();
for (Constructor cons : constructors2) {
    System.out.println("Get public constructors in Child: " + cons);
}

//출력
Get public constructors in both Parent and Child: public test.Child()

브리지 패턴
추상적인 것과 구체적인 것을 분리하여 연결하는 패턴

의존 객체 주입 프레임워크(DI) -> 결국 스프링 프레임워크

정적 팩터리 메서드 단점

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

컬렉션 프레임워크의 유틸리티 구현 클래스를 상속할 수 없음을 나타냄
상속(구현상속에 한정적)은 재사용하는 강력한 수단이지만, 항상 최선을 아님, 잘못 사용하면 오류를 내기 쉬운 소프트웨어(아이템 18 내용 참고)
결국 상속을 하지 못하는게 일종의 장점이 될 수 있다는 의미

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

API 설명에 명확히 드러나지 않으니 사용자는 정적 패턱터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야함

정리

정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작적 public 생성자를 제공하던 습관이 있다면 고치자.

728x90

+ Recent posts