클라이언트가 클래스의 인스턴스를 얻는 대표적인 방법으로 public 생성자를 이용한 방법이 있다.
[public 생성자를 이용한 인스턴스 생성 방법]
public class Car {
private String name;
public Car(final String name) {
this.name = name;
}
}
...
Car car = new Car("자동차1");
이 외에도 인스턴스를 얻는 방법이 있는데 바로 정적 팩터리 메서드를 사용하는 것이다.
[정적 팩터리 메소드를 이용한 인스턴스 생성 방법]
public class Car {
private String name;
private Car(final String name) {
this.name = name;
}
public static Car createCar(final String name){
return new Car(name);
}
}
...
Car car = Car.createCar("자동차2");
클래스는 이처럼 public
생성자 대신(혹은 생성자와 함께) 정적 팩터리 메서드를 클라이언트에게 제공해준다.
생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면, 정적 팩터리 메서드는 이름을 통해 반환될 객체의 특성을 묘사할 수 있다.
[생성자와 정적 팩터리 메서드 비교 예시]
public class BigInteger extends Number implements Comparable<BigInteger> {
private BigInteger(int numBits, Random rnd) {
this(1, randomBits(numBits, rnd));
}
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}
}
위의 코드를 보면 생성자인 BigInteger
와 정적 팩터리 메서드인 BigInteger.probablePrime
중 [어느 쪽이 값이 소수인 BigInteger
를 반환] 한다는 의미를 더 잘 설명하는지 쉽게 알 수 있다.
이렇게 이름을 갖는 정적 팩터리 메서드는 유용하게 사용할 수 있다.
예컨대, 생성자는 하나의 시그니처로는 생성자를 하나만 만들 수 있다. 하지만 정적 팩터리 메서드는 시그니처가 같은 생성자가 여러개 필요할 경우 유용하게 사용이 가능하다.
[하나의 시그니처로 두개의 생성자를 만들려는 경우]
public class User {
private String name;
private String address;
public User(final String address) {
this.address = address;
}
public User(final String name) {
this.name = name;
}
}
//에러 발생
만약, 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 경우, 생성자를 정적 팩터리 메서드로 바꾸고 차이를 잘 드러내는 이름을 지어주는 것이 좋다.
[정적 팩터리 메서드로 여러 생성자를 대체하는 경우]
public class User {
private String name;
private String address;
public static User enterAddress(final String address) {
User user = new User();
user.address = address;
return user;
}
public static User registerUser(final String name){
User user = new User();
user.name = name;
return user;
}
}
...
User userName = User.registerUser("User1");
User userAddress = User.enterAddress("Seoul");
불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체생성을 피할 수 있다.
불변 클래스: 객체 생성 후 변경이 불가능한, 가변적이지 않은 클래스를 말한다.
[인스턴스 캐싱을 통한 불필요한 객체 생성 방지]
public class User {
private static final User user = new User();
public static User getInstance(){
return user;
}
}
...
User user = User.getInstance();
생성 비용이 큰 같은 객체가 자주 요청되는 상황이라면 위처럼 인스턴스를 재활용하는 방식을 통해 성능을 향상시킬 수 있다. (이와 비슷한 기법으로 플라이웨이트 패턴 기법이 있다.)
또한, 생성자의 접근제어자를 private
으로 설정함으로써 객체 생성을 정적 팩터리 메서드로만 가능하게 하도록 제한할 수 있다.
인스턴스를 공유를 통해 반복적인 인스턴스 생성을 방지하여 메모리 낭비를 줄이는 방식이다.
하나의 클래스 인스턴스로 여러 개의 가상 인스턴스를 제공하고 싶을 때 사용하는 패턴이다. 이 패턴은 많은 수의 객체를 생성해야할 때 주로 사용된다.
플라이 웨이트 패턴은 아래 중 해당하는 상황이 많을수록 적용하기 적합하다.
객체의 내적속성은 객체를 유니크하게 하는 것이고, 외적 속성은 클라이언트의 코드로부터 설정되어 다른 동작을 수행하도록 사용되는 특성이다.
예를 들어, Circle
이라는 객체는 color
와 width
라는 외적 속성을 가질 수 있다.
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아있게 할지 철저히 통제할 수 있다. 이런 클래스를 [인스턴스 통제 클래스] 라고 한다.
그렇다면 인스턴스를 통제하는 이유는 무엇일까?
싱글턴 패턴: 전역변수를 사용하지않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴.
이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 제공한다. 또한 이는 생성자 역할을 하는 정적 팩터리 메서드가 반환값을 가지고 있기 때문에 가능한 능력이다.
[하위 타입 객체 반환 예시]
public class Level {
...
public static Level of(int score) {
if (score < 50) {
return new Basic();
} else if (score < 80) {
return new Intermediate();
} else {
return new Advanced();
}
}
...
}
이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있다. 다시 말해 인터페이스를 구현하고 있는 클래스를 노출시키지 않을 수 있고 프로그래머가 굳이 구현 클래스가 무엇인지 알아보지 않아도 된다. 즉, 객체 생성을 캡슐화 할 수 있다.
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
[입력 매개변수에 따른 하위 타입 객체 반환]
public class Car {
private String type;
public static Car classifyCarType(final String carType) {
if ("Manual".equals(carType)) {
return new manualCar();
} else {
return new autoCar();
}
}
static class manualCar extends Car {
}
static class autoCar extends Car {
}
}
...
Car car = Car.classifyCarType("Auto");
이를 통해 사용자는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없어진다.
즉, 클라이언트를 구현체로부터 분리해준다.
[하위 타입을 반환하는 정적 팩토리 메소드]
public class Level {
public static Level of(int score) {
return new Basic();
}
}
class Basic extends Level {
}
위의 Level.of(int score)
를 클라이언트가 직접 구현하지 않게 함으로써 분리할 수 있다. 다시 말해, Level.of(int score)
메소드의 로직이 변하더라도 클라이언트는 수정없이 그대로 동일한 메소드를 사용할 수 있다.
[로직이 변경된 하위 타입을 반환하는 정적 팩토리 메서드]
public class Level {
// 정적 팩토리 메서드
public static Level of(int score) {
if (score < 50) {
return new Basic();
} else if (score < 80) {
return new Intermediate();
} else {
return new Advanced();
}
}
}
class Basic extends Level {
}
class Intermediate extends Level {
}
class Advanced extends Level {
}
...
이 덕분에 메소드는 그대로 유지한채 아래 코드처럼 구현체만 바꿔 끼울 수 있다.
Level level1 = new Basic();
Level level2 = new Intermediate();
상속을 하려면 public
이나 protected
생성자가 필요한데 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
다시 말해, [일반적으로 정적 팩토리 메서드는 private
접근 제어자를 제공하기 때문에 하위 클래스를 만들 수 없다] 라는 말이다.
정적 팩토리 메서드를 갖는 클래스는 생성자를 private
으로 처리한다. 그 이유는, 정적 팩토리 메서드를 포함한 클래스 자체가 인스턴스를 생성할 때 public
생성자를 사용하지 않고 정적 팩토리 메서드를 사용한다는 의도를 가지고 있기 때문이다. 만약 하나의 클래스에 public
생성자와 정적 팩토리 메서드를 함께 포함하고 있다면 다른 개발자들이 이 클래스의 의도를 파악하지 못할 것이다. 따라서 정적 팩토리 메서드를 제공하는 클래스는 public
이나 protected
생성자를 사용하지 않는것이 일반적이다.
위 내용을 바탕으로 생각해보면 정적 팩토리 메서드를 가진 클래스는 private
생성자를 가지고 있을 것이다. 만약, 상속을 해야 한다면 부모 클래스의 생성자를 뜻하는 super()
를 반드시 호출해야 한다. 하지만 정적 팩토리 메서드를 가진 클래스의 생성자는 private
이므로 상속이 불가능하다.
따라서 하위 클래스를 만들 수 없다.
따라서 API문서에 잘 명시하고 일반적으로 널리 사용되는 규약에 따라 메소드의 이름을 작성하여야 한다.
from : 하나의 매개변수를 받아서 객체를 생성
Date d = Date.from(instance);
of : 여러개의 매개 변수를 받아서 객체를 생성
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf : from과 of의 더 자세한 버전
BigInteger prime = BigInteger.valudOf(Integer.MAX_VALUE);
getInstance / instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
StackWalker luke = StackWalker.getInstance(options);
newInstance / create : 새로운 인스턴스를 생성
Object newArray = Array.newInstace(classObject, arrayLen);
get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
FileStore fs = Files.getFileStore(path);
new[OtherType] : 다른 타입의 인스턴스를 생성.
BufferedReader br = Files.newBufferedReader(path);
되도록이면 생성자 보다는 정적 팩토리 메소드를 사용할 것을 추천한다.
public
생성자는 인스턴스 생성의 의미만 가질때 사용하도록 하자.
정적 팩터리 메서드는 객체 인스턴스를 생성하는 생성자 역할을 하는 클래스 메서드로 볼 수 있다. 객체간 형 변환이 필요할 때 또는 여러번의 객체 생성이 필요한 경우 정적 팩토리를 사용하는것이 유용하다. 단, 정적 팩토리 메서드만 존재하는 경우 상속이 불가능하니 주의하여 사용하여야 한다.