본문으로 바로가기
TaeyoungKim.dev

Dart factory 생성자와 named constructor 차이: 기존 객체를 다시 쓰려면?

앱작성 약 3분 읽기TaeyoungKim
LinkedInX

문자열에서 객체를 만드는 함수를 보다가 factory Port.fromText(...)를 만났다. 이름이 붙은 생성자면 모두 factory여야 할까? 그렇지 않다. 이름은 호출 경로를 구분하는 표지이고, factory는 호출 때마다 새 객체를 만들지 않아도 되는 생성 방식을 선택한다.

일반 named constructor는 어떤 일을 할까?

도식의 두 경로는 호출 문법보다 객체 정체성이 다르다. 일반 생성자는 호출마다 새 객체로 이어지고, factory는 캐시를 확인해 이미 만든 객체로 되돌아갈 수 있다.

Port.direct(8080)처럼 이름을 붙인 일반 생성자는 전달된 값으로 새 Port를 만든다. 같은 값으로 두 번 호출해도 두 객체는 서로 다르다. 값의 초기화만 필요하다면 이것으로 충분하다.

dart
class Port {
  final int value;
  Port.direct(this.value);
}

void main() {
  final first = Port.direct(8080);
  final second = Port.direct(8080);
  print(identical(first, second)); // false
}

여기서 identical은 포트 숫자가 같은지 아니라 정확히 같은 객체인지 확인한다. 숫자가 같은 객체 둘을 만든다는 사실이 곧 성능 문제는 아니다. 객체 수명이나 동일성이 요구사항에 영향을 주는 경우에만 중요한 차이다.

factory는 왜 기존 객체를 반환할 수 있을까?

이번에는 텍스트로 받은 포트 번호를 검사하고 같은 번호의 객체를 다시 사용해 보자. 아래 캐시는 개념 확인용이다.

dart
class Port {
  final int value;
  static final Map<int, Port> _cache = {};

  Port.direct(this.value);
  Port._(this.value);

  factory Port.fromText(String raw) {
    final value = int.tryParse(raw);
    if (value == null || value < 1 || value > 65535) {
      throw FormatException('invalid port');
    }
    return _cache.putIfAbsent(value, () => Port._(value));
  }
}

void main() {
  final one = Port.fromText('8080');
  final two = Port.fromText('8080');
  print('${one.value} ${identical(one, two)}');
  print(identical(Port.direct(8080), Port.direct(8080)));

  try {
    Port.fromText('99999');
  } on FormatException catch (error) {
    print(error.message);
  }
}
text
8080 true
false
invalid port

fromText는 문자열을 숫자로 바꾸고 범위를 검사한다. 값이 8080이라면 _cache에 있는 객체를 반환하고, 없다면 비공개 생성자 Port._로 만든 뒤 저장한다. 반면 direct는 호출마다 새 객체를 만든다. 99999는 포트 범위 밖이므로 캐시를 건드리기 전에 실패한다.

파싱만 한다면 factory가 꼭 필요한가?

아니다. 문자열 파싱이나 입력 검증은 일반 named constructor의 초기화 로직이나 별도 정적 함수로도 구성할 수 있다. 여기서 factory가 필요한 이유는 이미 만든 Port를 다시 반환하려는 선택 때문이다. 생성 방식보다 호출 이름이 마음에 들어서 factory를 붙이는 식으로 결정하면 구조만 복잡해진다.

캐시는 언제 오히려 부담이 될까?

위 _cache는 호출한 포트 번호를 계속 보관한다. 가능한 숫자가 제한돼 있어도 앱 수명 동안 값이 쌓일 수 있고, 상태를 가진 객체를 공유하면 한 화면의 변경이 다른 화면에 영향을 줄 수도 있다. 지금 예제는 value만 있는 작은 불변 객체라 동일성을 보여 주기 쉽지만, 이를 곧바로 모든 모델 객체에 적용할 이유는 없다.

디버깅할 때는 먼저 ‘동일한 값’이 필요한지 ‘동일한 인스턴스’가 필요한지 테스트로 나누자. 값만 비교하면 되는 대부분의 단순 데이터에는 새 객체를 만드는 명료한 생성자가 낫다. 캐시가 실제 병목을 줄이는지, 객체 수명과 메모리 비용이 어떤지 확인한 뒤 재사용을 선택해야 한다.

핵심 요약: 이름과 생성 계약을 따로 읽는다

일반 named constructor는 이름을 붙인 새 객체 생성 경로다. factory는 같은 이름으로 호출해도 기존 객체나 다른 생성 경로의 결과를 반환할 수 있다. 입력 파싱 자체가 factory의 의무는 아니며, 재사용의 이득과 공유 상태의 부담을 함께 판단해야 한다.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#Dart#factory#named constructor#객체 재사용#캐시

함께 읽으면 좋은 글