스프링을 공부하며 Setter를 사용하지 않고 정적 팩토리 메서드를 사용하는 이유가 무엇일지 궁금해 찾아보았습니다.
해당사이트 내용을 가져왔습니다.
https://johngrib.github.io/wiki/pattern/static-factory-method/
https://devlog-wjdrbs96.tistory.com/256
객체 생성을 캡슐화하는 기법입니다.
좀 더 구체적으로는 객체를 생성하는 메소드를 만들고, static으로 선언하는 기법입니다.
자바로 코딩할 때 흔하게 볼 수 있는 valueOf 메서드가 정적 팩토리 메서드의 한 예라 할 수 있습니다.
BigInteger answer = BigInteger.valueOf(42L); // BigInteger 42를 리턴한다
static으로 선언된 메서드이며, new BigInteger(...)를 은닉하고 있다는 사실을 알 수 있습니다.
valueOf 외에, 정적 팩토리 메서드의 이름으로 흔히 사용되는 것들은 다음과 같습니다.
이펙티브 자바 에서는 다음과 같은 규칙을 소개하고 있습니다.
생성자 대신 정적 팩터리 메서드를 사용할 수 없는지 생각해 보라.
Effective Java에서는 다음 규칙을 지켰을시 나타나는 장단점을 다음과 같이 설명합니다.
장점
- 이름이 있으므로 생성자에 비해 가독성이 좋다.
- 호출할 때마다 새로운 객체를 생성할 필요가 없다.
- 하위 자료형 객체를 반환할 수 있다.
- 형인자 자료형(parameterized type) 객체를 만들 때 편하다.
단점- 정적 팩토리 메서드만 있는 클래스라면, 생성자가 없으므로 하위 클래스를 못 만든다.
- 정적 팩토리 메서드는 다른 정적 메서드와 잘 구분되지 않는다. (문서만으로 확인하기 어려울 수 있음)
다음은 전사와 마법사가 나오는 판타지 게임 소스 코드의 일부입니다.
class Character {
int intelligence, strength, hitPoint, magicPoint;
public Character(int intelligence, int strength, int hitPoint, int magicPoint) {
this.intelligence = intelligence; // 지능
this.strength = strength; // 힘
this.hitPoint = hitPoint; // HP
this.magicPoint = magicPoint; // MP
}
// 정적 팩토리 메소드
public static Character newWarrior() {
return new Character(5, 15, 20, 3); // 전사는 힘과 HP가 높다
}
// 정적 팩토리 메소드
public static Character newMage() {
return new Character(15, 5, 10, 15); // 마법사는 지능과 MP가 높다
}
}
만약 생성자를 사용해 전사나 마법사를 생성한다면 다음과 같을 것입니다.
Character warrior = new Character(5, 15, 20, 3);
Character mage = new Character(15, 5, 10, 15);
변수명이 없었다면 5, 15, 20, 3 같은 연속된 숫자만으로는 캐릭터의 직업을 알아보기 어려웠을 것이다.
하지만 정적 팩토리 메서드를 사용한다면 좀 더 읽기 쉬운 코드가 됩니다.
Character warrior = Character.newWarrior();
Character mage = Character.newMage();
사실 위와 같이 마법사와 전사를 만드는 코드는 정적 팩토리 메서드를 호출할 때마다 new Character(...)를 호출합니다.
그러나 immutable 객체를 캐시해서 사용한다면 굳이 일일이 new 같은 비싼 연산을 사용할 필요가 없습니다. 이러한 장점은 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다.
다음은 개요에서 호출 코드의 예로 사용했던 java.math.BigInteger.valueOf메서드의 코드입니다.
public static final BigInteger ZERO = new BigInteger(new int[0], 0);
private final static int MAX_CONSTANT = 16;
private static BigInteger posConst[] = new BigInteger[MAX_CONSTANT+1];
private static BigInteger negConst[] = new BigInteger[MAX_CONSTANT+1];
static {
/* posConst에 1 ~ 16까지의 BigInteger 값을 담는다. */
/* negConst에 -1 ~ -16까지의 BigInteger 값을 담는다. */
}
public static BigInteger valueOf(long val) {
// 미리 만들어둔 객체를 리턴한다
if (val == 0)
return ZERO;
if (val > 0 && val <= MAX_CONSTANT)
return posConst[(int) val];
else if (val < 0 && val >= -MAX_CONSTANT)
return negConst[(int) -val];
// 새로운 객체를 만들어 리턴한다
return new BigInteger(val);
}
위와 같은 방법을 사용하면 흔히 사용하는 0 같은 값을 호출시마다 return ZERO; 가 호출되기때문에 일일이 new를 통해 생성하는 일을 피할 수 있습니다.
따라서 객체 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올릴 수 있습니다.
정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있습니다. 이런 클래스를 인스턴스 통제 클래스 라고 합니다.
인스턴스를 통제하면 클래스를 싱글톤으로, 인스턴스화 불가로 만들 수도 있습니다.
불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있습니다.
(a == b일 때만 a.equals(b) 성립)
인스턴스 통제는 플라이웨이트 패턴의 근간이 되며, 열거 타입은 인스턴스가 하나만 만들어짐을 보장합니다.
public class Test {
private String name;
private static final Test GOOD_STUDY = new Test();
public static Test Test_goodStudy() {
return GOOD_STUDY;
}
public static void main(String[] args) {
Test test = Test_goodStudy();
}
}
위와 같이 싱글톤을 이용해서 메소드를 여러 번 호출해도 객체가 한번만 만들어지게 만들 수도 있습니다.
플라이웨이트 패턴이란? (Flyweight pattern)
데이터를 공유하여 메모리를 절약하는 패턴, 공통으로 사용되는 객체는 한번만 사용되고 Pool에 의해서 관리, 사용된다.
(JVM의 String Pool에서 같은 String이 잇는지 먼저 찾는다. [불변객체 String])
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없습니다.
public class Foo {
public static Foo getFoo(boolean flag) {
return flag ? new TestFoo() : new BarFoo();
}
static class BarFoo extends Foo {
}
static class TestFoo extends Foo {
}
public static void main(String[] args) {
Foo foo1 = Foo.getFoo(true); // TestFoo
Foo foo2 = Foo.getFoo(false); // BarFoo
}
}
위의 코드를 보면 매게변수 flag에 따라 반환 타입이 달라지는 것을 볼 수 있습니다. 이렇게 정적 팩토리 메소드를 사용하면 유연하게 구조를 갖출 수 있습니다.
리턴하는 객체의 타입을 유연하게 지정할 수 있습니다.
다음은 어느 가상의 인터넷 쇼핑몰에서 할인 코드를 처리하는 정적 팩토리 메서드이다.
class OrderUtil {
public static Discount createDiscountItem(String discountCode) throws Exception {
if(!isValidCode(discountCode)) {
throw new Exception("잘못된 할인 코드");
}
// 쿠폰 코드인가? 포인트 코드인가?
if(isUsableCoupon(discountCode)) {
return new Coupon(1000);
} else if(isUsablePoint(discountCode)) {
return new Point(500);
}
throw new Exception("이미 사용한 코드");
}
}
class Coupon extends Discount { }
class Point extends Discount { }
할인 코드의 규칙에 따라 Coupon과 Point 객체를 선택적으로 리턴하고 있습니다.
이를 위해서는 두 하위 클래스가 같은 인터페이스를 구현하거나, 같은 부모 클래스를 갖도록 하면 됩니다.
만약 파일을 분리하기 애매한 작은 클래스가 있다면 private class를 활용할 수도 있습니다.
Lombok의 RequiredArgsConstructor를 사용하면 정적 팩토리 메소드를 쉽게 만들 수 있습니다.
import lombok.RequiredArgsConstructor;
// ↓ 정적 팩토리 메소드 이름
@RequiredArgsConstructor(staticName = "of")
public class BlogUser {
private final Long id;
private final String name;
}
위와 같이 staticName을 선언하면 롬복이 of라는 이름을 가진 정적 팩토리 메소드를 만들어줍니다.
즉, 다음과 같이 사용할 수 있습니다.
BlogUser user = BlogUser.of(1L, "JohnGrib");