ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter / 플러터] 제네릭, 제네릭 하는데 그게 뭐야? 개념 살펴보기
    개발일지/flutter 2021. 6. 10. 19:00

    안녕하세요 개발하는남자 개남입니다. 

    이번에 다룰 내용은 플러터에서 자주자주 마주치면서 흔하게 사용했지만 활용하는 방법을 모를 수 있거나 개념을 명확하게 모르는 분들에게 도움을 드리고자 이렇게 제네릭을 정리하게 되었습니다. 


    Generics이란?

    제네릭은 개발을 하면서 자주 접할 수 있는데요 대표적으로 많이 활용되는 부분에는 List를 정의할 때 많이 활용됩니다. 

    List <String> 혹은 List <int> 이렇게 사용하면서 리스트에 String 타입만 담을 것인지 , int 타입만 담을 것인지를 정할 수 있는데요 여기서 이미 제네릭을 사용한 것입니다. 바로 < ... > 괄호를 활용하여 그 안에 타입을 지정하는 것을 제네릭이라고 합니다. 

     

    Generics를 왜 사용하는 것일까요? 

    타입 안정성, Type Safety 를 위해서 사용합니다. 다시 말해, class의 Type을 안전하게 사용하기 위한 목적입니다. 

    이렇게 글로만 설명을 하면 이해가 어려우니 한가지 예를 들어서 왜 Generics를 사용하면 Type Safety 가 되는지 살펴보겠습니다. 

     

    Generics라는 것이 없다고 가정하고 진행하겠습니다.

    List list = [];

    위와 같이 리스트를 선언했다고 가정하겠습니다. 

    그렇다면 위의 list는 타입이 지정되어있지 않기 때문에 자유롭게 String이나 int 혹은 custom class 등을 넣을 수 있게 됩니다. 

    어떤 개발자가 초반에 int만을 사용할 목적으로 위의 list 를 만들었다고 가정하겠습니다. 

    list.add(1); 
    list.add(3); 
    list.add(5); 
    list.add(7);

    원하는 대로 int 데이터만 넣었습니다. 지금까지는 문제될 요소는 없습니다. 

    하지만 시간이 지나 유지보수를 하다가 다른 곳에서 위의 list 데이터를 접근해서 데이터를 넣어야 하는 상황이고 넣어야 되는 데이터는 서버로부터 받는 데이터라고 생각해 봤을 때 서버에서 보내주는 데이터는 int 타입이 아닌 '2' String 타입 하지만 int로 형 변환이 가능한 데이터라고 가정하겠습니다. 

    list.add("2");

    이렇게 코드가 작성이 되며 코드상 문제가 발생하지 않게 됩니다 이유는 모든 유형을 받을 수 있는 list로 선언 되었기 때문입니다. 

    list의 모든 데이터를 합하는 연산 작업이 있다고 한다면 문제가 러닝타임 때에 발생하게 됩니다. 

    int sum = 0; 
    list.forEach((element) { 
       sum += (element as int); 
    }); 
    print(sum);

    위 로직에서 Type casting 오류가 발생하게 됩니다. 

    바로 이러한 이유에서 제네릭을 사용하는 것입니다.

    var list = <int>[];

    명확하게 int 타입을 지정하게 되면서 다른 유형의 데이터를 list 안에 담으려고 할 때에 오류를 보여주게 되면서 개발자가 실수를 하지 못하도록 만들어 주게 됩니다. 이것이 바로 제네릭이 Type Safety의 목적을 두고 있다는 점입니다. 

     


    Generics 활용의 이점

    제네릭을 활용하면 좋은 이점으로는 위에 언급한 타입을 지정하므로 인해 타입 캐스팅에 대한 안정성을 보장해주는 이점이 있다고 말씀 드렸습니다. 추가적으로 다트(도큐먼트)에서는 효과적인 코딩과 중복 소스 방지에 대한 내용을 소개하고 있습니다.

    하지만 제네릭을 이해한다고 해도 직접 제네릭을 설계해서 만들어 볼 일이 많이 없었고 어느때에 제네릭을 사용하여 효과적인 개발을 할 수 있는지도 모르겠더라고요. 

    그래서 한번 쇼핑몰에서 상품 데이터의 대한 예제를 만들어서 제네릭을 사용하는 것과 사용하지 않는 것의 대한 차의를 비교해 보려고 합니다.

     

    상품을 간단하게 모델링을 하도록 하겠습니다. 

    enum ProductType { HomeAppliances, Clothing, DailyNecessity }
    
    class Product {
      int price;
      int amount;
      String title;
      String description;
      ProductType type;
      Product({
        required this.price,
        required this.amount,
        required this.title,
        required this.description,
        required this.type,
      });
    }
    

    상품의 가격과 수량 그리고 상품이름 및 설명 그리고 상품의 카테고리를 갖고 있는 간단한 모델입니다.

    상품 카테고리는 간단히 3개만 만들었습니다. 가전(HomeAppliances), 의류(Clothing), 생필품(DailyNecessity)

    하지만 상품 데이터는 이것 외에도 더 많은 정보가 있을 것입니다. 또한 그뿐 아니라 상품에 대한 옵션이 카테고리별로 다르게 부여될 수 있습니다. 예를 들어 가전 카테고리 상품들에는 선택할 수 있는 쿠폰이 제공된다거나 , 의류에만 선택할 수 있는 다 향한 옵션들이 있을 수 있습니다. 하여 다음과 같이 카테고리의 클래스에 옵션들을 변수명을 달리해서 넣을 수 있도록 만들어 보겠습니다. 

    class HomeApplicance {
      String? option1;
      String? option2;
      HomeApplicance({this.option1, this.option2});
    }
    
    class Clothing {
      String? option3;
      String? option4;
      Clothing({this.option3, this.option4});
    }
    
    class DailyNecessity {
      String? option5;
      String? option6;
      DailyNecessity({this.option5, this.option6});
    }

    자 그럼 Product 클래스에 type에 따라 가전이라면 가전 옵션을 넣어주고 의류면 의류 옵션을 넣어주는 등의 작업을 진행하게 될 것입니다.

    class Product {
      int price;
      int amount;
      String title;
      String description;
      ProductType type;
    
      HomeApplicance? homeApplicance;
      Clothing? clothing;
      DailyNecessity? dailyNecessity;
    
      Product({
        required this.price,
        required this.amount,
        required this.title,
        required this.description,
        required this.type,
      });
    
      setProductMoreInfoWithHomeAppliances(HomeApplicance _homeAppliances) {
        homeApplicance = _homeAppliances;
      }
    
      setProductMoreInfoWithClothing(Clothing _clothing) {
        clothing = _clothing;
      }
    
      setProductMoreInfoWithDailyNecessity(DailyNecessity _dailyNecessity) {
        dailyNecessity = _dailyNecessity;
      }
    }

    위의 소스처럼 상품이 가질 수 있는 상품의 추가 옵션 클래스를 받을 수 있도록 추가적으로 수정하였습니다. 

    (여기서는 상속을 쓰면 좀 더 소스가 간결화될 수 있지만 지금의 예제에서는 상속을 제외한 제네릭의 유무를 비교하는 것이 목적이라는 점 참고 바랍니다.)

    상품 클래스를 관리하는 어떠한 곳에서 해당 상품의 타입의 따라 추가 옵션에 대해서 setProductMoreInfoWith의 method를 통해 데이터를 추가 주입을 하게 됩니다. 그럼 이제 화면에서 상품 객체에서 옵션을 사용하기 위해 get 함수를 만들어 주도록 하겠습니다.

    class Product {
      int price;
      int amount;
      String title;
      String description;
      ProductType type;
    
      ...
    
      dynamic get moreInfo {
        switch (type) {
          case ProductType.HomeAppliances:
            return homeApplicance;
          case ProductType.Clothing:
            return clothing;
          case ProductType.DailyNecessity:
            return dailyNecessity;
        }
      }
    }

    자 이제 만들어진 클래스를 활용을 할 때 어떻게 사용되는지 살펴보겠습니다. 

    //api 통해서 상품 호출
    var homeApplianceProduct = Product(
      price: 1000,
      amount: 100,
      title: 'TV',
      description: 'TV 설명',
      type: ProductType.HomeAppliances,
    );
    
    homeApplianceProduct.setProductMoreInfoWithHomeAppliances(
        HomeApplicance(option1: '옵션1', option2: '옵션2'));
    
    //화면
    print((homeApplianceProduct.moreInfo as HomeApplicance).option1);

    위와 같이 상품을 api를 통해 주입해주고 homeAppliances 이기 때문에 moreInfo에 HomeAppliances 클래스를 추가적으로 만들어서 옵션 1과 옵션 2를 넣어 줬습니다. 그러고 나서 화면에서는 형 변환을 통해 option1을 출력하여 원하는 결과를 얻어 낼 수 있는 것이죠 

    이렇게 개발을 해도 문제는 없겠지만 불필요한 데이터 변수들이 많아지고 형변환을 해줘야 하며 형변환을 잘못해줬을 때는 타입 캐스팅 오류를 낳기도 하겠지요. 

    자 그렇다면 위의 소스를 제네릭을 활용하여 적용하게 된다면 어떻게 되는지 살펴보도록 하겠습니다. 

     

    상품 클래스는 다음과 같이 사용할 수 있습니다.

    class Product<T> {
      int price;
      int amount;
      String title;
      String description;
      ProductType type;
    
      T? _moreInfo;
    
      Product({
        required this.price,
        required this.amount,
        required this.title,
        required this.description,
        required this.type,
      });
    
      setProductMoreInfo(T moreInfo) {
        _moreInfo = moreInfo;
      }
    
      T? get moreInfo => _moreInfo;
    }
    

    Product 객체를 생성할 때 타입을 지정하여 moreInfo 데이터에 대해 다양성을 해결할 수 있습니다. 

    이렇게 사용하게 되면 사용되는 소스에서는 

    var homeApplianceProduct = Product<HomeApplicance>(
      price: 1000,
      amount: 100,
      title: 'TV',
      description: 'TV 설명',
      type: ProductType.HomeAppliances,
    );
    
    homeApplianceProduct
        .setProductMoreInfo(HomeApplicance(option1: '옵션1', option2: '옵션2'));
    
    //화면
    print(homeApplianceProduct.moreInfo!.option1);

    위 소스처럼 Product 생성 시 HomeAppliance로 지정을 해주면 moreInfo 데이터 타입은 자동적으로 HomeApplicance 가 되는 것입니다.  또한 장점으로는 화면에서 활용을 할 때에 형 변환을 해주지 않고도 사용이 가능해지는 장점이 있습니다. 그렇게 되면 타입 캐스팅 오류도 방지할 수 있게 되겠죠. 

     


    자 이렇게 제네릭을 활용하면 좋은 이점에 대해서 살펴보았는데요 가장 중요한 것은 많이 활용을 해봐야 익숙해지고 어떤 상황에서 써야 하는지 머릿속에서 그려지실 것입니다. 다소 글로 설명하기가 어려운 점이 있는데 제 유튭 채널에 오셔서 제네릭 관련 영상을 보시면 상속에 대한 내용도 포함하여 비교를 하고 있으니까요 한번 시청해주시면 도움이 되실 것입니다. 

    영상 링크는 아래에 위치해 있습니다. (좋아요, 구독을 필수 ㅋㅋ)

    개발하는남자 제네릭 정리 영상

    댓글

Designed by Tistory.