오류 알림을 Notice.error('Retry')로 만들었다. 이름은 분명 error인데, 만들어진 객체의 blocking은 false다. 화면은 이 알림을 가볍게 닫아도 되는 것으로 처리한다. 이름표만 엄격하고 실제 상태는 딴판인 셈이다.
원인은 생성 경로가 둘인데 필드 초기화 규칙도 두 곳에 적혀 있기 때문이다. Dart의 이름 있는 생성자와 this(...) 전달 생성자를 사용해 같은 오류 알림을 다시 만들어 보자.
Dart named constructor는 기본 생성자와 무엇이 다를까?
Notice(message: 'Saved')처럼 클래스 이름만 호출하는 것은 이름 없는 생성자다. Notice.error('Retry')처럼 뒤에 이름을 붙이면 **이름 있는 생성자(named constructor)**를 호출한다. error라는 이름 덕분에 객체를 만드는 의도는 또렷해진다.
하지만 이름이 필드 값을 자동으로 검증해 주지는 않는다. 두 생성자가 각각 message, level, blocking을 채운다면, 어느 한쪽의 규칙을 바꾼 뒤 다른 쪽을 놓칠 수 있다. 오류 여부를 나타내는 blocking을 한 경로에서만 잘못 설정하면 코드는 통과해도 객체의 뜻이 어긋난다.
생성자마다 필드를 초기화하면 왜 값이 갈릴까?
다음 코드는 같은 Retry 알림을 두 방식으로 만든다. 두 버전을 한 파일에서 비교하려고 클래스 이름에 Manual과 Redirect를 붙였다. NoticeManual.error에는 일부러 잘못 복제한 blocking = false를 남겼고, NoticeRedirect.error는 다른 생성자로 초기화를 전달한다. 코드를 main.dart에 저장한 뒤 dart --enable-asserts main.dart로 실행할 수 있다.
class NoticeManual {
final String message;
final String level;
final bool blocking;
NoticeManual({required this.message, this.level = 'info'})
: blocking = level == 'error';
NoticeManual.error(this.message)
: level = 'error',
blocking = false; // 따로 적은 규칙이 어긋남
}
class NoticeRedirect {
final String message;
final String level;
final bool blocking;
NoticeRedirect({required this.message, this.level = 'info'})
: blocking = level == 'error';
NoticeRedirect.error(String message)
: this(message: message, level: 'error');
}
void main() {
final manual = NoticeManual.error('Retry');
final redirected = NoticeRedirect.error('Retry');
final ordinary = NoticeRedirect(message: 'Saved');
print('manual: ${manual.level}, blocking=${manual.blocking}');
print('redirect: ${redirected.level}, blocking=${redirected.blocking}');
print('ordinary: ${ordinary.level}, blocking=${ordinary.blocking}');
assert(manual.blocking == false);
assert(redirected.blocking == true);
assert(ordinary.blocking == false);
}실행 결과는 다음과 같다.
manual: error, blocking=false
redirect: error, blocking=true
ordinary: info, blocking=false첫 번째 객체는 level이 error인데 blocking은 false다. 오류가 나지 않아 더 찾기 어려운 종류의 실수다. 두 번째 객체는 오류 알림의 blocking을 true로, 일반 알림은 false로 만들었다. 아래의 assert 세 줄도 이 차이를 확인한다.
this(...) 전달 생성자는 무엇을 한곳으로 모을까?
NoticeRedirect.error는 이름 있는 생성자이면서 **전달 생성자(redirecting constructor)**다. this(message: message, level: 'error')가 이름 없는 생성자에게 일을 넘기므로 blocking = level == 'error'를 다시 쓰지 않는다. error라는 호출 방식은 유지하고, 상태를 결정하는 규칙은 한곳에 둔 것이다.
두 이름은 서로 경쟁하는 생성자 종류가 아니다. named는 호출할 때의 이름을, redirecting은 초기화를 다른 생성자로 넘기는 방식을 말한다. 그래서 하나의 생성자가 동시에 둘 다 될 수 있다.
이 방식도 모든 상태 오류를 없애 주지는 않는다. 여기서는 level에 임의의 문자열을 넣을 수 있어 'erorr' 같은 오타는 여전히 가능하다. 허용할 상태가 정해져 있다면 enum 등으로 값을 제한하고, 중요한 규칙은 한 경로에서 검사하는 편이 안전하다. 반대로 공유할 초기화 규칙이 없다면 굳이 전달 경로를 늘릴 이유도 없다.
핵심 요약
이름 있는 생성자는 Notice.error(...)처럼 만드는 의도를 드러낸다. 필드를 각 생성자에서 따로 채우면 규칙이 어긋날 수 있다. 같은 규칙을 따라야 한다면 this(...)로 이름 없는 생성자에 전달해 초기화 책임을 한곳에 모으자. 생성자의 이름뿐 아니라 실제 필드 값도 테스트로 확인하는 것이 마지막 안전장치다.

