본문 바로가기
Study/객체지향 시스템 디자인 원칙

[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 2장. 코드를 작게 유지하기

by Nahwasa 2025. 7. 16.

목차

     

      이 글은 ['객체지향 시스템 디자인 원칙'을 읽으며 정리한 내 생각 - LLM과의 Q&A 기반 사고 확장] 글을 챕터별로 나눈 시리즈 중 하나 입니다. 전체 흐름을 보고 싶거나, 다른 챕터를 확인하고 싶으시다면 위 통합글을 참고해주세요.

     

      이 글은 책 내용을 요약하거나 정리한 글이 아닙니다. 책을 읽고 난 뒤 제 생각을 중심으로 정리한 글 입니다. '객체지향 시스템 디자인 원칙'을 읽으며, 느낀점이나 좋았던 점 및 책에 제시된 연습문제를 LLM에게 알려줬습니다. 이를 바탕으로 제 생각을 기반으로 책의 연습문제를 변형하거나, 새로운 질문을 생성하게 했습니다. 그 후 제가 그 질문에 답하고, LLM과 토론하는 방식을 사용해 사고를 확장해봤습니다. 이 글은 그 질문과 답변을 모아둔 글 입니다. 자세한 작성 배경은 통합글에서 확인하실 수 있습니다.

     

     Q1, Q2와 같은 질문 외의 모든 답변은 LLM을 쓰지 않고 제가 직접 답변한 내용입니다. 이후 LLM과 토론하며 생각이 조금씩 바뀐 부분들도 있으나, 최대한 초안 답변으로 올렸습니다.

     제 생각을 적은 것으로, 일반적인 개발자들의 생각이나 사실과 다소 다를 수 있습니다.

     

     


     

    2장. 코드를 작게 유지하기

    Q1. “작게 쪼개기”의 본질은 ‘책임 분리’인가, ‘가독성 확보’인가?

    • 세영은 “작게 나누는 건 책임을 나누는 행위”라고 봤어.
    • 그렇다면 함수/클래스를 나눌 때 1차 판단 기준은 '역할'이어야 할까, '코드 길이'일까?

    실무에서 ‘길긴 한데 잘 읽히는 함수’ vs ‘짧지만 의도가 중첩된 함수’가 있을 때, 어떤 기준으로 판단했어?

     

     

    위 LLM의 질문에서 '세영은 "작게 나누는 건 책임을 나누는 행위"라고 봤다고 한 이유

      책을 읽으며 중간에 느낀점이나 좋았던 점을 노션에 적어두고 이후 LLM에게 그대로 줬습니다. 이 글은 질문, 답변만 들어가도 너무 양이 많아져서 해당 내용은 제외했습니다. 그것도 있고, 질문 답변은 애초에 블로그에 포스팅할껄 상정하고 적은거라 좀 부드럽게 적었는데, 느낀점은 좀 쌔거나 문맥 무지성으로 적은 경우가 많아서 그런거 걸러내기도 귀찮아서요 ㅋㅋ 아무튼 아래의 내용때문에 LLM이 저렇게 질문한겁니다.

    • 저자는 작은 단위는 항상 큰 단위보다 낫다고 거의 확정적으로 말하고 있어. 물론 맞는 말이야. 근데 개인적으론 이런 생각도 해. 결국 함수나 클래스로 나뉜 부분은 각각의 책임을 나눠가졌다고 볼 수 있어. 함수가 isPrimeNumber(int num) 이렇게 생겼다고 해보자. 코드 중간에 이게 있다면, 당연히 소수 판별하는 함수겠거니 하고 넘어가게 돼. 근데 알고보니 개발자가 실수로 ‘1’도 true를 리턴하게 만들어둔거면, 버그가 날 수 있겠지. 즉, 저자의 말은 맞긴한데, 팀원들의 상향 평준화가 없다면 오히려 작은 단위가 더 버그를 만들수도 있다고 봐. 근데 이건 너무 억까 케이스인 것 같긴 하네 ㅋㅋ 어쨌든 내가 말하고 싶은건, 너무 단순하게 ‘작은 단위’가 낫다고만 말하는건 아쉬워. 추상화 수준이 서로 너무 다른 함수들이 모여있거나, 함수나 클래스로 빼는것도 일종의 책임의 분리라는걸 이해하지 못하고 단순히 ‘너무 기니까 분리한다’ 이렇게 생각할 수도 있다고 봐.
    • 뒤로 가니 비슷한 얘기가 나오긴 하네!

     

    내 답변

      1차 판단 기준은 역할이 맞아. 넓게 볼 때 ‘역할’은 결국 interface와 같은 추상화이기도 해. 캡슐화의 관점에서 볼 때도 역할 먼저 따지는게 좋지. ‘코드 길이’는 로직이 만들어지고 난 후의 리팩터링 대상이지 최초 설계의 대상은 아니라고 생각해. 개인적으로 코드 길이가 적당히 긴 것 까지는 크게 문제있다고 보지 않아서 더 그런 것 같네.

     

      알고리즘 짤 때는 의도적으로라도 ‘짧지만 의도가 중첩된 함수’ 형태로 많이 짜. 물론 알고리즘 문제도 객체지향 설계로 짜야 유연하게 짤 수 있는 문제들도 있어(예 : 백준 17081 - RPG Extreme). 근데 보통의 알고리즘 문제들은 절차지향적으로 짜도 문제 없고, 오히려 간편할 때도 많아. 즉, 객체들의 협력보다는 단순한 단위 기능에 대한 문제가 많아. 이 경우라면 사실 함수를 나누던 안나누던 한 모니터에 최대한 들어와야 내가 디버깅하기 좋긴 하더라고.

     

      반면에 실무 코드 짤 때는 길더라도 잘 읽히는 함수를 선호해. 함수나 변수명을 일종의 주석으로 생각하고, 심지어 이렇게 짤 때도 있어. if (num > limit) 대신, if (isNumOverLimit(num)) 이런식으로. 물론 이건 주로 조건문이 복잡할 때 저렇게 하긴 하지.

    ‘어떤 기준’에서 판단하냐고 한다면, 그냥 이후 유지보수 하는 사람이 나한테 최대한 안물어볼 수 있게 하는걸 기준으로 해. 결국 가독성이나 로직의 단순함이 더 중요하단거지.

     

     

    Q2. 좋은 주석의 조건?

    주석에 대한 견해가 계속 바뀌어왔는데, 그럼 지금의 세영은 “좋은 주석”이란 어떤 조건을 갖춰야 한다고 생각해?

    • 과거: Javadoc → 클린코드 영향 → 현재는 “그때그때 다르다”
    • 지금까지의 경험을 돌아볼 때,
      • 남이 쓴 주석 중 최고였던 것은 뭐였어?
      • 반대로 아예 주석이 없거나 거짓 주석 때문에 생긴 사고가 있었다면?

     

    내 답변

      설계에 정답이 없듯, 좋은 주석에도 정답은 없다고 생각해. 예를들어 일반적으로 좋은 테스트 코드는 아니지만, 특정 경우에만 동작하게 매크로 같이 통합 테스트를 해주는 테스트 코드를 최근에 짠 적이 있어. 하위 이용기관이 붙을 때 마다 그 테스트 코드에 외부 시스템 주소 바꾸고, 컨디션 풀어서 돌리면 모든 외부 API에 대해 하위 이용기관이 제대로 만들었는지 테스트해주는 구조야. 테스트 코드 관점에선 좋은게 아니지만, 실제 제작과정 및 이후 유지보수가 엄청 편해지는거지. 물론 외부로 별도 시스템으로 만들어도 되지만, 추가 관리 포인트를 만들고 싶지 않았어. 이 테스트 코드엔 내가 주석을 위 내용으로 장황하게 달아놨어. 즉, 이런 테스트코드를 짜게 된 명분에 대해 설명해두었어.

     

      주석도 마찬가지라고 봐. 사실 책마다 하는 얘기도 다르잖아? 물론 다들 예외케이스는 의례 붙이곤하지만, 저자들의 말투만 봐도 기본적으로 부정적인지 긍정적인지 정도는 판단 가능하지. 난 대학생때 연구 과제에서 프레임워크를 짤 때 해당 프레임워크를 사용하는 다른 학교나 업체분들이 IDE에 각 함수나 클래스 설명이 떠야하니 주석을 많이 썼어. 사회에서 개발자 시작하고 나선 클린코드 책 및 내 취향 변화로 주석을 잘 안썼어. 지금은 사실 써야한다, 말아야한다 이런 내 주관 자체가 없어. 왜냐면 그냥 써서 이득이 클 것 같으면 쓰고, 아닌 것 같으면 안쓰면 된다고 보거든. 그래서 ‘좋은 주석’의 조건은 현재 상황에 어울리는 주석인가? 라고 생각해.

     

      만약 라이브러리를 만든다고 해볼께. 이 땐 모든 클래스 및 함수마다 주석을 상세히 다는게 맞아. 심지어 클린코드 등의 책에 있는, 주석에 html 태그같은거 쓰지 말라는 것도 무시해도 된다고 봐. 솔직히 태그까지 달아서 써야 라이브러리 사용하는 측에서 IDE에서 해당 함수 툴팁볼 때 훨씬 가독성 좋게 보이거든.

     

      반면에 뭐 세그먼트 트리 자료구조를 프로젝트에 넣어야된다고 해볼께. 물론 세그먼트 트리는 복잡하고, 로직적으로 설명해줘야 할게 많아. 근데 클래스명이 SegmentTree 라면, 굳이 상세하게 주석을 달아야 할까? 달더라도 ‘세그먼트 트리 자료구조 이해 필요’ 정도만 달아도 되겠지.

     

      남이 쓴 주석 중 최고였던 건 약간 밈 짤로 본건데, 예전 게임 개발자가 작성해다는 다음 주석이야. “Somehow things have been miraculously working, I’m not shure why”, “This is horrible. I’m not allowed to change the save structure, because we’re too close to final.” 정말 신뢰성 넘치고 진심이 담긴 좋은 주석이라 생각해 ㅋㅋ

     

      장난이고, 애초에 내가 유지보수 경험이 거의 없고 대부분 내가 만든걸 내가 유지보수한 경우가 많아. 그래서 남이 쓴 주석 중 최고였던 것이라고 해봐야 일하면서 다른 직원이 쓴 것 보다는, 라이브러리의 설명쪽이 많았다고 봐. 그 중에 이게 아닐까 싶네. 자바 ArrayDeque 주석에 써 있는 “This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.” 이거. 이거보고 알고리즘 짤 때 ArrayDeque를 많이 썼었는데, 실제로도 더 빨랐어.

     

      아예 주석이 없거나 거짓 주석때문에 생긴 사고는 사실 없어. 직전에도 말했듯 유지보수 경험이 많지 않기도 하고, 애초에 코드도 잘 안믿는데 주석은 더 안믿긴해. 물론 라이브러리는 기본적으로 믿고 보긴한데, 실무 코드는 기본적으로 안믿는 스탠스로 가거든.

     

     

    Q3. 세영은 리팩터링 전에 “전체를 한눈에 보기” 위해 어떤 방법을 사용해?

    • 단순히 호출 순서만 따라가?
    • 아니면 흐름도 / 시나리오 / 역할 분리 등을 따로 그리거나 정리해?
    • “지금 이게 왜 복잡한 거지?”를 판단할 때 참고하는 세영만의 단서 리스트가 있어?

    예전 실무에서 복잡한 코드를 리팩터링한 경험 중, 이해나 설계를 돕기 위해 만든 너만의 기준이나 방식이 있다면 알려줘.

     

    내 답변

      리팩터링 전에 전체를 한눈에 보기에 대해 대답해보자면, 반대로 리팩터링을 어느 수준에서 할거냐에 따라 전체를 한눈에 보려고 하지 않는 경우가 많아. 난 코드 뿐 아니라 업무적으로 결합도를 줄이는 방향을 좋아해. 그때그때 내가 리팩터링 하려는 추상화 수준에 따라 어느 수준부터 볼지도 정해져. 프로젝트 자체의 아키텍처를 리팩터링 하려고 한다면, 프론트엔드, 백엔드, 인프라, 외부시스템, … 이런 큰 단위로 봐야겠지. 백엔드 패키지 구조를 리팩터링 하려고 한다면 백엔드의 각 모듈들을 보게 될거야. 뭐 기능이 추가될 부분에 대해 디자인 패턴을 넣어 OCP를 만족하게 해주도록 리팩터링 할 수도 있겠지. 아주 작게는 클래스명이나 함수명을 리팩터링 하려 할 수도 있겠지. 에를들어 난 AbstractChargeService, ChargeService, … 이런 식으로 이름에 기술적인 부분이 들어가는걸 별로 안좋아해. 그러니 ChargeService, RegularChargeService 이런식으로 바꾸겠지.

     

      그래서 단순히 호출 순서만 따라가? → 호출 순서 보다는, 서로 다른 역할이나 책임을 가진 단위로 봤을 때 그 책임을 잘 수행하는지 위주로 봐. 즉, 책임과 경계 위주로 봐.

     

      흐름도 / 시나리오 / 역할 분리 등을 따로 그리거나 정리해? → 응. 실제로도 난 코드 짜는 시간은 전체 개발 시간의 10%도 안되는 것 같아. 전체적인 구조 먼저 머리속이나 문서로 정리하고, 코드는 맨 마지막에 작성해. 클래스 다이어그램, 시퀀스 다이어그램을 특히 자주 그리는 편이야.

     

      ‘지금 이게 왜 복잡한 거지?’에 대한 단서 리스트 → 이 부분은 좀 직관에 의존하는 것 같아. 그냥 ‘이후 나한테 최대한 연락이 안오도록’을 생각하며 짜는 것 같아. 또, 당시 내 지식 수준에 따라서도 매번 달라지는거라 더더욱 그런 것 같네. 어쨌든 기본적으로 SOLID 중 OCP를 가장 중요하게 생각해. 즉, 이후 요구사항이 들어왔을 때 기존 코드를 얼마나 안건드리고 기능을 추가할 수 있냐이지. 그렇다고 너무 오버 엔지니어링이 되면 안되니까, 그런 요구사항이 나올만한 부분 위주로 처리해둬. 그러니 내 생각에 여기에 기능이 추가된다고 했을 때 유연하게 유지보수가 가능하냐를 직관적으로 판단한다가 내 기준이겠네.

     

      실무에서 경험한 복잡한 코드 리팩터링에 대해 이해나 설계를 돕기 위해 만든 너만의 기준이나 방식 → 사실 ‘복잡한 코드 리팩터링’ 이라고 한다면, 전체 구조에 대한 변경이라 생각해. 예를들어 기존 방식을 이벤트 메시지 기반 방식으로 바꾼다고 해볼께. 이럴 때 난 논리적으로 문제 없을 때 까지 코드보다는 내 상상 혹은 다이어그램을 기준으로 먼저 생각해. 문제가 없다고 판단되는 방법을 찾아야 코드 수정으로 들어가.

     

     

    Q4. 만약 세영이 ‘2장을 모르는 주니어’에게 설득해야 한다면…

    "왜 코드를 작게 유지해야 하는지"를 단순한 은유나 비유로 설명해줘.

    • 함수 = 마법 주문? / 클래스 = 부대 단위? / 코드 = 레시피?
    • 또는 본인이 겪었던 “와 이건 너무 길어서 못보겠다…” 했던 코드 사례를 차용해도 좋아.

    주니어가 "길긴 하지만 그냥 다 한눈에 보면 되는 거 아니에요?"라고 묻는다면 뭐라고 대답할래?

     

    내 답변

      나 알고리즘 짤때 절차지향적으로 짜둔 코드가 많아서 ㅋㅋㅋ 그거 보여주면서 어떤 코드가 더 좋아보이냐고 물어보는 식으로 할듯.

    private void solution() throws Exception {
            n = Integer.parseInt(br.readLine());
            dp = new int[n+1][3];
            v = new boolean[n+1];
            edges = new List[n+1];
            for (int i = 0; i <= n; i++) edges[i] = new ArrayList<>();
            for (int i = 1; i < n; i++) {
                StringTokenizer st = new StringTokenizer(br.readLine());
                int a = Integer.parseInt(st.nextToken());
                int b = Integer.parseInt(st.nextToken());
                edges[a].add(b);
                edges[b].add(a);
            }
    
            arr = new int[n+1][3];
            for (int i = 1; i <= n; i++) {
                StringTokenizer st = new StringTokenizer(br.readLine());
                arr[i][0] = Integer.parseInt(st.nextToken());
                arr[i][1] = Integer.parseInt(st.nextToken());
                arr[i][2] = Integer.parseInt(st.nextToken());
            }
            
            int ans = 0;
            edges[0].add(1);
            for (int i = 0; i < 3; i++) ans = max(ans, find(0, i));
    
            char[] selected = new char[n+1];
            Stack<int[]> stk = new Stack<>();
            for (int i = 0; i < 3; i++) {
                if (dp[0][i] == ans) {
                    stk.push(new int[]{0, i});
                    break;
                }
            }
    
            while (!stk.isEmpty()) {
                int[] cur = stk.pop();
                int idx = cur[0];
                int color = cur[1];
                selected[idx] = COLOR[color];
    
                for (int next : edges[idx]) {
                    if (v[next]) continue;
                    v[next] = true;
    
                    int nextColor = -1;
                    int nextMax = 0;
                    for (int i = 0; i < 3; i++) {
                        if (color == i) continue;
    
                        if (nextMax < dp[next][i]) {
                            nextMax = dp[next][i];
                            nextColor = i;
                        }
                    }
    
                    stk.push(new int[]{next, nextColor});
                }
            }
    
            StringBuilder sb = new StringBuilder();
            sb.append(ans).append('\\n');
            for (int i = 1; i <= n; i++) sb.append(selected[i]);
            System.out.println(sb);
        }

     

    “위 코드로 보는게 좋아? 아니면 아래처럼 보는게 좋아?”

    private void solution() throws Exception {
            parseInput();
            init();
            findMaximumAnswer();
            trackingColor();
            printAnswer();
        }

     

      물론 저것도 절차지향적이긴하지만, 그렇다고 해도 코드 이해 측면에서 차이가 확 날듯.

     

      길긴 하지만 그냥 다 한눈에 보면 되는 거 아니에요? → 솔직히 말해서 나도 그정도면 괜찮다고 보긴 해서 굳이 설득해야되나 싶네. 코드 길이가 중요한게 아니라, 클래스나 함수의 책임과 역할을 명확히 하는게 중요한거라고 봐. 물론 책임이 응집되어 있더라도 너무 길다면 그땐 기능단위로도 좀 빼거나 하겠지. 어쨌든 ‘이정도면 괜찮다’ 정도면 된다고 봐. 한 눈에 다 보여도 무슨 역할인지 모르겠다면 잘못짠거고, 한 눈에 다 안보일만큼 꽤 길어도 무슨 역할인지 명확하게 파악될 정도로 잘 짰다면 난 그걸 더 높게 평가해.

    댓글