책/Effective Java 3판

[Effective Java] 2장 : 객체 생성과 파괴

JJJaewon 2024. 2. 13. 23:27
반응형

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

장점

  • 이름을 가질 수 있음
  • 호출될 때마다 인스턴스를 생성하지 않아도 됨
  • 반환 타입의 하위 타입 객체를 반환할 수 있음
  • 입력 매개변수에 따라 다른 클래스를 반환할 수 있음
  • 정적 팩토리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 됨

 단점

  • 상속 불가능
  • 프로그래머가 찾기 어려움
public static void main(String[] args) {
        MyClass myClass = MyClass.getInstance();

        MyClass myClass1 = MyClass.getInstance();

        myClass.print("hello world");
        myClass1.print("sdfsdf");

        System.out.println(myClass.equals(myClass1));
        System.out.println(myClass.hashCode() + ", " + myClass1.hashCode());
    }

public class MyClass {

    private static final MyClass INSTANCE = new MyClass();

    private MyClass() {

    }

    public void print(String s) {
        System.out.println(s);
    }

    public static MyClass getInstance() {
        return INSTANCE;
    }
}

 

 

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

  • 생성자에 매개변수가 너무 많으면 필요 없는 부분까지도 고려해서 넣어줘야함

점층적 생성자 패턴

  • 이를 해결하기 위해 점층적 생성자 패턴 (생성자 오버로딩) 을 사용했지만, 이 역시 필요없는 부분이 생길 수 있음
public class Board {
    private Integer id;
    private String title;
    private String content;
    private String writer;
    private Integer likeCount;

    public Board(Integer id) {
        this(id, "");
    }

    public Board(Integer id, String title) {
        this(id, title, "");
    }

    public Board(Integer id, String title, String content) {
        this(id, title, content, "");
    }

    public Board(Integer id, String title, String content, String writer) {
        this(id, title, content, writer, 0);
    }

    public Board(Integer id, String title, String content, String writer, Integer likeCount) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.writer = writer;
        this.likeCount = likeCount;
    }
}

 

자바 빈즈 방식

  • 빈 객체 만든 뒤 setter → 이 역시 불편하고, setter를 하나라도 빼먹으면 불완전한 상태의 객체를 사용하게 된다.
  • 자바 빈즈 방식(JavaBeans)
public class Board {
    private Integer id = -1;
    private String title = "";
    private String content = "";
    private String writer = "";
    private Integer likeCount = 0;

    public Board() {}

    public void setId(Integer id) {
        this.id = id;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public void setWriter(String writer) {
        this.writer = writer;
    }

    public void setLikeCount(Integer likeCount) {
        this.likeCount = likeCount;
    }
}

 

빌더(Builder) 패턴

public class Board {
    private final Integer id;
    private final String title;
    private final String content;
    private final String writer;
    private final Integer likeCount;
    
    public static class Builder {
        // 필수
        private final Integer id;

        // 선택
        private String title = "";
        private String content = "";
        private String writer = "";
        private Integer likeCount = 0;

        public Builder(Integer id) {
            this.id = id;
        }
        
        public Builder title(String value) {
            title = value;
            return this;
        }
        
        public Builder content(String value) {
            content = value;
            return this;
        }
        
        public Builder writer(String value) {
            writer = value;
            return this;
        }
        
        public Builder likeCount(Integer value) {
            likeCount = value;
            return this;
        }
        
        public Board build() {
            return new Board(this);
        }
    }
    
    private Board(Builder builder) {
        this.id = builder.id;
        this.title = builder.title;
        this.content = builder.content;
        this.writer = builder.writer;
        this.likeCount = builder.likeCount;
    }
}

Board board = new Board.Builder(1)
                .title("title")
                .content("content")
                .writer("writer")
                .likeCount(0)
                .build();
  • 객체를 생성하기 위해 빌더를 생성해야함 → 성능이 약간 떨어질 수 있음
  • 그러나 매개변수의 개수가 많거나 계속 많아지는 경우 매우 유용
  • 각 매개변수를 저장하는 메서드에서 유효성 검사를 하고, build() 에서 최종 불변식을 검사할 수 있다.

 

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

  • 싱글톤(Singleton) : 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • 클래스를 싱글톤으로 만들면 클라이언트 테스트가 어렵다.

첫번째 방식 - private 생성자, public static final 필드

public class MyClass {
    public static final MyClass INSTANCE = new MyClass();
    
    private MyClass() {}
}
  • private 생성자는 MyClass를 INSTANCE에 초기화할 때 딱 한번만 사용됨
  • 장점
    • 간결함
    • 누가봐도 싱글턴임이 보임

두번째 방식 - private 생성자, 정적 팩터리 메서드

public class MyClass {
    private static final MyClass INSTANCE = new MyClass();

    private MyClass() {}

    public static MyClass getInstance() {
        return INSTANCE;
    }
}
  • 장점
    • API 손대지 않고도 싱글턴 아니게 바꿀 수 있다.
    • 제네릭 싱글턴으로 바꿀 수 있다.
    • 정적 팩터리 메서드 참조를 공급자(Supplier)로 사용 가능 → MyClass::getInstance

세번째 방식 - 열거형 사용

public enum MyClass {
    INSTANCE;

    public void print(String s) {
        System.out.println(s);
    }
}

MyClass m = MyClass.INSTANCE;

m.print("hello world");
  • 직렬화에도 끄떡없고, 리플렉션 공격에서도 완벽히 막아줌
  • 약간 사용하기 어색함
  • 상속 같은게 필요한 경우 사용 불가능

 

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

  • 정적 필드, 메서드로만 이루어진 유틸리티 클래스는 굳이 인스턴스가 필요 없다.
  • 간단하게 private 생성자를 만들면 쉽게 인스턴스화를 막을 수 있다.
  • private 생성자는 상속을 막는 효과도 있다 → 모든 생성자는 상위 클래스의 생성자를 호출하는데, 이 때 호출이 불가능하기 때문이다.

 

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

  • 정적 유틸리티 클래스나 싱글턴의 경우 사용하는 자원에 따라 동작 방식이 달라지는 클래스에서는 적합하지 않다.
// 정적 유틸리티 클래스
public class SpellChecker {
    private static final Lexicon dictionary = new Lexicon();
    
    private SpellChecker() { }
    
    public static Boolean isValid(String word) {
        return false;
    }
}

// 싱글턴
public class SpellChecker {
    private final Lexicon dictionary = new Lexicon();

    private SpellChecker() { }
    public static final SpellChecker INSTANCE = new SpellChecker();
    
    public static Boolean isValid(String word) {
        return false;
    }
}
  • 이 경우, dictionary가 다양하게 사용되어야 하는 경우 적합하지 않다.
  • 만약 필드를 final로 하지 않고 수정할 수 있는 메서드를 추가한다면?
    • 멀티스레드 환경에서는 적합하지 않음

생성자 주입

public class SpellChecker {
    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = dictionary;
    }
    
    public Boolean isValid(String word) {}
}
  • 인스턴스를 생성할 때 생성자에 필요 자원을 넘겨주는 방식
  • 클래스가 직접 new로 만드는 것 또한 안좋음 → 불변성이 완벽히 깨짐
  • 생성자에 Supplier<? extends dictionary> 를 통해 팩토리 메서드를 받는 것 또한 좋다.
    • 팩토리 메서드로 해당 객체는 싱글톤으로 만들 수 있기 때문 ← 자원 낭비 X

 

아이템 6 - 불필요한 객체 생성을 피하라

  • 똑같은 기능 객체를 매번 생성하는 것보다는 당연히 재사용하는 게 좋다.
    • 불변 객체는 언제든 재사용 가능
      • String s = new String(”b”); → 완전히 비효율 - “b” 자체가 String()으로 만들려는 것과 똑같다.
      • String s = “b”; 가 더 나음 → “b” 가 또 나올 경우 이를 재사용
    • 가변 객체 또한 변경만 되지 않는다면 재사용이 가능
  • 오토 박싱은 성능에서는 좋지 않다.
    • 위의 경우 i가 더해질 때마다 매번 새로운 Long 객체를 만들어 더하게 된다.
  • Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) sum += i;
  • 그럼 방어적 복사는 하지 말아야하나?
    • 방어적 복사를 안했을 때 피해 >>>>> 불필요 객체 생성 피해
      • 전자는 심각한 오류 초래할 수도, 후자는 그냥 성능만 좀 안좋아짐

 

아이템 7 - 다 쓴 객체 참조를 해제하라

public class MyStack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public MyStack() {
        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);
        }
    }
}
  • 위 코드의 문제 : pop할 때 그냥 size— 만 수행한다 → 가비지 컬렉터가 해당 위치에 있는 객체를 수거하지 않는다.
    • 진짜 잘못하면 OutOfMemoryError 나올수도 있음
    • 왜 그런가?
      • 배열 안에 있으니 가비지 컬렉터 입장에서는 활성화된 객체라고 판단
    • 해결
      • null로 초기화해주면 해당 위치의 객체는 연결이 끊김 → 가비지 컬렉터가 수거해감
public Object pop() { 
	if (size == 0) { 
		throw new EmptyStackException(); 
	} 
	Object result = elements[--size]; 
	elements[size] = null; // 핵심 
	return result; 
}
  • 그렇다고 매번 null로 초기화해줄 필요는 없음
    • 위 경우 배열을 통해 스택이 직접 메모리를 관리하기 때문에 문제가 발생한 것
    • 즉, 직접 메모리를 관리할 때에만 사용
  • 캐싱의 경우 WeakHashMap을 사용하면 알아서 수거해감
    • 그러나 이 경우에만 유용하다고 한다.

 

아이템 9 - try-finally 보다는 try-with-resources를 사용하라

  • try-finally의 문제점
    • 사용 자원이 많아질수록 코드가 더럽다.
    • close() 자체에서도 예외가 발생할 수도 있는데, 이 경우 디버깅이 매우 어렵다.
  • try-with-resources
    • 해당 자원이 AutoCloseable 인터페이스를 구현해야함
    • try() { } 에서 ( ) 안에 가용 자원을 넣어주면 된다.
    • 여기에서도 catch를 사용할 수 있다.
static void copy(String src, String dst) throws IOException {
        try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[10];
            int n;
            while ((n = in.read(buf)) >= 0) {
                out.write(buf, 0, n);
            }
        }
    }

 

 

 

 

https://product.kyobobook.co.kr/detail/S000001033066

반응형