본문 바로가기
Programming/Effective Java

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

by s.rookie 2022. 2. 20.

서론


객체를 생성 할 때 일반적으로 사용되는 방법은 `public 생성자`를 사용하는 방법일 것이다. 하지만 `생성자` 말고도 객체를 생성하는 방법이 있는데 바로 오늘 포스팅의 주제인 `정적 팩터리 메서드`를 사용하는 방법이다.

 

정적 팩터리 메서드를 설명함에 2가지 방법을 통해 객체를 생성하는 방법을 코드로 살펴보도록 하자.

 

생성자를 활용한 객체 생성

public class Character {
    private int strStat;
    private int dexStat;
    private int intStat;
    private int luckStat;
    
    public Character(int strStat, int dexStat, int intStat, int luckStat) {
        this.strStat = strStat;
        this.dexStat = dexStat;
        this.intStat = intStat;
        this.luckStat = luckStat;
    }
}

 

정적 팩터리 메서드를 활용한 객체 생성

public class Character {
    private int strStat;
    private int dexStat;
    private int intStat;
    private int luckStat;
    
    public Character(int strStat, int dexStat, int intStat, int luckStat) {
    	this.strStat = strStat;
        this.dexStat = dexStat;
        this.intStat = intStat;
        this.luckStat = luckStat;
    }
    
    public static Character createWarrior(int strStat, int dexStat, int intStat, int luckStat) {
    	return new Character(strStat, dexStat, intStat, luckStat);
    }
    
    public static Character createRepparee(int strStat, int dexStat, int intStat, int luckStat) {
    	return new Character(strStat, dexStat, intStat, luckStat);
    }
}

 

이제 밑에서 장점들과 단점들에 대해서 하나씩 살펴보도록 하자.

 

정적 팩터리 메서드의 장점


1. 이름을 가질 수 있다

생성자로 객체를 생성할 경우 매개변수와 생성자 그 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 밑의 코드는 `전사 캐릭터`를 생성자를 통해 객체로 생성하는 과정이다.

 

Character warrior = new Character(12, 10, 8, 10);

 

위의 코드를 통해 `전사 캐릭터`임을 유추할 수 있는 힌트는 변수명을 제외하고는 없다. 이처럼 각 생성자가 어떤 역할을 하는지 정확히 기억하기 어려워 잘못된 객체를 생성할 수 있다.

 

하지만 정적 팩터리 메서드를 사용한다면 좀 더 직관적인 코드가 된다. 밑의 코드를 한번 살펴보자.

 

Character warriror = Character.createWarrior(12, 10, 8, 10);

 

위의 코드처럼 `정적 팩터리 메서드`로 객체를 생성하게 되면 `createWarrior()`와 같은 이름을 가질 수 있게 됨으로 좀 더 직관적으로 코드를 알 수 있게 된다.

 

 

또 다른 예시로 프리코스 과정에서 수행했던 숫자 야구 게임을 예시로 들어보자.

 

public class BallsFactory {
    private static final int START_NUMBER_IN_RANGE = 1;
    private static final int END_NUMBER_IN_RANGE = 9;
    
    public static Balls createInputBalls(List<String> ballNumbers) {
    	return new Balls(ballNumbers.stream()
                        .map(Ball::new)
                        .collect(Collectors.toList());
    }
    
    public static Balls createRandomBalls() {
        final List<Ball> randomNumbers = new ArrayList();
        while (randomNumbers.size() != Balls.VALID_LENGTH_OF_BALLS) {
            // 랜덤한 숫자 야구공을 만드는 메서드
        }
        return new Balls(randomNumbers);
    }
}

 

`createInputBalls`, `createRandomBalls` 모두 야구공들의 객체를 생성하고 반환하는 정적 팩터리 메서드이다. 메서드의 이름을 통해서 입력을 통해 야구공들을 생성하는지 아니면 랜덤으로 야구공들을 생성하는지 직관적으로 이해할 수 있을 것이다.

 

 

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

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

 

public class Coffee {
    private int price;
    private String makeTime;
    
    private static final Coffee AMERICANO = new Coffee(4500, 5);
    private static final Coffee LATTE = new Coffee(5100, 7);
    
    public static Coffee getAmericano() {
    	return AMERICANO;
    }
    
    public static Coffee getLatte() {
    	return LATTE:
    }
}
public class Application {
    public static void main(String... args) {
        Coffee americano = Coffee.getAmericano();
        Coffee latte = Coffee.getLatte();
    }
}

 

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있게 된다.

추후 API 개발시 하위 타입 객체를 반환하도록 적용하면 구현 클래스를 외부에 공개하지 않고 그 객체를 반환할 수 있어 API 문서를 작게 유지 할 수 있다. 이는 API의 복잡도를 완화시켜 쉽게 사용할 수 있게 된다는 장점이 있다. 

 

한번 코드를 통해 확인해보자.

 

public interface Level {
    static Basic getBasic() {
        return new Basic();
    }
    
    static Intermediate getIntermediate() {
        return new Intermediate();
    }
}

 

정적 팩터리 메서드의 네이밍을 이용해서 하위 클래스의 타입으로 반환이 가능하다!! 이를 통해 클라이언트는 타입을 알 필요가 없게 된다.

 

 

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

매개변수에 따라 다양한 하위타입의 객체를 반환할 수도 있다. 전체적인 흐름은 위의 내용과 비슷하니 코드를 통해 장점을 확인할 수 있을 것이다.

 

public class Car {
    public static Car getCar(String name) {
        if ("Hyundai".equals(name)) {
        	return new Hyundai();
        }
        if ("Kia".equals(name)) {
        	return new Kia();
        }
    }
}

public class CarFactory {
    Car hyundaiCar = Car.getCar("Hyundai");
    Car kiaCar = Car.getCar("Kia")'
}

 

 

5. 클라이언트를 구현체로부터 분리해줄 수 있다.

해당 예시는 책도 그렇고 대부분의 블로그에서 JDBC를 예시로 들고 있다. 하지만 JDBC의 getConnection()의 동작과정에 대해서 모르는 분도 있기에 다른 예시로 설명하려고 한다. (혹, 틀린부분이 있으면 댓글을 남겨주셨으면 한다.)

 

 

클라이언트가 레벨을 얻는 로직을 구현해야 된다고 가정해보자. 프레임워크 개발자가 다음과 같은 코드를 구현하였다.

 

// 구현체
public class Level { ... }

public class Basic extends Level {
    private int score;
    
    public Basic(int score) {
    	this.score = scroe;
    }
}

 

클라이언트는 다음과 같이 로직을 구현해야 할 것이다.

 

Level level = new Basic();

 

이때 갑자기 서비스에 새로운 규칙이 새로 추가되었다. Baisc은 60점 미만의 학생들에게 Intermediate는 80점 미만의 학생들에게 주어지는 규칙이 된 것이다. 프레임워크 개발자는 다음과 같이 코드를 수정하였다.

 

public class Level { ... }

public class Basic extends Level { ... }

public class Intermediate extends Level { ... }

 

클라이언트도 프레임워크가 업데이트 됨에 따라 클라이언트 코드를 다음과 같이 변경해야 한다.

 

Level level;

if (score < 60) {
    level = new Basic();
} 
if (score < 80) {
    level = new Intermediate();
}

 

결국, 레벨을 산출하는 로직(규정)이 달라짐에 따라 매번 클라이언트가 로직을 수정해야 하는 상황이 생길 것이다.

 

 

이렇게 시스템이 변경될 때마다 클라이언트가 로직을 수정하는 단점 `정적 팩토리 메서드`를 통해 구현체만 바꿔끼울 수 있는 형태로 변경하여 클라이언트가 로직을 수정하지 않고도 업데이트 된 프레임워크를 사용할 수 있는 구조를 가지게 된다.

 

다음 코드를 통해 그 구조를 살펴보자.

 

public class Level {
    // 정적 팩토리 메서드
    public static Level from(int score) {
        if (score < 60) {
            return new Basic();
        } 
        if (score < 80) {
            return new Intermediate();
        }
    }
}
// 클라이언트
Level level = Level.from(// 점수 값);

 

코드를 보면 클라이언트 코드는 변경되지 않는다. `정적 팩터리 메서드`를 통해서 점수 값만 인자로 보내주면 해당하는 구현체로 알아서 바꿔끼워 반환해주는 것을 확인할 수 있게 된다.

 

즉, 한 문장으로 정리하자면 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 되기 때문에 이렇게 유연한 구조를 가져갈 수 있게 된다.

 

 

정적 팩터리 메서드의 단점


1. 상속을 하려면 public이나 protected 생성자가 필요하니 상속을 할 수 없다.

해당 부분이 과연 단점일까?? 상속보단 컴포지션이라는 말이 있다. 

오히려 해당 부분을 장점으로 받아들일 수도 있다고 생각한다. 

 

상속보다 컴포지션에 대한 내용을 다음 글을 참고를 추천한다.

 

[Effective Java] Item18. 상속보다는 컴포지션을 사용하라

상속이 안전 할 때 상위 클래스와 하위클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서 사용한다. 확장할 목적으로 설계되었고 문서화도 잘되었다. 상속이 안전하지 않을때 다른 패키

jyami.tistory.com

 

2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

해당 부분도 역시 단점일까 라는 생각이 든다.

대부분의 협업시 팀에서는 메서드 네이밍에 대한 규칙이 있기 때문에 찾기 어렵다라는 말에는 개인적으로 동의할 수가 없다.

 

 

정리

많은 장점이 있는 정적 팩터리 메서드.

 

생성자보다는 정적 팩터리 메서드를 사용하는 것을 권장하지만 그렇다고 무분별한 사용보다는 장점을 활용할 수 있으며 팀에서의 규칙을 정의하여 잘 활용하도록 하자.

 

 

reference.

http://www.yes24.com/Product/Goods/65551284
https://slf4me.com/post/effective-java/01/

댓글