본문 바로가기

Back-end/Java

[JAVA] 정적 팩토리 메서드란 무엇이고 왜 사용할까?

✅ 정적 팩토리 메서드

  • 쉽게 말해 생성자로 인스턴스를 생성하지 않고, static Method를 사용해 인스턴스를 생성하는 방식이다.
  • 간단한 예시로 여러 장르(genre)를 가지는 게임(Game) 객체에 대한 코드를 살펴보자.
생성자
public class Game {
    private String genre;
 
    public Game(String genre) {
        this.genre = genre;
    }
}
public static void main(String[] args) {
        Game game = new Game("FPS");
    }

 

정적 팩토리 메서드

 

public class Game {
    private String genre;
 
    private Game(String genre) {
    }
 
    public static Game from(String genre) {
        return new Game(genre);
    }
}

 

 public static void main(String[] args) {
        Game game = Game.from("RPG");
    }

 

이렇게만 봤을 때는 "그냥 익숙한 생성자 방식을 사용해서 인스턴스를 생성하면 안돼?"라고 생각할 수 있다.

 

나도 처음에 이걸 굳이 왜 쓰는거지라는 생각이 먼저 들었고 겉멋인가 생각이들었다.


하지만 정적 팩토리 메서드 패턴이 아무 의미 없이 등장하진 않았을 것이다.


그래서 이번 포스팅에서는 정적 팩토리 메서드를 왜 사용하는가, 그리고 언제 사용하면 좋은지에 대해 다뤄보려한다.

 

 


 

✅ 왜 정적 팩토리 메서드를 사용하는가?

 

🖥️ 장점 1. 이름을 가질 수 있다.

 

게임별로 게임 장르와, 이용 등급이 있다고 가정해보자.

 

생성자를 사용할 때

 

public class Game {
    private String genre;
    private String rating;
    
    public Game(String genre, String rating) {
        this.genre = genre;
        this.rating = rating;
    }
}

 

public static void main(String[] args) {
    Game fpsForAdult = new Game("fps", "청소년이용불가")
    Game rpgForAll = new Game("rpg", "전체이용가")
}

 

 

Game 객체의 내부 구조를 모르는 사람은 Game 인스턴스를 생성할 때 몇 번째 인자에 어떤 값이 들어가야 하는지 모른다.


이는 자바 특성상 생성자는 클래스 이름으로 고정이기 때문에 발생하는 문제이다.


이 코드를 혼자서만 보면 상관없지만, 협업같은 환경에서 다른 사람이 코드만으로 의도가 파악할 수가 없다.


정적 팩토리 메서드를 사용한다면 어떻게 바뀔까?

 

정적 팩토리 메서드를 사용할 때

 

생성자로 인스턴스 생성하는 것을 방지하기 위해 생성자의 접근 제어자를 private로 설정한다.

public class Game {
    private String genre;
    private String rating;
 
    private Game(String genre, String rating) {
        this.genre = genre;
        this.rating = rating;
    }
    
    public static Game adultAccessFrom(String genre) {
        return new Game(genre, "청소년이용불가");
    }
 
    public static Game fullAccessFrom(String genre) {
        return new Game(genre, "전체이용가");
    }
}

 

public static void main(String[] args) {
    Game fpsForAdult = Game.adultAccessFrom("fps");
    Game rpgForAll = Game.fullAccessFrom("rpg");
}

 

 

이런식으로 어떤 인스턴스를 생성할 지 메서드 이름을 통해 파악이 가능해지기 때문에


Game 객체 내부 구조를 알 필요가 없다.


훨씬 가독성이 좋아진 것을 볼 수 있다.

 

🖥️ 장점 2. 호출될 때 마다 인스턴스를 새로 생성하지 않아도 된다. (싱글톤으로 활용 가능)

public class Game {
    private static final Game instance = new Game();
 
    private Game() {}
 
    public static Game getInstance() {
        return instance;
    }
}

 

public static void main(String[] args) throws IOException {
    Game game1 = Game.getInstance();
    Game game2 = Game.getInstance();
 
    System.out.println(game1);
    System.out.println(game2);
 
    System.out.println(game1 == game2);
}

 

 

위처럼 싱글톤으로 객체를 하나만 만들어두고 이를 재사용하여 불필요한 객체 생성을 막을 수 있다.


오로지 하나의 객체만 반환하기 때문에 메모리를 절약할 수 있다는 장점이 있다.


그렇다고 무조건 싱글톤 방식이 좋은 것은 아니다.


한번만 생성되어 상태를 공유되어야 하는 경우에 유용하게 사용할 수 있지만,

 

 각 객체가 다른 상태를 가져야 한다면 위 방식은 적절하지 않을 것이다.


상황에 맞게 사용하자.

 

🖥️ 장점 3. 파라미터에 따라 다른 객체를 반환할 수 있다.

메서드이기 때문에 파라미터를 받을 수 있고, 그렇기 때문에 파라미터 별로 분기처리하여 

 

다른 객체를 반환하도록 할 수 있다.


3번의 장점과 연결한다면 Car 인터페이스를 구현하는 구현체 클래스들을 파라미터 별로 반환할 수 있는 것이다.

 

public interface Car {}
class SportsCar implements Car {}
class SuvCar implements Car {}
 
public static Car createCar(String type) {
    if (type.equals("sports")) {
        return new SportsCar();
    }
    if (type.equals("suv")) {
        return new SuvCar();
    } 
    throw new IllegalArgumentException("해당 타입의 자동차가 존재하지 않습니다 : " + type);
}

 

 

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

public class CarFactory {
 
    private CarFactory() {
    }
 
    public static Car createCar(String type) {
        if (type.equals("sports")) {
            return new SportsCar();
        }
        if (type.equals("suv")) {
            return new SuvCar();
        } 
        throw new IllegalArgumentException("해당 타입의 자동차가 존재하지 않습니다 : " + type);
    }
}

 

여기에 전기자동차 (ElectricCar)가 추가되었다고 가정해보자.

 

public class CarFactory {
 
    private CarFactory() {
    }
 
    public static Car createCar(String type) {
        if (type.equals("sports")) {
            return new SportsCar();
        }
        if (type.equals("suv")) {
            return new SuvCar();
        } 
        if (type.equals("electric")) {
            return new ElectricCar();
        }
        throw new IllegalArgumentException("해당 타입의 자동차가 존재하지 않습니다 : " + type);
    }
}

 

위 처럼 ElectricCar 클래스를 만들고 CarFactory 클래스의 팩토리 메서드에  ElectricCar객체를 생성하는 메서드만 추가하면 된다.

✏️공부한 내용

어떤 타입의 객체를 반환할지 결정하는 로직이 팩토리 메서드 내부에 있기 때문에


클라이언트는 팩토리 메서드를 호출하기만 하면 되고, 클라이언트는 어떤 클래스의 객체가 생성되는지 알 필요가 없다.


새로운 타입의 객체가 필요해진다면 팩토리 메서드만 수정하면 된다.


즉, 유연한 확장성을 가질 수 있고, 내부 구현을 드러내지 않아 캡슐화할 수 있다는 장점도 있다.