이펙티브 자바 2장

qq·2024년 1월 24일
0
post-thumbnail

이펙티브 자바 2장

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

  • 점층적 생성자 패턴
    public class NutritionFacts {
        
        private final int calories;
        private final int fat;
        private final int sodium;
    
        public NutritionFacts(int calories) {
            this(calories, 0, 0);
        }
    
        public NutritionFacts(int calories, int fat) {
            this(calories, fat, 0);
        }
    
        public NutritionFacts(int calories, int fat, int sodium) {
            this.calories = calories;
            this.fat = fat;
            this.sodium = sodium;
        }
    }
    문제 : 확장하기 어렵다.
  • 자바빈즈 패턴
    public class NutritionFacts {
    
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
    
        public NutritionFacts() { } //매개변수 없는 생성자로 객체를 만든 뒤,
    																//setter 메소드로 값을 설정한다.
    
        public void setCalories(int calories) {
            this.calories = calories;
        }
    
        public void setFat(int fat) {
            this.fat = fat;
        }
    
        public void setSodium(int sodium) {
            this.sodium = sodium;
        }
    }
    객체 하나를 만드려면 메서드를 여러개 호출해야 하고 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 일관성이 무너지면 클래스를 불변으로 만들 수 없는 큰 단점이 존재한다
  • 빌더 패턴 필요한 객체를 직접 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
    public class NutritionFacts {
    
        private final int calories;
        private final int fat;
        private final int sodium;
        
        public static class Builder {
            //필수 매개변수
            private final int calories;
            
            //선택 매개변수
            private int fat = 0;
            private int sodium = 0;
            
            public Builder(int calories) {
                this.calories = calories;
            }
            
    				//setter 메소드
            public Builder fat(int val) {
                fat = val;
                return this;
            }
            
            public Builder sodium(int val) {
                sodium = val;
                return this;
            }
            
            public NutritionFacts build() {
                return new NutritionFacts(this);
            }
        }
        
        private NutritionFacts(Builder builder) {
            calories = builder.calories;
            fat = builder.fat;
            sodium = builder.sodium;
        }
    }
    NutritionFacts cocaCola = new NutritionFacts(250).fat(30).sodium(20).build();
    유연하다는 장점이 있지만 단점은 객체를 만들기 전에 빌더 부터 만들어야 한다. 생성 비용이 크진 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 하지만 계층적으로 설계된 클래스와 함께 쓰기에 빌더가 좋다
    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.requireNonNull(topping));
                        return self();
                    }
                    abstract Pizza build();
                    
                    protected abstract T self();
                    }
                    
                    Pizza(Builder<?> builder){
                        toppings = builder.toppings.clone();
                    }
                    
      }
    Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상 메서드인 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;
            }
        }
        
        private NyPizza(Builder builder){
            super(builder);
            size = builder.size();
        }
        
        }

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

  • 먼저 싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • 싱글턴을 만드는 방법 3가지
    1. Private 생성자와 public static 필드

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

      해당 클래스가 싱글턴임이 API에 명백히 드러난다. public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다.

      문제 :

      • 권한이 있는 클라이언트에서 리플레션 API를 이용해 private 생성자를 호출 할 수 있다
      • 생성되는 시점을 조절할 수 없다
        • 방어 방법: 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던진
    2. 정적 팩터리 메서드를 public static 멤버로 제공되는 방식

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

      장점:

      1. API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다
      2. 정적 팩터리를 제너릭 싱글톤 팩터리로 만들 수 있다(public static필드가 final이니 절대로 다른 객체를 참조할 수 없기 때문이다)
      3. 정적 펙터리의 메서드 참조를 공급자로 사용할 수 있다. 가령 Elvis::getInstance를 Supplier로 사용하는 식.

      하지만 여기서 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어지지 않으려면 이 코드를 추가해야한다

      // 싱글턴임을 보장해주는 readResolve 메서드
      private Object readResolve() {
          // '진짜' Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡다.
          return INSTANCE;
      }
    3. 원소가 하나인 열거 타입을 선언한다

      public enum Elvis {
          INSTANCE;
          public void leaveTheBuilding() { ... }
      }

      장점:

      1. 간결하다
      2. 추가 노력없이 직렬화할수 있다
      3. 복잡한 직렬화 상황이나 리플렉션 공격에도 제 2의 인스턴스가 생기는 일을 완벽히 막아준다

      문제점:

      1. 싱글턴이 Enum 외의 클래스를 상속해야 한다면, 이 방법은 사용할 수 없다
      2. 열거 타입이 다른 인터페이스를 구현하도록 선언할 수 없다.

💡Item 4. 인스턴스를 막으려거든 private 생성자를 사용하라

·정적 메서드와 정적 필드만 담은 클래스는 객체 지향적으로 사고하지 않는 사람들이 종종 남용하는 방식이지만, 나름의 쓰임새가 있다.

  1. java.lang.Math, java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.

  2. java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수 있다.

  3. final 클래스와 관련된 메서드를 모아 놓을 때 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가기 때문이다.

  • private 생성자를 추가해 클래스의 인스턴스화를 막아서 사용할 수 있다. (생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만든다)
// 인스턴스를 만들 수 없는 유틸리티 클래스
public class UtilityClass {
    // 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
    // 악의적 리플렉션을 막을 수 있다.
    private UtilityClass() {
        throw new AssertionError();
    }
}
  • 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스를 생성자를 호출해야하는데, 이를 private 선언으로 막아버렸기 때문이다.

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

많은 클래스가 하나 이상의 자원에 의존한다. 사용하는 자원에 따라 동작이 달라지는 클래스에 경우 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.

static 유틸리티를 잘못 사용한 예시- 유연하지 않고 테스트하기 어렵다.

public class SpellChecker {
	private static final Lexicon dictionary = ...; // 의존하는 리소스 (의존성)
	private SpellChecker() {} // 객체 생성 방지 

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

인스턴스를 만들 필요가 없기 때문에 private한 생성자를 만들어주고, public static 메소드들이 있다.

싱글턴을 잘못 사용한 예- 유연하지 않고 테스트하기 어렵다.

public class SpellChecker { 
	private final Lexicon dictionary; 

	public boolean isValid(String word) { return true; } 
	public List<String> suggestions(String typo) { return null; }
}

의존 객체 주입

제대로 사용하려면 의존성을 바깥으로 분리하여 외부로부터 주입받도록 해야 한다. 쉽게 말하면 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.

public class SpellChecker { 
	private final Lexicon dictionary; 

	private SpellChecker(Lexicon dictionary) { 
    		this.dictionary = Objects.requireNonNull(dictionary); 
 	} 

	public boolean isValid(String word) { return true; } 
	public List<String> suggestions(String typo) { return null; }

불변을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있다

팩토리 메서드 패턴

생성자에 자원 팩터리를 넘겨주는 방

예시 Supplier<T> 인터페이스

@FunctionalInterface
public interface Supplier<T> {
		/**
     * Gets a result.
     *
     * @return a result
     */
    T get(); // T 타입 객체를 찍어낸다
}
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

Supplier<T>를 입력으로 받는 메서드는 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한

public class SpellChecker { 
	private final Lexicon dictionary; 

	private SpellChecker(Supplier<Lexicon> dictionary) { 
    		this.dictionary = Objects.requireNonNull(dictionary); 
 	} 

	public boolean isValid(String word) { return true; } 
	public List<String> suggestions(String typo) { return null; } 

	public static void main(String[] args) {
		Lexicon lexicon = new KoreanDictionary();
		SpellChecker spellChecker = new SpellChecker(() -> lexicon);
		spellChecker.isValid("hello");
	}
}

interface Lexicon{}

class KoreanDictionary implements Lexicon {}
class TestDictionary implements Lexicon {} // test가 가능해짐

결론 : 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.

💡Item 6. 불필요한 객체 생성을 피하라

똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다

profile
백엔드 개발자

0개의 댓글