맞는데 왜 틀릴까..?

Java/Effective Java

[Effective Java] Item 1~6. 객체 생성과 파괴

안도일 2023. 3. 1. 17:54

 

객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하고, 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법, 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령을 알아보자. - Effective Java p7

 

 

정적 팩토리 메서드 

생성자 호출 방식(new)이 아닌, 메서드 호출 방식으로 객체를 생성하는 것.

 

 

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

 

1-1 이름을 가질 수 있다.

 

생성자로 호출 하는 방식은 해당 클래스의 생성자가 여러 가지일 경우에 어떤 객체가 반환되는지 쉽게 알 수 없다.

 

예를 들어보자

 

유저 클래스

public class User {

    private String id;
    private String password;

   }

 

public class User {

    private String id;
    private String password;

    public User(String id, String password) {
        this.id = id;
        this.password = password;
    }
    
    public User(String id){
        this.id = id;
        this.password = "pass";
    }
}

 

첫번째 생성자는 id와 password를 모두 생성해 주고 두 번째 생성자는 id만 입력받고 기본 패스워드인 pass로 객체를 생성한다.

 

 

new 방식 생성자 

public static void main(String[] args) {
        User user1 = new User("user1", "qwe123");
        User user2 = new User("user2");
    }

 

User 클래스 내의 무슨 생성자를 호출하는지 알기가 어렵다.

이런 문제를  바로 정적 팩토리 메서드로 방지할 수 있다.

정적 팩토리 메서드를 사용하면 클래스 내에 여러 생성자가 있는 경우 각 생성자를 사용할 때 보다, 어떤 객체가 반환되는지 쉽게 유추할 수 있다.

 

 

 

정적 팩토리 메서드 방식

public class User {

    private String id;
    private String password;

    public static User createUser(String id, String password) {
        return new User(id, password);
    }

    public static User createNoPassword(String id) {
        return new User(id, "pass");
    }

    private User(String id, String password) {
        this.id = id;
        this.password = password;
    }
}

 

public static void main(String[] args) {
        User user1 = createUser("user1", "qwe123");
        User user2 = createNoPassword("user2");
    }

 

createUser, createNoPassword와 같이 이름을 통해 클래스 내의 어떤 생성자가 반환되는지 쉽게 유추할 수 있다.

 

 

 

1-2 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

 

인스턴스를 미리 만들어 놓아 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

 

public class User {

    private String id;
    private String password;
    
    public User(){
    }

    //미리 BASIC_USER를 생성해놓음
    public static final User BASIC_USER = new User();

    //메서드 호출시 생성해 놓은 인스턴스 리턴
    public  static User getUser(){
        return BASIC_USER;
    }
    
    public static void main(String[] args) {
       //호출될 때마다 인스턴스를 새로 생성하지 않고
        // 기존에 생성해놓은 BASIC_USER를 가져옴
        User user = User.getUser();
    }
}

 

 

불변클래스 보장

 

 

1-3 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

 

반환타입의 하위 타입 즉 자신을 상속받는 객체도 반환할 수 있다.

 

public class User {

    private String id;
    private String password;

    public User(){
    }

    //getUser의 매개변수에 따라 true면 User를 false면 ExUser를 반환함.
    public static User getUser(boolean flag){
        return flag ? new User() : new ExUser();
    }

    //User를 상속 받는 ExUser 
    static class ExUser extends User{
    }

    public static void main(String[] args) {
        User user = User.getUser(false);
    }
}

 

 

 

1-4 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

 

예를 들어 EnumSet 클래스가 있는데, 입력 매개변수의 개수에 따라 RegularEnumSet, JumboEnumSet의 인스턴스를 반환한다.

 

무슨 소리냐 하면

 

enum Color{
	RED, BLUE, WHITE
 }

public static void main(String[] args) {
      
	EnumSet<Color> colors = EnumSet.allOf(Color.class);
	EnumSet<Color> blueAndWhite = EnumSet.of(Color.BLUE, Color.WHITE)
 }

 

위 코드에서 매개변수인 RED, BLUE, WHITE 등 의 개수에 따라 EnumSet의 하위 클래스인 RegularEnumSet, JumboEnumSet 중 하나가 반환된다.

 

 

클라이언트는 팩터리가 반환해 주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없다. 그저 EnumSet의 하위 클래스이기만 하면 된다.

 

 

 

1-5 정적 팩터리 메서드를 작성하는 시점에서는 반환할 객체의 클래스가 존재하지 않아도 된다.

 

 

 

단점 1: 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다. 즉 상속할 수 없다.

 

 

단점 2: 정적 팩터리 메서드는 프로그래머가 찾기 어렵다. 생성자처럼 API 설명에 명확히 드러나지 않으니 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해 주자.

 

 


 

2. 생성자에 매개변수가 많다면 빌더를 고려하라

 

 

빌더 패턴(Builder pattern)

 

  1.  클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
  2.  빌더 객체가 제공하는 일종의 setter 메서드들로 원하는 선택 매개변수들을 설정한다.
  3.  마지막으로 매개변수가 없는 build 메서드를 호출해 우리에게 필요하도록 커스텀한 객체를 얻는다.

 

 

public class NutritionFacts{

    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder{
        //필수 매개변수
        private final int servingSize;
        private final int servings;

        // 선택 매개변수 : 기본값으로 초기화
        private int calories =0;
        private int fat =0;
        private int sodium =0;
        private int carbohydrate =0;

        //필수 매개변수만으로 생성자 호출
        public Builder(int servingSize, int servings){
            this.servingSize = servingSize;
            this.servings = servings;
        }

        //setter 메서드로 원하는 선택 매개변수 설정
        public Builder calories(int val)
            {calories = val; return this;}
        public Builder fat(int val)
            {fat = val; return  this;}
        public Builder sodium(int val)
            {sodium = val; return this;}
        public Builder carbohydrate(int val)
            {carbohydrate = val; return this;}

        //매개변수가 없는 build 메서드를 호출해 객체 생성
        public NutritionFacts build(){
            return new NutritionFacts(this);
        }

    }


    public NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;

    }

    public static void main(String[] args){
        NutritionFacts cocaCola = new Builder(240,8).
                calories(100).sodium(35).carbohydrate(27).build();
    }
}

 

 

빌더의 setter 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.

 

 

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

각 계층의 클래스에 관련 빌더를 멤버로 정의하자. 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다.

 

public abstract class Pizza{
    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>>{
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping){
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

        //하위 클래스는 이 메서드를 오버라이딩 하여 this를 반환하도록 해야한다.
        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();
    }
}

 

 

public class NyPizza extends Pizza{
    public enum Size {SMALL, MEDIUM, LARGE}
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder>{
        private final Size size;

        public Builder(Size size){
            this.size = Objects.requireNonNull(size);
        }

        @Override public NyPizza build(){
            return new NyPizza(this);
        }

        @Override protected Builder self() {return this;}
    }

    private NyPizza(Builder builder){
        super(builder);
        size = builder.size;
    }
}

 

NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();

 

빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 또한 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. 

하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명심하자. 그러니 애초에 빌더로 시작하는 편이 나을 때가 많다.

 


 

3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.

 

싱글턴(Singleton) 이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

 

 

 

 

 


4. 인스턴스화를 막으려거든 private 생성자를 사용하라.

 

단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을때가 있다.

 

정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한것이 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어 준다. 즉, 매개변수를 받지 않는 public 생성자가 만들어지며, 사용자는 이처럼 의도치 않게 생성자가 만들어지는 상황을 목격한다.

 

이를 피하는 방법으로 종종 클래스를 추상 클래스로 만든다. 추상 클래스로 선언한다면 객체화할 수 없기 때문에 의도치 않은 생성자가 만들어지는 상황을 막을 수 있다. 하지만 하위 클래스를 만든다면 이 또한 인스턴스화 할 수 있기 때문에 근본적인 대책은 아니다. 

 

이 방법 말고 정말 간편한 방법이 있는데, 바로 private 생성자를 추가하기만 하면 클래스의 인스턴스화를 막을 수 있다.

 

컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때 뿐이고, 명시적 생성자가 private이니 클래스 바깥에서  접근 할 수 없다. 이 방식은 상속을 불가능하게 하는 효과도 있다.

 

public class UtilityClass{
    // 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
    private UtilityClass(){
        throw new AssertionError();
    }
    
    // .. 코드 생략
}

 

 


5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

 


 

6. 불필요한 객체 생성을 피하라.

 

똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다. 

 

// 아래 문장은 실행될 때마다 String 인스턴스를 새로 만든다.
// 반복문이나 빈번히 호출되는 메서드에 있다면 자원낭비가 심하다.
String s = new String("hello");


// 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.
String s = "hello";

 

 

생성 비용이 아주 비싼 객체도 더러 있다. 이러한 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용 하자.

 

 

 

 

 

 

 

https://velog.io/@cjh8746/%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9CStatic-Factory-Method

 

정적 팩토리 메서드(Static Factory Method)

정적 팩토리 메서드란 무엇인가?

velog.io