2025. 3. 17. 21:32ㆍEffective-Java
오늘은 그동안 이펙티브 자바를 공부하면서 정리한 내용을 복습할겸 글을 작성해볼 것이다.
item1의 내용은 단순히 생성자를 쓰기보다 정적 팩토리 메서드를 사용하는것을 고려하라는 것인데 그 이유에 대해 알아보자
일반적으로 우리는 객체를 생성할 때 생성자를 사용한다.
자바를 처음 배울 때부터 자연스럽게 사용하는 방식이며 익숙한 방법이다.
그렇다면 왜 정적 팩토리 메서드를 고려하는 걸까?
public class Person {
private String name;
private int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
}
}
정적 팩토리 메서드는 객체를 생성하는 역할을 하는 정적 메서드이다. 생성자 대신 아래와 같이 정적 팩토리 메서드를 정의 할 수 있다.
public class Person {
private String name;
private int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
}
//생성자 대신 정적 팩토리메서드로 무슨 역할을 하는지 더 정확히 나타낼 수 있음
public static Person of(String name, int age) {
return new Person(name, age);
}
}
이 방식이 생성자보다 좋은 이유는 이름을 가질 수 있기 때문이다.
예를 들어 Person.of(name, age) 라고 하면 이 객체를 생성하는 메서드 라는 것을 직관적으로 알 수 있다.
하지만 이 정도 이유만으로 굳이? 정적 팩토리 메서드를 사용해야하나? 라는 생각이 들 수 있다.
그래서 정적 팩토리 메서드가 유용한 경우 5가지를 살펴보자.
- 정적 팩토리 메서드 사용이 좋은 상황
1. 객체 생성의 이름을 통해 의도를 명확히 해야 할 경우
(위의 코드에서 충분히 보여줬으므로 생략)
2. 객체 생성 로직이 복잡하거나 추가 작업이 필요할 경우
public static User createWithValidation(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("나이는 0 이상이어야 합니다.");
}
return new User(name, age);
}
생성자와 달리 이런 예외처리 같은 검증 로직을 추가하기 좋다.
3. 같은 매개변수로 특정 객체를 재사용해야 할 경우 (캐싱)
public class Person {
// 캐싱을 활용한 동일 인스턴스 반환
private static final Map<String, Person> CACHE = new HashMap<>();
private String name;
private int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
}
public static Person of(String name, int age) {
String key = name + age;
if (!CACHE.containsKey(key)) {
CACHE.put(key, new Person(name, age));
}
return CACHE.get(key);
}
}
Person p1 = Person.of("Alice", 25);
Person p2 = Person.of("Alice", 25);
System.out.println(p1 == p2); // true (같은 객체를 반환)
이러한 방식으로 같은 인스턴스를 재사용하여 메모리 사용량과 불필요한 객체 생성을 줄일 수 있다.
4. 싱글턴 도는 불변 객체를 생성해야 할 경우
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
getInstance()를 사용하여 항상 같은 객체를 반환해 객체 생성을 제한할 수 있다.
5. 하위 클래스나 인터페이스를 반환해야 할 경우
// 인터페이스 정의
public interface Shape {
void draw();
}
// 인터페이스 구현체 1 - 원
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("원을 그립니다.");
}
}
// 인터페이스 구현체 2 - 사각형
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("사각형을 그립니다.");
}
}
// 정적 팩토리 메서드 활용
public class StaticFactoryMethodEx3 {
/*
* 인터페이스(Shape) 타입을 반환하지만,
* 실제로는 Circle 또는 Rectangle 객체를 반환할 수 있음
*/
public static Shape createShape(String type) {
if (type.equals("circle")) {
return new Circle();
} else if (type.equals("rectangle")) {
return new Rectangle();
}
throw new IllegalArgumentException("정의되지 않은 도형");
}
public static void main(String[] args) {
Shape circle = createShape("circle");
Shape rectangle = createShape("rectangle");
circle.draw(); // "원을 그립니다."
rectangle.draw(); // "사각형을 그립니다."
}
}
생성자를 사용했다면 고정된 타입을 반환해야하지만 정적 팩토리 메서드를 이용하기 때문에 유연하게 타입 반환이 가능하다.
또한 createShape() 메서드를 확장하면 새로운 도형을 추가해도 기존 코드에는 영향이 가지 않는다. 즉 OCP원칙을 준수한다.
이렇게 좋은 장점만 있으면 반드시 정적팩토리 메서드만 써야할까?
당연히 생성자가 유리한 부분도 있다.
- 생성자 사용이 좋은 상황
1. 단순 객체 생성
2. 클래스 설계가 단순하고 확장 필요 없을 경우
3. 새로운 객체 생성이 항상 보장되어야 할 경우
이러한 상황에는 생성자를 사용하는것이 낫다.
하지만 대부분의 경우는 정적 팩토리 메서드가 유리한점이 많으므로 무작정 public 생성자를 이용하기보다는 한번 생각하고 정적팩토리 메서드를 이용할 생각을 해보는게 나을 것이다.
item 1의 내용은 여기까지이다.
- 프로젝트에 어떻게 적용할 것인가?
내 생각을 조금 넣어보자면 우선 나는 spring을 이용한 백엔드 개발자니까 이 부분을 어떻게 사용하면 좋을까를 조금 고민해봤다.
프로젝트를 진행하면서 리팩토링 과정에서 entity -> dto 또는 dto -> entity 로 변환하는 과정을 map을 이용하기보다 정적 팩토리 메서드를 쓰면 코드를 재사용 할 수 도 있고 의도가 분명하기에 더 편리한 것 같다. 또한 엔티티를 생성하는 과정에서 무작정 public 생성자를 사용하기보다 빌더패턴과 결합해서 사용하면 더욱 편리하게 entity를 만들 수 있다는걸 느꼈다. 물론 이 과정에는 jpa가 내부적으로 기본 생성자를 필요로 하기 때문에 private으로 제한할 수 없어 protected를 사용해야 한다. 상황에 따라 엔티티의 필드를 반환해 줄 수도 있고 여러모로 편리한 점이 많은것 같다.