나의개발일지

Clean Code) PrintPrimes 리펙터링 본문

잡다한 이야기

Clean Code) PrintPrimes 리펙터링

아. 이렇게 하면 될거 같은데.. 2025. 7. 2. 22:27
728x90



📌 PrintPrimes

오늘은 클린코드 책에서 나온 재미있는 예제를 분석해 볼 예정이다. PrintPrimes 라는 Literate Programming에 나오는 예제를 자바 버전으로 변경한 예제이다.

 

먼저 코드롤 살펴보자.

package literatePrimes;

public class PrintPrimes {
    public static void main(String[] args) {
        final int M = 1000;
        final int RR = 50;
        final int CC = 4;
        final int WW = 10;
        final int ORDMAX = 30;
        int P[] = new int[M + 1];
        int PAGENUMBER;
        int PAGEOFFSET;
        int ROWOFFSET;
        int C;
        int J;
        int K;
        boolean JPRIME;
        int ORD;
        int SQUARE;
        int N;
        int MULT[] = new int[ORDMAX + 1];
        J = 1;
        K = 1;
        P[1] = 2;
        ORD = 2;
        SQUARE = 9;

        while (K < M) {
            do {
                J = J + 2;
                if (J == SQUARE) {
                    ORD = ORD + 1;
                    SQUARE = P[ORD] * P[ORD];
                    MULT[ORD - 1] = J;
                }
                N = 2;
                JPRIME = true;
                while (N < ORD && JPRIME) {
                    while (MULT[N] < J)
                        MULT[N] = MULT[N] + P[N] + P[N];
                    if (MULT[N] == J)
                        JPRIME = false;
                    N = N + 1;
                }
            } while (!JPRIME);
            K = K + 1;
            P[K] = J;
        }
        {
            PAGENUMBER = 1;
            PAGEOFFSET = 1;
            while (PAGEOFFSET <= M) {
                System.out.println("The First " + M + " Prime Numbers --- Page " + PAGENUMBER);
                System.out.println();
                for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++) {
                    for (C = 0; C < CC; C++)
                        if (ROWOFFSET + C * RR <= M)
                            System.out.format("%10d", P[ROWOFFSET + C * RR]);
                    System.out.println();
                }
                System.out.println("\f");
                PAGENUMBER = PAGENUMBER + 1;
                PAGEOFFSET = PAGEOFFSET + RR * CC;
            }
        }
    }
}

 

이 코드 예제는 에라테네스채 알고리즘을 활용하여 소수를 판별하여 출력해주는 프로그램이다. 하지만 소스코드가 한 클래스에 묶여 있고, 변수명과 많은 반복문 들여쓰기로 인해 코드를 알아보기가 쉽지 않다. 이를 클린 코드 책에서 나오는 리펙터링 된 코드를 보면서 왜 이렇게 리펙터링 했는지 알아볼 것이다.

 


📌 PrintPrimes 리펙터링 

SOLID원칙의 단일책임의 원칙을 위배하고 있는 기존 코드에서, 각각의 책임과 역할을 나누어 프로그램을 실행하는 main이 있는 PrimePrinter클래스, 소수를 생성하는 PrimeGenerator 클래스, 생성된 소수를 출력하는 RowColumnPagePrinter 클래스로 나누었다.

 

이렇게 클래스를 나누게 되면 각 클래스의 역할이 확실하게 구분되며 소수를 생성하고 -> 출력한다는 워크플로우가 PrimePrinter 클래스만 봐도 쉽게 확인할 수 있다.

package literatePrimes;

public class PrimePrinter {
    public static void main(String[] args) {
        final int NUMBER_OF_PRIMES = 1000;
        int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);

        final int ROWS_PER_PAGE = 50;
        final int COLUMNS_PER_PAGE = 4;
        RowColumnPagePrinter tablePrinter = new RowColumnPagePrinter(ROWS_PER_PAGE, COLUMNS_PER_PAGE,
                "The First " + NUMBER_OF_PRIMES + " Prime Numbers");
        tablePrinter.print(primes);
    }
}

 

 

소수를 판별하는 PrimeGenerator 클래스를 확인해보자. 이 클래스의 기본적인 동작은 다음과 같다.

 

  • 2는 소수로 고정하고 시작
  • 이후 홀수 후보들만 검사
  • 이미 발견된 소수를 이용해서, 합성수(소수가 아닌 수)를 배제
  • 특정 소수의 곱의 최소 배수를 관리하면서 중복 계산을 피함

 

 

구조를 살펴보면 기존 코드대비 추상화의 깊이가 잘 설정되어 있고 top-down 방식으로 코드를 내려가면서 읽을 수 있게 잘 리펙터링 되었다.

package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
    private static int[] primes;
    private static ArrayList<Integer> multiplesOfPrimeFactors;

    protected static int[] generate(int n) {
        primes = new int[n];
        multiplesOfPrimeFactors = new ArrayList<Integer>();
        set2AsFirstPrime();
        checkOddNumbersForSubsequentPrimes();
        return primes;
    }

    private static void set2AsFirstPrime() {
        primes[0] = 2;
        multiplesOfPrimeFactors.add(2);
    }

    private static void checkOddNumbersForSubsequentPrimes() {
        int primeIndex = 1;
        for(int candidate = 3; primeIndex < primes.length; candidate += 2) {
            if(isPrime(candidate))
                primes[primeIndex++] = candidate;
        }
    }

    private static boolean isPrime(int candidate) {
        if(isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
            multiplesOfPrimeFactors.add(candidate);
            return false;
        }
        return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
    }

    private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
        int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
        int leastRelevanMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
        return candidate == leastRelevanMultiple;
    }

    private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
        for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
            if(isMultipleOfNthPrimeFactor(candidate, n))
                return false;
        }
        return true;
    }

    private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) {
        return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
    }

    private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
        int multiple = multiplesOfPrimeFactors.get(n);
        while(multiple < candidate)
            multiple += 2 * primes[n];
        multiplesOfPrimeFactors.set(n, multiple);
        return multiple;
    }
}

 

 

출력 클래스인 RowColmnPagePrinter클래스도 마찬가지이다. 추상화 수준과, top-down방식, 변수명 등을 신경쓰면서 리펙터링을 진행하였다.

package literatePrimes;

import java.io.PrintStream;

public class RowColumnPagePrinter {
    private int rowsPerPage;
    private int columnsPerPage;
    private int numbersPerPage;
    private String pageHeader;
    private PrintStream printStream;

    public RowColumnPagePrinter(int rowsPerPage, int columnsPerPage, String pageHeader) {
        this.rowsPerPage = rowsPerPage;
        this.columnsPerPage = columnsPerPage;
        this.pageHeader = pageHeader;
        numbersPerPage = rowsPerPage * columnsPerPage;
        printStream = System.out;
    }

    public void print(int[] data) {
        int pageNumber = 1;
        for (int firstIndexOnPage = 0; firstIndexOnPage < data.length; firstIndexOnPage += numbersPerPage) {
            int lastIndexOnPage = Math.min(firstIndexOnPage + numbersPerPage - 1, data.length - 1);
            printPageHeader(pageHeader, pageNumber);
            printPage(firstIndexOnPage, lastIndexOnPage, data);
            printStream.println("\f");
            pageNumber++;
        }
    }

    private void printPage(int firstIndexOnPage, int lastIndexOnPage, int[] data) {
        int firstIndexOfLastRowOnPage = firstIndexOnPage + rowsPerPage - 1;
        for (int firstIndexInRow = firstIndexOnPage; firstIndexInRow <= firstIndexOfLastRowOnPage; firstIndexInRow++) {
            printRow(firstIndexInRow, lastIndexOnPage, data);
            printStream.println("");
        }
    }

    private void printRow(int firstIndexInRow, int lastIndexOnPage, int[] data) {
        for (int column = 0; column < columnsPerPage; column++) {
            int index = firstIndexInRow + column * rowsPerPage;
            if (index <= lastIndexOnPage)
                printStream.format("%10d", data[index]);
        }
    }

    private void printPageHeader(String pageHeader, int pageNumber) {
        printStream.println(pageHeader + " ---Page " + pageNumber);
        printStream.println("");
    }

    public void setOutput(PrintStream printStream) {
        this.printStream = printStream;
    }
}

 

리펙터링된 코드들을 보면 기존의 코드보다 코드양이 상당히 늘어났다. 하지만 리펙터링을 함으로써 여러 이점을 가져가게 된다.

  • 코드의 가독성이 좋아진다.
  • 단일책임의 원칙을 지킴으로 만약 소수판별 알고리즘이 변경되면 PrimeGenerator클래스만 변경하면 된다.
  • 각 기능이 분리되어 테스트하기가 용이하다.

🤔 느낀점

이번 PrintPrimes 예제를 클론코딩 해보면서 무엇이 좋은 코드인지 많이 생각해본것 같다. 내가 보통 알고리즘문제를 풀면 PrintPrimes예제처럼 코딩을 해왔던것 같다. 이것은 잘못된 방식인것을 알게 되었고, 이제 리펙터링을 하는 습관을 들여야겠다.

 

 

 

728x90
반응형

'잡다한 이야기' 카테고리의 다른 글

Gmail 2단계 인증 및 앱 비밀번호 생성하기  (0) 2024.01.07