ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [이펙티브 자바 : 1장] 객체 생성과 파괴
    Java & Kotlin 2020. 6. 21. 17:15
    반응형

    이번 장은 객체의 생성과 파괴를 다룬다. 객체를 만들어야 할 때와 만들지 말아야 할 떄를 구분하는 법, 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법, 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령을 알아본다.

     

     

     

     

     

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

     

     

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

    * 디자인 패턴에서의 팩터리 메서드(Factory Method)와 다르다.

     

     

    장점 1 : 이름을 가질 수 있다.

     

    생성자 BigInteger(int, int, Random)에 비해 정적 팩터리 메서드인 BigInteger.probablePrime()은 '값이 소수인 BigInteger를 반환한다'라는 의미를 더 잘 설명한다.

     

    이름을 가질 수 있는 정적 팩터리 메서드에는 시그니처1에 대한 제약이 없다.

    한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.

     

    public static void main(String[] args) {
    
      // 생성자를 사용하여 값이 소수인 BigInteger 생성
      BigInteger integer1 = new BigInteger(5, 3, new Random());
    
      // 정적 팩터리 메서드를 사용하여 값이 소수인 BigInteger 생성
      BigInteger integer2 = BigInteger.probablePrime(5, new Random());
      
    }

     

     

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

     

    불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 대표적인 예인 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다. 따라서 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 준다.

     

    public final class Boolean {
    
        private static final Boolean TRUE = new Boolean(true);
        private static final Boolean FALSE = new Boolean(false);
        
        private final boolean value;
        
        private Boolean(boolean b) {
        	this.value = b;
        }
        
        // 정적 팩터리 메서드
        public static Boolean valueOf(boolean b) {
        	return b ? TRUE : FALSE;
        }
        
    }
    
    public class Test {
        public static void main(String[] args) {
      	Boolean b = Boolean.valueOf(true);
        }
    }

     

     

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

     

    반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다. 예를들어 Collections 클래스는 정적 팩터리 메서드를 통해 약 45개의 유틸리티 구현체를 제공한다. 컬렉션 프레임워크는 이 45개의 클래스를 공개하지 않기 때문에 API 외견을 훨씬 작게 만들 수 있었다. API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮췄다.

     

     

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

     

    EnumSet 클래스는 public 생성자 없이 오직 정적 팩터리만 제공하는데, OpenJDK에서는 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다.

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

    원소가 64개 이하면 RegularEnumSet, 65개 이상이면 JumboEnumSet을 반환한다. 클라이언트는 이 두 클래스의 존재를 모르기 때문에 RegularEnumSet이 필요 없어지면 다음 릴리즈에 삭제해도 아무 문제가 없다. 반환되는 클래스가 EnumSet의 하위 클래스라면 어느 것이든 상관이 없다.

     

     

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

     

    JDBC의 경우,

    DriverManager.registerDriver()가 프로바이더 등록 API

    DriverManager.getConnection()은 서비스 액세스 API.

    그리고 Driver 서비스 프로바이더 인터페이스 역할을 한다.

     

    자바 6부터는 java.util.ServiceLoader라는 일반적인 용도의 서비스 프로바이더를 제공하지만, JDBC가 그 보다 이전에 만들어졌기 때문에 JDBC ServiceLoader를 사용하진 않는다.

    이때문에 자바 버전에 상관없이 이전의 코드들이 안정적으로 작동하게 만들 수 있다.

     

     

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

     

    이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.

     

     

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

     

    생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.

     

     

     

     

     

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

     

     

    대안 1 : 점층적 생성자 패턴

     

    public class NutritionFacts {
    
        private final int servingSize;
        private final int servings;
        private final int calories;
        private final int fat;
    
        public NutritionFacts(int servingSize) {
            this(servingSize, 0);
        }
    
        public NutritionFacts(int servingSize, int servings) {
            this(servingSize, servings, 0);
        }
    
        public NutritionFacts(int servingSize, int servings, int calories) {
            this(servingSize, servings, calories, 0);
        }
    
        public NutritionFacts(int servingSize, int servings, int calories, int fat) {
            this.servingSize = servingSize;
            this.servings = servings;
            this.calories = calories;
            this.fat = fat;
        }
    
    }

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

     

     

    대안 2 : 자바빈즈 패턴

     

    public class Main {
    
        private int servingSize = -1;
        private int servings    = -1;
        private int calories    = 0;
        private int fat         = 0;
    
        public Main() { }
    
        public void setServingSize(int servingSize) { this.servingSize = servingSize; }
        public void setServings(int servings) { this.servings = servings; }
        public void setCalories(int calories) { this.calories = calories; }
        public void setFat(int fat) { this.fat = fat; }
        
    }

     

    단점 1 : 객체 하나를 만들려면 메서드를 여러 개 호출해야 한다.

    단점 2 : 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.

    단점 3 : 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야 한다.

     

     

    대안 3 : 빌더 패턴

     

    점층적 자바 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비하였다. 

     

    클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개 변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그런 다음 빌더가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.

    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 {
        	// 2개의 필드는 필수이므로 Builder객체를 생성할 때 매개변수로 받는다.
            private final int servingSize;
            private final int servings;
    		
            // 선택 필드는 0으로 초기화한다.
            private  int calories     = 0;
            private  int fat          = 0;
            private  int sodium       = 0;
            private  int carbohydrate = 0;
    
            public Builder(int servingSize, int servings) {
                if(servingSize < 0) throw new IllegalArgumentException("Serving 크기가 0보다 작습니다.");
                this.servingSize = servingSize;
                this.servings = servings;
            }
    		
            // 빌더가 제공하는 세터 메서드. 필수가 아니다.
            public Builder calories(int calories) { this.calories = calories; return this; }
            public Builder fat(int fat) { this.fat = fat; return this; }
            public Builder sodium(int sodium) { this.sodium = sodium; return this; }
            public Builder carbohydrate(int carbohydrate) { this.carbohydrate = carbohydrate; return this; }
    		
            // 클라이언트가 build 메서드를 호출하면 객체가 생성된다.
            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).bulde();

     

     

     

     

     

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

     

     

    싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스이다.

    함수와 같은 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다.

     

    구현 방식 공통 : 생성자를 private으로 선언한다.

     

     

    구현 방식 1 : public static final 필드 방식

    public class Elvis {
        
        // 인스턴스에 유일하기 접근할 수 있는 수단
        public static final Elvis INSTANCE = new Elvis();
        
        private Elvis() {}
        
    }

    private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 한 번만 호출된다. public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

     

    한 가지 예외 상황은 권한이 있는 클라이언트가 AccessibleObject.setAccessible()을 사용해 private을 호출할 수 있다. 싱글턴은 이러한 공격을 방어하기 위해 생성자를 수정하여 두 번째 객체가 호출되려 할 때 예외를 던지게 만든다.

     

    장점 1 : 해당 클래스가 싱글턴임이 API에 명백히 드러난다.

    장점 2 : 간결하다.

     

     

    구현 방식 2 : 정적 팩터리 방식의 싱글턴

    Elvis.getInstance는 항상 같은 객체의 참조를 반환한다.

    public class Elvis {
    
        public static final Elvis INSTANCE = new Elvis();
    
        private Elvis() { }
        
        // 인스턴스에 유일하기 접근할 수 있는 수단
        public static Elvis getInstance() {
            return INSTANCE;
        }
    
    }

     

    장점 1 : API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.

     

    장점 2 : 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.

    제네릭 싱글턴 팩터리란?
    때때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 하는데, 제네릭을 활용하면 좋다.
    제네릭을 사용하지 않았다면 요청 타입마다 형변환하는 정적 메서드를 만들어야 했을 것이다.

    @SuppressWarnings("unchecked")
    public static <T> Comparator<T> reverseOrder() {
        return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
    }

     

    장점 3 : 정적 팩터리 메서드 참조를 공급자(supplier)로 사용할 수 있다.

    Supplier란?
    java.util.function 패키지의 함수형 인터페이스 중 하나
    매개변수는 없고 리턴 값을 있다.


    Elvis::getInstance를 Supplier<Elvis>로 이용하는 방식이다.

    이러한 장점들이 굳이 필요하지 않다면 public 필드 방식이 좋다.

     

     

    구현 방식 3 : 원소가 하나인 열거 타입 선언

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

    public 필드 방식과 비슷하지만, 더 간결하고, 추가 노력 없이 직렬화할 수 있고, 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

     

    가장 좋은 방법이지만 싱글턴이 Enum 외에 클래스를 상속해야 할 때 이 방법은 사용할 수 없다.

     

     

    단점 1 : 싱글턴을 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.

     

    인터페이스를 구현한 싱글턴이 아니라면 싱글턴 인스턴스를 mock 오브젝트2로 대체할 수 없기 때문이다.

     

     

     

     

     

    [ITEM 4] 인스턴스화를 막으려거든 private 생성자를 사용하라

     

     

    단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때 사용한다. 추상클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 하위 클래스를 만들어 인스턴스화하면 그만이기 때문이다. 대신 방법은 간단한데 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

    public class UtilityClass {
    	// 기본 생성자가 만들어지는 것을 막는다 (인스턴스화 방지용).
        private UtilityClass() {
        	throw new AssertionError();
        }
    
    	...
    }

    생성자가 private이니 외부에서 호출할 수 없고 실수로 내부에서 생성자를 호출할 것을 대비해 AssertionError를 던지도록 구현했다. 이 방법은 상속을 불가능하게 하는 효과도 있다.

     

     

     

     

     

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

     

     

    많은 클래스가 하나 이상의 자원에 의존한다.

     

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

    public class SpellChecker {
        private static final Lexicon dictionary = new Lexicon();
        
        private SpellChecker() {}
        
        public static boolean isValid(String word) { ... }
        public static List<String> suggestions(String typo) { ... }
    }

     

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

    public class SpellChecker {
        private final Lexicon dictionary = new Lexicon();
    
        private SpellChecker() {}
        public static SpellChecker INSTANCE = new SpellChecker(...);
    
        public boolean isValid(String word) { ... }
        public List<String> suggestions(String typo) { ... }
    }

    dictionary가 하나의 클래스로 고정되어있다. 실전에서는 SpellCheck가 여러 용도의 사전을 사용할 수 있어야한다.

     

    의존 객체 주입 : 유연성과 테스트 용이성을 높여준다.

    public class SpellChecker {
        private final Lexicon dictionary;
        
        public SpellChecker(Lexicon dictionary) {
        	this.dictionary = Objects.requireNonNull(dictionary);
        }
        
        public boolean isValid(String word) { ... }
        public List<String> suggestions(String typo) { ... }
    }

    인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.

     

     

     

     

     

    [ITEM 6] 불필요한 객체 생성을 피하라

     

     

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

     

    하지 말아야 할 극단적인 예 : String s = new String("bikini");

    이 문장은 실행될 때마다 String 인스턴스를 새로 만든다. 

     

    개선된 버전 : String s = "bikini"; (메모리에 한 번 생성되면 같은 문자열을 가지는 객체가 이 주소를 참조한다.)

    이 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.

     


    1. 메서드 시그니처(Method Signature)는 메서드 이름과 매개변수 리스트의 조합이다.

     

    2. mock object는 테스트에 사용하는 가짜(모의) 객체이다.

    반응형
Designed by Tistory.