728x90
아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라.
- 장점
- 이름을 가질 수 있다.
- 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
- 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
- 단점
- 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
- 핵심정리
- 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.
- 생성자
장점:
- 간결성: 객체 생성 코드가 간결하고 직관적입니다.
- 상속: 생성자를 통해 상속받은 클래스에서 객체 생성을 조작할 수 있습니다.
단점:
- 유연성 부족: 객체 생성 로직을 변경하기 어렵습니다.
- 가독성 저하: 매개변수가 많거나 복잡한 경우 가독성이 떨어질 수 있습니다.
- 정적 팩토리 메서드
장점:
- 유연성: 객체 생성 로직을 쉽게 변경하거나 추가할 수 있습니다.
- 가독성 향상: 메서드 이름으로 객체 생성 의도를 명확하게 표현할 수 있습니다.
- 캐싱 가능: 성능 향상을 위해 캐싱을 적용할 수 있습니다.
단점:
- 간결성 저하: 객체 생성 코드가 다소 복잡해질 수 있습니다.
- 상속 불가: private 생성자를 사용하는 경우 상속 불가능합니다.
- API 문서 불편: API 문서에서 정적 팩토리 메서드 찾기가 어려울 수 있습니다.
아이템2. 생성자에 매개변수가 많다면 빌더를 고려하라
- 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
- 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식.
- 핵심정리
- 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간경하고, 자바빈즈보다 훨씬 안전하다.
@Builder
public class User {
private final String name;
private final int age;
private final Address address;
// ...
}
@Builder
public class User {
private final String name;
private final int age;
private final Address address;
// ...
}
빌더 패턴 사용 시 주의 사항
- 빌더 클래스는 불변 객체여야 합니다.
- 빌더 클래스는 생성자를 통해 필수 값을 받아야 합니다.
- 빌더 클래스는 옵션 값을 설정하는 메소드를 제공해야 합니다.
- 빌더 클래스는 build() 메소드를 통해 객체를 생성해야 합니다.
빌더 패턴 사용 시 장점
- 객체 생성 과정을 단순화합니다.
- 객체 생성 과정을 단계별로 제어할 수 있습니다.
- 불변 객체를 쉽게 만들 수 있습니다.
빌더 패턴 사용 시 단점
- 코드 양이 증가할 수 있습니다.
- 빌더 클래스를 관리해야 합니다.
아이템3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
- 싱글턴 : 인스턴스를 오직 하나만 생성할 수 있는 클래스
- 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워 질 수 있다.
- private 생성자를 통한 싱글턴 보장하는 예시 코드
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 생성자를 private으로 설정하여 외부에서 직접 호출 불가능
}
public static Singleton getInstance() {
return INSTANCE;
}
// ... 싱글턴 객체의 메서드 구현 ...
}
- Singleton 클래스의 생성자는 private 접근 제어자를 사용하여 외부에서 직접 호출할 수 없도록 설정
- getInstance() 메서드는 정적 메서드로 구현되어 외부에서 쉽게 접근할 수 있으며, 항상 동일한 INSTANCE 객체를 반환
- INSTANCE 객체는 클래스 로딩 시점에 생성되어 메모리에 상주
- 열거 타입(enum) 을 통한 싱글턴 보장 예시
public enum Singleton {
INSTANCE;
// ... 싱글턴 객체의 메서드 구현 ...
}
- Singleton 열거 타입은 단 하나의 인스턴스 (INSTANCE)만을 가질 수 있다.
- INSTANCE 객체는 열거 타입 선언 시점에 자동으로 생성
- 열거 타입의 자연스러운 특성을 활용하여 싱글턴 패턴을 간결하게 구현할 수 있다.
아이템4. 인스턴스화를 막으려든 private 생성자를 사용하라.
추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
// 인스턴스화를 막기 위해 private 생성자를 사용하는 예시 코드
public class NonInstantiableClass {
// private 생성자를 선언하여 클래스 외부에서 접근을 막는다.
private NonInstantiableClass() {
throw new AssertionError("This class cannot be instantiated.");
}
// 공개 메서드
public static void publicMethod() {
System.out.println("This is a public method.");
}
// private 메서드
private void privateMethod() {
System.out.println("This is a private method.");
}
}
// 클래스 사용 예시
public class Main {
public static void main(String[] args) {
// NonInstantiableClass 객체를 생성하려고 하면 오류가 발생한다.
// NonInstantiableClass nonInstantiableClass = new NonInstantiableClass();
// public 메서드는 호출 가능하다.
NonInstantiableClass.publicMethod();
// private 메서드는 호출 불가능하다.
// NonInstantiableClass.privateMethod(); // compile error
}
}
아이템5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.
사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 사용한다.
- 핵심정리
- 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접 만들게 해서도 안 된다. 대신 필요한 자원을 생성자에 넘겨주자. 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.
- 객체 생성과 의존성 주입을 한 번에 수행할 수 있습니다.
- 생성자를 통해 필요한 자원을 명확하게 정의할 수 있습니다.
- 의존 객체 주입 패턴 ( 파라미터 사용 )
public interface Car {
void drive();
}
public interface Engine {
void start();
}
public class ElectricCar implements Car {
private Engine engine;
public ElectricCar(Engine engine) {
this.engine = engine;
}
@Override
public void drive() {
engine.start();
System.out.println("Electric car is driving!");
}
}
public class GasolineEngine implements Engine {
@Override
public void start() {
System.out.println("Gasoline engine is starting!");
}
}
public class Main {
public static void main(String[] args) {
// 생성자에 필요한 자원을 직접 넘겨줍니다.
Engine engine = new GasolineEngine();
Car car = new ElectricCar(engine);
car.drive();
}
}
아이템6. 불필요한 객체 생성을 피하라
박싱된 타입(참조타입) 보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
- 반복적인 문자열 객체 생성
// 불필요한 객체 생성 코드
for (int i = 0; i < 100; i++) {
String str = "Hello, world!";
// ...
}
// 개선된 코드
String str = "Hello, world!";
for (int i = 0; i < 100; i++) {
// ...
}
- 정규표현식 객체 생성
// 불피요한 객체 생성 코드
String text = "This is a sample text.";
if (text.matches("[a-zA-Z ]+")) {
// ...
}
// 개선된 코드
Pattern pattern = Pattern.compile("[a-zA-Z ]+");
String text = "This is a sample text.";
if (pattern.matcher(text).matches()) {
// ...
}
- 불필요한 객체 래핑
// 불필요한 객체 생성 코드
int value = 10;
Integer integerValue = new Integer(value);
// 개선된 코드
int value = 10;
- 불필요한 객체 복사
// 불필요한 객체 생성 코드
String str1 = "Hello, world!";
String str2 = new String(str1);
// 개선된 코드
String str1 = "Hello, world!";
String str2 = str1;
아이템7. 다 쓴 객체 참조를 해제하라.
- 메모리 누수
- 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것.
- 캐시 또한 메모리 누수를 일으키는 주범 → weakHashMap 사용하기
- 핵심정리
- 메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 한다. 그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.
JVM 이 자동으로 메모리를 해제하지 않는 경우
- 객체 참조 유지
- obj 변수를 null 값으로 대입하거나, 지역 변수로 사용하는 등 참조를 해제해야 한다.
public class MemoryLeak {
private static MyObject obj;
public static void main(String[] args) {
obj = new MyObject(); // obj에 대한 참조 유지
// ...
}
}
class MyObject {
// ...
}
- 순환 참조
- 순환 참조를 끊어야 한다.
public class MemoryLeak {
private MyObject obj1;
private MyObject obj2;
public MemoryLeak() {
obj1 = new MyObject();
obj2 = new MyObject();
obj1.ref = obj2; // obj1이 obj2를 참조
obj2.ref = obj1; // obj2가 obj1을 참조
}
public static void main(String[] args) {
new MemoryLeak();
// ...
}
}
class MyObject {
MyObject ref;
// ...
}
- 정적 변수 사용
- 정적(static) 변수는 프로그램 종료까지 메모리에 유지된다.
- 정적 변수에 더 이상 필요 없는 개체를 할당하지 않도록 주의
public class MemoryLeak {
private static MyObject obj;
public static void main(String[] args) {
obj = new MyObject(); // 정적 변수 obj에 대한 참조 유지
// ...
}
}
class MyObject {
// ...
}
- 네트워크 소켓 종료 시 메모리 해제 안 됨
public class MemoryLeak {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
// ...
}
}
}
- 네트워크 소켓을 종료해도 소켓 관련 메모리는 자동으로 해제되지 안됨.
- socket.close() 메서드를 사용하여 소켓을 명시적으로 닫아야 메모리 해제.
아이템8. finalizer와 cleaner 사용을 피하라.
- 한줄 정리
- 그냥 쓰지말자.
아이템9. try-finally 보다는 try-with-resources를 사용하라
- 핵심정리
- 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources 를 사용하자. 예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다. try-finally 로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources 로는 정확하고 쉽게 자원을 회수 할 수 있다.
try-finally와 try-with-resources의 차이점
1. 리소스 관리 방식
- try-finally:
- finally 블록에서 직접 리소스를 해제해야 합니다.
- 예외 발생 여부에 관계없이 항상 실행됩니다.
- 코드가 복잡하고 오류 가능성이 높습니다.
- try-with-resources:
- 자동으로 리소스를 해제합니다.
- try 블록 종료 시 또는 예외 발생 시 리소스가 자동으로 닫힙니다.
- 코드가 간결하고 안전합니다.
2. 예외 처리
- try-finally:
- finally 블록에서 발생한 예외는 try 블록의 예외를 덮어씁니다.
- 디버깅이 어렵습니다.
- try-with-resources:
- try 블록과 finally 블록에서 발생한 모든 예외를 확인할 수 있습니다.
- 디버깅이 용이합니다.
3. 가독성
- try-finally:
- 코드가 길고 복잡해집니다.
- 가독성이 떨어집니다.
- try-with-resources:
- 코드가 간결하고 명확합니다.
- 가독성이 높습니다.
4. 사용 시기
- try-finally:
- 리소스를 직접 제어해야 하는 경우
- finally 블록에서 특별한 처리를 해야 하는 경우
- try-with-resources:
- 닫아야 하는 리소스가 있는 경우
- 코드를 간결하게 작성하고 싶은 경우
// try - finally 예제
public class TryFinallyExample {
public static void main(String[] args) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("test.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
// try - with - resources
public class TryWithResourcesExample {
public static void main(String[] args) {
// try () 안에 여러 개의 객체 선언 가능
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// try 블록이 종료되거나 예외가 발생하든 항상 실행
}
}
}
- try-with-resources 작성 방법
try (자원1; 자원2; ...; 자원N) {
// 코드 블록
} catch (예외1 e1) {
// 예외 처리 코드
} catch (예외2 e2) {
// 예외 처리 코드
} finally {
// finally 블록 (선택 사항)
}
728x90
'Books > Effective Java' 카테고리의 다른 글
3장. 모든 객체의 공통 메서드 (1) | 2024.04.03 |
---|
댓글