아래 링크에서 진행하는 기존 프로젝트의 리팩토링에 대한 기록을 남겨두고자 함

몇일마다 문제가 해결되는지, 어떤 고충과 해결방법들이 있었는지에 대해

우선 간단하게 일기형식으로 작성해두고 나중에 완료 후 ReadMe에 정리해서 반영할 예정

 

https://smallpants.tistory.com/271

https://github.com/batsalee/TaskManager/tree/version-2.0-refactoring

 

2024-07-14

11:03 문서 작성 최종 완성

13:39 CMake 기반 프로젝트 생성 및 ReadMe.md 작성

 

2024-07-18

16:25

json 라이브러리 선정 완료

C++에서 json 사용하게 해주는 라이브러리는 RapidJson과 nlohmann 두개정도가 대표적인듯 했다.

이 중 대략적으로 스타일 보고 자료양봤을때 RapidJson쓰면 무난할듯 해서 RapidJson으로 선택 완료

그 후 GitHub에서 라이브러리 다운 받아서 프로젝트 폴더에 넣은 후 CMakeList에 반영했다.

 

19:40

리팩토링 시작 및 기능 구현 시작

오늘은 우선 3가지 기능정도만 구현하면서 클래스 분리했다.

1) FileManager, FileReader 클래스 : ifstream에서 스트림만 획득하게 했다.

그 후 해당 내용을 string에 담는다.

2) JsonManager, JsonParser 클래스 : string에 담긴 내용을 json 형식으로 파싱한다.

그 후 결과 내용을 Document에 담는다.

3) Schedule 클래스 : Document의 내용을 Schedule 클래스 멤버변수인 이중리스트에 담는다.

이제 Schedule 클래스는 이중리스트 관리자 역할만 한다.

 

가) 고충과 해결방법

(1) 좀 당황스러운게 github 브랜치에다가 커밋하면 잔디가 안깔린다...?

어차피 수정내역 옵시디언에 남겨서 그걸로 깔리긴 하겠지만 이거때문에 또 빼먹을수도 있으니 조심해야할듯하다...

(2) Qt Creator 인코딩 문제

Qt Creator 문제라고 봐야할듯한데 기본 인코딩이 UTF-8로 되어있었다. json도 기본적으로 UTF-8 인코딩이라고 하길래 이러면 따로 인코딩 없이 잘 호환 될거라고 생각해서 그대로 쓰려고 했었다.

근데 문제는 cout이나 cerr로 한글 출력하면 한글이 다 깨져버렸다. 이게 왜 이런지 한참 찾다가 결국 EUC-KR로 변경하고나서 잘 출력됐다. 결국 json에서 Qt로 올때는 또 EUC-KR로 인코딩 해야할듯하다.

(3) 파일 경로 설정

Schedule이 담겨있는 json파일을 어디에 둬야할지 좀 헤맸다.

일단 상대경로를 작성해야 하는데 실행파일 혹은 main.cpp 둘 중 하나일테니 둘 다 해봤는데 안됐다...

아무리 해봐도 안되서 version 1.0꺼 봤는데 이상하게 현재폴더에 하위폴더로 두고 했길래 보니까 Qt Creator는 빌드를 프로젝트 폴더 내에 Debug 폴더를 만들고 그 안에서 하는걸 확인했다. 그래서 해당 실행파일로부터의 상대경로로 설정하니까 잘 됐다.

 

나) 다음 계획

다음 계획은 qml쪽 포팅해서 이중리스트 내용 qml에 보이게 만드는것부터 시작해야겠다.

그래야 qml에서 지웠을때 이중리스트에도 반영되게 하는 등 사용할 수 있으니 qml부터 간다.

알고리즘 공부만 하다가 리팩토링이랑 기능개선 추가 같이 하니까 좀 재밌는것같다.

 

2024-07-19

우선 version 1.0에서 작성해뒀던 qml 파일들을 그대로 옮겨줬다.

근데 이 작업이 3시간쯤 걸렸다...

그냥 옮기고 CMakeLists.txt에 등록해주면 끝일줄 알았다.

하지만 아니었다.

 

가) 고충과 해결방법

(1) 프로젝트의 qml인식과 오류해결

막상 qml들을 옮겨주고 실행해보니 main.qml에서 불러오는 내가 만든 다른 qml들을 인식을 못했고

version1.0에서도 이런 경험이 있었는데 QMake파일을 빌드해주면 해결이 됐었다.

 

근데 문제는 난 지금 CMake로 빌드하고 있고 아무리 찾아봐도 CMake 빌드라는 기능은 없었고

전체 프로젝트 빌드도 해보고 구글링도 해보고 gpt한테도 물어보고 해도 뭘 해도 다 인식이 안됐다.

 

구글링할때 많은 해결법들이 CMakeLists.txt 파일에 qt_add_qml_module 이라는 내용을 수정해주라고 했었다.

근데 내 CMakeLists.txt에는 qt_add_qml_module옵션이 있지도 않았고, 그래서 직접 작성해서 추가해주면 프로그램이 실행조차 안됐었다.

 

결국 이렇게 3시간쯤 흐르다가 문득 프로젝트 파일에 뭔가 필요없어 보이는 폴더들이 많아서 하나하나 확인해봤고 거기서 해결법을 찾았다.

프로젝트 자체 파일 중 구석에 있는 파일하나에 qt_add_qml_module라는 옵션이 있었고 거기에 내가 추가한 qml들을 나열해주니 인식이 됐다.....

프로젝트 루트경로에 있는 CMakeLists.txt가 아닌 서브폴더에 있는 qmlModule이라는 파일이었다.

왜 이게 숨어있어서 이렇게 시간을 낭비하게 하는건지, 또 qml을 추가하면 자동으로 저기에 내용을 추가해줘야지 왜 이걸 직접 해야하는지도 이해가 안된다...

 

아무튼 위 옵션에 내가 추가한 qml들을 나열하고 실행해보니 실행이 잘 됐고 아직 구현이 완벽하지 않아서 안뜨는 내용들도 있었지만 시계나 버튼등은 일단 확실히 제 위치에 뜨는걸 확인했다.

 

(2) 근데도 오류는 남아있다.

위 설정을 마친 후 실행이 잘 되고 qml이 화면에 잘 뜨기까지 했다.

근데 코드에는 오류가 있다고 떴다.

오류 내용은 Unknown component. (M300) 였고 관련해서 검색해보니 비슷한 오류를 겪는 사람이 아주 많았는데 일단 상대경로를 import해주는 방법으로 해결했다.

main.qml에 import "./qml"를 추가해줬는데 이게 제대로 된 해결법인진 모르겠다. 일단 경로 지정해서 qml 파일을 인식했으니 오류가 사라진거같긴한데 QMake때는 저런거 없이도 잘 됐어서 왠지 다른 프로젝트 파일에 설정해주는게 더 권장되는 방법이 아닐까 싶어서 찝찝하다.

 

나) 다음 계획

일단 어찌됐든 qml도 포팅해줬으니 다음 목표는 Schedule 객체가 관리하는 이중리스트와 qml의 taskList간의 연동이다.

둘이 연동이 되야 일단 내용이 잘 왔는지, 잘 지워지고 있는지 확인이 가능하므로

 

2024-07-22

16:51

Schedule클래스의 scheduler 객체와 qml간의 integration 완료

이건 version 1.0부터 integration하는 방법이 너무 여러종류가 뭘 써야하는지 도저히 몰라서 이것저것 썼었는데

여기서부턴 Q_PROPERTY로 통합해서 작성하는걸로 결정

#include <QObject>

class Schedule : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QList<QList<QString>> schedule_list READ GetScheduleList NOTIFY ListChanged)
...

signals:
    void ListChanged();
}

위처럼 QObject 상속받고, Q_OBJECT매크로 걸어주고, Q_PROPERTY에 내용 나열해주면 된다.

Q_PROPERTY에 대한 설명을 남겨두자면

1) 첫번째에 있는 schedule_list는 qml쪽에서 접근할 이름을 지정해준 것

2) READ에 작성하는 함수는 qml에서 schedule_list이라는 이름으로 접근시 어떤 함수를 사용하면 될지 지정

3) WRITE함수도 작성할 수 있는데 여기선 안했는데 이건 qml에서 schedule_list라는 이름으로 뭔가 변경했을때 어떤 함수가 해당 내용을 반영할지 지정

4) NOTIFY는 변경이 발생했을때 일으킬 signal을 지정, 주로 WRITE함수쪽에서 변경 후 emit으로 시그널 발생

 

여기까지 완성 후 qml쪽에 내용이 뜨는것까지 확인 완료

근데 뭔가 줄간격이나 버튼 모양같은게 version1.0 qml을 그대로 가져온건데도 조금씩 달라져있어서 해당 부분은 나중에 기능조정 후에 디자인 조정좀 해야할것같음

 

20:02

Schedule 클래스의 insertTask, updateTask, deleteTask함수 구현 및 qml과 연동 완료
값 변경을 위해 우클릭시 기존의 값이 뜨도록 qml 구조 변경

새로 발생한 고민
기존엔 할일 문자열 자체에 #, @, ★을 포함시켜서 일의 중요도를 표시했었음
근데 지금 json 배열로 사용하려고 보니 해당 중요도를 포함시킬 방법이 없는 상황
그래서 결국 json 배열 구조를 배열안에 객체가 들어가게 해서 객체가 중요도를 포함하게 할지 고민중
혹은 문자열 자체에 포함시키는것도 방법인데 기존에 C#같은걸 입력하려면 #때문에 문제가 됐어서 제약이 좀 있음

 

2024-07-23

18:11

어제의 고민이었던 Task의 중요도 표현에 대한 개선 완료

애초에 내가 인터프리터 클래스를 직접 구현하는게 아니라 xml이나 json을 선택하기위해 고민했던 이유부터가 실제 내용과 중요도의 분리때문이었는데 json의 기능 자체에 집중하느라 위 사실을 잊고있었다.

 

해결방법으로는 QList<QList<QString>> 형태로 이중리스트를 보관하는게 아니라 

Task라는 struct를 만들고, struct의 멤버변수로 제목과 중요도를 표현하게 하고 QList<QList<Task>> 형태로 변경했다.

이를 위해 Task라는 구조체가 qml에서 사용될 수 있도록 등록하는 단계가 필요했다.

아래 내용이 Task 구조체의 내용이다.

#pragma once

#include <QObject>
#include <QString>
#include <QMetaType>

using Task = struct task {
    Q_GADGET
    Q_PROPERTY(QString title MEMBER title)
    Q_PROPERTY(qint32 importance MEMBER importance)

public:
    QString title;
    qint32 importance;
};

Q_DECLARE_METATYPE(Task)

위처럼 Q_GADGET같은 매크로나 Q_DECLARE_METATYPE(Task)같은 키워드로 Task를 qml에서 사용 가능한 타입으로 등록해주는 과정이 필요했다.

그 후 qml측에서 modelData.title이나 modelData.importance같은 형태로 호출해서 사용 가능해졌다.

이제 C# 같은 문구도 자유롭게 사용 가능할듯하다.

 

19:46

가) 해결한 일

프로그램 종료시점에는 이중리스트를 다시 json으로 파싱하고, 파싱한 내용을 파일에 작성해야한다.

이를 위해 rapidjson의 기능들로 json이 담긴 string을 만들고 fstream의 ofstream으로 파일 내용을 갱신해야 한다.

위 기능까지는 이미 기존에 version 1.0에서 사용한 코드중 일부를 가져오거나, rapidjson 라이브러리 예시코드를 잘 활용해서 구현은 해냈다.

 

나) 문제점

(1) qml쪽에서 디테일한 부분에서의 문제

Task를 새로 insert하거나, 기존의 Task를 delete할때 스크롤이 가장 위로 올라가버린다.

난 기존의 위치를 유지하기를 원한다.

version 1.0에서도 같은 문제가 발생해서 해결했던 내용이다.

근데 qml 구조를 변경하고, C++과 qml의 integration 구조를 변경하다보니 이 문제가 다시 발생하고있다.

디테일을 따질때 해결해야 할 문제이므로 당장에 해결하기보다 나중을 위해 기록해둔다.

 

(2) JsonManager 및 하위 클래스들의 필요성이 너무 없다.

대부분의 내용을 rapidjson 라이브러리가 해결해주다보니 두 클래스는 그냥 rapidjson 헤더들을 포함하고 정작 내용은 없는 헤더가 되어버렸다.

그래서 지금 생각은 Schedule클래스의 현 ConvertJsonToScheduleList(구 MakeScheduleList)함수가 사실상 Json to 이중리스트 함수이므로 JsonParser 클래스로 해당 내용을 옮길까 생각도 하고있다.

 

(3) 오늘 작성한 json파싱 및 파일저장 코드를 어디에 반영하느냐이다.

Schedule 클래스의 소멸자에 작성하면 가장 쉽긴한데 Schedule클래스와 FileWriter 클래스의 의존성이 너무 강해진다.
기존에 있던 Make이중리스트 함수도 의존성때문에 분리할지 고민하던 참인데 이것까지 합치는게 맞나 싶다.
어찌해야 할까 이 고민이 오늘의 작업을 마무리하면서 남은 과제인듯하다.

이 고민을 하는 이유는 차후에 오늘의 일정뿐 아니라 다른 날의 일정도 창에 띄우게 만들지도 모른다.
그럼 그냥 소멸자가 아닌 Update함수를 호출해야 할수도 있다.
이러면 그냥 종료시점에 Update함수를 호출하게 만드는게 맞을지도 모른다.
이 상황을 생각하지 않는다면 두가지 방법정도가 있을듯하다.

- 소멸자에 작성(가장 쉬운 방법)

- 종료시점에 발생하는 시그널에 슬롯함수를 연결

QObject::connect(&app, &QCoreApplication::aboutToQuit, cleanupFunction);

위 코드는 app이 종료되는 순간 aboutToQuit시그널을 발생시키며, 그에 대한 슬롯함수로 cleanupFunction을 내가 작성해서 연결해주는 내용이다.

 

아무튼 오늘 밤사이엔 구현을 어디쪽에 해야할지 고민해봐야겠다.

당장에야 소멸자에 구현해두고 다른 디테일들을 구현하는게 나을지도 모르겠다.

 

2024-07-25

리팩토링이라는 용어를 그냥 막연하게 사용하고 있었다.

기존의 클래스 구조들을 객체지향의 원칙을 준수하게 변경하고, 디자인패턴을 적용할 수 있으면 적용하며, 코드 중복을 줄이거나 클래스간의 의존성을 낮추는 등의 모든 작업을 통칭해서 사용하고 있었는데

리팩토링에 관해서도 전문적인 책들이 많은걸 보니 가볍게 말하기 힘든 깊은 내용들이 많은듯 하다.

차후에 공부해보기 아주 좋은 주제인것같다.

관련 책으로 martin fowler의 refactoring이라는 책이 있었다.

 

어쨋든 Two Hats라는 말이 있다.

리팩토링과 기능추가를 동시에 진행하면 안된다는 말이다.

지금까지 리팩토링을 하면서 해당 클래스에 기존에 부족했던 기능들을 추가하기까지 하고 있었는데 

리팩토링과 기능추가를 분리해야한다는 말이다.

 

일단 최우선적으로 기존의 기능을 모두 수행할 수 있게 구조 정리만 하는걸 우선적으로 마쳐야할듯하며

그 다음에 기능추가를 해야할듯하다.

사실 이미 인터프리터 클래스 역할을 하던 함수를 json구조로 바꾸면서 rapidjson으로 처리하다보니 해당 클래스가 인터프리터클래스가 되어버리는 꼴이 됐고 그 과정에서 많은 기능개선이 이루어지긴 했다.

그래도 알람같은 기능은 아예 기능추가니까 리팩토링 후에 추가해보는걸로 한다.

 

그리고 어제의 고민과제였던 종료시 처리할 함수들의 내용을 어디에 위치할것인지에 대한 답을 얻어가는듯하다.

답은 비지터 패턴을 적용하는쪽으로 기울고 있고 결국 클래스 내에 작성하면 가장 깔끔한데 커플링이 너무 생기는점이 맘에 안들었는데 이걸 비지터패턴을 적용해서 저장하게 만든다면 그래도 최소한의 의존성으로 처리가 가능할듯하다.

 

2024-07-29

몇일간 클래스간 관계와 커플링에 대해 고민하며 디자인패턴을 복습하는 시간을 가졌다.

또한 검색도 많이 해보고 ai에게도 물어본 결과 약간의 커플링으로 코드 구성이 간소화된다면 차라리 약간의 커플링을 택하는게 맞을지도 모르겠다는 결론을 내렸다.

오히려 단일책임원칙을 준수하며 커플링을 최소화하는것을 주 목적으로 두고 전체 구조를 구성하려고 하다보니 쓸데없는 기능들을 더 추가해야 하는 상황이 생기곤 했다.

 

 

그래서 내린 결론에 따라 프로그램 구조를 초기에 계획한 구조에서 조금 변경해볼까 한다.

 

1. 프로그램 실행시

1) Date 객체 생성, 오늘 날짜 획득함수 호출

2) Schedule 객체 생성

3) Date 객체 기반 경로에 따라 FileManager가 파일 유무 확인 후 생성 및 읽어오기
4) JsonParser가 이중리스트 생성
5) 생성된 이중리스트를 Schedule의 멤버변수인 이중리스트에 넣음

 

위 과정에서 고민했던 부분은 최대한 의존성을 줄이기 위해 Composition보다는 Aggregation이라도 쓰려고 했는데

FileManager객체와 JsonParser객체의 생애주기는 Schedule객체 생성시에만 필요할 뿐 그 후에는 필요가 없기때문에 차라리 내부 지역변수로 선언 후 사용하고 소멸시키는게 경제적이라고 판단했음

또한 날짜를 변경한다면 Schedule객체 하나만 새로 만들면 되는데 그걸 위해서 FileManager객체와 JsonParser객체까지 만드는건 코드 중복이 될 수 있다고 판단해서 그냥 Composition이 차라리 유리하다고 결론 지었음

 

2. 날짜 변경시

1) 원하는 날짜로 Date 객체 변경

2) 프로그램 실행시의 2 ~ 5와 동일하게 동작

 

그렇다면!!

팩토리메서드 패턴을 사용하기 아주 좋은 상황으로 보임

프로그램 실행시/날짜 변경시 둘의 차이는 Date 설정만 차이가 있을 뿐 그 후 과정이 모두 같으므로

 

3. 프로그램 종료시 && 날짜 변경시
1) Schedule클래스 소멸시에 저장로직 구현
- JsonMaker, FileWriter를 통해 멤버변수의 이중리스트 -> json -> File로 저장

만약 Schedule클래스의 소멸자에 작성한다면 커플링이 생기지만 코드는 가장 간결해짐

혹은 단일책임원칙을 강하게 준수하기 위해 비지터패턴을 적용해볼 수도 있음

 

위 고민들끝에 작성한 코드 프레임이다. 아래 코드를 봐가면서 구조를 변경하면 구조상에 문제는 거의 없어질듯하다.

#include <memory>

/* Date 클래스
관리중인 스케쥴의 날짜정보를 저장하는 클래스
싱글턴으로 구현
이유 1) 하나만 있으면 된다는점
이유 2) qml과 integration
이유 3) Schedule의 팩토리메서드 구현시 참조목적으로 전역접근
*/ 
class Date
{
private:
	int year;
	int month;
	int day;

	Date() = default;
	~Date() = default;

	Date(const Date& d) = delete;
	Date(Date&& d) = delete;
	Date& operator=(const Date & d) = delete;

public:
	static Date& instance()
	{
		static Date instance;
		return instance;
	}

	void GetTodayDate()
	{
		// GetTodayDate()는 QDate라이브러리에 지원해주는 기능 있으니 그냥 쓰면 될듯
	}	
};

/* Schedule 클래스
객체 생성부는 팩토리한테 맡기고
이중리스트 관리기능만 전담
다만 종료시점에 파일에 저장하는 부분에서 커플링이 생길 수 있음
그래서 이부분을 Saver 클래스를 별도로 만들고 비지터패턴으로 구현할까 고려중
*/ 
class Schedule { };

// 팩토리메서드
class ScheduleFactory
{
public:
	std::unique_ptr<Schedule> MakeSchedule()
	{
		Date& date = GetDate();
		std::unique_ptr<Schedule> schedule = std::make_unique<Schedule>();
		// FileManager 작업
		// JsonParser 작업
		// 결과를 schedule객체의 이중리스트에 보관

		return schedule;
	}

	virtual Date& GetDate() = 0;
};

class TodayScheduleFactory : public ScheduleFactory
{
public:
	Date& GetDate() override
	{
		Date& date = Date::instance();
		date.GetTodayDate();
		return date;
	}
};

class AnotherdayScheduleFactory : public ScheduleFactory
{
public:
	Date& GetDate() override
	{		
		Date& date = Date::instance();
		return date;
	}
};

int main() 
{
	std::unique_ptr<TodayScheduleFactory> tsf;
	std::unique_ptr<Schedule> schedule = tsf->MakeSchedule();
	tsf.reset();

	/* 변경발생시
	std::unique_ptr<AnotherdayScheduleFactory> asf;
	schedule = asf->MakeSchedule();	

	// 혹은 위 방식으로 갱신이 잘 안된다면 
	// schedule.list = asf->MakeSchedule().list같은 식으로
	// 이중리스트만 변경하는것도 괜찮지 않을까 싶음
	// 그리고 단일창에서 변경되게 할지, 새로운 창을 띄울지 고민해야함
	*/

	return 0;
}

 

이에 맞게 클래스다이어그램도 조정했다.

기존 파일에 있는 클래스다이어그램보다 아래 클래스다이어그램이 더 최신 버전이다.

classDiagram
    class FileManager{
        -file_path : std::string
    }
    class FileReader{
        -readFile() : std::string
    }
    class FileWriter{
        -writeFile(context : std::string&) : void
    }
    FileManager<|--FileReader
    FileManager<|--FileWriter

    class JsonManager{
    }
    class JsonParser{
        +makeList(fr : FileReader) QList~QList~string~~
    }
    class JsonSerializer{
        +makeJson(fw : FileWrite) JSON
    }
    JsonManager<|--JsonParser
    JsonManager<|--JsonSerializer

    FileManager--JsonManager

    class Alarm {
        alarms : Priority_Queue~timeData, string~        
    }
    JsonManager--Alarm

    class Schedule{
        -task_list : QList~QList~string~~
        -appendDayOfWeekTaskfile() : void
        -appendYesterDayTaskfile() : void
    }
    JsonManager--Schedule

    class Task{
        +title : QString 
        +importance : int
    }
    Task --o Schedule

    %% 팩토리메서드
    class ScheduleFactory {        
        +MakeSchedule() : std::unique_ptr<Schedule>
        +GetDate() : virtual Date&
    }
    class TodayScheduleFactory{
        +GetDate() Date& 
    }
    class AnotherdayScheduleFactory{
        +GetDate() Date& 
    }
    ScheduleFactory<|--TodayScheduleFactory
    ScheduleFactory<|--AnotherdayScheduleFactory

    ScheduleFactory--JsonManager
    ScheduleFactory--FileManager
    ScheduleFactory o..> Schedule

    class FolderOpener {
        -file_path : QString
        +openFolder(file_path : const QString&) void
    }

    class Date {
        %% 싱글턴
        -year : qint32
        -month : qint32
        -day : qint32
        +instance() static Date&
        +getTodayDate() void
    }
    FileManager-->Date

 

2024-07-31

오늘은 qml에서 드래그 앤 드롭이나 키입력시에만 이미지가 출력되게 만드는 등 다양한 기능을 테스트해보면서 자료조사를 했다.

근데 그냥 다 안된다. 검색해도 안나오고 코파일럿도 GPT도 다 틀린 답변만 몇번씩 반복해서 내뱉는다.

그나마 하나 작동하는걸 건졌는데 아래와 같다.

버튼을 누르면 사각형이 뜨고, 클릭하면 로그도 나온다. 

이걸 응용해서 할일 추가를 원하는 위치에 넣을 수 있게 만들 수 있지 않을까 싶다.

import QtQuick
import QtQuick.Window
import QtQuick.Controls

Window {
    visible: true
    width: 640
    height: 480
    title: "Rectangle Visibility Control"

    Rectangle {
        id: myRectangle
        width: 100
        height: 100
        color: "#555555"
        visible: false

        MouseArea {
            anchors.fill: parent
            onClicked: {
                console.log("clicked");
            }
        }
    }

    Button{
        onClicked: {
            myRectangle.visible ^= 1
        }
    }

}

 

위의 기능을 구현하려고 하니 문제는 모듈화로 인해 버튼의 위치와 나타날 이미지의 위치가 서로 다른 qml에 있다는 것이었다.

해결방법을 찾아본 결과 몇가지가 있었는데

1) signal과 slot(Connections)를 이용한 방법

2) property(속성, 멤버변수 느낌으로)을 사용한 방법이 있었다.

위 방법 외에도 몇가지 더 있긴 했는데 signal쪽이 책임소재도 분명하고 명백해보여서 채택했다.

 

구현한 방법은

Button이 있는쪽인 buttonList.qml에서 시그널을 선언하고, 버튼을 누르면 해당 시그널이 발생하게 만듬

이미지가 나타날쪽은

insertRect가 나타나야할 이미지인데 buttonList에서 발생하는 시그널을 받을 slot을 정의하는 목적으로 Connections를 만들고 해당 이벤트가 발생시 수행할 동작을 작성하면 된다.

위처럼 작성하면 버튼을 누를때마다 insertRect의 visible이 토글되고 해당 rect를 클릭해서 새로운 내용을 추가하게 만들 수 있다.

 

다음 과제)

토글해서 나온 Rect 이미지 제대로 만들어주고, index 얻어서 해당 위치에 insert되게 제대로 구현하기

코드들 헤더쪽은 클래스의 목적, 시퀀스 작성하고, cpp쪽은 함수의 기능 작성하기

파일 경로 제대로 설정하고 파일 만들기 및 덧붙이기 구현

 

2024-08-01

오늘 밤사이 qml관련 아주 많은 실험과 자료조사를 했다.

그 결과 드디어! 드래그해서 두 값을 바꾸거나 위치를 바꿀 수 있는 등의 기능을 찾아냈다.

하지만 우선 Two Hats원칙 준수를 위해 바로 반영하지 않고 킵해뒀다.

더불어 어제 한 내용도 마찬가지로 Two Hats원칙 준수를 위해 주석처리해뒀고 리팩토링부터 마무리해볼 생각이다.

 

그리고 어제 과제로 남겨뒀던 내용 중 헤더쪽에 클래스관계, 용도, 구현방식을 모두 작성했다.

 

그리고 디자인패턴을 공부할때 각 패턴들의 단점으로 주로 언급되었던 내용이 클래스의 갯수가 많이 늘어나게 된다는 점이었는데 역시 이를 느끼게 되었다.

헤더파일과 소스파일들이 모두 하나의 폴더안에 있었는데 너무 지저분해져서 코드들을 정리했다.

서로 관련있는 모듈끼리 폴더에 묶어넣었고 이에 맞게 프로젝트에서 헤더파일 include도 상대경로로 수정해줬다.

이상없이 잘 작동하는것까지 확인했으니 이제 정말 리팩토링의 마지막 단계이다.

 

1) 고민했던것
클래스간 의존도를 낮추기 위해 부모클래스의 virtual을 호출하고 실제 구현은 자식클래스의 concrete가 하게 만드는 방법을 쓴다고 하는데 이게 결국 다형성때문에 의존도가 낮아진다는건데 다형성이 필요한 상황이 아니라면 오히려 virtual 오버헤드만 만드는 낭비가 아닐까?

코파일럿의 답변
다음과 같은 상황에서는 약간의 커플링을 감수하는 것이 더 효율적일 수 있습니다:
클래스 간의 관계가 명확하고 변경될 가능성이 적을 때
코드의 복잡성을 줄이고 간결하게 유지하고 싶을 때
성능이 중요한 경우 (가상 함수 호출은 약간의 오버헤드를 발생시킬 수 있습니다)

 

남은 과제)

- cpp쪽 함수의 기능 작성하기

- 일정파일 경로 test -> 제대로 수정하고 덧붙이기 구현

- qml쪽 테스트했던 많은 내용들 반영해보기

나중에 함수에 const noexcept 달아주는거 잊지말기
jsonManager쪽에 json to QList 함수 만들어줘야 할듯하고 schedule객체랑 file_content 매개변수로 넘기면 될듯하고
파일을 만약 날짜별로 안만들고 today.json만 만든다면? 
그럼 파일이 존재하지 않는지 여부를 판단할게 아니라 파일의 날짜정보를 판단해야 함
그렇다면 파일 추가 순서는
0) 파일 날짜 확인, 오늘 처음 연거라면 아래내용, 오늘 이미 열었었다면 바로 파싱
1) 직접 등록한 일정
2) 매일할일
3) 해당요일 할일
4) 어제 안한일 순서로, 어제 안한일은 일정파일의 날짜정보랑 오늘 날짜가 다르면 filesystem으로 temp생성 후 삭제하면 좋을듯

!! json파싱부분 수정하기

parse함수는 string에서 얻어오는 반면 parseStream함수는 ifstream에서 바로 얻어오는거라서 오버헤드가 적다.


doc.Parse(jsonString);: 문자열(std::string 또는 const char*)로부터 JSON 데이터를 파싱합니다. 예를 들어, JSON 데이터가 문자열로 이미 메모리에 로드되어 있는 경우에 사용합니다.
const char* jsonString = "{\"name\":\"John Doe\",\"age\":30}";
rapidjson::Document doc;
doc.Parse(jsonString);

doc.ParseStream(isw);: 입력 스트림(std::istream)으로부터 JSON 데이터를 파싱합니다. 파일이나 네트워크 스트림 등에서 JSON 데이터를 읽어올 때 사용합니다.
std::ifstream ifs("data.json");
rapidjson::IStreamWrapper isw(ifs);
rapidjson::Document doc;
doc.ParseStream(isw);

 

2024-08-04

그간 고민했던것들을 최근 정리하고 있었는데 오늘 한번에 깔끔하게 정리됐으므로 기록으로 남김

 

오늘 해결한것
1) 위에 작성했던 남은 과제였던 cpp파일의 함수들의 기능 작성

2) 일정파일 경로 test.json에서 실제 날짜 경로로 구현 완료

3) 또한 고정 일정들인 각 요일 및 매일 할일 경로 구현 완료

4) 일정파일에 위 파일들의 내용을 읽어와서 반영하도록 구현 완료

 

5) rapidjson의 파싱 방식을 std::string에서 stream으로 변경시도해봤으나 오류가 발생해서 std::string으로 복구

관련해서는 아주 작은 오버헤드를 줄이고자 했던건데 오히려 std::string으로 두는쪽이 클래스의 존재의의에 더 적합해서 std::string으로 사용하는걸로 결정

 

6) FileManager 클래스의 존재의의 확정

일정파일의 존재 유무 확인을 위해 filesystem 라이브러리로 file경로를 확인하는 기능 구현이 필요했는데

Date객체에 year/month/day 값을 이용하면 되므로 사용하려 했었음

다만 Date 클래스는 단지 날짜를 관리하는 클래스일 뿐 날짜정보를 파일경로로 파싱하는것은 역할에 어울리지 않기에

Date 객체의 멤버변수들을 파일경로로 바꾸기 위해 어댑터패턴을 구현하기로 했고 DateAdapter와 DateAdapterInterface클래스를 구현했음

하지만 해당 내용을 구현하던 중 점차 모습이 파일시스템 관리자와 유사해졌고 결국 어댑터패턴 롤백 후 FileManager에 해당 내용을 옮겼음

그 결과 FileManager의 역할은 이제 관리중인 파일의 경로관리이며, 자식클래스인 FileReader는 읽기기능을, FileWriter는 쓰기 기능을 추가로 확장해서 구현함

 

7) JsonManager 클래스의 존재의의 확정

기존엔 ScheduleFactory에서 Schedule객체를 만들 때 일정파일을 읽어온 후 파싱하는 기능까지 수행했었음

하지만 이 내용은 추후 JsonManager로 옮겨야 겠다고 생각해뒀고 관련해서 주석으로 남겨뒀던 내용을 오늘 JsonManager의 JsonParser측으로 옮김

고충) 이 과정에서 일정파일이 2중 array구조인지, 혹은 1중 array 내에 object들이 들어있는 구조인지에 따라 파싱과정에서 오류가 발생했고 추후 저장구조와 적합한 2중 array구조로 모든 내용을 통일시켰음

 

그 결과 현재까지 진행된 상황은 아래와 같음

 

남은 할일)

1) 각 함수에 const noexcept 등 달아주기

2) 어제 안한일 추가되도록 처리하기

3) 종료시 저장로직 구현하기

 

위 내용정도만 하면 이제 리팩토링은 끝이라고 볼 수 있을 듯 함

그 후 기능 추가 구현할점은 아래와 같음

1) 해시태그 방식에서 json방식으로 변경하는 바람에 일정파일 직접 조작이 복잡해졌음, 관련 처리 기능을 gui로 구현

2) qml쪽 테스트했던 많은 내용들 반영해보기

3) 알람기능 구현

 

사실상 1번내용은 필수적이지만 거의 새로운 프로그램을 하나 만드는 수준이라 시간이 좀 걸릴듯하고

2, 3번은 아예 새로운 기능 확장이므로 차차 해볼 예정

이제 리팩토링은 1~2일선에서 마무리 될 듯하다.

 

2024-08-06

또 꿈에서 코딩하다 깻다
내용은 Date를 qml측에서 바꾼다면 Date변수도 Q_PROPERTY로 선언해야한다.
또한 이게 바껴서 notify된다면
1) Schedule객체를 소멸시키고 
2) ScheduleFactory에서 Schedule객체를 다시 만들게 해야한다.

즉 signal, slot으로 notify -> Schedule 삭제/생성이 된다.
결국 이게 말이 다른 클래스의 함수를 호출하는거지
사실 옵저버 패턴을 구현하는거나 다를바가 없게 된다.
옵저버 패턴 자체가 커플링이 생길수밖에 없는 구조긴 한데 
차라리 커플링에 대해 고민 많던 시점에 좋은 변명거리가 될 수 있을 듯 하다.

 

2024-08-14

며칠간 휴가도 다녀왔고 고민하던 내용들을 정리도 해봤다.

결국 오늘 많은 부분을 해결해냈다.

다만 완벽하지는 않은것같다. 코드를 조금 더 이쁘게 다듬을 수 있지 않을까라는 생각이 머리를 맴돈다.

하지만 보기에 이해 잘 되고 기능적으로 문제없다면 그냥 두는게 맞다고 스스로 되뇌이고 있다.

 

오늘 해결한일 1)

우선 Schedule의 소멸자에 종료시점 저장 로직을 구현했다.

이건 그냥 나중에 Saver 클래스만들어서 퍼사드로 구현해버리면 될일이라 다음번에 옮기면 된다.

 

오늘 해결한일 2)

JsonSerializer를 구현했는데 저장로직에서 이상한점이 너무 많았다.

저장할때마다 내용이 랜덤하게 바뀌질 않나, 이중리스트의 마지막 노드값으로 다른 노드들의 값이 변경되질 않나, 

인코딩이 다 깨지거나, null값이 "\u0000" 형태로 들어가는 등 별 이상한 문제가 계속 발생했었다.

처음 의심한 내용은 인코딩이 깨진게 아닐까 싶었다. 그래서 UTF8로도, EUC-KR로도, QString으로도, std::string으로도 다 바꿔가면서 해봤는데 결국 Value객체에 const char*형태로 값을 지정하고 넘기면 해결됐었다.

 

오늘 해결한일 3)

오늘 파일을 읽었었는지, 읽지 않았었는지 판단하는 opened 값을 json에 추가했다.

파일을 저장하는 시점에 opened : true 라는 문구가 json파일에 추가되게 했다.

 

오늘 해결한일 4)

JsonParser쪽에서 파싱한 후 opened가 있는지, true인지 확인하고

맞다면 그냥 있는 내용만 읽어오고 끝이고

아니라면 해당날짜내용, 매일할일, 요일에 할일을 다 추가하는 식으로 구현했다.

 

남은 할일)

1) 각 함수에 const noexcept 등 달아주기, 오늘 작업하고 새로 만든 함수들에 주석달기

2) 어제 안한일 추가되도록 처리하기

- 이걸 처리할때 아예 data.json을 만들어버리고 거기에 마지막 종료한 date날짜를 기록해버리면

해당 날짜와 실행한 날짜가 같은지 보는걸 opened대신 처리할 수도 있고 해당 date의 파일을 읽어오게 해도 되니까 더 코드가 간결해지지 않을까 생각중이다.

이걸 위해 폴더구조를 Schedule이 아니라 Data로 바꾸고

그 안에 schedule / fixed_schedule / long_term_goal / data.json 형태로 두면 좋을듯하다.

이게 처리되면 여러모로 만족스러워질듯하다.

3) 위의 2가지 일을 완료하면 리팩토링은 끝이다. 이젠 기능 개선의 단계이다.

- 먼저 일정파일 만드는 작업을 gui로 구현할 수 있어야 한다.

  - 고정일정이건, 오늘 일정이건, 다른날의 일정이건 다 변경 가능해야한다.

- 일정 파일에 일정 추가 위치를 내 맘대로 조절할 수 있어야 한다.

- 또한 이미 추가된 값들의 위치를 변경할 수 있어야 한다.

 

2024-08-19

오늘은 아주 많은 변경점이 있었다.

다만 안타까운점은 새로운 기능 추가도 있었지만 상당부분은 기존에 작성했던 내용들에 대한 개선 및 변경이었다.

지금까지 언제나 클래스 구조를 정하거나 디자인 패턴을 적용할때 근거가 있었다.
그런데 계속 발전시켜가다보니 바꾸는게 더 좋다라는 생각이 드는 경우가 많아서 다 갈아엎게 되는 경우도 생겼다.
마지막 하나의 기능을 앞두고 많은 고민을 한 결과 지금까지 한 내용들을 대부분 변경해야 하는 상황이 발생했따.
일 두번하는 상황을 막기위해 클래스 다이어그램도 먼저 그려두고 시작했는데도 결국 이런일이 발생하는 것에 대해 대응할 수 있는 익숙함이 생겼다.

해결한일 목록
1) Schedule 클래스 싱글턴으로 변경, 그래야 전역적인 접근점 생겨서 saver 구축하기 편함
2) Schedule 폴더 -> Data폴더로 변경 및 처리 코드 변경, 설정 관련 파일도 들어가므로 더 적합한 이름으로 변경
3) Data 폴더내에 init.json 만들고 프로그램 최초 실행 여부, 마지막으로 관리된 날짜 정보 저장
4) 각 클래스의 역할 명확하게 분리
5) Initializer 클래스 구현 및 내용 작성, 프로그램 최초 실행시 처리할 로직을 담당하는 클래스
6) Schedule 객체에 대한 팩토리메서드 패턴 삭제, 이를 대체하기 위해 Date와 Schedule 시그널-슬롯으로 연결 및 일부기능 Initializer가 대행
8) 마지막으로 관리된 날짜와 오늘의 날짜가 다르다면 마지막으로 관리된 날짜의 내용 리스트에 추가하도록 해서 어제 안한일 추가하는 로직 완료, 하지만 이 부분이 코드 간결성이 좋아보이지 않아서 고민중

남은 할일

1) saver 구축 -> 시그널 슬롯을 Schedule이 아니라 Saver가 담당하게 하면 Schedule은 전역적이니까 단일책임에 더 깔끔할듯
2) 주석처리
3) const noexcept
4) 어제 남은일 처리 코드의 간결성 고민

5) 복사생략 불가능 한곳 레퍼런스로 변경
결국 컴파일러 최적화 기법인 RVO, NRVO로 인해 반환은 그냥하고 받는것도 그냥 받는게 좋다
반면 string을 매개변수로 넘길땐 복사생략이 발생하지 않으니 레퍼런스로 넘기는게 유리하다.
둘의 차이를 인지하기

 

2024-08-21

이제 남은 할일들을 내일 완료하고 나면 사실상 리팩토링은 끝이라고 볼 수 있을 듯 하다.

리팩토링의 시작은 디자인패턴과 객체지향의 원칙등에 대해 자세히 공부한 후 적용해보기 위함이었다.

그래서 처음 리팩토링을 시작하면서 먼저 프로그램의 구조를 구상한 후 클래스 다이어그램을 그려놓고 시작했다.

하지만 막상 진행해보니 진행하면서 굳이 이렇게 해야하나? 싶은 점도 많았고 변경하는게 차라리 나아보이는 상황도 많았기에 처음에 잘 적용한 디자인패턴을 다시 없애고 단순하게 바꿔버리는 상황이 많이 생겼다.

또한 가장 집착했고 고민했던 부분은 코드의 재사용성 내지는 각 클래스간 커플링의 최소화였는데 결국 객체지향 프로그램이라는게 어느정도의 커플링은 발생할 수 밖에 없고 그걸 최소화하는게 최선이라고 결론지었다.

그래서 TaskListManager 클래스 외의 다른 클래스들은 모두 독립적인, 커플링이 전혀 없는 클래스로 구현하고 오직 TaskListManager가 그 클래스들을 이용하는 커플링의 중심이 되도록 구현했다.

어쩌면 오늘 조금 휴식한 후, 어쩌면 내일 리팩토링은 끝날듯하고 리팩토링으로 인해 쉽게 사용 가능하던 프로그램이 json이라는 구조를 사용하면서 더 복잡해진만큼 일정관리를 gui로 할 수 있게 하는 기능구현이 최우선일듯하다.

이 기능만 구현하면 정말 version 2.0이라고 부를만 할듯하다.


해결한 일
1) JsonManager의 JsonParser는 커플링 감소를 위해 결과물을 이중리스트로 반환하도 변경
2) Schedule 클래스의 이름 목적에 더 맞는 TaskListManager로 변경 및 모든 커플링의 중심역할을 맡게 함
3) TaskListManager 클래스에 이중리스트 관리 기능인 loadTaskList(), makeTaskList(), saveTaskList(), appendList(), adjustImportance() 기능 구현
4) 저장로직을 종료 시그널 발생시 슬롯함수로 saveTaskList()를 호출하도록 만들어서 구현
5) Initializer 클래스의 이름 목적에 더 맞는 ProgramInfo로 변경 및 다른 클래스와의 커플링 제거
6) Initializer 클래스의 Init()함수의 기능 main함수에서 절차지향적으로 수행하도록 변경
7) 각 함수간 return과 참조에 RVO와 레퍼런스 적용
8) 필요없어진 ScheduleFactory 클래스 삭제 후 backup폴더에 저장

 

남은 할일

1) 메인함수 정리
2) 주석처리
3) const noexcept

 

2024-08-24

# 해결한 일
// 1) 메인함수 정리
// 2) 날짜 변경시 emit 로직 메인함수에 주석 작성한대로 변경하기
// 3) append인지 push_back인지 통일 : QList에서는 둘이 동일하게 작동한다, append는 좀 더 Qt 스러운것(오버로딩된게 더 있음), push_back은 좀 더 STL 스러운 것
// 4) 주석처리
// 5) 객체들 public private 정리
// 6) getter들 const

 

# 남은 할일

1) 알람 클래스 만들고 메인에서 선언까지만
2) loadTaskList에 기존내용, loadSpecificDate에 date기반, loadFixed에 고정일정기반 로딩 구현
함수 내용이 상당히 중복되므로 템플릿 메서드화 하고싶지만 qml과 integration되는 객체라 객체를 두개 연결해야 하는데 문제는 고정일정 관리는 정말 사용 빈도가 낮을테니까 괜히 객체 하나 늘어서 리소스만 더 잡아먹게 만드는 꼴이 될 수 있다. 이게 C++내에서 처리하는거면 게으른 초기화라도 할텐데 integration 시점에 이미 객체를 만들어야 하니까 차라리 함수 오버로딩해서 공통부분 private에 묶어두고 public에서 공통부분 함수 호출하는 식으로 구현하면 어떨까 싶다.
3) 고민 TaskListManager가 싱글턴이어야 할 이유가 있는가? 이제 커플링이 얘로 향하는데 근거가 없다.
4) noexcept의 필요성에 대한 고민
5) 리팩토링 글 재작성
 => 클래스 다이어그램 다시 그리고, 사고 및 고찰쪽에 적은것들도 추가하고
      카톡 나에게 보낸 톡에도 뭐 있는데 확인하고

# 배운것
Qt의 signal slot은 동기적이다. 그래서 아래처럼 구현할 수 있다.
일정 변경시 dateChanged() 시그널이 발생하면 연결된 save 슬롯 함수가 작동하고 save가 끝나고 나서야 날짜 정보가 갱신되며 갱신 후 readyToLoad() 시그널이 발생하면 연결된 load 슬롯 함수가 작동하는식으로 동기적으로 순차진행된다.

void Date::setDate(QDate q_date)
{
    emit dateChanged(); // 사용자가 날짜를 변경하면 발생 -> 일정파일을 저장하도록 saveTaskList() 호출
    
    year = q_date.year();
    month = q_date.month();
    day = q_date.day();
    day_of_week = q_date.dayOfWeek();
    
    emit readyToLoad(); // 날짜 갱신됐으니 이 날짜로 다시 불러오라고 시그널 발생시킴 -> loadTaskList() 호출
}

 

2024-08-26

# 해결한 일
// 1) 알람 클래스 만들고 메인에서 함수 생성만
// 2) TaskListManager가 날짜 변경에 의해 loadTaskList()를 호출하는 경우 외에도, 고정 일정파일을 관리하기 위해 load하는 경우에는 경로를 다르게 설정해야함
이에 대해 함수를 qml측에서 직접 호출할 수 있도록 loadTaskList()를 Q_INVOKABLE로 변경했고 매개변수에 default 값으로 ""를 두고 ""라면 기본적인 시그널슬롯호출이므로 날짜로 처리, ""가 아닌 경로가 들어있다면 고정 일정파일로 경로설정하도록 구현해봤었는데 문제가 발생함

슬롯으로 등록한 함수인데 매개변수가 있는거니까 함수포인터 만들어서 슬롯에 등록하게 변경하던지 람다함수로 등록하던지 해야하는데 직관적이지가 않아서 문제가 생겼음
그래서 차라리 함수를 분리하는게 맞다고 판단해서 아예 새로운 함수를 만들고 공통부분은 loadTaskList()라는 함수에 묶어두고 기존의 특정 날짜의 내용을 불러오는 함수는 loadSpecificTaskList()로 변경하고 loadFixedTaskList()라는 고정일정을 불러오는 함수를 따로 만들어서 공통부분을 호출하게 구현했음
// 3) TaskListManager.cpp 주석달기

# 남은 고민거리
- noexcept에 대한 필요성

- TaskListManager가 싱글턴이어야 할 이유가 있는가?

 

# 배운것
1) slot이면서 동시에 Q_INVOKABLE일 수 있다.
즉 내가 직접 호출도 가능하고, 시그널에 의해 호출도 가능하다

public slots:
    Q_INVOKABLE void loadTaskList();


2) Qt 5.x 버전까진 QtQuick 2.15가 최신 버전이었고, Qt 6.x로 넘어오면서 QtQuick이 6.x로 버전명이 바뀌게 된 것이다.

3) 드롭다운은 ComboBox 타입을 쓰면 된다.
아래 코드는 아직 테스트 안해봤으니 확인해봐야한다.

ComboBox {
        id: mainComboBox
        width: 150
        model: ["Option 1", "Option 2", "Option 3"]

        // 추가 콤보박스를 위한 Popup
        Popup {
            id: popup
            width: 150
            height: 100
            anchors.top: mainComboBox.bottom
            anchors.horizontalCenter: mainComboBox.horizontalCenter

            // 오른쪽 콤보박스
            ComboBox {
                id: secondaryComboBox
                width: 150
                model: ["Sub-option A", "Sub-option B", "Sub-option C"]
                anchors.horizontalCenter: parent.horizontalCenter

                onCurrentIndexChanged: {
                    console.log("Selected from secondary: " + currentText);
                }
            }
        }

        // 마우스가 콤보박스 위에 있을 때 Popup 표시
        MouseArea {
            anchors.fill: parent
            onEntered: {
                popup.open();  // 마우스가 콤보박스 위에 있을 때 Popup 열기
            }
            onExited: {
                popup.close();  // 마우스가 콤보박스를 떠날 때 Popup 닫기
            }
        }

 

# 남은 할일

1) 리팩토링 글 재작성

2) 아래 내용 고민

append에서 이동연산자 쓰는게 이득이지 않나?

void TaskListManager::appendList(QList<Qlist>& appended_list)
{
    for (auto& inner_list : appended_list) {
        task_list.push_back(std::move(inner_list)); // 이동 연산 수행
    }
}


# 클래스 관계
FileManager ..> Date
FileManager - JsonManager
FolderOpener는 독립적
Tutorial도 독립적
JsonManager - ProgramInfo 또한 JsonManager - TaskListManager (마름모 꼴)
TaskListManager -> Task
Alarm 클래스 독립적

 

내일은 위에 적어둔 남은 고민, 남은 할일 마무리하면 된다.