Builder Design Pattern

🔥 Flutter에서 Builder 패턴이 유용한 상황들

1. 복잡한 위젯 구성 (가장 흔함)

dart

// 복잡한 커스텀 버튼 위젯
class CustomButton {
  final String text;
  final Color? backgroundColor;
  final Color? textColor;
  final EdgeInsets? padding;
  final BorderRadius? borderRadius;
  final VoidCallback? onPressed;
  final IconData? icon;
  final double? elevation;
  
  CustomButton._({
    required this.text,
    this.backgroundColor,
    this.textColor,
    this.padding,
    this.borderRadius,
    this.onPressed,
    this.icon,
    this.elevation,
  });
  
  // Builder 패턴 적용
  static CustomButtonBuilder builder(String text) {
    return CustomButtonBuilder._(text);
  }
}

class CustomButtonBuilder {
  final String _text;
  Color? _backgroundColor;
  Color? _textColor;
  EdgeInsets? _padding;
  BorderRadius? _borderRadius;
  VoidCallback? _onPressed;
  IconData? _icon;
  double? _elevation;
  
  CustomButtonBuilder._(this._text);
  
  CustomButtonBuilder backgroundColor(Color color) {
    _backgroundColor = color;
    return this;
  }
  
  CustomButtonBuilder textColor(Color color) {
    _textColor = color;
    return this;
  }
  
  CustomButtonBuilder padding(EdgeInsets padding) {
    _padding = padding;
    return this;
  }
  
  CustomButtonBuilder rounded([double radius = 8.0]) {
    _borderRadius = BorderRadius.circular(radius);
    return this;
  }
  
  CustomButtonBuilder onPressed(VoidCallback callback) {
    _onPressed = callback;
    return this;
  }
  
  CustomButtonBuilder withIcon(IconData icon) {
    _icon = icon;
    return this;
  }
  
  CustomButtonBuilder elevation(double elevation) {
    _elevation = elevation;
    return this;
  }
  
  Widget build() {
    return ElevatedButton.icon(
      onPressed: _onPressed,
      icon: _icon != null ? Icon(_icon) : SizedBox.shrink(),
      label: Text(_text, style: TextStyle(color: _textColor)),
      style: ElevatedButton.styleFrom(
        backgroundColor: _backgroundColor,
        padding: _padding,
        shape: RoundedRectangleBorder(
          borderRadius: _borderRadius ?? BorderRadius.zero,
        ),
        elevation: _elevation,
      ),
    );
  }
}

// 사용법
Widget myButton = CustomButton.builder("확인")
    .backgroundColor(Colors.blue)
    .textColor(Colors.white)
    .rounded(12.0)
    .padding(EdgeInsets.symmetric(horizontal: 24, vertical: 12))
    .withIcon(Icons.check)
    .elevation(4.0)
    .onPressed(() => print("클릭됨"))
    .build();

2. 네트워크 요청 설정

dart

class ApiRequest {
  final String url;
  final String method;
  final Map<String, String>? headers;
  final dynamic body;
  final Duration? timeout;
  final bool retryOnError;
  final int maxRetries;
  
  ApiRequest._({
    required this.url,
    required this.method,
    this.headers,
    this.body,
    this.timeout,
    this.retryOnError = false,
    this.maxRetries = 3,
  });
  
  static ApiRequestBuilder get(String url) {
    return ApiRequestBuilder._(url, 'GET');
  }
  
  static ApiRequestBuilder post(String url) {
    return ApiRequestBuilder._(url, 'POST');
  }
}

class ApiRequestBuilder {
  final String _url;
  final String _method;
  Map<String, String> _headers = {};
  dynamic _body;
  Duration? _timeout;
  bool _retryOnError = false;
  int _maxRetries = 3;
  
  ApiRequestBuilder._(this._url, this._method);
  
  ApiRequestBuilder headers(Map<String, String> headers) {
    _headers.addAll(headers);
    return this;
  }
  
  ApiRequestBuilder addHeader(String key, String value) {
    _headers[key] = value;
    return this;
  }
  
  ApiRequestBuilder body(dynamic body) {
    _body = body;
    return this;
  }
  
  ApiRequestBuilder timeout(Duration timeout) {
    _timeout = timeout;
    return this;
  }
  
  ApiRequestBuilder enableRetry({int maxRetries = 3}) {
    _retryOnError = true;
    _maxRetries = maxRetries;
    return this;
  }
  
  ApiRequest build() {
    return ApiRequest._(
      url: _url,
      method: _method,
      headers: _headers.isEmpty ? null : _headers,
      body: _body,
      timeout: _timeout,
      retryOnError: _retryOnError,
      maxRetries: _maxRetries,
    );
  }
}

// 사용법
ApiRequest loginRequest = ApiRequest.post('/api/login')
    .addHeader('Content-Type', 'application/json')
    .addHeader('Accept', 'application/json')
    .body({'username': 'user', 'password': 'pass'})
    .timeout(Duration(seconds: 10))
    .enableRetry(maxRetries: 2)
    .build();

ApiRequest getUserRequest = ApiRequest.get('/api/user/123')
    .addHeader('Authorization', 'Bearer token')
    .timeout(Duration(seconds: 5))
    .build();

3. 다이얼로그 Builder

dart

class CustomDialog {
  final String? title;
  final String? message;
  final List<DialogAction> actions;
  final Widget? customContent;
  final bool dismissible;
  final Color? backgroundColor;
  
  CustomDialog._({
    this.title,
    this.message,
    this.actions = const [],
    this.customContent,
    this.dismissible = true,
    this.backgroundColor,
  });
  
  static CustomDialogBuilder builder() {
    return CustomDialogBuilder._();
  }
}

class DialogAction {
  final String text;
  final VoidCallback onPressed;
  final bool isDefault;
  
  DialogAction(this.text, this.onPressed, {this.isDefault = false});
}

class CustomDialogBuilder {
  String? _title;
  String? _message;
  List<DialogAction> _actions = [];
  Widget? _customContent;
  bool _dismissible = true;
  Color? _backgroundColor;
  
  CustomDialogBuilder._();
  
  CustomDialogBuilder title(String title) {
    _title = title;
    return this;
  }
  
  CustomDialogBuilder message(String message) {
    _message = message;
    return this;
  }
  
  CustomDialogBuilder addAction(String text, VoidCallback onPressed, {bool isDefault = false}) {
    _actions.add(DialogAction(text, onPressed, isDefault: isDefault));
    return this;
  }
  
  CustomDialogBuilder customContent(Widget content) {
    _customContent = content;
    return this;
  }
  
  CustomDialogBuilder dismissible(bool dismissible) {
    _dismissible = dismissible;
    return this;
  }
  
  CustomDialogBuilder backgroundColor(Color color) {
    _backgroundColor = color;
    return this;
  }
  
  Future<void> show(BuildContext context) {
    return showDialog(
      context: context,
      barrierDismissible: _dismissible,
      builder: (context) => AlertDialog(
        backgroundColor: _backgroundColor,
        title: _title != null ? Text(_title!) : null,
        content: _customContent ?? (_message != null ? Text(_message!) : null),
        actions: _actions.map((action) => TextButton(
          onPressed: action.onPressed,
          child: Text(action.text, style: TextStyle(
            fontWeight: action.isDefault ? FontWeight.bold : FontWeight.normal,
          )),
        )).toList(),
      ),
    );
  }
}

// 사용법
// 간단한 확인 다이얼로그
CustomDialog.builder()
    .title("삭제 확인")
    .message("정말로 삭제하시겠습니까?")
    .addAction("취소", () => Navigator.of(context).pop())
    .addAction("삭제", () {
        // 삭제 로직
        Navigator.of(context).pop();
    }, isDefault: true)
    .show(context);

// 커스텀 컨텐츠 다이얼로그
CustomDialog.builder()
    .title("설정")
    .customContent(Column(
        mainAxisSize: MainAxisSize.min,
        children: [
            SwitchListTile(
                title: Text("알림 설정"),
                value: true,
                onChanged: (value) {},
            ),
            SwitchListTile(
                title: Text("자동 업데이트"),
                value: false,
                onChanged: (value) {},
            ),
        ],
    ))
    .addAction("닫기", () => Navigator.of(context).pop())
    .backgroundColor(Colors.grey[100])
    .show(context);

4. Director 패턴을 활용한 테마 Builder

dart

// Director 역할을 하는 클래스
class ThemeDirector {
  final CustomButtonBuilder _builder;
  
  ThemeDirector(this._builder);
  
  // 기본 버튼 스타일 (Director가 지시)
  Widget buildPrimaryButton(String text, VoidCallback onPressed) {
    return _builder
        .backgroundColor(Colors.blue)
        .textColor(Colors.white)
        .rounded(8.0)
        .padding(EdgeInsets.symmetric(horizontal: 16, vertical: 12))
        .elevation(2.0)
        .onPressed(onPressed)
        .build();
  }
  
  // 경고 버튼 스타일 (Director가 지시)
  Widget buildWarningButton(String text, VoidCallback onPressed) {
    return _builder
        .backgroundColor(Colors.orange)
        .textColor(Colors.white)
        .rounded(12.0)
        .padding(EdgeInsets.symmetric(horizontal: 20, vertical: 14))
        .withIcon(Icons.warning)
        .elevation(4.0)
        .onPressed(onPressed)
        .build();
  }
  
  // 성공 버튼 스타일 (Director가 지시)
  Widget buildSuccessButton(String text, VoidCallback onPressed) {
    return _builder
        .backgroundColor(Colors.green)
        .textColor(Colors.white)
        .rounded(20.0)
        .padding(EdgeInsets.symmetric(horizontal: 24, vertical: 16))
        .withIcon(Icons.check_circle)
        .elevation(6.0)
        .onPressed(onPressed)
        .build();
  }
}

// 사용법
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final director = ThemeDirector(CustomButton.builder(""));
    
    return Column(
      children: [
        director.buildPrimaryButton("로그인", () {}),
        SizedBox(height: 10),
        director.buildWarningButton("주의", () {}),
        SizedBox(height: 10),
        director.buildSuccessButton("완료", () {}),
      ],
    );
  }
}

🎯 Builder 패턴을 사용해야 하는 경우

사용하면 좋은 상황

  • 생성자 매개변수가 5개 이상

  • 선택적 매개변수가 많은 경우

  • 불변 객체를 만들고 싶은 경우

  • 복잡한 설정이 필요한 객체

  • 같은 구조로 다양한 변형이 필요한 경우

사용하지 않는 것이 좋은 상황

  • 간단한 객체 (매개변수 3개 이하)

  • 자주 변경되지 않는 객체

  • 성능이 매우 중요한 경우 (객체 생성 오버헤드)

💡 핵심 정리

  • Builder: 객체를 "어떻게" 만들지 정의

  • Director: 객체를 "무엇을", "어떤 순서로" 만들지 지시

Last updated