오류 알림을 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(...)로 이름 없는 생성자에 전달해 초기화 책임을 한곳에 모으자. 생성자의 이름뿐 아니라 실제 필드 값도 테스트로 확인하는 것이 마지막 안전장치다.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

