본문 바로가기
취미노트/코딩공부

[flutter] 14 객체지향 구조 1, Abstraction

by 복습쟁이 2024. 9. 1.
반응형

이번에 알아볼 것

  • 객체지향 프로그램의 특징
  • Abstraction (추상화)
  • 지난 포스팅까지 진행한 Workout Tracker App의 객체지향화

 

 

 

 

객체지향과 추상화의 이해

List 구조

-> 데이터의 단순 나열로, 데이터 간 관계성을 코드를 설계한 개발자 본인만 명확히 인지하고 있음

-> 암묵적인 룰. 안정적이지 못함.

 

Class 구조

-> 관계성 있는 데이터들의 그룹화를 통해 누구나 관계성을 명확히 인지 가능

-> 성문화된 구조. 안정적

 

 

 

절차지향 : 코드의 순차적인 처리에 따라 프로그램이 유기적으로 연결됨

오늘의 운동 설정 -> 운동을 한다 -> 운동결과를 평가한다 -> 오늘 소모 칼로리 계산 -> SNS 자랑하기

 

객체지향 : 성격별로 데이터와 절차를 개별 그룹으로 묶어서 구조적으로 접근

위 절차지향의 프로세스를 객체지향에서는 총 3개의 Class로 묶을 수 있다.

첫번째로 지난 포스팅에서 만들어보았던 개별 운동 하나를 나타내는 Workout Class이다.

다음으로 WorkoutManager이다. 운동 전체를 관리하는 성격을 지닌 Class이다.

마지막으로 SNS 업로드 관련 기능만 모은 SNS Class와 그 아래 서브 클래스들이다.

 

 

 

 

왜 객체지향인가?

예를 들어, SNS 공유하기 기능에 카카오톡 업로드만 현재 구현되어 있다고 가정해 보자.

그런데 내가 카카오톡을 탈퇴하여 더이상 카카오톡 업로드는 필요 없고, 대신 인스타그램 업로드가 필요하다.

이러한 상황에서 절차지향 프로세스는 전체 코드 중 SNS 업로드 부분으로 찾아가서, 카카오톡 관련 기존 코드를 모두 삭제 한 뒤, 인스타그램 업로드 관련 코드를 새로 짜서 넣어야 수정이 된다.

반면, 객체지향 프로세스에서는 SNS 업로드 Class에 가서 카카오톡 코드와 동일한 클래스로 인스타그램을 만든 뒤 SNS 업로드 클래스에 주입만 해 주면 된다.

구조를 흔들지 않고, 기능을 변경할 수 있다는 것이 객체지향의 핵심이다. SNS 클래스는 그대로 있고, 새로운 것을 붙여서 주입만 하면 되는 것이다.

프로그램 기능이 복잡하고 클 수록 개인이 코드 작성과 관련된 절차를 모두 기억하기 어렵다. 구조적으로 분리를 해 줘야 유지보수 및 관리가 용이하다.

 

 

객체지향 프로그래밍의 4가지 특징

Abstraction 상화 : 공통된 특성의 [추출

Encapsulation 캡슐화 : 캡슐로 감싸 외부의 접근을 제한

Inheritance 상속 : 부모클래스의 능력을 자식클래스에 전달

Polymorphism 다형성 : 클래스의 메쏘드를 재정의함으로써 상황에 맞게 선택할 수 있게 함

 

 

 

 

 

Abstraction 이란?

SNS Upload기능을 구현하기 위해서는 다음 왼쪽과 같은 절차가 필요하다고 가정할 때,

오른쪽이 공통된 기능이 추상화된 것이다. 

 

 

* 스마트폰이 뭐야? 라는 질문에, "네모난 박스 형태 기계야" 라고 답을 할 수도 있고, "통화를 할 수 있는 기계야", "게임을 할 수 있는 기계야" 등등 다양한 설명이 나올 수 있다. 추상화에 대한 설명이나 객체지향 또한 그러하다. 이 설명도 맞고 저 설명도 맞다. 머리로 완벽한 이해를 하려 하기 보다는 부단한 코딩으로 몸으로 익히도록 하자.

 

 

 

 

 

Workout Tracker App 추상화 실습

 

import 'package:workout_tracker/workout.dart';

class WorkoutManager {
  List<Workout> workouts = [
    Workout('싯업',50),
    Workout('친업', 15),
    Workout('스쿼트', 100),
    Workout('푸시업', 45),
    Workout('버피', 30),
  ];
}

새 dart 파일을 만들고 WorkoutManager라는 클래스를 만들었다.

그리고 그 안에 모든 운동을 관리할 예정이니, 운동들이 있던 workouts 리스트를 그대로 떠 왔다.

이렇게 되면 본래 리스트를 참조하고 있던 코드들이 모두 오류가 날 것이다.

 

class _WorkoutTrackerPageState extends State<WorkoutTrackerPage> {

  WorkoutManager workoutManager=WorkoutManager();

  List<Row> workoutsToday = [];
  int workoutIndex = 0;
  List<Row> resultWorkouts = [];
  TextEditingController workoutController = TextEditingController();

클래스를 만들었으니 객체를 하나 만들어야 참조할 수 있다.

STF위젯에서 객체의 위치는 보편적으로 state class 최상단에 해준다.

[클래스명] [변수명] = [컨스트럭터](빈칸 : 기본 컨스트럭터) 구조이다.

이를 기반으로 아래쪽에 에러가 나는 부분들을 전에 포스팅 했던 workoutName을 workouts로 바꿨던 것 처럼 바꿔주면 된다.

 

 

import 'package:workout_tracker/workout.dart';

class WorkoutManager {
  List<Workout> workouts = [
    Workout('싯업',50),
    Workout('친업', 15),
    Workout('스쿼트', 100),
    Workout('푸시업', 45),
    Workout('버피', 30),
  ];
  int workoutIndex = 0;
}

몇 번째 운동에 접근할 지를 결정해주는 workoutIndex라는 값도 Abstraction 이 가능하다. main에서 해당 구문을 잘라서 워크아웃 매니저 클래스에 집어넣고, 기존에 인덱스를 바라보던 녀석들을 워크아웃 매니저 객체를 통해 바라보게 수정을 해 주면 된다.

 

 

 

일반 변수가 클래스 안으로 들어가면 프로퍼티, 멤버변수

일반 함수가 클래스 안으로 들어가면 메쏘드, 멤버함수

기능은 같지만, 선언되는 위치에 따라 용어가 바뀐다.

 

운동을 관리한다는 관점에서 특정을 가진 기능 중,

오늘의 운동을 출력하는 부분도 매니저에 옮길 수 있다. 

 

List<Row> worksoutToday=[]; 와

initstate의 for문에서 작동하여 Row를 실제로 출력해 주는 부분을 옮기면 된다.

 

import 'package:flutter/material.dart';
import 'package:workout_tracker/workout.dart';

class WorkoutManager {
  List<Workout> workouts = [
    Workout('싯업',50),
    Workout('친업', 15),
    Workout('스쿼트', 100),
    Workout('푸시업', 45),
    Workout('버피', 30),
  ];
  int workoutIndex = 0;

  List<Row> getWorkoutsToday(){
    
    return [];
  }
}

1. 일단 메쏘드를 하나 생성한다. getWorkoutsToday(){}

이 메쏘드가 리턴하는 값은 workoutsToday에 담을 Row들이다. 즉 List<Row>다. Row라는 객체가 정의된 파일인 material.dart를 임포트해준다.

2. 바디 부분 리턴이 없어서 함수명에 오류가 표시된다. 빈 리스트를 주어 일단 함수의 형태를 완성한다.

 

List<Row> getWorkoutsToday(){
   for (var i=0;i<workoutManager.workouts.length;i++) {
     workoutsToday.add(
       Row(
         mainAxisAlignment: MainAxisAlignment.spaceBetween,
         children: [
           Text('${i+1}.${workoutManager.workouts[i].name}'),
           Text('${workoutManager.workouts[i].goal}회'),
         ],
       ),
     );
     
   return [];
  }
}

3. for 문을 복사 붙여넣기 해준다. 에러나는 부분을 일부 조정해 주어야 한다.

 

List<Row> getWorkoutsToday() {
  List<Row> workoutsToday = [];
  for (var i = 0; i < workouts.length; i++) {
    workoutsToday.add(
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('${i + 1}.${workouts[i].name}'),
          Text('${workouts[i].goal}회'),
        ],
      ),
    );
  }
  return [];
}

4. workoutManager가 이미 현재 파일 동일한 클래스에 있으므로, 객체로 접근하지 않고 바로 변수명으로 접근하게 수정

5. workoutsToday는 메인에만 있는 변수라, 새로 선언해 준다.

6. 이렇게 되면, getWorkoutsToday를 호출하면 for 문을 돌며 Row가 생성되어 workoutsToday에 담기게 된다.

 

List<Row> getWorkoutsToday() {
  List<Row> workoutsToday = [];
  for (var i = 0; i < workouts.length; i++) {
    workoutsToday.add(
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('${i + 1}.${workouts[i].name}'),
          Text('${workouts[i].goal}회'),
        ],
      ),
    );
  }
  return workoutsToday;
}

7. workoutsToday에 리턴이 되도록 해 준뒤

 

@override
void initState() {
  // TODO: implement initState
  super.initState();

  workoutsToday=workoutManager.getWorkoutsToday();
  
}

8. main.dart에서 이 메쏘드를 활용하면 된다. 기존 for문을 지우고, workoutsToday는 workoutManager에 있는 getWorkoutsToday 메쏘드를 호출하면, 이 녀석이 오늘의 운동에 출력될 Row를 만들어서 workoutsToday 로 출력해준다.

initState에 있던 복잡한 for문들이 모두 워크아웃 매니저로 추상화되어 들어간 것이다.

 

class WorkoutManager {
  List<Workout> workouts = [
    Workout('싯업', 50),
    Workout('친업', 15),
    Workout('스쿼트', 100),
    Workout('푸시업', 45),
    Workout('버피', 30),
  ];
  int workoutIndex = 0;
  List<Row> resultWorkouts = [];
  List<Row> getWorkoutsToday() {
    List<Row> workoutsToday = [];
    for (var i = 0; i < workouts.length; i++) {
      workoutsToday.add(
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('${i + 1}.${workouts[i].name}'),
            Text('${workouts[i].goal}회'),
          ],
        ),
      );
    }
    return workoutsToday;
  }
}

9. resultWorkouts 관련 기능도 옮겨줄 수 있다. 먼저 resultWorkouts 변수를 workout_manager.dart에 만들어주자.

OutlinedButton(
  onPressed: (){

    int userInputCount=int.parse(workoutController.text);
    Icon icon;
      if(userInputCount<workoutManager.workouts[workoutManager.workoutIndex].goal){
        icon=Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,);
      }else{
        icon=Icon(Icons.sentiment_satisfied_rounded,color: Colors.green,);
      }

    setState(() {
      resultWorkouts.add(
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('${workoutManager.workoutIndex+1}. ${workoutManager.workouts[workoutManager.workoutIndex].name}'),
            Text('${userInputCount}회/${workoutManager.workouts[workoutManager.workoutIndex].goal}회'),
            icon
          ],
        ),
      );
    });

10. resultWorkouts은 기존에 main에서 setState에 위치하여 Row를 아이콘의 조건에 맞게 담는 역할을 했다.

 

void addResultWorkout(){

  Icon icon;
  if(userInputCount<workoutManager.workouts[workoutManager.workoutIndex].goal){
    icon=Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,);
  }else{
    icon=Icon(Icons.sentiment_satisfied_rounded,color: Colors.green,);
  }

  resultWorkouts.add(
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text('${workoutManager.workoutIndex+1}. ${workoutManager.workouts[workoutManager.workoutIndex].name}'),
        Text('${userInputCount}회/${workoutManager.workouts[workoutManager.workoutIndex].goal}회'),
        icon
      ],
    ),
  );

}

11. resultWorkout에 Row를 add하는 함수를 하나 만들자. 리턴 타입 없게 void addResultWorkout(); 을 만들어보자

12. 내용으로는 아이콘을 만드는 부분 및 선언하는 부분을 가져오고, 더하는 부분을 가져올 것이다.

 

void addResultWorkout(int userInputCount){

  Icon icon;
  if(userInputCount<workouts[workoutIndex].goal){
    icon=Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,);
  }else{
    icon=Icon(Icons.sentiment_satisfied_rounded,color: Colors.green,);
  }

  resultWorkouts.add(
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text('${workoutIndex+1}. ${workouts[workoutIndex].name}'),
        Text('${userInputCount}회/${workouts[workoutIndex].goal}회'),
        icon
      ],
    ),
  );

}

13. 하나씩 에러를 처리하면 된다. 먼저 workoutManager를 삭제하여 간단한 에러를 처리해 주고, 메인에 있는 UserInputCount를 직접 받아오게 함수 안에 넣어준다. 이렇게 하고, 메인에 있는 관련 코드를 삭제하면 addResult 기능이 workout_Manager로 들어왔다.

 

OutlinedButton(
  onPressed: (){

    int userInputCount=int.parse(workoutController.text);

    setState(() {
      workoutManager.addResultWorkout(userInputCount);
    });
    
    if(workoutManager.workoutIndex>=workoutManager.workouts.length-1){
      workoutManager.workoutIndex=0;
    }else{
      workoutManager.workoutIndex++;
    }
    workoutController.clear();
  },
  child: Text('제출'),
),

14. 삭제한 기존 코드 대신, workoutManager의 addResultWorkout(userInputCount); 함수를 호출하여 기존 기능을 처리토록 한다. 이 때, int userInputCount=int.parse(workoutController.text); 부분은 유저 입력을 직접 받아와 만들어 주는 부분으로 메인에 있어야 한다.

 

15. 메인에 남아있는 resultWorkout 리스트와 화면을 출력해 주는 부분도 수정해 주면 Abstraction이 끝난다. 이렇게 됨으로써 workout 관련 내용을 수정하려면 main이 아닌 workout_manager 파일을 열어서 수정하도록 구조적으로 변환이 되었고, 기능의 삽입 또는 삭제도 메인에서는 다른 함수를 호출하는 것으로 간소화가 되었다.

 

 

 

 

* 다음 포스팅에서도 이어서 계속

728x90
반응형

댓글