해당 포스팅은 유튜브 영화&드라마 리뷰 영상 큐레이션 플랫폼
Plotz
를 개발하면서 도입된 기술 및 방법론에 대한 내용을 다루고 있습니다.
다운로드 링크 : 앱스토어 / 플레이스토어
일반적으로 앱 내에서 자주 사용되는 리소스(컬러, 폰트, 등등)를 관리하기 위해 유틸리티 클래스
를 정의하여 사용합니다. 그런데 자주 사용되는 만큼 중요하지만 놓치기 쉬운 부분들이 있습니다. 이번 글에서는 유틸리티 클래스를 좋은 구조로 작성하는 방법에 대해 다루어 보려고 합니다.
본 포스팅에서는 개선이 필요한 코드를 단계적으로 고쳐 나가며 메모리 관점
에서 어떤 부분을 개선해야 할지 집중적으로 설명합니다.
또한, 아래 개념에 대해 다룹니다.
class AppColor {
Color darkGrey = const Color(0xFFBBBAC1);
Color lightGrey = const Color(0xFF1F1F20);
Color red = const Color(0xFFFF484E);
}
AppColor라는 클래스는 프로젝트에서 자주 사용되는 color 값들을 정의하고 있습니다.
이렇게 구성된 코드는 앱 전반에 걸쳐 AppColor 클래스를 기반의 color 값들을 불러올 수 있습니다.
다만, 의도대로 작동은 하겠지만 효율적인 코드라고 보기는 힘듭니다.
이유는 color값이 필요할 때마다 매번 인스턴스를 생성
해야하기 때문입니다. 그리고 매번 인스턴스를 생성하는건 메모리를 효율적으로 관리한다고 볼 수 없죠.
먼저 클래스 인스턴스에 대해 간단히 짚고 갑시다. 클래스 인스턴스는 클래스에서 정의한 속성과 메서드를 가진 객체를 말합니다. 즉, 클래스를 기반으로 생성된 실제 데이터가 클래스 인스턴스입니다.
스타크래프트로 비유하자면 클래스
자체는 배럭
, 그리고 배럭에서 생성된 마린
을 클래스 인스턴스
라고, 설명드릴 수 있겠네요.
하지만 클래스 코드가 적혀 있다고 모두 인스턴스를 생성하는 것은 아닙니다.
// 인트턴스 생성 코드
final darkGrey= Palette().darkGrey,
final ximya = Student(name: "ximya", major: "cs")
// 인스턴스 생성 코드 X
final red = Colors.red;
final textAlign = TextAlignVertical.top;
darkGrey
변수에는 Palette클래스의 인스턴스가 담겨 있을 겁니다.(엄밀히 말하면 인스턴스의 주소가 담겨있음)
반면, Colors.red
나 TextAlignVertical.top
과 같은 값은 클래스의 인스턴스가 아니라, 클래스에서 정의한 정적 멤버(static member)입니다. 이 값들은 클래스가 로드될 때 메모리에 할당되며, 클래스의 인스턴스를 생성하지 않아도 참조할 수 있습니다.
즉 final red = Colors.red
코드는 인스턴스를 생성하고 저장하는 코드가 아닙니다.
만약 헷갈리시면 하나만 기억하시면 됩니다.
인스턴스를 생성하는 코드는 필히 생성자 구문
을 필요합니다. 즉 ()
소괄호를 적어야 하는 거죠.
코드에 소괄호가 감싸져 있다면 클래스 인스턴스를 생성하는 코드
, 반대의 경우라면 이미 메모리상에 올라와 있는 값을 가져온 코드
라고 이해하시면 됩니다.
그럼, 실제 AppColor 클래스가 사용된 코드가 메모리 위에서 어떻게 작동하는지 알아보겠습니다.
body: ListView(
children: [
Container(
margin: const EdgeInsets.only(bottom: 20),
color: AppColor().darkGrey, // -> 클래스 인스턴스 생성
height: 100,
width: 100,
),
Container(
margin: const EdgeInsets.only(bottom: 20),
color: AppColor().lightGrey, // -> 클래스 인스턴스 생성
height: 100,
width: 100,
),
Container(
margin: const EdgeInsets.only(bottom: 20),
color: AppColor().red, // -> 클래스 인스턴스 생성
height: 100,
width: 100,
),
],
),
위 코드에서 ListView를 사용하여 세 개의 Container 위젯을 생성하고, 각각 다른 색상으로 배경을 설정하고 높이와 너비를 100으로 지정하였습니다. 그리고 AppColor
클래스를 사용하여 각 Container 위젯의 배경색을 지정합니다. 앞서 언급했듯이 클래스 인스턴스
를 생성하는 방식으로 color 속성에 값을 전달 합니다.
메모리 구조를 대략적으로 표현한다면 위 그림과 같습니다. AppColor클래스 인스턴스들이 힙 영역에 저장되어 있는 구조입니다.
본 포스팅에서메모리 할당 방식에 대해서 깊게 설명하지는 않지만 2가지 개념만 숙지해주세요.
- 클래스 인스턴스는
힙(Heap)
메모리 영역에 저장됨.- 정적 변수 & 전역 변수들을
데이터(Data)
영역에 저장됨.
만약 여러 Widget에서 AppColor에 정의된 color 값들을 많이 사용해야 한다고 하면 그만큼 더 인스턴스를 매번 생성해야 하는 구조이기 때문에 메모리를 많이 차지하겠죠. 힙 영역에는 수 많은 인스턴스가 쌓이게 될 것 같습니다.
매번 color 인스턴스들을 생성하는 게 좋은 구조는 아닌 것은 명백해 보입니다. 이제 효율적인 구조로 변경해 봅시다.
어차피 프로젝트 전반에 걸쳐 자주 사용되는 Color 값들이라면 한 번에 다 생성해 놓고 필요할 때마다 쓰는 방식
은 어떨까요?
그럼 전역변수로 Color값들을 관리하는 방식을 고려해 볼 수 있겠네요.
// lib/utilities/colors.dart
const Color darkGrey = const Color(0xFFBBBAC1);
const Color lightGrey = const Color(0xFF1F1F20);
const Color red = const Color(0xFFFF484E);
이렇게 한 소스파일에 Color 값들을 전역으로 정의해 둔다면 프로젝트가 로드 될 때 모든 Color값들이 메모리상에서 올라갈 것이고, 이전 코드처럼 매번 인스턴스를 생성할 일도 없어질 것입니다.
이제는 메모리상에서 Color 값들이 어떻게 할당될지 확인해 봅시다.
body: ListView(
children: [
Container(
margin: const EdgeInsets.only(bottom: 20),
color: darkGrey,
height: 100,
width: 100,
),
Container(
margin: const EdgeInsets.only(bottom: 20),
color: lightGrey,
height: 100,
width: 100,
),
Container(
margin: const EdgeInsets.only(bottom: 20),
color: red,
height: 100,
width: 100,
),
],
),
클래스 인스턴스 값을 생성하지 않기 때문에 힙
영역에 color 값들 위치하지 않고 데이터
영역에 저장되어 있습니다. 결과적으로 데이터 영역에 할당된 값들을 쉽게 참조할 수 있는 상태이기 때문에 리소스를 효율적으로 사용할 수 있게 됩니다.
좀 더 자세히 확인해 볼까요 🤔
전역으로 관리하는 방법
과 클래스 인스턴스 값으로 관리하는 방법
을 비교하여, 메모리 구조를 통해 더 효율적인 방법이 어떤 것인지 쉽게 알 수 있습니다.
여러 위젯에서 Color 값들이 필요하다고 가정한다고 했을 때, 전자의 경우 전역으로 관리되고 있는 Color 값들을 참조하면 되기 때문에 큰 리소스가 발생되지 않습니다.
반면 클래스 인스턴스로 값을 할당해야 하는 경우 매번 클래스 객체를 생성하기 때문에 오버헤드
가 발생할 수밖에 없죠.
color 값들을 전역으로 선언하여 관리하는 방법이 리소스를 효율적으로 사용할 수 있는 부분이 장점이지만, 적절한 그룹화
나 관리 없이 전역 범위에
서 정의되어 있어 코드의 구조와 가독성을 떨어트린다고 볼 수 있습니다.
그럼 어떻게 코드를 구조화 할 수 있을까요?
이때 클래스
를 적절하게 이용하면됩니다.
class AppColor {
static const darkGrey = const Color(0xFFBBBAC1);
static const lightGrey = const Color(0xFF1F1F20);
static const red = const Color(0xFFFF484E);
}
위 코드처럼 color 값들을 정적 상수(static const)
로 선언하여 클래스 인스턴스화
없이 사용할 수 있습니다.
정리하자면 앞서 소개해 드린 두 번째 방식(전역으로 관리)과 기능적으로 동일하게 작동
된다고 볼 수 있습니다.
이번에도 메모리 구조를 살펴 봅시다. 예시는 이전과 동일합니다.
body: ListView(
children: [
Container(
margin: const EdgeInsets.only(bottom: 20),
color: AppColor.darkGrey, // -> 인스턴스 생성 X
height: 100,
width: 100,
),
Container(
margin: const EdgeInsets.only(bottom: 20),
color: AppColor.lightGrey, // -> 인스턴스 생성 X
height: 100,
width: 100,
),
Container(
margin: const EdgeInsets.only(bottom: 20),
color: AppColor.red, // -> 인스턴스 생성 X
height: 100,
width: 100,
),
],
),
여기서 재미있는건 분명 클래스를 이용하고 있지만 힙
영역에 인스턴스를 생성하지 않고 데이터
영역에 color 값들이 저장되어 있다는 것입니다.
그 이유는 AppColor 클래스가 처음 로드될 때 static properties
(정작 상수 값)들이 데이터 영역에 할당되기 때문입니다.
static properties
는 프로그램의 수명 동안 메모리에 남아 있으며, 다른 모든 인스턴스와 공유됩니다. 이러한 이유로, 메모리 사용을 최적화하고 전체 애플리케이션에서 공통으로 사용되는 값을 저장하는 데 유용합니다.
자, 이제 마지막 단계 입니다.
이전에 소개드린 접근방식도 충분히 괜찮은 코드라고 볼 수 있지만 좀 더 프로
답게 코드를 작성할 수 있는 방법이 있습니다.
abstract class AppColor {
AppColor._();
static const darkGrey = const Color(0xFFBBBAC1);
static const lightGrey = const Color(0xFF1F1F20);
static const red = const Color(0xFFFF484E);
}
위 AppColor 클래스처럼 클래스의 목적이 static properties
을 제공하기 위함이라면 추상
(abstract) 클래스를 이용하면 됩니다. 추상 클래스를 사용하면 AppColor 클래스의 인스턴스를 만들 수 없으므로 클래스를 실수로 인스턴스화하는 것을 방지
합니다.
AppColor._();
, 이렇게 prviate 생성자를 적는 이유도 마찬가지입니다. 개발자가 실수로 인스턴스화 하는 코드를 작성하는 실수를 방지할 수 있습니다.
또한, 실수를 방지할 수 있는 것 뿐만 아니라 정적 상수를 사용하는 데 초점
을 맞추기 때문에 더 가독성을 고려하고 명시적인 코드라고 볼 수 있습니다.
결론적으로, abstract class
, private constructor
을 적용하지 않아도 기능에 전혀 문제가 없지만 코드의 목적을 명확하게 나타내고, 실수로 인스턴스화되는 것을 방지하기 위해서 사용이 권장됩니다.
이번 포스팅에서는 어떻게 효율적인 구조로 유틸리티 클래스를 작성하는 방법에 대해 알아보았습니다.
그럼 순삭 앱에서 실제 적용된 코드 사례를 몇 가지 소개드리며 글을 마무리 하겠습니다 😀
abstract class AppPages {
AppPages._();
static final routes = [
// 스플래쉬
GetPage(
name: AppRoutes.splash,
page: SplashScreen.new,
binding: SplashBinding(),
),
// 로그인
GetPage(
name: AppRoutes.login,
page: LoginScreen.new,
binding: LoginBinding(),
),
// 컨텐츠 상세
GetPage(
name: AppRoutes.contentDetail,
page: ContentDetailScreen.new,
binding: ContentDetailBinding(),
),
// 검색
GetPage(
name: AppRoutes.search,
page: SearchScreen.new,
binding: SearchBinding(),
),
.....
}
abstract class AppRoutes {
AppRoutes._();
// 스플래시
static const splash = '/';
// 로그인
static const login = '/login';
// 탭
static const tabs = '/tabs';
.....
}
abstract class AppSpace {
AppSpace._();
static const size2 = SizedBox(width: 2, height: 2);
static const size4 = SizedBox(width: 4, height: 4);
static const size6 = SizedBox(width: 6, height: 6);
static const size7 = SizedBox(width: 7, height: 7);
.....
}
abstract class AppFireStore {
AppFireStore._();
static final FirebaseFirestore _db = FirebaseFirestore.instance;
static FirebaseFirestore get getInstance => _db;
}
항상 글 너무 잘 보고 있습니다! 만드신 plotz앱도 써보고 있는데 너무너무 잘 만드셔서 깜짝 놀랐어요. 앞으로 더 좋은 글 기대하겠습니다!