JAVA

[이펙티브 자바] 객체 생성과 파괴

ohyujeong 2024. 7. 13. 15:47

객체는 언제 어떻게 생성해야할까? 객체의 불필요한 생성을 피해서 효율적인 코드를 작성하는 방법을 알아보자

 

 

item 01. 생성자(new) 대신 정적(static) 팩터리 메서드

장점

  1. 팩터리 메서드는 메서드의 "이름" 을 지을 수 있음으로, 객체의 특성 묘사가 가능
    • 어떤 때에 이런 장점이 유용한가?
      • 하나의 시그니처는 하나의 생성자만 가질 수 있다. 시그니처의 순서가 개수가 다를 때 매번 새로운 생성자를 통해 객체를 생성해야 한다. -> 생성자는 이름이 없기 때문에 이럴 경우 각각의 다른 시그니처로 생성하는 객체가 어떤 객체인지 명확하게 파악하기 힘들다.
      • 이 때, 팩터리 메서드로 이름을 명명하고, 팩터리 메서드를 통해 객체를 생성하면 해당 객체가 어떤 역할인지 파악하기 쉽다.
  2. 호출 시마다 인스턴스 새로 생성 안 됨
    • 자바는 new 키워드를 쓰는 순간 메모리의 heap영역에 새로운 객체가 올라가게 된다.
    • 팩터리 메서드는 같은 요청에 같은 객체를 반환함으로, 불필요한 객체 생성을 피할 수 있다.
  3. 반환 타입의 하위 타입의 객체로도 반환이 가능하다
    • 하위 타입의 실제 구현 클래스를 몰라도, 반환 타입을 통해 하위타입의 객체를 얻을 수 있다. -> 유연성 증가
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체 반환이 가능하다.
    • 3번과 관련돼서, 반환 타입의 하위 타입이기만 하면 괜찮다.
  5. 정적 팩터리 메소드 작성 시점에는 반환할 객체의 클래스 존재 안 해도 된다.
메서드 이름으로 객체 생성 목적 명확히!
public static Person createStudent(String name, int age) {
    return new Person(name, age, "Student");
}

public static Person createTeacher(String name, int age) {
    return new Person(name, age, "Teacher");
}


//Shape의 하위타입 반환 가능!
public static Shape createCircle(double radius) {
    return new Circle(radius);
}

public static Shape createRectangle(double width, double height) {
    return new Rectangle(width, height);
}


//입력 매개변수에 따라 하위타입이기만 하다면 다른 객체 반환 가능
public static PaymentProcessor getPaymentProcessor(String type) {
    if ("creditCard".equals(type)) {
        return new CreditCardProcessor();
    } else if ("paypal".equals(type)) {
        return new PaypalProcessor();
    } else {
        throw new IllegalArgumentException("Unknown payment type: " + type);
    }
}

 

단점

  1. 상속 하려면, public이나 protected 생성자 필요해서, 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • 하지만 상속은 상위클래스 구현에 따라 하위클래스가 영향을 받게 되면서 캡슐화가 깨진다는 취약점이 있음으로, 이 단점은 상속 대신 컴포지션(상위 클래스 인스턴스를 하위클래스의 private filed로서 구성요소로 사용함) 유도한다는 점에서 장점으로 볼 수도 있다.
  2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
    • 생성자처럼 명확하게 드러나지 않아서, 정적 팩터리 메서드를 통해 인스턴스화할 방법을 프로그래머가 찾아야 한다.
    • 약속된 명명 방식을 사용해서 이러한 단점을 극복하고 있다.
      • from : 매개 변수 하나 받아서 해당 타입의 인스턴스 반환하는 형변환 메서드 Date d = Date.from(instant);
      • of : 여려 매개변수 받아 적합한 타입의 인스턴스 반환하는 집계 메서드 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
      • 등이 있다. 

 

그럼 어떤 때에는 생성자가 정적 팩터리 메서드보다 유용?

  • 간단한 객체의 경우거나, 의존성 주입 할 때 생성자가 더 직관적
//생성자로 UserSerivce에 UserRepository 객체 의존성 주입
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void saveUser(User user) {
        userRepository.save(user);
    }
}

 

item 02. 생성자에 매개변수 많으면(최소 4개 이상) 빌더 고려 

  • 생성자와 정적 팩터리 모두 선택적 매개변수에 대응하기 어렵다는 단점 있음
public class NutritionInfo {
    private final int servingSize; // 필수 매개변수
    private final int totalServings; // 필수 매개변수
    private final int caloriesPerServing; // 필수 매개변수
    private final int totalFat; // 선택 매개변수
    private final int transFat; // 선택 매개변수
    private final int sodium; // 선택 매개변수
    private final int cholesterol; // 선택 매개변수

    public NutritionInfo(int servingSize, int totalServings, int caloriesPerServing, int totalFat, int transFat, int sodium, int cholesterol) {
        this.servingSize = servingSize;
        this.totalServings = totalServings;
        this.caloriesPerServing = caloriesPerServing;
        this.totalFat = totalFat;
        this.transFat = transFat;
        this.sodium = sodium;
        this.cholesterol = cholesterol;
    }


// 생성자로 객체 생성하면 점층적 생성자 패턴이 나타남
    // 기본 생성자
    public NutritionInfo(int servingSize, int totalServings, int caloriesPerServing) {
        this(servingSize, totalServings, caloriesPerServing, 0);
    }

    // totalFat 추가 생성자
    public NutritionInfo(int servingSize, int totalServings, int caloriesPerServing, int totalFat) {
        this(servingSize, totalServings, caloriesPerServing, totalFat, 0);
    }

    // transFat 추가 생성자
    public NutritionInfo(int servingSize, int totalServings, int caloriesPerServing, int totalFat, int transFat) {
        this(servingSize, totalServings, caloriesPerServing, totalFat, transFat, 0);
    }

    // sodium 추가 생성자
    public NutritionInfo(int servingSize, int totalServings, int caloriesPerServing, int totalFat, int transFat, int sodium) {
        this(servingSize, totalServings, caloriesPerServing, totalFat, transFat, sodium, 0);
    }


 // cholesterol 추가 생성자
    public NutritionInfo(int servingSize, int totalServings, int caloriesPerServing, int totalFat, int transFat, int sodium, int cholesterol) {
        this.servingSize = servingSize;
        this.totalServings = totalServings;
        this.caloriesPerServing = caloriesPerServing;
        this.totalFat = totalFat;
        this.transFat = transFat;
        this.sodium = sodium;
        this.cholesterol = cholesterol;
    }

 

  • 인스턴스 생성할 때, 원하는 매개변수 모두 포함한 생성자 중 길이가 가장 짧은 것을 호출하는 방식으로 사용
    • 원치 않는 매개변수에도 값을 지정해줘야한다는 단점
    • 매개변수 개수가 많아질수록 코드가 헷갈리고 의미도 명확해지지 않음
    • 매개변수 순서를 바꿔서 건네도, 컴파일러가 잡아내지 못하기 때문에 런타임 때 의도치 않은 동작이 발생하거나 에러가 발생할 수 있음
    • 자바빈즈 패턴 - 기본 생성자 1개, set 메서드로 선택매개변수 세팅하는 방법의 패턴이 있었지만..-> 객체 하나가 완성될 때까지 일관성 보장X, 객체 1개를 만들기 위해 여러 개의 set 메서드 호출, 클래스를 불변으로 만들지 못한다는 치명적인 단점이 존재함

빌더 패턴1) 필수 매개변수만으로 생성자(혹은 정적 팩터리) 호출하여 빌더 객체 얻음

2) 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수 설정

3) 마지막으로 매개변수가 없는 build() 메서드 호출해서 불변 객체 얻음

 

빌더는 생성할 클래스 안에 정적 멤버 클래스로 만드는 게 보통이다.

 

public class NutritionInfo {
    private final int servingSize; // 필수 매개변수
    private final int totalServings; // 필수 매개변수
    private final int caloriesPerServing; // 필수 매개변수
    private final int totalFat; // 선택 매개변수
    private final int transFat; // 선택 매개변수
    private final int sodium; // 선택 매개변수
    private final int cholesterol; // 선택 매개변수

//정적 멤버 클래스로 빌더를 만듦
    public static class Builder {
    
        private final int servingSize; // 필수 매개변수
        private final int totalServings; // 필수 매개변수
        private final int caloriesPerServing; // 필수 매개변수
        
        //선택 매개변수 기본값으로 초기화
        private final int totalFat = 0;
    	private final int transFat = 0;
    	private final int sodium = 0;
    	private final int cholesterol = 0;


//필수 매개변수만으로 생성자 호출해서 빌더 객체를 생성함
        public Builder(int servingSize, int totalServings, int caloriesPerServing) {
            this.servingSize = servingSize;
            this.totalServings = totalServings;
            this.caloriesPerServing = caloriesPerServing;
        }


        public Builder totalFat(int totalFat) {
            this.totalFat = totalFat;
            return this; 
        }

        public Builder transFat(int transFat) {
            this.transFat = transFat;
            return this;
        }

        public Builder sodium(int sodium) {
            this.sodium = sodium;
            return this;
        }

        public Builder cholesterol(int cholesterol) {
            this.cholesterol = cholesterol;
            return this;
        }

        public NutritionInfo build() {
            return new NutritionInfo(this);
        }
        
        private NutritionInfo (Builder Builder){
 			servingSize = builder.servingSize;
 			....
		}
    }


// 객체 생성 예시
NutritionInfo nutritionInfo1 = new NutritionInfo.Builder(200, 5, 150).build();

NutritionInfo nutritionInfo5 = new NutritionInfo.Builder(200, 5, 150)
        .totalFat(10)
        .transFat(0)
        .sodium(300)
        .cholesterol(0)
        .build();
}

 

빌더 패턴을 사용하면 클래스는 불변, 빌더의 세터 메서드는 빌더 자신 반환해서 연쇄적으로 호출 가능

잘못된 매개변수 발견하려면 빌더 생성자와 메서드에서 입력 매개변수를 검사하는 코드, build 메서드 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하는 코드 추가

 

cf) 불변식- 프로그램 실행, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건. ex) 리스트의 크기는 음수가 될 수 없고 반드시 0이상

cf) 불변객체 - 어떠한 변경도 허용 안 함

 

그런데 빌더 패턴만 사용하면 객체 생성 로직이 빌더 클래스에 그대로 노출된다는 단점이 있음

 

+ 그래서

정적 팩터리 메서드 내부에 빌더 패턴을 구현할 경우

장점 1. 정적 팩터리 메서드 이름 명명을 통해 객체 생성의 명확한 의미를 드러낼 수 있다.

장점 2. 객체 생성 로직을 정적 팩터리 메서드 내부에 감추고 빌더만 반환함으로써 캡슐화 유지

 

단점 1. 코드 복잡성 증가

 

 

item 03. private 생성자나 Enum으로 싱글턴 보장하기

 

싱글턴 class : 인스턴스 오직 하나만 생성할 수 있는 class

생성자를 private으로 감추고, 인스턴스 접근 수단으로 public static 멤버를 작성함

 

싱글턴 만드는 방법 3가지

1. public static 멤버를 final로 만듦 -> private 생성자가 public static final 멤버를 초기화할 때 딱 한 번만 호출돼서 인스턴스가 시스템 전체에서 하나뿐임을 보장함

2. 정적 팩터리 메서드를 public static 멤버로 제공한다 -> 항상 같은 객체 참조 반환함으로 싱글턴 보장

3. 원소가 하나인 Enum 타입 선언

 

item 04.인스턴스화 막으려면 private 생성자 사용

  • pulibc 기본 생성자는, 명시된 생성자가 없을 때만 자동으로 생성됨으로, private 생성자를 명시적으로 작성해서 기본 public 생성자가 생기는 것을 방지해서 인스턴스화를 막는다.
  • 언제 왜 사용?
    • 정적 멤버만 있는 유틸리티 클래스와 같이 인스턴스로 만들어 사용하려고 설계한 클래스가 아닌 경우에.. 혹시 모를 인스턴스화를 방지하는 것이다. 
    • 추상 클래스로는 인스턴스화 못 막음 -> 하위클래스로 구현해서 인스턴스화 가능하기 때문에

 

item 05. 자원 직접 명시 X, 의존 객체 주입 사용

  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스와 싱글턴 방식은 부적합하다
    • 정적 유틸리티는 객체 생성을 막고, 싱글턴은 한 번 생성되면 불변이기 때문에..
  • 필요한 자원을 클래스에 직접 명시하지 않고, 인스턴스화 할 때 생성자에 필요한 자원을 넘겨주는 방식으로 사용하자! 
    • EX) 맞춤법 검사기를 생성할 때 의존객체인 사전을 주입함 (한국어 사전, 영어 사전.. 등 다양한 사전의 종류를 넘겨줄 수 있다.)

즉, 자원의 종류에 의존해서 작동하는 클래스라면, 클래스 자신이 직접 자원을 명시하지 않고, 외부에서 자원을 넘겨주거나 그 자원을 만들어주는 factory 메서드를 넘겨서 사용하자!

'JAVA' 카테고리의 다른 글

싱글톤 패턴과 프록시 패턴  (1) 2024.05.01
IoC, DI, AOP 와 Spring  (1) 2024.05.01
자바의 제네릭(Generic)  (0) 2024.04.08
자바의 신 인사이트 정리  (0) 2024.03.21