컨텐츠 검색
클래스 분리 연습: 영화 데이터를 관리하는 프로그램 구현하기

2025. 12. 19. 20:32알고리즘

클래스 분리가 여전히 어려워서, 먼저 풀어본 뒤
“나는 어떻게 접근했고, AI는 어떤 사고 흐름으로 설계했는지”를 비교했다.

  • 1-1, 2-1, 3-1 … : 내가 접근한 방식
  • 1-2, 2-2, 3-2 … : AI가 접근한 방식

1단계: 요구사항 정리 (Problem Definition)

1-1. 내가 한 문제 재정의 (기능 목록 명확화)

  • 제목과 평점이 들어있는 영화 목록을 출력한다.
  • 제목으로 영화를 검색하여 제목과 평점을 출력한다.
  • 평점 기준 내림차순 정렬된 목록을 출력한다.
  • 입력 받은 평점 이상인 영화만 제목과 평점을 출력한다.

1-2. AI 방식: 요구사항을 변하지 않는 것 / 변하는 것으로 분해

가장 먼저 던지는 질문:

앞으로 무엇이 늘어나거나 바뀔까?

바뀌지 않는 것은 한 클래스에 둬도 되고,
바뀌는 것은 분리 대상이다.

  • 변하지 않는 것: 영화 데이터(제목, 평점), 목록 출력 기능, 제목 검색 기능
  • 변하는 것: 목록을 처리하는 방식(정렬, 필터, 앞으로 추가될 기능)

→ 이 단계에서 Strategy / Factory / State 후보가 자연스럽게 떠오른다.


2단계: 의사코드 vs 데이터 구조

2-1. 내가 쓴 의사코드 (기능 나열 형태): 작성하고 보니 문제 재정의와 거의 동일해졌음.

  • 영화 목록을 출력한다.
    • 영화 목록 출력 시, 영화 수만큼 제목과 평점을 출력한다.
  • 영화를 검색한다.
    • 영화 제목과 입력값이 일치하면 해당 영화의 제목과 평점을 출력한다.
  • 평점 기준으로 내림차순 정렬된 영화 목록을 출력한다.
  • 입력받은 평점 이상인 영화만 출력한다.

2-2. AI 방식: 기능을 보고 자료구조를 선택한다.

  • 목록 출력/정렬/필터 → 전체 순회가 기본 → vector<Movie>
  • 제목으로 검색 → 키 기반 탐색 필요 → map<title, rating> (또는 unordered_map)

3단계

3-1. 내가 한 데이터 추출

  • 제목(string) 타입
  • 평점(double) 타입

3-2. AI 방식: “조건문 폭발”을 사전에 감지

  • 정렬/필터/추가 기능을 Manager 안에 넣기 시작하면 보통 이런 형태가 된다:
if (mode == SORT) ...
else if (mode == FILTER) ...
else if (mode == ...)

이게 보이는 순간:

  • 기능 추가 때마다 관리 클래스 수정
  • 유지보수 비용 상승

결론: 관리 클래스는 실행만 하고, 어떻게 처리하는지는 외부로 위임한다.


4단계

4-1. 내가 한 함수 추출

  • 특정 평점 이상 영화 필터링
  • 영화 목록 출력
  • 영화 제목으로 검색
  • 영화 평점 기준 정렬

4-2. AI 방식: “어떻게”를 늦추고 “무엇”부터 정의

먼저 인터페이스(규약)를 만든다. 질문은 이거:

  • 정렬이든 필터든 공통은?
    → “영화 목록을 받아서 무언가 처리한다.”
  • 그래서 동사 하나로 요약:
    • process
  • 이게 추상화의 중심이 된다:
    • process(vector<Movie>& movies)

5단계: 클래스 분리에서 막히는 지점 (SRP 관점 추가)

5-1. 내가 고민했던 분리 (기능별 Utility 분해)

처음엔 아래처럼 기능별 클래스로 나누려 했고,
그제야 “각 기능에 어떤 데이터가 필요한지”를 생각하게 됨.

  • Movie: 제목/평점
  • MovieSearch: 제목 필요
  • MovieSort: 정렬
  • MoviePrint: 제목/평점 필요
  • MovieFilter: 복사 후 필터
  • MovieManager: 위 로직을 모아 일반화
// 생각하는 int main의 호출 형태:
int main() {
MovieManager* movieManager;
movieManager->moviePrint();

movieManager->movieSearch("title");

movieManager->movieSort();
movieManager->moviePrint();

movieManager->movieFilterPrint(rating);

return 0;
}

하지만 이 분리를 끝까지 밀면 방향이 헷갈리고 시간 소모가 커져 중단함.

5-2. AI 방식: “책임(SRP)”로 경계를 긋는다

질문:

이 클래스가 바뀌는 이유가 두 개 이상인가?

  • YES → 분리
  • NO → 유지

여기서 보통 갈라지는 책임:

  • 데이터 관리
  • 처리 로직
  • 생성 책임
  • 흐름 제어

이 문제에 적용하면:

  • MovieManager는 “데이터 소유 + 기본 기능(출력/검색)”까지만 들고
  • “정렬/필터 같은 처리 로직”은 Processor로 넘기는 게 자연스럽다.

6단계: “교체 가능해야 하나?” → Strategy 확정

AI가 던지는 체크 질문:

  • 런타임 교체 ⭕ → Strategy / State
  • 생성만 다름 ⭕ → Factory
  • 호출을 객체화 ⭕ → Command
  • 흐름 일부 단계만 다름 ⭕ → Template Method

이 문제는:

  • 정렬도, 필터도 런타임에 바꿔 끼워서 실행한다
    → Strategy가 딱 맞음

그래서 구체 전략 매핑:

  • 평점 내림차순 정렬 → RatingSorter : MovieProcessor
  • 특정 평점 이상 필터 → RatingFilter : MovieProcessor

7단계: main은 “전략 선택”만 한다

  • main이 어떤 전략을 쓸지 결정
  • manager는 그 전략을 실행만 함

전체 설계 관점:

  • 데이터/기본 기능: Manager
  • 처리 로직: Processor들
  • 선택/조합: main

패턴은 “이름 붙이기” 단계

여기서 중요한 순서:

  1. 먼저 구조가 잡힘
  2. 그 다음 “아, 이거 Strategy네”라고 이름이 붙음

반대로 “Strategy를 써야지”부터 시작하면 설계가 어색해질 수 있음.


구현 코드

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

/*
 [1] Movie
 - 문제에서 다루는 최소 단위 데이터
 - "영화"라는 개념을 제목(title)과 평점(rating)으로 단순화
 - 값 자체가 의미이므로 struct 사용
*/
struct Movie {
    string title;
    double rating;
};

/*
 [2] MovieProcessor (추상화된 처리 전략)
 - 영화 목록을 "어떻게 처리할지"에 대한 공통 규약
 - MovieManager는 구체적인 처리 방식(정렬/필터 등)을 몰라도 됨
 - process 하나만 강제 → 다형성 기반 확장
*/
class MovieProcessor {
public:
    virtual void process(vector<Movie>& movies) = 0;
    virtual ~MovieProcessor() = default;
};

/*
 [3] MovieManager (데이터 소유자 / 컨텍스트)
 - 영화 데이터를 직접 관리하는 클래스
 - 무엇을 할지는 알고 있지만, 어떻게 처리할지는 모름
 - 처리 로직은 MovieProcessor에게 위임
*/
class MovieManager {
private:
    // 영화 목록 (순회, 정렬, 필터의 대상)
    vector<Movie> movies;

    // 제목 기반 빠른 검색을 위한 인덱스
    map<string, double> movieMap;

public:
    // 초기 데이터 구성 책임
    MovieManager() {
        movies = {
            {"Inception", 9.0},
            {"Interstellar", 8.6},
            {"The Dark Knight", 9.1},
            {"Memento", 8.4}
        };

        // 검색을 빠르게 하기 위해 보조 자료구조 구성
        for (const auto& movie : movies) {
            movieMap[movie.title] = movie.rating;
        }
    }

    // 전체 영화 목록 출력 (가장 기본 기능)
    void printMovies() {
        cout << "영화 목록:\n";
        for (const auto& movie : movies) {
            cout << "제목: " << movie.title
                << ", 평점: " << movie.rating << "\n";
        }
    }

    // 제목으로 영화 검색 (정렬/필터와 무관한 기능)
    void findMovie(const string& title) {
        auto it = movieMap.find(title);
        if (it != movieMap.end()) {
            cout << "영화 제목: " << it->first
                << ", 평점: " << it->second << "\n";
        }
        else {
            cout << "해당 영화는 목록에 없습니다.\n";
        }
    }

    /*
     [핵심 확장 포인트]
     - 어떤 Processor가 들어오든 상관없이 실행만 담당
     - MovieManager는 알고리즘 변경에 닫혀 있음(OCP)
    */
    void processMovies(MovieProcessor& processor) {
        processor.process(movies);
    }
};

/*
 [4] RatingSorter (정렬 전략)
 - "영화를 평점 기준으로 정렬한다"는 요구사항을 담당
 - MovieProcessor 인터페이스를 구현하여 전략으로 동작
*/
class RatingSorter : public MovieProcessor {
public:
    void process(vector<Movie>& movies) override {
        // 정렬 규칙을 외부로 드러내기 위해 static 비교 함수 사용
        sort(movies.begin(), movies.end(), compareMovies);

        cout << "평점 기준 정렬된 영화 목록:\n";
        for (const auto& movie : movies) {
            cout << "제목: " << movie.title
                << ", 평점: " << movie.rating << "\n";
        }
    }

    // sort에 전달될 비교 규칙 (내림차순)
    static bool compareMovies(const Movie& a, const Movie& b) {
        return a.rating > b.rating;
    }
};

/*
 [5] RatingFilter (필터 전략)
 - "특정 평점 이상만 출력"이라는 또 다른 처리 요구 담당
 - 내부 상태(minRating)를 가지는 전략
*/
class RatingFilter : public MovieProcessor {
private:
    double minRating;

public:
    explicit RatingFilter(double minRating)
        : minRating(minRating) {
    }

    void process(vector<Movie>& movies) override {
        cout << "평점 " << minRating << " 이상인 영화 목록:\n";
        for (const auto& movie : movies) {
            if (movie.rating >= minRating) {
                cout << "제목: " << movie.title
                    << ", 평점: " << movie.rating << "\n";
            }
        }
    }
};

/*
 [6] 실행 흐름
 - MovieManager는 데이터 관리
 - Processor들은 처리 방식만 담당
 - 실행 시점에 어떤 전략을 쓸지 선택
*/
int main() {
    MovieManager manager;

    cout << "1. 영화 목록 출력\n";
    manager.printMovies();

    cout << "\n2. 영화 검색 (예: Interstellar)\n";
    manager.findMovie("Interstellar");

    cout << "\n3. 평점 기준 정렬 및 출력\n";
    RatingSorter sorter;
    manager.processMovies(sorter);

    cout << "\n4. 평점 8.5 이상인 영화 필터링 및 출력\n";
    RatingFilter filter(8.5);
    manager.processMovies(filter);

    return 0;
}


/* 결과
1. 영화 목록 출력
영화 목록:
제목: Inception, 평점: 9
제목: Interstellar, 평점: 8.6
제목: The Dark Knight, 평점: 9.1
제목: Memento, 평점: 8.4

2. 영화 검색 (예: Interstellar)
영화 제목: Interstellar, 평점: 8.6

3. 평점 기준 정렬 및 출력
평점 기준 정렬된 영화 목록:
제목: The Dark Knight, 평점: 9.1
제목: Inception, 평점: 9
제목: Interstellar, 평점: 8.6
제목: Memento, 평점: 8.4

4. 평점 8.5 이상인 영화 필터링 및 출력
평점 8.5 이상인 영화 목록:
제목: The Dark Knight, 평점: 9.1
제목: Inception, 평점: 9
제목: Interstellar, 평점: 8.6

*/

패턴 감지용 표

문제 유형 자연스럽게 나오는 패턴
처리 방식 다양 Strategy
생성 방식 다양 Factory
상태별 행동 변화 State
실행 취소 / 매크로 Command
공통 흐름 + 일부 단계만 다름 Template Method
데이터 변경 알림 Observer

실무 기준 “언제 분리하나”

  • SOLID는 목표가 아니라 방향이다.
  • 규모·변경 가능성에 비해 과하면 오히려 나쁜 코드이다.

아래 중 2개 이상이면 분리:

  • 같은 클래스가 자주 수정된다
  • 기능 추가할 때 if/else가 늘어난다
  • 테스트하기 어렵다
  • 협업 중 충돌이 잦다
  • “이 클래스 왜 이렇게 큼?”이 떠오른다