이펙티브 자바 2장을 읽고 정리해둔다.
우선 이번 주제에서 다룰 내용의 주요 핵심은 아래와 같다.
핵심을 기억하면서 다음 내용들을 정리해보겠다.
public 생성자를 사용해서 객체를 생성하는 방법말고 static factory 메서드를 사용해서 만드는 방법도 있다.
이 방법에 대해서 소개한다.
들어가기 앞서 생성자는 무엇이고 정적 팩터리 메서드는 무엇인가?
생성자는 객체 생성시 호출하는 메서드이다.
그렇다면? 생성자의 특징은 뭘까?
public class Cup {
private String type;
private String name;
public Cup() {
}
public Cup(String type) {
this.type = type;
}
public Cup(String name) {
this.name = name;
} // 매개변수 구성이 같아서 만들 수 없음.
}
public class CupMain {
public static void main(String[] args) {
Cup cup = new Cup();
Cup cup = new Cup("type");
}
}
생성자의 특징에 대해서 알아보았다. 그렇다면 정적 팩토리 메서드란 무엇일까?
객체의 생성을 담당하는 클래스 메서드라고 한다.
여기서 팩토리는 객체 생성을 단순화하고, 생성 로직을 쉽게 관리할 수있도록 도와준다고 한다.
그럼, 정적 팩토리 메서드의 장단점을 알아보자!
이름을 가질 수 있다 .
public class Cup {
private String types;
private String pattern;
public static Cup ofTypes(String types) {
Cup cup = new Cup();
cup.types = types;
return cup;
}
public static Cup ofPattern(String pattern) {
Cup cup = new Cup();
cup.pattern = pattern;
return cup;
}
}
public class CupMain {
public static void main(String[] args) {
Cup cupOfType = Cup.ofTypes("Goblet");
Cup cupOfPattern = Cup.ofPattern("dot");
}
}
호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
import java.util.HashMap;
import java.util.Map;
public class CupFactory {
// 객체를 캐싱할 Map
private static final Map<String, Cup> cupCache = new HashMap<>();
// 정적 팩토리 메서드: 캐시된 객체가 있으면 반환, 없으면 생성 후 캐싱
public static Cup getCupTypes(String types) {
return cupCache.computeIfAbsent(types, Cup::new);
}
}
반환 타입의 하위 타입 객체를 반환할 수 있다.
// 상위 클래스
public class CupFactory {
public static CupFactory createCup(String type) {
if ("Coffee".equalsIgnoreCase(type)) {
return new CoffeeCup();
}
if ("Tea".equalsIgnoreCase(type)) {
return new TeaCup();
}
return new Cup();
}
}
// 하위 클래스들
class CoffeeCup extends CupFactory {
}
class TeaCup extends CupFactory {
}
class Cup extends CupFactory {
}
public class Main {
public static void main(String[] args) {
CupFactory coffeeCup = CupFactory.createCup("Coffee");
CupFactory teaCup = CupFactory.createCup("Tea");
CupFactory defaultCup = CupFactory.createCup("Unknown");
}
}
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
장점은 알아보았으니 단점도 알아봐야한다.
개발을 하다보면 객체 내 필드가 많은 경우 매개변수가 많아져서 복잡성이 증가할 때가 많다.
단순 복잡성 뿐 아니라 매개변수의 자료형이 동일한 경우 데이터를 잘못 전달하는 불상사도 발생할수도 있다.
이러한 부분들을 고려해 매개변수가 많은 경우 빌더 패턴을 선택하는게 낫다고 설명한다.
빌더 패턴을 사용하면 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
그래서 빌더 패턴이 뭘까?
매개변수가 많고 복잡한 객체 생성 문제를 해결하기 위한 패턴이라고한다.
핵심은 객체를 구성하는 과정에서 명확하게 각 필드를 지정 할 수 있도록하고, 최종적으로 완성된 객체를 반환한다.
메서드 체이닝 방식을 사용해서 코드의 명확성과 유지보수성을 높일 수 있다.
장점은 점층적 생성자 패턴처럼 객체 안정성이 있다.
단점은 사용하기 좋지만 하나 빼먹는다고 오류를 잡아주지 않는다. 즉, null을 허용하여 필드에 값을 넣지 않으면 자료형의 기본값으로 들어간다. 휴먼 에러가 발생 할 우려가 있다.
public class Animal {
private String type;
private String name;
private String color;
private String gender;
private int age;
private String point;
// 매개변수가 많은 생성자
public Animal(String type, String name, String color, String gender, int age, String point) {
this.type = type;
this.name = name;
this.color = color;
this.gender = gender;
this.age = age;
this.point = point;
}
public static class Builder {
private String type;
private String name;
private String color;
private String gender;
private int age;
private String point;
//필수 매개변수
public Builder(String name, String type) {
this.type = type;
this.name = name;
}
public Builder color(String val) {
color = val;
return this;
}
public Builder gender(String val) {
gender = val;
return this;
}
public Builder age(int val) {
age = val;
return this;
}
public Builder point(String val) {
point = val;
return this;
}
public Animal build() {
return new Animal(this);
}
}
private Animal(Builder builder) {
name = builder.name;
color = builder.color;
gender = builder.gender;
age = builder.age;
point = builder.point;
}
}
public class AnimalMain {
public static void main (String[] args) {
Animal animal = new Animal.Builder("kiki", "puppy")
.color("white")
.gender("female")
.age(1)
.point("cute")
.build();
}
}
싱글턴 패턴을 구현 할 때 private 생성자를 사용하거나 enum타입을 활용해 유일한 인스턴스를 제공하는 방식으로 싱글턴을 보장하라는 말이다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public void leaveTheBuilding() {...}
}
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public static Elvis getInstance {
return INSTANCE;
}
public void leaveTheBuilding() {...}
}
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {...}
}
public class Utils {
// private 생성자: 외부에서 객체 생성을 막음
private Utils() {
throw new AssertionError("인스턴스화 할 수 없습니다.");
}
// 정적 메서드
public static int add(int a, int b) {
return a + b;
}
}
public static void main(String[] args) {
// 정적 메서드를 호출하여 사용
int result = Utils.add(5, 3);
System.out.println(result);
Utils utils = new Utils(); // 컴파일 에러 또는 런타임 에러발생
}
클래스가 내부적으로 하나이상의 자원에 의존하고 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋으며 직접 만들게 해서도 안된다.
이런 부분들을 대신해 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식으로 의존 객체 주입을 사용하라는 것이다.
의존 객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다. 또한 불변을 보장한다.
불변을 보장한다는 것은 인스턴스 생성 후 종료까지 변경되지 않는다는 것이다.
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = dictionary;
}
}
똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하라는 것이다.
책에서도 예시로 나와 있듯이, 많은 개발자들이 String s = "hi";와 같이 문자열 리터럴을 선언하는 방식을 자주 사용한다. 이 방식은 자바의 문자열 풀(String Pool)을 활용하여, 같은 문자열 리터럴이 있을 경우 기존의 객체를 재사용한다.
하지만 만약 String s = new String("hi");와 같이 생성자를 이용해 문자열 객체를 생성한다면, 실행될 때마다 새로운 인스턴스가 만들어진다. 이렇게 하면 같은 값을 가진 객체가 계속해서 중복 생성되며, 메모리 낭비가 발생할 수 있다.
객체를 생성하는 데는 비용이 따르며, 재사용하는 것이 훨씬 효율적이다. 특히, 같은 객체를 여러 번 생성하게 되면 메모리 사용량이 증가하고, GC가 자주 발생하여 시스템 성능 저하로 이어질 수 있다.
따라서 불필요하게 같은 기능의 객체를 반복해서 생성하는 것보다, 이미 존재하는 객체를 재사용하는 것이 성능 최적화와 메모리 절약에 매우 중요하다.
사용이 끝난 객체를 더이 상 참조하지 않도록 하여, 불필요한 메모리를 차지하는 상황을 방지하라는 것이다.
여기서 말하는 메모리 3명의 누수범이 있다.
짧게 알아보도록 하자.
객체는 사용 후 다 쓴 참조는 null로 참조 해제 한다.
private Object[] elements;
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}하지만, 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효범위 밖으로 밀어내는 것이라고 한다.
캐시는 weakHashMap을 사용하여 메모리 누수를 방지한다.
우선 weakHashMap은 키에 대한 참조가 약(weak)한 형태를 말하며, GC가 키에 대한 강한 참조가 없으면 해당 객체를 메모리에서 제거할 수있도록 설계되었다. 즉, 캐시의 키가 더 이상 필요하지 않으면 weakHashMap은 자동으로 해당 항목을 제거한다.
public class WeakHashMapEx{
public static void main (String[] args) {
Map<Object, String> cache = new WeakHashMap<>();
Object key1 = new Object();
cache.put(key1, "cache1");
Object key2 = new Object();
cache.put(key2, "cache2");
key1 = null; //참조 제거
...
}
}
리스너 (Listener) 혹은 콜백 (Callback) 이라고 불리는 것은,
클라이언트가 등록 및 사용하고 난 후에 명확히 해지하지 않는다면 계속해서 쌓여간다.
이럴 경우, 콜백을 약한 참조 (weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해갈 수 있다.
그 대표적인 예시가 바로 앞서 한번 말했던 WeakHashMap 이다.
finalizer와 cleaner 둘 다 자바에서 객체 소멸자를 제공한다.
그 중 finalizer는 예측할 수없고 상황에 따라 위험할 수 있어 일반적으로 불필요하다고한다.
또한 cleaner는 finalizer보다 덜 위험하지만, 여전히 예측할 수없고, 느리고, 일반적으로 불필요하다.
이런 사유 말고 쓰임새가 있지만 기본적으로 쓰지말아야한다는 말이다.
그이유가 뭘까?
이런 이유들이 있다고하는데, finalizer와 cleaner는 어디에 사용되는걸까?
결론, cleaner(java 8 → finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원회수용으로만 사용하자!
자바 라이브러리에서 사용되는 자원들 중 직접 닫아줘야 하는 자원이 많은데, 자원 닫기를 놓치는 경우가 있어서 try-with-resources를 활용하여 자원을 자동으로 해제할 수 있도록 사용하라는 말이다.
import java.io.*;
try {
FileInputStream fis = new FileInputStream("example.txt");
} finally {
fis.close();
}
try (FileInputStream fis = new FileInputStream("example.txt")) {
// 파일 작업 수행
} catch (IOException e) {
e.printStackTrace();
}