정적 팩토리 메소드에 대하여

이건회·2023년 3월 13일
1

자바

목록 보기
6/8

개요


우테코 미션을 진행하여 여럿 크루들이 생성자를 직접 사용하는 대신, 정적 팩토리 메소드라는 것을 사용해 객체 생성자 호출을 메소드로 넘기는 것을 볼 수 있었다. 정적 팩토리 메소드라는 용어를 많이 들어보긴 했으나, 정확히 어떤 경우에, 또 어떠한 방식으로 사용할 수 있는지에 대한 이해도가 낮았기에 본 글을 작성하게 되었다.

정적 팩토리 메소드란?


이펙티브 자바(조슈아 블로크 저)의 아이템 1 제목에서는 다음과 같은 이야기를 한다

생성자 대신 정적 팩토리 메소드를 고려하라

얼핏 보면 생성자를 쓰지 말라는 문장 같아 보이나 사실은 정적 팩토리 메소드 또한 생성자를 사용한다. 단지 생성자를 바깥에서 직접 호출하냐, 정적 팩토리 메소드를 통해 대신 호출하냐의 차이다.

예를 들어 다음과 같은 클래스가 있다고 가정해 보자

public class Player {

    private final String name;

    public Player(String name) {
        this.name = name;
    }
}

통상적으로는 아래와 같은 new 생성 방식을 통해 해당 객체 인스턴스를 생성할 것이다.

public class Main {

    public static void main(String[] args) {
        Player player = new Player("messi");
    }

}

그러나 만약 Player에 객체를 생성하는 static 메소드(createPlayer)를 아래처럼 만들어 보자

public class Player {

    private final String name;

    public Player(String name) {
        this.name = name;
    }

    public static Player createPlayer(String name) {
        return new Player(name);
    }
}

그럼 이제 아래와 같은 방식으로 new 생성자 대신 createPlayer 메소드를 통해 객체를 생성할 수 있다.

public class Main {

    public static void main(String[] args) {
        Player player = Player.createPlayer("messi");
    }

}

이렇게 객체를 생성할 때 생성자를 직접 사용하는 것 대신, static 메소드를 통해 생성자를 대신 호출하게 만들어 객체를 생성하는 메소드를 정적 팩토리 메소드라 한다.

정적 팩토리 메소드, 왜 쓰는가?


이펙티브 자바 아이템 1에서는 다음과 같은 정적 팩토리 메소드의 장점을 설명하고 있다.

정적 팩토리 메소드의 장점
1. 이름을 가질 수 있다
2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다
5. 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다

뭐라뭐라 하는데 얼핏 봐서는 잘 이해가 가지 않으므로 직접 코드를 치면서 이해해 보자

장점 1 : 이름을 가질 수 있다


예를 들어 축구선수라는 아래와 같은 객체가 존재할 때, 특수 포지션인 골키퍼 여부를 확인하기 위해 goalKeeper라는 필드가 있다고 가정해 보자.

public class Player {

    private final String name;
    private boolean goalKeeper;

    public Player(String name) {
        this.name = name;
    }

    public Player(String name, boolean goalKeeper) {
        this.name = name;
        this.goalKeeper = goalKeeper;
    }

    public static Player createFieldPlayer(String name) {
        return new Player(name, false);
    }

    public static Player createGKPlayer(String name) {
        return new Player(name, true);
    }
}

일반 필드플레이어를 생성하는 createFieldPlayer와 골키퍼를 생성하는 createGKPlayer, 두 정적 팩토리 메소드가 존재한다.

public class Main {

    public static void main(String[] args) {

        Player fieldPlayer = Player.createNormalPlayer("메시");
        Player goalKeeper = Player.createGKPlayer("조현우");
    }

}

만약 Player의 코드를 보지 않은 채로 위 코드를 본다고 가정하자. 필드 플레이어 객체를 만든다는 행위, 골키퍼를 만들어낸다는 행위를 메소드명만 보고도 유추해내기 쉽다. 그만큼 어떤 객체가 반환되는지 이해가 쉬워 코드 가독성이 높아지는 효과가 있다.

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


이펙티브 자바 아이템 1의 글 내용을 인용해 보도록 하겠다.

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

위 문장에서 드러나듯 캐싱을 사용할 때 정적 팩토리 메소드의 장점을 잘 활용할 수 있다.
예를 들어 아래와 같이 신호등에 대한 객체를 작성했다고 가정해 보자

public class TrafficLight {

    private static final Map<String, String> trafficLight = new HashMap<>();

    static {
        trafficLight.put("green", "go");
        trafficLight.put("yello", "warn");
        trafficLight.put("red", "stop");
    }

    public static String from(String color){
        return trafficLight.get(color);
    }

}

Main

public class Main {

    public static void main(String[] args) {

        System.out.println(TrafficLight.from("green"));
        System.out.println(TrafficLight.from("yellow"));
        System.out.println(TrafficLight.from("red"));
    }

}

//console
go
warn
stop

from이라는 정적 팩토리 메소드를 작성함으로 인해, 자주 사용되는 인스턴스 값을 가져와 반환할 수 있다. 캐싱과 정적 팩토리 메소드를 사용해 불필요한 객체 생성 비용을 없앤 것이다.

장점 3 : 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다


이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다. API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.

public class Player {

    private final String name;

    public Player(String name) {
        this.name = name;
    }

    public static Player of(String name, String position) {
        if (position == "gk") {
            return new GoalKeeper(name);
        }
        if (position == "mf") {
            return new MidFielder(name);
        }
        if (position == "df") {
            return new Defender(name);
        }
        if (position == "fw") {
            return new Forward(name);
        }
        return new Player(name);
    }
}

Player 객체의 of 메소드를 사용해 position 값에 맞는 하위 객체를 분기처리를 통해 반환하고 있다. 구현 클래스의 API를 확인하지 않아도 어떤 객체를 반환하는지 확인 가능하다.

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


장점 3과 어느정도 이어지는 내용이다. 반환하는 클래스가 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환해도 상관이 없다. 즉 매개변수가 어떻냐에 따라 각자 상황에 맞는 구현체를 반환할 수 있다는 뜻이다.

public static Player of(String name, String position) {
        if (position == "gk") {
            return new GoalKeeper(name);
        }
        if (position == "mf") {
            return new MidFielder(name);
        }
        if (position == "df") {
            return new Defender(name);
        }
        if (position == "fw") {
            return new Forward(name);
        }
        return new Player(name);
    }

위에서 든 예시 코드이지만 다시 가져와봤다. position이라는 파라미터에 어떤 입력이 들어오냐에 따라 반환되는 포지션(하위타입)이 다르다.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

자바 EnumSet의 noneOf 정적 팩토리 메소드다. 파라미터인 elementType에 따라 반환하는 구현체가 바뀌는 것을 확인할 수 있다.

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


이 말은 맨 처음에는 이해하기 어렵지만 결국 3, 4번과 어느 정도 이어지는 말이다. 인터페이스, 혹은 클래스를 작성하는 시점에 하위타입의 클래스가 없더라도. 새롭게 만드는 클래스가 상위 타입의 인터페이스 혹은 클래스를 상속 받으면 얼마든지 갈아낄 수 있다는 뜻이다.
즉 인터페이스나 상위 클래스로 반환하도록 미리 지정해 두고, 나중에 하위타입을 써먹을 일이 있을 때 반환만 하위타입으로 갈아껴주면 된다는 말이다.

public interface Coach {
}
public class Team {

    public static List<Coach> getCoach(){
        return new ArrayList<>();
    }

}

예를 들어 위처럼 Coach라는 인터페이스를 설계하고 아직 구현체를 작성하지 않았더라도, 위의 getCoach같은 메소드를 미리 만들 수 있다.

public class HeadCoach implements Coach{
}
public class Team {

    public static List<Coach> getCoach(){
        return List.of(new HeadCoach());
    }

}

향후 다음과 같은 headCoach라는 구현체가 등장할 때 갈아끼울 수 있다.

정적 팩토리 메소드의 단점은 무엇일까?


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

위의 예시에서는 그냥 기본 생성자도 public으로 선언했지만, 보통 정적 팩토리 메소드를 사용할 때는 객체 생성 시 완벽하게 정적 팩토리 메소드만 사용하게 하도록 위해 기본 생성자를 아래와 같이 private로 감추곤 한다

public class Player {

    private final String name;

    private Player(String name) {
        this.name = name;
    }
    ...
}

그러나 이 경우, 해당 클래스가 상속을 사용하고자 할 경우 public이나 protected 생성자가 필요한데, private으로 막혀 있으므로 상속을 할 수 없다. 정확히는 상속하려는 클래스의 생성자를 호출하지 못한다.

public class GoalKeeper extends Player{

    public GoalKeeper(String name) {
        super(name);
    }
}


상위타입 생성자를 private으로 막고 나니 기존에 Player를 상속하던 GoalKeeper 클래스가 super를 통해 부모 클래스 생성자를 호출하지 못하는 것을 확인할 수 있다.

그러나 이 제약은, 상속보다 컴포지션(조합)을 사용해야 하고, 객체를 불변 타입으로 만들기 위해서는 이 제약을 지켜야 하므로 오히려 장점일 수도 있다.

단점 2 : 정적 팩토리 메소드는 개발자가 찾기 어렵다


보통 기본 생성자를 사용할 때는 API설명에 명확히 드러나 있어 따로 사용법을 찾을 필요가 없으나, 기본 생성자가 막혀있고 메소드를 호출해야 하는 정적 팩토리의 특성상 다른 개발자가 정적 팩토리 메소드를 바라볼 때 어떤 기능을 하는지, 또 그 정적 팩토리 메소드가 어디에 있는지 등등을 찾기 어렵다는 단점이 있다.

그러나 이는 API 문서 작성을 잘 하거나, 정적 팩토리 메소드 명명 규칙을 잘 따른다면 어느 정도 보완 가능하다.

정적 팩토리 메소드 명명 방식


from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드
ex) Day monday = Day.from("mon");

of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드
ex) Set<Rank> 38광땡 = EnumSet.of("3광","8광");

valueOf : from과 Of를 조금 더 자세히 쓰고 싶을 때 사용
ex) BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

instance 혹은 getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않음
ex) StackWalker luke = StackWalker.getInstance(options);

create 혹은 newInstance : instance 혹은 getInstance와 비슷하나, 반드시 새로운 인스턴스를 생성해 반환함을 보장한다는 점이 특징
ex) Car car = Car.createCar();

get[Type] : getInstance와 같으나, 생성하는 객체의 클래스 내부가 아닌, 생성하는 객체의 클래스가 아닌 다른 클래스에 해당 객체를 생성하는 메소드를 정의할 때 사용함. [Type]은 팩토리 메소드가 반환할 객체의 실제 이름을 사용
ex) FileStore fs = Files.getFileStore(path);

new[Type] : getType과 같으나, 반드시 새로운 인스턴스를 생성함. 즉 newInstance와 같으나, 생성하는 객체의 클래스 내부가 아닌, 생성하는 객체의 클래스가 아닌 다른 클래스에 해당 객체를 생성하는 메소드를 정의할 때 사용함. [Type]은 팩토리 메소드가 반환할 객체의 실제 이름을 사용
ex) BufferedReader br = Files.newBufferedReader(path);

[Type] : getType과 newType을 간결하게 쓰고 싶을 때 사용
ex) List<Complaint> litany = Collections.list(legacyLitny);

profile
하마드

0개의 댓글