본문 바로가기
Books/Effective Java

2장. 객체 생성과 파괴

by YoonJong 2024. 3. 30.
728x90

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

  • 장점
    • 이름을 가질 수 있다.
    • 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
    • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
  • 단점
    • 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
  • 핵심정리
    • 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

  1. 생성자

장점:

  • 간결성: 객체 생성 코드가 간결하고 직관적입니다.
  • 상속: 생성자를 통해 상속받은 클래스에서 객체 생성을 조작할 수 있습니다.

단점:

  • 유연성 부족: 객체 생성 로직을 변경하기 어렵습니다.
  • 가독성 저하: 매개변수가 많거나 복잡한 경우 가독성이 떨어질 수 있습니다.
  1. 정적 팩토리 메서드

장점:

  • 유연성: 객체 생성 로직을 쉽게 변경하거나 추가할 수 있습니다.
  • 가독성 향상: 메서드 이름으로 객체 생성 의도를 명확하게 표현할 수 있습니다.
  • 캐싱 가능: 성능 향상을 위해 캐싱을 적용할 수 있습니다.

단점:

  • 간결성 저하: 객체 생성 코드가 다소 복잡해질 수 있습니다.
  • 상속 불가: 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 이 자동으로 메모리를 해제하지 않는 경우

  1. 객체 참조 유지
    • obj 변수를 null 값으로 대입하거나, 지역 변수로 사용하는 등 참조를 해제해야 한다.
public class MemoryLeak {
  private static MyObject obj;

  public static void main(String[] args) {
    obj = new MyObject(); // obj에 대한 참조 유지
    // ...
  }
}

class MyObject {
  // ...
}
  1. 순환 참조
    • 순환 참조를 끊어야 한다.
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;

  // ...
}
  1. 정적 변수 사용
    • 정적(static) 변수는 프로그램 종료까지 메모리에 유지된다.
    • 정적 변수에 더 이상 필요 없는 개체를 할당하지 않도록 주의
public class MemoryLeak {
  private static MyObject obj;

  public static void main(String[] args) {
    obj = new MyObject(); // 정적 변수 obj에 대한 참조 유지
    // ...
  }
}

class MyObject {
  // ...
}
  1. 네트워크 소켓 종료 시 메모리 해제 안 됨
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

댓글