책/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);
}
}
}
반응형