ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 2장 - 객체 생성과 파괴
    📕 book/이펙티브 자바 2022. 10. 25. 13:18

    📌 생성자 대신 정적 팩터리 메서드를 고려하라


    클라이언트는 클래스의 인스턴스를 얻는 전통적인 수단으로는 public 생성자이다.

    클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다.

    정적 팩터리 메서드 (static factory method)

    • 해당 클래스의 인스턴스를 반환한다 (객체를 생성하는 메서드)
    • ex) Optional의 of() : ‘new’ 키워드 대신 ‘of()’ 메서드를 이용해 객체를 만든다.

    장점

    1. 이름을 가질 수 있다.

    public class Hat {
    
    	private String color;
    	private int cost;
    
    	private Hat(String color, int cost) {
            this.color= color;
            this.cost = cost;
        }
    
    	public static Hat createRedHat(int cost) {
    		return new Hat("red", cost);
    	}
    
    	public static Hat createBlueHat(int cost) {
    		return new Hat("blue", cost);
    	}
    }
    

    위 코드가 있을 때, 외부에서 인스턴스를 얻을 때

    Hat hat = new Hat(”red”, 3000); 보다

    Hat hat = Hat.createRedHat(3000); 이런 식으로 호출하는 것이 의미 파악하기에 더 쉽다.

     

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

    • 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피한다.
    • 따라서, 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 준다.
    public class Book {
        private String type;
        
        private static final Book poemBook = new Robot("poem");
        private static final Book novelBook = new Robot("novel");
    
        public Book(String type) {
            this.type = type;
        }
    
        public static Book getInstancePoemBook() {
            return poemBook;
        }
    
        public static Book getInstanceNovelBook() {
            return novelBook;
        }
    }
    
    Book novelBook = Book.getInstanceNovelBook();
    

    이런 식으로 인스턴스를 캐싱해서 사용한다.

     

    인스턴스 통제 클래스
    • 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스를 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있다.
    • 이런 클래스를 인스턴스 통제 클래스라고 한다.

    인스턴스를 통제하는 이유 ❓

    • 이를 통해 클래스를 싱글턴 으로 만들 수도
    • 인스턴스화 불가 로 만들 수 도 있다.
    • 또한, 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장 할 수 있다.
    • (a == b 일 때만, a.equals(b) 가 성립)

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

      ⇒ 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성’을 선물한다.

    생성자를 사용하면 하위 클래스를 반환할 수 없지만 정적 팩토리 메서드를 사용하면 하위 클래스를 마음대로 리턴할 수 있다.

     

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

    • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
    • ex) EnumSet 클래스
      • 원소의 수에 따라 두 가지 하위 클래스중 하나의 인스턴스를 반환
      • 원소가 64개 이하면 RegularEnumSet 인스턴스 반환
      • 원소가 65개 이상이면 JumboEnumSet 인스턴스 반환

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

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

    단점

    1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들수 없다.
    2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
      • API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 지어야 한다.

     

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


    정적 팩터리와 생성자는 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.

    ❓매개변수가 많다면???

    • 점층적 생성자 패턴
    • 자바빈즈 패턴
    • 빌더 패턴

    점층적 생성자 패턴

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

    확장성이 좋지 않고 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

    자바빈즈 패턴

    매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식

    public class NutritionFacts {
        
        private final int calories = 0;
        private final int fat = 0;
        private final int sodium = 0;
    
        public NutritionFacts() {}
    
        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;
            }
            
            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 클래스는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아 둔다.

    • 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.(method chaining)

    이 코드는 쓰기 쉽고 읽기 쉽다.

    계층적으로 설계된 클래스와 함께 쓰기에 좋다.

     

    하지만, 객체를 만들기 전에 빌더부터 만들어야 하고 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.

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


    싱글턴 (singleton)

    • 인스턴스를 오직 하나만 생성할 수 있는 클래스

    싱글턴을 만드는 방식

    • private 생성자, public static 멤버가 final 필드인 방식
    • private 생성자, 정적 팩토리 메서드를 public static 필드로 제공
    • 원소가 하나인 Enum 타입 선언

    private 생성자, public static 멤버가 final 필드인 방식

    public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        private Elvis() { }
    
        public void leaveTheBuilding() { }
        ...
    }
    
    Elvis elvis1 = Elvis.INSTANCE;
    Elvis elvis2 = Elvis.INSTANCE;
    
    elvis1 == elvis2 // true;
    elvis.equals(elvis2) // true;
    

    public, protected 생성자가 없으므로 Elvis 클래스가 초기화 될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

     

    예외 - 딱 한 가지

    • 리플렉션 API인 AccessibleObject.setAccessible 을 사용해 private 생성자를 호출할 수 있다.

     

    private 생성자, 정적 팩토리 메서드를 public static 필드로 제공

    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 인스턴스는 결코 만들어지지 않는다.

    장점

    • 마음이 바뀌면 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
      • 정적 팩토리 메서드(getInstance)가 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.
    • 원하면 정적 팩터리를 제네릭 싱글턴 팩터리 로 만들 수 있다.
    • 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
      • supplier - 매개변수 ❌, 리턴 값 ⭕
      • Elvis::getInstance ⇒ Supplier<Elvis>

     

    원소가 하나인 열거(Enum)타입 선언

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

    public 필드 방식과 비슷하지만, 더 간결하고 추가 노력 없이 직렬화할 수 있다.

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

     

    📌 인스턴스화를 막으려거든 private 생성자를 사용하라


    static 메서드와 static 필드만을 담은 클래스

    → 객체지향적이지 않다, 그러나 나름 쓰임새가 있다.

    • java.lang.Mathjava.util.Arrays처럼 기본 타입 값 이나 배열 관련 메소드 들을 모아놓은 것
    • java.util.Collections처럼 특정 인터페이스 를 구현하는 객체를 생성해주는 정적 메소드 or 팩토리 를 모아놓은 것
    • final 클래스와 관련한 메소드들을 모아놓을 때도 사용한다.
    • (final 클래스를 상속해 하위 클래스에 메소드를 넣는 것은 불가능하기 때문이다.)

    추상클래스로 만드는 것은 인스턴스화를 막을 수 없다.

    • 하위 클래스를 만들어 인스턴스화 하면 그만

    컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때 뿐이다.

    private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

    • 이는 상속을 불가능하게 하는 효과도 있다.
      • 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막힘.

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


    의존 객체 주입

    • 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식
    public class SpellChecker {
    	private final Lexicon dictionary;
    
    	public SpellChecker(Lexicon dictionary) {
    		this.dictionary = Objects.requireNonNull(dictionary);
    	}
    
    	public boolean isValid(String word) {...}
    	public List<String> suggests(String typo) {...}
    }

    의존 객체 주입은 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.

    이 패턴의 괜찮은 변형으로 생성자에 자원 팩터리를 넘겨주는 방식이 있다.

    • 팩터리 : 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
    • 즉, 팩터리 메서드 패턴을 구현한 것.
    • ex) Supplie<T> 인터페이스
    Mosaic create(Supplier<? extends Tile> tileFactory) {...}

    Supplier<T>를 입력으로 받는 메서드는 명시한 타입(Tile)의 하위 타입이라면 무엇이든 생성할 수 있는 팩토리를 넘길 수 있다.

    의존 객체 주입이 유연성과 테스트 용이성을 개선해주지만, 의존성이 수 천개 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다.

    • Dagger, Guice, Spring같은 의존 객체 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있다.

     

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


    똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

    • 재사용은 빠르고 세련되다
    • 불변 객체는 언제든 재사용할 수 있다.

    생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.

    • ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

    생성자는 호출할 때 마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않음!

    정규 표현식

    String.mathes 메서드는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.

    static boolean isRomanNumeral(String s) {
    	return s.matches("^(?=.)M*(C[MD]|D?C{0,3}(X[CL]|L?X{0,3})(I[XV]|V?I{0,3}$");
    }
    • 위 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져 곧바로 가비지 컬렉션의 대상이 된다.
    public class RomanNumerals {
    	private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3}(X[CL]|L?X{0,3})(I[XV]|V?I{0,3}$");
    
    	static boolean isRomanNumeral(String s) {
    			return ROMAN.matcher(s).matches();
    	}
    }
    • 성능 개선을 위해 불변인 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 메서드가 호출될 때마다 이 인스턴스를 재사용한다.

    오토박싱 (Auto Boxing)

    오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술

    • 오토박싱은 기본타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아님
    private static long sum() {
    	Long sum = 0L;
    		
    	for (long i = 0 ; i < Integer.MAX_VALUE ; i++) {
    			sum += i;
    	}
    	return sum;
    }

    sum 변수를 long 이 아닌 Long으로 선언해서

    불필요한 Long 인스턴스가 2^{31} 개나 만들어 진다.

    ( long 타입의 i 가 Long 타입인 sum에 더해질 때 마다 불필요한 Long 인스턴스가 생성된다 )

    박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자

    📌 다 쓴 객체 참조를 해제하라


    자바처럼 가비지 컬렉터를 갖춘 언어를 사용하면 메모리 관리에 신경쓰지 않아도 된다고 오해할 수 있다.

    ⇒ 절대 아님 ❌

    public class Stack {
    	private Object[] elements;
    	private int size = 0;
    	private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    	public Stack() {
    			elements = new Object[DEFAULT_INITIAL_CAPACITY];
    	}
    
    	public void push(Object e) {
    			ensureCapacity(); //원소를 위한 공간을 at least 1개 이상 확보한다.
    			elements[size++] = e;
    	}
    
    	public Object pop() {
    		if (size == 0)
    			throw new EmptyStackException();
    		return elements[--size];
    	}
    		
    	private void ensureCapacity() {
    		if(elements.length == size) 
    			elements = Arrays.copyOf(elements, 2 * size + 1);
    	}
    }

    위 코드는 어떤 테스트도 거뜬히 통과할 것이고 문제가 없어 보인다.

    하지만 오래 실행하다보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

    ‘메모리 누수’

    public Object pop() {
    	if (size == 0)
    		throw new EmptyStackException();
    	return elements[--size]; // 이 부분
    }

    위 코드에서는 스택이 커졌다가 줄어들 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.

    • pop()이 동작하면 스택에서 객체가 빠져나가고 스택이 줄어든다.
    • 스택에서 꺼내진 객체들은 가비지 컬렉터가 회수하지 않는다
    • 더 이상 그 객체가 필요하지 않더라도

    why❓

    • 스택이 그 객체들의 다 쓴 참조(obsolete reference) 를 여전히 가지고 있기 때문
    • 다 쓴 참조 : 앞으로 다시 쓰지 않을 참조

     

    가비지 컬렉션 언에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다.

    • 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다.

    ❗ 해결 방법 : 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다!!

    public Object pop() {
    	if (size == 0)
    		throw new EmptyStackException();
    	Object result = elements[--size]; 
    	elements[size] = null; // 다 쓴 참조 해제
    	return result;
    }
    

    객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

    • 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효범위(scope) 밖으로 밀어내는 것

    null 처리를 해야 하는 경우

    1. 자기 메모리를 직접 관리하는 클래스
      • 위의 Stack 클래스는 자기 메모리는 직접 관리한다.
      • elements 배열로 저장소 풀을 만들어 원소들을 관리
      • 배열의 활성 영역에 속한 원소들은 사용되고 비활성 영역은 쓰이지 않음.
      가비지 컬렉터는 이 사실을 알 수 없다.

      그러므로, 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더 쓰지 않을 것을 가비지 컬렉터에 알려야 함.

    2. 캐시객체 참조를 캐시에 넣고, 그 사실을 잊은 채 객체를 다 쓴 후에도 캐시에 놔두는 일이 있다.해법
      • 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만든다.
      • 다 쓴 엔트리는 그 즉시 자동으로 제거된다
      • WeakHashMap은 이러한 상황에서만 유용하다..!
    3. 리스너 혹은 콜백
      콜백을 등록만 하고 명확히 해지하지 않으면 콜백이 계속 쌓여만 간다.
      콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해 간다.
      ex) WeakHashMap에 키로 저장한다.

      약한 참조 (Weak Reference)
      • 약한 참조만 가진 객체는 GC의 대상이 된다
      • 주로 캐시에 활용
      • java.lang 패키지의 WeakReference 클래스를 사용
      • WeakReference<Integer> weak = new WeakReference<Integer>(item);

      부드러운 참조(Soft Reference) : 메모리가 부족하면 GC의 대상이 된다. 부족하지 않으면 대상 ❌
      • SoftReference<Integer> soft = new SoftReference<>(item);
        • 원본은 없고 대상을 참조하는 객체만 존재

      강한 참조 (Strong Reference)
      : 일반적인 참조로 강한 참조를 가진 객체는 GC의 대상 ❌
      • 일반적인 유형의 참조 ( Integer item = 1 )

     

    📌 finalizer 와 cleaner 사용을 피하라


    자바는 두 가지 객체 소멸자를 제공

    • finalizer
    • cleaner

    finalizer

    finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요

    클래스에 finalizer를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있다.

    cleaner

    finalizer 보단 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요

    사용하지 않아야 하는 이유

    1. 이 둘은 즉시 수행된다는 보장이 없다.
      ⇒ 따라서, 제때 실행되어야 하는 작업은 절대 할 수 없다.

    2. 수행 시점 뿐 아니라 수행 여부조차 보장하지 않는다.
      ⇒ 상태를 영구적으로 수정하는 작업에서는 절대 finalizer, cleaner에 의존하면 안된다.

    3. 심각한 성능 문제
      • finalizer 가 가비지 컬렉터의 효율을 떨어뜨린다.
    4. finalizer를 사용한 클래스는 finalizer 공격에 노출되 심각한 보안 문제를 일으킬 수 있다.

    📌 try-finally 보다 try-with-resources 를 사용


    try-finally는 제대로 자원을 회수하도록 짜기 어렵고 중첩 try문의 경우 둘 다 예외가 발생해도 내부 예외만 출력한다는 문제가 있다.

    try-with-resources 형태를 사용해주는 것이 좋다.

     

    try-with-resoureces 는 자원 사용 후 자동으로 close()해주고 예외 발생시 모두 발생할 수 있다.

    close()를 사용하니 AutoClosableimplement 해줘야 한다.

    • 코드의 실행 위치가 try문을 벗어나면 try(..) 에서 선언된 객체의 close() 메소드들을 호출한다!!

    좋지 않은 방법 ❌

    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try{
        	return br.readLine();
        } finally {
        	br.close();
        }
    }

    책에서 권장 방법 📚

    static String firstLineOfFile(String path) throws IOException {
        try(BufferedReader br = new BufferedReader(new FileReader(path))){
        	return br.readLine();
        } catch (IOException e) {
        	return dfaultVal;
        }
    }

    좋은 방법 👍

    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try(br){
        	return br.readLine();
        } catch (IOException e) {
        	return dfaultVal;
        }
    }

    참고 자료

    이펙티브 자바 Effective Java 3/E - YES24

    '📕 book > 이펙티브 자바' 카테고리의 다른 글

    7장 - 람다와 스트림  (0) 2022.11.07
    6장 - 열거타입과 애너테이션  (0) 2022.11.04
    5장 - 제네릭  (0) 2022.11.01
    4장 - 클래스와 인터페이스  (0) 2022.11.01
    3장 - 모든 객체의 공통 메서드  (0) 2022.10.26

    댓글

Designed by Tistory.