어플리케이션 레이어의 유스케이스에서 비즈니스 로직을 처리한 뒤, 도메인 객체를 반환하는 과정에서 흥미로운 상황이 발생했다.

내가 정의한 도메인 엔티티는 생성자의 접근 제어자가 private으로 설정되어 있어서, 외부에서 직접 인스턴스를 생성할 수 없었다.
그래서 아래 이미지처럼, 해당 엔티티 내부에 정적(static) 팩토리 메서드를 정의해 외부에서 인스턴스를 생성할 수 있도록 구현했다.

이 상황에서 자연스럽게 몇 가지 의문이 떠올랐다.
static은 정확히 무엇이고, 어떻게 동작할까? 혹시 내가 지금까지 잘못 이해하고 있었던 건 아닐까?
생성자를 private으로 막아두고, 정적 메서드를 통해 인스턴스를 생성할 수 있게 한다면,
굳이 생성자를 제한한 이유는 무엇일까?
이제, 위 질문들에 대해 하나씩 파헤쳐보자.
static은 Java(또는 다른 객체지향 언어)에서 "클래스 수준에서 동작하는 것"을 의미한다.
일반적인 멤버 변수나 메서드는 인스턴스를 생성해야 사용할 수 있지만, static이 붙은 요소는 객체를 생성하지 않아도(Class 이름만으로) 접근할 수 있다.
static 키워드를 통해 생성된 정적 멤버들은 Heap 영역이 아닌, JVM의 Static(또는 Method) 영역에 저장된다.
이 영역은 클래스가 처음 로딩될 때 메모리에 할당되며, 프로그램이 종료될 때까지 유지된다.
정적 멤버는 모든 인스턴스가 동일한 공간을 공유하기 때문에, 어디서든 접근이 가능하다는 장점이 있다. 따라서 공용 상수나 유틸리티 메서드처럼, 객체 상태와 무관한 기능을 구현할 때 효과적으로 활용된다.
이러한 정적 멤버들은 모든 인스턴스가 공유하기 때문에, 어디서든 접근 가능하다는 장점이 있다. 즉, 별도의 인스턴스를 만들지 않아도 특정 기능이나 값에 접근할 수 있다는 점에서 유틸성 메서드나 공용 상수 등에서 매우 유용하게 활용된다.
하지만 주의할 점도 있다.
static 영역은 Garbage Collector의 관리 대상이 아니기 때문에, 한 번 메모리에 올라간 정적 데이터는 프로그램 종료 전까지 계속 남아 있게 된다.
즉, static을 과도하게 사용하거나 불필요하게 남용할 경우, 메모리 누수와 유사한 상황이 발생하거나, 시스템 성능에 악영향을 줄 수 있다.
또한 테스트가 어려워지고, 구조가 유연하지 못한 코드로 이어질 가능성도 높다.
정적 메서드는 인스턴스 없이 호출되기 때문에, 인스턴스 변수나 메서드에는 직접 접근할 수 없다.
하지만 내부에서 new 키워드를 사용해 생성자를 호출함으로써,
필요한 값을 전달하고, 그 값을 가진 새로운 객체를 생성해 반환하는 방식으로 사용된다.
그렇다면, 왜 정적 팩토리 메서드를 사용할까?
정적 메서드가 어떻게 객체를 생성하는지는 이제 이해가 된다.
그런데 여전히 의문이 남는다.
왜 생성자의 접근 제어자를 굳이 private으로 막아놓고, 다시 정적 팩토리 메서드를 통해 객체를 생성하도록 설계하는 걸까?
결국 정적 메서드를 통해 객체를 생성할 수 있다면, 굳이 생성자를 private으로 막을 필요가 있는 걸까?
1. 객체 생성을 통제할 수 있다
생성자를 public으로 열어두면 어디서든 객체를 생성할 수 있다. 하지만 정적 팩토리 메서드만을 통해 생성하도록 제한하면, 객체를 어떤 방식으로, 어떤 조건 하에서 생성할지 개발자가 제어할 수 있게 된다.
예를 들어, 동일한 값으로 생성된 객체가 중복되길 원치 않는 경우, 팩토리 메서드 내부에서 캐싱 로직을 넣는 것도 가능하다.
2. 의도를 드러낼 수 있다
정적 팩토리 메서드는 이름을 가질 수 있기 때문에,
생성하는 목적이나 용도를 코드 자체로 명확히 표현할 수 있다.
User.from(email)
User.withName("Alice")
User.anonymous()
3. 불변 객체나 재사용 가능한 객체를 만들기 쉬워진다
정적 메서드 안에서는 객체를 생성하기 전후로 추가 로직을 삽입할 수 있기 때문에,
불변 객체를 생성하거나,
싱글톤/캐시 전략을 적용하는 등 객체 생성을 효율적으로 관리할 수 있다.
public static User of(String name) {
if (cache.containsKey(name)) return cache.get(name);
User user = new User(name);
cache.put(name, user);
return user;
}
4. 구현체를 숨기고 유연한 구조를 만들 수 있다
정적 메서드는 반환 타입을 인터페이스나 추상 클래스처럼 상위 타입으로 선언할 수 있다.
이 덕분에 외부에는 구현체를 숨기면서도,
상위 타입만을 의존하는 유연한 설계를 할 수 있다.
public interface Shape { ... }
public class Circle implements Shape { ... }
public static Shape circle(double radius) {
return new Circle(radius);
}
결국 생성자를 막고 정적 팩토리 메서드를 사용하는 이유는
객체 생성을 단순히 “가능하게” 하기 위한 것이 아니라, “더 잘 통제하고, 더 잘 설명하고, 더 잘 유지보수하기 위한 설계”인 것이다.