글 검색

공개된 글의 제목과 본문을 검색합니다.

    닫기 Esc

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

    java
    public static Boolean valueOf(boolean b) {
    		return b ? Boolean.TRUE : Boolean.FALSE;
    }
    Boolean 래퍼타입의 정적 생성 팩토리 메서드

    정적 팩터리 메서드를 사용하기 이전에는 어땟는가?#

    전통적인 public 접근자의 생성자를 이용해 객체를 생성했었다.

    정적 팩터리 메서드를 사용하면 뭐가 좋은가?#

    • 생성자가 이름을 가질 수 있다.
      • BigInteger(int, int, Random) 보다는 BigInteger.pobablePrime이 명확한 의미를 가진다.
    • 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
      • 생성자를 통해서 만들어진 객체는 매번 새로운 객체를 만들어낸다.
      • 하지만 정적 팩터리 메소드를 통한 호출은 미리 생성해둔 객체를 전달할 수 있다.
        • 때문에 불변의 객체를 미리 만들어 둘 수 있다!
      • 이런 클래스를 인스턴스 통제 클래스라 한다.
    • 반환 타입의 하위 타입 객체를 반환할 수 있다.
      • 반환할 객체를 자유롭게 선택할 수 있게 하는 유연성을 선물한다.
        • 이 유연성을 유지하면 구현 클래스를 공개하지 않고도 객체를 반환할 수 있다.
      • 컬렉션 프레임워크는 45개의 클래스를 공개하지 않기 때문에 API 외견을 훤씬 작게 만들 수 있었다.
        • API가 작아진 것은 물론 개념적인 무게를 낮춰 익혀야 하는 개념의 수와 난이도를 낮췄다.
    • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
      • 반환 타입이 하위타입이기만 하다면 어떤 클래스의 객체를 반환하든 상관없다.
    • 정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않아도 된다.
      • 이런 유연함은 서비스 제공자 프레임워크를 만다는 근간이 된다.
      • 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다.
        • 구현체의 동작을 정의하는 서비스 인터페이스
        • 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API
        • 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API
      • 서비스 제공자 프레임워크는 여러 변형이 있고
        • 브리지 패턴, 의존 객체 주입(DI)도 강력한 서비스 제공자라고 생각할 수 있다

    정적 팩터리 메서드의 단점?#

    • 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
      • 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 제약을 지켜야한다는 점에서 오히려 장점으로 받아들일 수 있다.
    • 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
      • 철저한 문서화가 필요하다.
      java
      // from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
      Date d = Date.from(instant);
      
      // of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
      Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
      
      // instance 혹은 getInstance: 매개변수로 명시한 인스턴스를 반환, 같은 인스턴스임을 보장하진 않는다.
      StackWalker luke = StackWalker.getInstance(options);
      
      // create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스 생성을 보장
      Object newArray = Array.newInstance(classObject, arrayLen);
      
      // getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용 ("Type"은 팩터리 메서드가 반환할 객체의 타입)
      FileStore fs = Files.getFileStore(path); 
      
      // newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용
      BufferedReader br = Files.newBufferedReader(path);
      
      // type: getType과 newType의 간결한 버전
      List<Complaint> litany = Collections.list(legacyLitany);

    결론#

    • 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
    • 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

    2. 생성자 매개변수가 많다면 빌더를 고려하라#

    java
    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;
    
        private NutritionFacts(Builder builder) {
            servingSize = builder.servingSize;
            servings = builder.servings;
            calories = builder.calories;
            fat = builder.fat;
            sodium = builder.sodium;
            carbohydrate = builder.carbohydrate;
        }
    
        public static class Builder {
            // 필수 매개변수
            private final int servingSize;
            private final int servings;
    
            // 선택 매개변수
            private int calories = 0;
            private int fat = 0;
            private int sodium = 0;
            private int carbohydrate = 0;
    
            // 필수 매개변수만을 담은 Builder 생성자
            public Builder(int servingSize, int servings) {
                this.servingSize = servingSize;
                this.servings = servings;
            }
    
            // 선택 매개변수의 setter, Builder 자신을 반환해 연쇄적으로 호출 가능
            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;
            }
            
            // build() 호출로 최종 불변 객체를 얻는다.
            public NutritionFacts build() {
                return new NutritionFacts(this);
            }
        }
    }

    생성자를 만들어야 하는데 매개변수가 너무 많다…#

    • 모두 동일한 타입의 서로 다른 값들을 저장하는 필드 변수들로 인해서 너무나도 길어진 생성자!
      • 점층적으로 필요한 값들의 인자만 받아서 생성하자! (점층적 생성자 패턴)
        • 아 그런데… 너무 생성자가 많아져서 코드를 읽기도 어렵고 유지보수가 너무 어렵다…
        • 그리고 인자를 잘못줘도 런타임에서 발견되기 때문에 사전에 오류를 잡아내기가 어렵다…
      • 그러면 Setter를 통해서 값의 삽입을 진행하면 어떨까? (자바빈즈 패턴)
        • 아 그런데 이러면 모든 값들을 재설정 해야하니 final타입으로 변수를 생성할 수 없어 불변성을 가지는 객체를 만들 수 없구나...!
        • 객체를 얼리고 사용하면 되지만, 실수해서 얼리지 않으면 런타임 오류가 생겨버린다…

    어디 생성할때 명확하게 어떤 매개 변수에 어떤 값이 들어가고 불변으로도 만들 수 있는 방법이 없을까?#

    대신 귀여운 빌더패턴을 드리겠습니다.

    • 빌더 패턴은 파이썬, 스칼라, 자바스크립트 등에 있는 명명된 선택적 매개변수를 흉내낸 패턴 (코틀린도 있답니다.)
    • 원한다면 디폴드 값을 넣을 수 도있어요!
    • 추가적으로 빌더에 외부의 공격에 대비한 필드 검사를 추가해야합니다!

    Lombok으로 쉽게 빌더 패턴을 생성할 수 있어요!#

    java
    @Builder
    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;
    }

    결론#

    • 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.
    • 매개변수 중 다수가 필수가 아니거나 같은 타입이면 더 그렇다.
    • 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

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

    싱글턴이란?#

    • 메모리 내에 오직 하나만의 인스턴스를 생성할 수 있는 디자인패턴의 일종
    • 스프링에서 생성되는 빈도 싱글톤의 형태로 생성됩니다. (옵션에 따라 다름)
    • 싱글턴은 무상태성을 가져야하며 싱글턴을 사용하는 클래스는 테스트가 어렵다!

    싱글톤의 장점#

    • 한번의 생성으로 객체의 재사용이 가능하기 때문에 메모리 낭비를 방지할 수 있음
    • 싱글톤으로 생성된 객체는 전역성을 띄기 때문에 공유가 용이하다.

    싱글톤의 단점#

    • 싱글턴을 사용하는 클래스는 테스트하기 어렵다.
    • 싱글턴은 올바르게 구현하기가 어렵다.
    • 싱글턴은 상속받기가 어렵다.
    • 멀티스레드 환경에서 하나의 인스턴스를 보장하기 어렵다.

    전통적인 싱글톤을 만드는 방법#

    java
    public class Singleton {
        private static Singleton uniqueInstance;
        private Singleton() {}
        public static Singleton getInstance() {
            if (uniqueInstance == null) {
                uniqueInstance = new Singleton();
            }
            return uniqueInstance;
        }
    }
    • 멀티스레드 환경에서 하나의 인스턴스를 보장하기 어렵다

    조금 개선된 싱글톤 생성 방법#

    java
    public class Singleton {
        private static Singleton uniqueInstance;
        private Singleton() {}
        public static synchronized Singleton getInstance() {
            if (uniqueInstance == null) {
                uniqueInstance = new Singleton();
            }
            return uniqueInstance;
        }
    }
    • synchronized 예약어를 통해서 하나의 인스턴스만을 보장하지만 성능이 떨어진다.
    • 동기화가 필요한 시점은 오로지 인스턴스의 생성을 진행할 때 뿐이기 때문에 매번 동기화를 하는것은 비효율적임

    조금 더 개선된 싱글톤 생성 방법#

    java
    public class Singleton {
        private static Singleton uniqueInstance = new Singleton();
        private Singleton() {}
        public static synchronized Singleton getInstance() {
            return uniqueInstance;
        }
    }
    방법1 - 인스턴스를 처음부터 만들어준다.
    java
    public class Singleton {
        private volatile static Singleton uniqueInstance;
        private Singleton() {}
        public static synchronized Singleton getInstance() {
            if (uniqueInstance == null) {
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }
    방법2 - DCL을 사용하여 동기화 코드를 줄인다.

    가장 이상적인 방법 (Enum 이용)#

    java
    public enum Elvis {
    		INSTANCE;
    
    		public void leaveTheBuilding() { ... }
    }
    • Enum은 메모리내에 단 하나의 인스턴스만을 JVM이 보장한다.
      • 또한 리플렉션을 이용한 값 변경에도 인스턴스가 생기는 일을 완벽하게 막아준다.
      • 심지어 직렬화도 용이하다!
    • 때문에 대부분의 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

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

    가끔 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을때가 있을 것이다.

    • 객체지향 적으로 사고하지 않는 이들이 종종 남용하는 방식이기에 그리 곱게 보이지는 않지만, 분명 나름의 쓰임새가 있다.
    • java.lang.Math와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.

    추상클래스를 만들면 되는게 아닐까?#

    • 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
      • 하위 클래스를 만들어 인스턴스화 하면 그만이기 때문!

    생성자를 private로 만들어서 클래스의 인스턴스화를 막자#

    java
    public class UtilityClass {
    		// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용)
    		private UtilityClass() {
    				throw new AssertionError(); // 생성자 내부 호출 시 명시적인 에러 던지기
    		}
    		...
    }
    • 생성자가 private이기 때문에 상속이 불가능하다.

    여담#

    • 정적 메소드, 변수는 별도의 메모리 영역에 보관되기 때문에 GC대상이 아니다!
    • 때문에 메모리를 조금이라도 아껴서 사용할 때 용이하다.

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

    • 많은 클래스가 하나 이상의 자원에 의존한다.
    java
    public class SpellChecker {
    		private static final Lexicon dictionary = ...;
    		private SpellChecker() {} // 객체 생성 방지 
    		
    		public static boolean isValid(String word) { ... } 
    		public static List<String> suggestions(String typo) { ... } 
    }
    정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다
    java
    public class SpellChecker { 
    		private static final Lexicon dictionary = ...;
    
    		private SpellChecker(...) {}
    		public static SpellChecker INSTANCE = new SpellChecker(...);
    		
    		public static boolean isValid(String word) { ... } 
    		public static List<String> suggestions(String typo) { ... } 
    }
    싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.
    java
    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; }
    }
    의존성 주입을 통해서 유연성과 테스트 용이성을 높여준다.

    자바8 Supplier<T>#

    java
    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }
    
    Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
    함수형 인터페이스를 통해서 제공이 가능하다.

    정리#

    • 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
    • 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.

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

    문자열 객체#

    java
    // 매번 새로운 인스턴스를 생성
    String s = new String("bikini");
    // 한번 생성된 인스턴스를 재활용 함
    String s = "bikini";

    정적 타입#

    java
    static boolean isRomanNumeralSlow(String s) {
        return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
    
    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 메모리)에 생성되기 때문에 매번 새로운 인스턴스를 생성하지 않고 캐싱된 객체를 이용할 수 있다.

    오토박싱#

    java
    private static long sum() {
    		Long sum = 0L;
    		for(long i=0; i<=Integer.MAX_VALUE; i++) {
    				sum += i;
    		}
    		return sum;
    }
    • 우리 자바 프로그래머가 가장 조심해야 하는 문제라고 생각한다!
    • 오토박싱은 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.

    오해 금지#

    • 객체 생성은 비싸니 피해야 한다로 오해하면 안 된다.
    • 특히나 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
      • 프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.
    • 그렇다고 단순히 객체 생성을 피하기 위해 자신만의 객체 풀(pool)을 만들지는 말자.
    • 요즘 JVM의 GC는 상당히 잘 최적화 되어서, 가벼운 객체를 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.

    방어적 복사#

    • 방어적 복사가 필요한 상황에서 객체를 재사용했을 때 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자.

    7. 다 쓴 객체 참조를 해제하라#

    java
    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();
            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); 
        }
    }
    메모리 누수가 일어나는 곳을 찾아봐요!
    • GC는 마크앤 스윕을 통해서 레퍼런스 카운트를 검사하는데 배열 내 해제되지 못한 레퍼런스 카운팅 때문에 영원히 해제되지 못한채 구천을 떠돌다 결국 OOM을 맞이할 것이다.
    java
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }
    메모리를 꼭 해제해주도록 합시다!
    • 다 쓴 참조를 null 처리 하면 다른 이점도 따라온다. 만약 null 처리한 참조를 실수로 사용하려면 프로그램은 즉시 NullPointerException을 던지며 종료된다.
      • 다 쓴 참조를 해제하는 가장 좋은 방법은 스코프 밖으로 밀어내는 것

    메모리 누수가 발생하는 이유#

    • 일반적으로 자기 메모리를 직접 관리하는 클래스라면 항시 메모리 누수에 주의해야 한다.
      • 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해줘야 한다.
    • 캐시 역시 메모리 누수를 일으키는 주범이다.
      • 객체 참조를 캐시에 넣고 이 사실을 잊은체 그 객체를 다 쓴뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있다.
    • 메모리 누수의 세 번째 주범은 바로 리스너 혹은 콜백이다.
      • 클라이언트 코드가 콜백을 등록할 수 있는 API를 만들고 콜백을 뺼 수 있는 방법을 제공하지 않는다면, 계속해서 콜백이 쌓이기만 할 것이다.
      • 이것 역시 WeakHashMap을 사용해서 콜백을 Weak 레퍼런스로 저장하면 GC가 이를 즉시 수거해 해결할 수 있다.

    8. finalizer와 cleaner 사용을 피하라#

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

    • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 대부분 불필요하다.
    • cleaner는 finalizer보다는 덜 위험하지만 여전히 예측할 수 없고, 느리며, 보통은 불필요하다.
      • C++ 프로그래머에게 소멸자와는 다른 개념

    finalizer와 cleaner는 호출된 후 언제 실행될 지 알 수 없다.#

    • 즉, 제때 실행되어야 하는 작업을 절대 할 수 없다.
    • 성능저하도 일으킨다.

    finalizer나 cleaner를 대신해줄 묘안 AutoCloseable#

    java
    public class Room implements AutoCloseable {
        private static final Cleaner cleaner = Cleaner.create();
    
        private static class State implements Runnable {
            int numJunkPiles;
    
            State(int numJunkPiles) {
                this.numJunkPiles = numJunkPiles;
            }
    
            @Override
            public void run() {
                System.out.println("방 청소");
                numJunkPiles = 0;
            }
        }
    
        private final State state;
        private final Cleaner.Cleanable cleanable;
        public Room(int numJunkPiles) {
            state = new State(numJunkPiles);
            cleanable = cleaner.register(this, state);
        }
        @Override
        public void close() throws Exception {
            cleanable.clean();
    		}
    }
    • 인터페이스로 구현하면 close가 호출된다.

    finalizer와 cleaner는 언제 쓸까?#

    • 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안정망 역할
    • 네이티브 피어와 연결된 객체 에서 사용
      • 네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 의미함

    9. try-finally 보다는 try-with-resource를 사용하라#

    java
    public static String firstLineOfFile(String path) throw IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }
    과거의 코드 try-finally
    • 디버깅이 어려워질 수 있다.
    • 가독성이 떨어진다. (try-finally문 중첩시)
    • 제대로 close하지 않으면 자원이 회수되지 못하는 문제가 발생할 수 있다.
    java
    public static String firstLineOfFile(String path) throw IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        } catch (Exception e) {
            return defaultVal;
        }
    }
    현대적인 더 나은 코드 try-with-resource
    • try-with-resource를 사용하면 AutoCloseable인터페이스가 구현된 객체의 close 메서드를 자동으로 호출한다.
    • close에서 발생한 예외는 숨겨지고 첫 번째 예외가 기록된다.
      • 이렇게 숨겨진 예외들은 스택 추적 내역에 suppressed 꼬리표를 달고 출력된다.
      • 자바7에서 Throwable에 추가된 getSuppressed 메서드를 쓰면 프로그램 코드에서 가져올 수 있다.
    전체 글 둘러보기