[이펙티브 자바] 2장 - 객체 생성과 파괴

couque·2021년 3월 26일
0

[아이템 1] 생성자 대신 정적 팩터리 메서드를 고려하라

  • 인스턴스를 생성하는 방법에는 public 생성자를 사용하는 방식과 정적 팩터리 메소드를 사용하여 생성하는 방법이 있다.
  • 아래의 예시는 Item 클래스 내부의 함수이다.

public 생성자

  • 클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단이다.
  • 클래스의 생성자를 호출하여, 인스턴스를 생성한다.
public Item(){
 // 생성에 필요한 로직
}

정적 팩터리 메서드(Static Factory Method)
- public static 함수 내부에서 인스턴스를 생성하고, 그 인스턴스를 반환하는 방식이다.
- 생성에 대한 부분을 해당 함수에서 처리하므로, 기본 생성자는 외부에서 별도로 생성할 수 없게 제약을 거는 것이 좋다.

private Item() {
}

public static Item createItem(){
    Item item = new Item();
    // 생성에 필요한 로직
    return item;
}

그렇다면 정적 팩터리 메소드가 무엇일까?

  • 정적 팩터리 메서드(Static Factory Method)클래스 내부에서 인스턴스를 반환하는 함수를 의미한다.
// 예시
public static Boolean valueOf(boolean b) {
	return b ? Boolean.TRUE : Boolean.FALSE;
}
  • 정적 팩토리 메서드의 장단점은 다음과 같다.

장점

  1. 이름을 가질 수 있다.
  2. 호출될 때마다, 인스턴스를 새로 생성하지는 않아도 된다.
  3. 반환 타입의 하위 타입 객체를 반환할 수 있다.
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지않아도 된다.

단점

  1. 상속을 하려면 public, protected 생성자가 필요하니, 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
  2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

1. 이름을 가질 수 있다.

  • 정적팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
BigInteger(int, int, Random) // 생성자
BigInteger.probablePrime(); // 정적 팩토리 메서드

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

  • 불변클래스는 인스턴스를 미리 만들어놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용한다면 불필요한 객체 생성을 피할 수 있다.
  • 이는 플라이웨이트 패턴과 비슷한 기법이다.
Boolean.valueOf(boolean) // 객체를 아예 생성하지 않음 (객체 참조)
  • 반복되는 요청에 대해 같은 객체를 반환하는 식이라면, 정적 팩터리 방식의 클래스는 인스턴스를 통제할 수 있다. 이는 플라이웨이트 패턴의 근간이 된다.

플라이웨이트 패턴: 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 서로 공유하여 사용하도록 하여 메모리 사용량을 최소화하는 소프트웨어 디자인 패턴

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

  • 반환할 객체의 클래스를 자유롭게 선택하게 하는 유연성을 준다.
  • API를 만들 때 이를 응용하면 구현클래스를 공개하지 않고도 그 객체를 반환하여 API를 작게 유지할 수 있다.
  • 이는 인터페이스를 정적팩터리의 반환형으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심기술이다.
  • 예를 들어 자바 컬렉션 프레임워크는 여러 유틸리티 구현체를 제공하고, 이를 인스턴스화가 불가능한 java.util.Collections에서 정적팩터리 메소드를 통해 얻게 한다.
// 구현체를 반환하지만 인터페이스만으로 다룸
List<Integer> list = Collections.unmodifiableList(); 

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

  • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환해도 상관없다.
  • EnumSet 클래스는 public 생성자 없이 정적팩터리만 제공하는데, 원소의 수에 따라 하위 클래스 중 하나의 인스턴스를 반환한다.
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
  Enum<?>[] universe = getUniverse(elementType);
  if (universe == null)
    throw new ClassCastException(elementType + " not an enum");

  if (universe.length <= 64)
    return new RegularEnumSet<>(elementType, universe);
  else
    return new JumboEnumSet<>(elementType, universe);

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

  • 이는 서비스 제공자 프레임워크를 만드는 근간이 된다.

서비스 제공자 프레임워크: 서비스 구현체(Provider)를 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리

// 서비스 제공자 프레임워크 예시
// 인터페이스
public interface CarInterface {
}

public class Car {
	// 서비스 접근 API
	public List<CarInterface> getInstance() {
    	return new ArrayList<>();
    }
}

public class client{
  public static void main(String [] args) {
 	// 현재 CarInterface의 구현체가 없음에도 사용 가능함
  	List<CarInterface> list = Car.getInstance();
    
    // 나중에 CarInterface의 구현체 CarImpl이 생기면 아래와 같이 사용 가능
    CarInterface car = new CarImpl();
    list.add(carImpl);
  }

명명규칙

  • from
  • of
  • valueOf
  • instance, getInstance
  • create, newInstance
  • getType
  • newType
  • type

[아이템 2] 생성자에 매개변수가 많다면 빌더를 고려하라

  • 인스턴스를 생성하는 방식에는 정적 팩터리 & 생성자 외에도 자바빈즈 패턴, 빌더 패턴이 있다.
  • 생성자에 매개변수가 많다면 빌더 패턴을 쓰는 것을 추천한다.

1. 정적 팩터리 & 생성자

  • 정적 팩터리 & 생성자에선 선택적 매개변수가 많을 때 점층적 생성자 패턴으로 대응한다.
  • 점층적 생성자 패턴 : 매개변수를 1개 받는 생성자 ~ n개 받는 생성자까지 생성하는 패턴
  • 단점: 이는 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다는 단점이 있다.
// 생성자 예시
public class NutritionFacts {
    private final int servingSize; 
    private final int serving;
    private final int calories;
    ...
    
    public NutritionFacts(int servingSize) {
    	this(servingSize, 0)
    }
    
    public NutritionFacts(int servingSize, int servings) {
		this(servingSize, servings, 0)
    }
    
    ...
   }

2. 자바빈즈 패턴

  • 자바빈즈 패턴 : 매개변수가 없는 생성자로 객체를 만든 후, 세터를 호출해 원하는 매개변수의 값을 설정하는 방식
  • 코드가 길어지긴 하지만 점층적 생성자 패턴보단 인스턴스를 만들기 쉽고, 읽기 쉬워진다.
  • 단점: 객체 하나를 만들려면 메소드를 여러개 호출해야 하고, 객체가 완성되기 전까지 일관성이 무너진 상태라는 단점이 있다.
  • 이를 완화하고자 freezing을 사용한다.
  • freezing: 생성이 끝난 객체를 수동으로 얼리고, 얼리기 전에는 사용할 수 없도록 한다.

3. 빌더 패턴

  • 빌더 패턴은 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비하였다.
  • (1) 클라이언트는 필요한 객체를 직접 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
  • (2) 이후 빌더가 제공하는 세터 메소들로 원하는 매개변수들을 설정한다.
  • (3) 마지막으로 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.
// 빌더 패턴 예시
public class NutritionFacts {
    private final int servingSize; 
    private final int serving;
    private final int calories;
    ...
    
   	public static class Builder {
    	private final int servingSize; // 필수 매개변수
        private final int servings;
        
        priavte int calories = 0; // 선택 매개변수
        private int fat = 0;
        
        public Builder(int servingSize, int servings) {
        	this.servingSize = servingSize;
            this.serving = serving;
        }
        
        public Builder calories(int val) {
        	calories = 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;
     }
        
    }
   }
  • 빌더의 세터 메서드들은 자신을 반환하기 때문에 연쇄적으로 호출이 가능하다.
  • 이를 플루언트 API 또는 메서드 연쇄라고 한다.
// 빌더 패턴 사용 예시
NutritionFacts cocaCola = new NutritionFact.Builder(240, 8)
	.calories(100).fat(35).build();

빌더 패턴 계층 클래스 예시

  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰이기 좋다.
// 계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴
public abstract class Pizza {
	public enum Topping {HAM, MUSHROOM, ONION }
    final Set<Topping> toppings;
    
    abstract static class Builder<T extends Builder<T>> {
    	EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addToping(Topping topping) {
        	toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();
        
        // 하위 클래스는 이 메서드를 재정의 하여 this를 반환함.
        protected abstract T self(); 
    }
}

// 뉴욕피자
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;}
    }
}

장점

  • 빌더 패턴 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
  • 객체마다 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

단점

  • 빌더 생성 비용이 크다.

[아이템 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라

  • 싱글턴: 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워진다. -> 인터페이스가 없다면 mock을 생성하기 어렵기 때문
  • 싱글턴을 만드는 방식은 세 개다. public static 멤버가 final 필드인 방식정적 팩터리 방식의 싱글턴, 열거 타입 방식의 싱글턴이다.
  • 앞선 두 방식은 생성자는 private으로 감춰두고 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 마련해 둔다.

1. public static 멤버가 final 필드인 싱글턴

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
 	private Elvis() { ...}
}
  • private 생성자는 Elvis.INSTANCE를 초기화할 때 한 번만 호출된다.
  • 해당 코드가 싱글턴임이 API에 명백히 드러나며 간결하다.

2. 정적 팩터리 방식의 싱글턴

public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    public static Elvis getInstance() {return INSTANCE;}
}
  • INSTANCE.getInstance()는 항상 같은 객체의 참조를 반환하므로 제 2의 인스턴스는 만들어지지 않는다.
  • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있고, 정적 팩터리를 제네릭 싱글턴 팩토리로 만들 수 있다는 장점이 있다. 또한 메서드 참조를 공급자로 사용할 수 있다.

[아이템 4] 인스턴스화를 막으려거든 Private 생성자를 사용하라

  • 정적 멤버만 담은 클래스를 만들 경우, 인스턴스로 만들어 쓰려고 설계한 게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들게 되어 혼동을 준다.
  • private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
  • 이 방식은 상속을 불가능하게 하는 효과도 있다.
  • Lombok의 @NoargsConstructor(AccessLevel.PRIVATE)를 사용할 수도 있다.

+@ ) 스프링에서 Class가 Private일 경우 객체를 어떻게 만들까?

  • 생성이 필요한 경우 BeanUtil.java의 리플랙션에서 권한을 수정한다.

[아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용해라

  • 보통 클래스는 하나 이상의 자원에 의존한다. 아래는 자원에 의존하는 잘못된 예시들이다.
  1. 정적 유틸리티를 잘못 사용한 예
// 맞춤법 검사기
private class SpellChecker {
    private static final Lexicon dictionary = ...;
    private SpellChecker() {}; // [아이템 4, 객체 생성 방지]
}
  1. 싱글턴을 잘못 사용한 예
// 맞춤법 검사기
private class SpellChecker {
    private static final Lexicon dictionary = ...;
    private SpellChecker(...) {};
    public static SpellChecker INSTANCE = new SpellChecker(...);
}
  • 위의 케이스들은 유연하지 않고, 테스트가 어렵다. 여러개의 사전을 사용하지 못한다.
  • final을 빼고 사전 교체 메서드를 만들 수도 있겠지만 어색하고 오류를 내기 쉬우며 멀티 스레드 환경에선 사용할 수 없다.
  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적절하지 않다.
  • 즉, 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
  1. 의존 객체 주입
  • 클래스는 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.
  • 이런 경우에 의존 객체 주입 방식을 사용한다.
  • 의존 객체 주입: 인스턴스를 생성할 때, 생성자에 필요한 자원을 넘겨주는 방식.
// 맞춤법 검사기
private class SpellChecker {
    private static final Lexicon dictionary;

    private SpellChecker(Lexion dictionary) {
    	this.dictionary = Objects.requireNonNull(dictionary);
    }
}
  • 의존 객체 방식은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해 준다.

0개의 댓글