-
[Effective Java] 2장 : 객체 생성과 파괴책/Effective Java 3판 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); } } }
반응형'책 > Effective Java 3판' 카테고리의 다른 글
[Effective Java] 10장 : 예외 (0) 2024.02.14 [Effective Java] 8장 : 메서드 (1) 2024.02.14 [Effective Java] 4장 : 클래스와 인터페이스 (2) 2024.02.14