본문 바로가기

개발 & IT/프론트엔드

Dart 메서드 vs Getter: 언제 무엇을 사용해야 할까?

Flutter/Dart로 개발하다 보면 클래스의 속성에 접근하는 방법을 선택해야 할 때가 있습니다. 메서드로 구현할지, getter로 구현할지 고민되는 순간들이 있죠. 오늘은 이 둘의 차이와 올바른 선택 기준을 명확하게 정리해보겠습니다.


1. 기본적인 차이점

메서드 방식

class Rectangle {
  double width;
  double height;
  
  // 메서드로 구현
  double area() {
    return width * height;
  }
  
  double perimeter() {
    return 2 * (width + height);
  }
  
  Rectangle(this.width, this.height);
}

// 사용
void main() {
  final rect = Rectangle(10, 5);
  print(rect.area());       // ← 괄호 필요
  print(rect.perimeter());  // ← 괄호 필요
}

Getter 방식

class Rectangle {
  double width;
  double height;
  
  // Getter로 구현
  double get area => width * height;
  double get perimeter => 2 * (width + height);
  
  Rectangle(this.width, this.height);
}

// 사용
void main() {
  final rect = Rectangle(10, 5);
  print(rect.area);       // ← 괄호 없음 (속성처럼)
  print(rect.perimeter);  // ← 괄호 없음
}

핵심 차이: 메서드는 () 괄호를 붙여 호출하고, getter는 속성처럼 접근합니다.


2. 왜 Getter를 사용해야 할까?

이유 1: Dart 스타일 가이드 권장

Dart 공식 문서는 "계산 비용이 낮고, 부수 효과가 없으며, 상태를 나타내는 것"은 getter로 구현하도록 권장합니다.

// ❌ Java/C# 스타일 (Dart에서는 비권장)
class User {
  String _name;
  
  String getName() {
    return _name;
  }
  
  void setName(String value) {
    _name = value;
  }
}

// ✅ Dart 스타일
class User {
  String _name;
  
  String get name => _name;
  
  set name(String value) {
    _name = value;
  }
}

이유 2: Dart 표준 라이브러리와 일관성

Dart의 모든 기본 타입들이 getter를 사용합니다:

final list = [1, 2, 3];
list.length;     // getter
list.isEmpty;    // getter
list.first;      // getter
list.last;       // getter

final text = "Hello";
text.length;     // getter
text.isEmpty;    // getter

final now = DateTime.now();
now.year;        // getter
now.month;       // getter

만약 이것들이 메서드였다면?

// 이상한 코드가 됨
list.length();   // 어색함
text.isEmpty();  // 뭔가 실행하는 느낌
now.year();      // 부자연스러움

이유 3: 가독성과 의도 표현

class BankAccount {
  double _balance;
  List<Transaction> _transactions;
  
  // "상태"를 나타내는 것 → Getter
  double get balance => _balance;
  bool get hasTransactions => _transactions.isNotEmpty;
  int get transactionCount => _transactions.length;
  
  // "행동/액션"을 나타내는 것 → 메서드
  void deposit(double amount) {
    _balance += amount;
  }
  
  void withdraw(double amount) {
    _balance -= amount;
  }
  
  List<Transaction> searchTransactions(String query) {
    return _transactions.where((t) => t.contains(query)).toList();
  }
}

void main() {
  final account = BankAccount();
  
  // 자연스러운 읽기
  print(account.balance);           // 잔액 확인
  print(account.hasTransactions);   // 거래 내역 있는지 확인
  
  // 액션 수행
  account.deposit(1000);            // 입금 실행
  account.withdraw(500);            // 출금 실행
  account.searchTransactions('cafe'); // 검색 실행
}

3. 언제 Getter를 사용할까?

✅ Case 1: 속성처럼 보이는 것

class Person {
  String firstName;
  String lastName;
  DateTime birthDate;
  
  // 파생된 속성들 → Getter
  String get fullName => '$firstName $lastName';
  int get age {
    final now = DateTime.now();
    int age = now.year - birthDate.year;
    if (now.month < birthDate.month ||
        (now.month == birthDate.month && now.day < birthDate.day)) {
      age--;
    }
    return age;
  }
  bool get isAdult => age >= 18;
  
  Person(this.firstName, this.lastName, this.birthDate);
}

void main() {
  final person = Person('김', '철수', DateTime(1990, 5, 15));
  
  print(person.fullName);  // 속성처럼 자연스럽게
  print(person.age);       // 나이 확인
  print(person.isAdult);   // 성인 여부 확인
}

✅ Case 2: 간단한 계산

class ShoppingCart {
  List<Product> items;
  
  // 간단한 계산 → Getter
  int get itemCount => items.length;
  double get subtotal => items.fold(0, (sum, item) => sum + item.price);
  double get tax => subtotal * 0.1;
  double get total => subtotal + tax;
  
  ShoppingCart(this.items);
}

void main() {
  final cart = ShoppingCart([
    Product('사과', 1000),
    Product('바나나', 2000),
  ]);
  
  print('상품 수: ${cart.itemCount}');
  print('소계: ${cart.subtotal}');
  print('세금: ${cart.tax}');
  print('총액: ${cart.total}');
}

✅ Case 3: 상태 체크

class FileUpload {
  String? fileName;
  int bytesUploaded;
  int totalBytes;
  
  // 상태 체크 → Getter
  bool get hasFile => fileName != null;
  double get progress => bytesUploaded / totalBytes;
  bool get isComplete => progress >= 1.0;
  bool get isInProgress => progress > 0 && progress < 1.0;
  String get status {
    if (isComplete) return '완료';
    if (isInProgress) return '업로드 중';
    return '대기 중';
  }
  
  FileUpload(this.fileName, this.bytesUploaded, this.totalBytes);
}

4. 언제 메서드를 사용할까?

✅ Case 1: 계산 비용이 큰 작업

class ImageProcessor {
  List<int> imageData;
  
  // ❌ Getter로 하면 안 됨 (계산 비용이 큼)
  // List<int> get processed { ... }
  
  // ✅ 메서드로 구현
  List<int> processImage() {
    // 복잡한 이미지 처리 알고리즘
    // 수십만 번의 연산...
    return transformedData;
  }
  
  Future<List<int>> processImageAsync() async {
    // 비동기 처리
    return await heavyComputation();
  }
  
  ImageProcessor(this.imageData);
}

✅ Case 2: 부수 효과가 있는 경우

class Counter {
  int _count = 0;
  List<int> _history = [];
  
  int get currentCount => _count;  // ✅ 단순 조회
  
  // ❌ Getter로 하면 안 됨 (상태 변경)
  // int get nextCount {
  //   _count++;
  //   return _count;
  // }
  
  // ✅ 메서드로 구현
  int increment() {
    _count++;
    _history.add(_count);
    return _count;
  }
  
  void reset() {
    _count = 0;
    _history.clear();
  }
}

✅ Case 3: 매개변수가 필요한 경우

class Calculator {
  // 매개변수 필요 → 메서드
  double add(double a, double b) => a + b;
  double multiply(double a, double b) => a * b;
  
  // 이전 계산 결과 기반 → Getter 가능
  double? _lastResult;
  double? get lastResult => _lastResult;
  
  double calculate(double a, double b, String operation) {
    switch (operation) {
      case '+': _lastResult = add(a, b); break;
      case '*': _lastResult = multiply(a, b); break;
      default: throw Exception('Unknown operation');
    }
    return _lastResult!;
  }
}

✅ Case 4: 외부 시스템과 상호작용

class DatabaseConnection {
  // ❌ Getter로 하면 안 됨
  // List<User> get users { ... } // DB 쿼리!
  
  // ✅ 메서드로 구현
  Future<List<User>> fetchUsers() async {
    // 데이터베이스 쿼리
    return await database.query('users');
  }
  
  Future<void> saveUser(User user) async {
    await database.insert('users', user.toJson());
  }
}

class ApiClient {
  // ✅ 메서드로 구현
  Future<Response> get(String endpoint) async {
    return await http.get(endpoint);
  }
  
  Future<Response> post(String endpoint, Map data) async {
    return await http.post(endpoint, body: data);
  }
}

5. 실전 예제

예제 1: 전자상거래 상품

class Product {
  String name;
  double price;
  int stock;
  double discount; // 0.0 ~ 1.0
  
  // Getter: 파생된 속성들
  double get discountedPrice => price * (1 - discount);
  bool get isOnSale => discount > 0;
  bool get isInStock => stock > 0;
  bool get isLowStock => stock > 0 && stock < 10;
  String get stockStatus {
    if (stock == 0) return '품절';
    if (stock < 10) return '재고 부족';
    return '재고 있음';
  }
  
  // 메서드: 액션들
  bool canPurchase(int quantity) {
    return stock >= quantity;
  }
  
  void purchase(int quantity) {
    if (!canPurchase(quantity)) {
      throw Exception('재고가 부족합니다');
    }
    stock -= quantity;
  }
  
  void restock(int quantity) {
    stock += quantity;
  }
  
  Product(this.name, this.price, this.stock, {this.discount = 0.0});
}

void main() {
  final product = Product('노트북', 1000000, 5, discount: 0.1);
  
  // Getter 사용 (속성처럼)
  print(product.discountedPrice);
  print(product.isOnSale);
  print(product.stockStatus);
  
  // 메서드 사용 (액션)
  if (product.canPurchase(2)) {
    product.purchase(2);
  }
  product.restock(10);
}

예제 2: 사용자 프로필

class UserProfile {
  String username;
  String email;
  String? phoneNumber;
  DateTime? lastLoginAt;
  List<String> roles;
  
  // Getter: 상태 체크
  bool get hasPhone => phoneNumber != null;
  bool get isVerified => email.endsWith('@verified.com');
  bool get isAdmin => roles.contains('admin');
  bool get hasRecentActivity {
    if (lastLoginAt == null) return false;
    final diff = DateTime.now().difference(lastLoginAt!);
    return diff.inDays < 7;
  }
  String get displayName => username.toUpperCase();
  
  // 메서드: 액션
  void login() {
    lastLoginAt = DateTime.now();
  }
  
  void logout() {
    // 로그아웃 로직
  }
  
  void addRole(String role) {
    if (!roles.contains(role)) {
      roles.add(role);
    }
  }
  
  bool hasPermission(String permission) {
    // 권한 체크 로직
    return isAdmin || roles.contains(permission);
  }
  
  UserProfile(this.username, this.email, this.roles);
}

6. 베스트 프랙티스

✅ DO: Getter 사용

// 상태/속성을 나타내는 것
bool get isValid { ... }
bool get hasData { ... }
String get displayText { ... }
int get count { ... }
double get total { ... }

// O(1) 계산
String get fullName => '$firstName $lastName';
double get area => width * height;

✅ DO: 메서드 사용

// 액션/행동을 나타내는 것
void save() { ... }
void update() { ... }
void delete() { ... }

// 매개변수가 필요한 것
bool contains(String value) { ... }
double calculate(int x, int y) { ... }

// 계산 비용이 큰 것
List<Item> filterItems() { ... }
Future<Data> fetchData() async { ... }

// 부수 효과가 있는 것
int increment() { _count++; return _count; }
void reset() { _count = 0; }

❌ DON'T: 잘못된 사용

// ❌ 부수 효과가 있는 getter
int get nextId {
  _counter++;  // 상태 변경!
  return _counter;
}

// ❌ 계산 비용이 큰 getter
List<int> get sortedList {
  return items.sort();  // O(n log n) 연산!
}

// ❌ 비동기 getter (불가능)
// Future<String> get data async { ... }  // 컴파일 에러!

// ❌ 파라미터 있는 getter (불가능)
// String get format(String pattern) { ... }  // 컴파일 에러!

7. 판단 기준 요약

Getter를 사용하세요:

  • ✅ 상태를 조회하는 것
  • ✅ 간단한 계산 (O(1) ~ O(n), 빠름)
  • ✅ 파생된 속성
  • ✅ 부수 효과 없음
  • ✅ 매개변수 불필요

메서드를 사용하세요:

  • ✅ 액션/행동을 수행하는 것
  • ✅ 복잡한 계산 (느림)
  • ✅ 부수 효과가 있는 것
  • ✅ 매개변수가 필요한 것
  • ✅ 외부 시스템 호출

8. 마무리

구분 메서드 Getter

문법 obj.method() obj.property
용도 액션/행동 상태/속성
매개변수 가능 불가능
부수효과 가능 피해야 함
비동기 가능 불가능
Dart 스타일 행동에 사용 상태에 사용 ✅

 

기억할 것:

  • Getter는 "속성처럼" 보이고 동작해야 합니다
  • 메서드는 "무언가를 하는" 액션을 나타냅니다
  • 의심스러울 때는 Dart 표준 라이브러리를 참고하세요
반응형