자바스크립트를 활성화 해주세요

Clean Code 10장: "클래스" 정리

 ·  ☕ 11 min read

10장 클래스를 읽으면서 내용에 대해 보충하거나, 개인적인 의견으로 반박하거나, 고민해 볼 부분에 대해 적어보려 한다.

클래스 체계

자바의 표준 코딩 컨벤션에 따르면 클래스 작성 시 배치 순서가 존재한다. 요약하면 다음과 같다.

  1. static 상수/변수
  2. 인스턴스 변수
  3. 생성자
  4. 메서드

같은 조건에서는 (ex. static 상수/변수가 여럿일 때) public, protected, 패키지(접근 한정자 없음), private 순으로 배치한다.

메서드의 경우 모든 public 메서드 정의 이후 protected/private 메서드를 정의하는 방식이 아니라, 현재 정의한 public 메서드에서 내부적으로 호출한 다른 접근자의(protected/private 등) 메서드를 순서대로 정의하는 방식이다.

캡슐화

우리는 기본적으로 변수와 내부적으로 사용되는 유용한 함수들(utility functions)을 private하게 선언하는 것을 선호한다. 하지만 테스팅 과정에서 변수들이나 유용한 함수들을 직접 접근해야 해서, protected로 변경해야 하는 경우가 존재한다. (최대한 private 상태를 유지할 방법을 고민해보고, 더 이상 좋은 방법이 없을때만 변경하는 식으로 해야 한다.)

매우 이상한 내용이라 생각한다. (내가 이런 상황을 경험하지 못한 것이 크겠지만, 일종의 객체지향의 기본 원칙을 뒤집는듯한 주장처럼 들려서 더더욱 반발하고 있다.)

  1. protected의 목적은 상속 시 자식 클래스가 접근하기 위함이다.
  2. 원래 private해야 하나, 상속 과정에서 자식 클래스(구체화 클래스)가 접근할 수 있으면서도 public하지 않게 하기 위함이다.
  3. 캡슐화의 목적 중 하나가 허가되지 않은 방식(public 메서드 호출 외의 방식)으로 객체의 상태를 변경하는 것을 막는 것으로 알고 있다.
  4. 이런 목적으로 수행된 캡슐화를 테스트 때문에 접근자를 변경한다?

접근자 변경의 문제는 다음과 같이 고려되어야 한다고 생각한다.

  1. 멤버 변수의 경우, 상태 값을 가져오는 것도 허가된 방식(public 메서드 호출)으로 기대하는 상태를 확인해야 한다.
    내부적으로 관리하는 변수라서 private하게 선언되었다면, 이 값은 테스트를 통해 검증할 값이 아니다.
    (원칙적으로 공개하지 않는 값인데, 굳이 확인을 하겠다는 것은 객체의 설계를 무시하겠다는 것이다.)
  2. 해당 객체의 상태가 메서드 호출 순서 등에 따라 바뀌는 등, 객체의 속성이 복잡한 경우(ex. 원형 버퍼/큐 등) 내부 변수를 통해 유효성을 쉽게 확인하고 싶을 것이다.
    하지만 이런 경우는 확인하고자 하는 경우에 대한 시나리오를 만들어서 테스트를 수행하는 방식으로 검증하는 것이 옳다고 생각한다.
  3. 해당 객체의 상태가 비결정적일 경우(해당 객체/프로그램 외부 상황에 의해 결과가 변경), 이런 검증은 단위 테스트의 범위를 넘어갈 가능성이 높다. (ex. DB 연결, 네트워크 통신 등)
    외부 상황에 따른 내부적인 논리만 확인하려 할 경우, Mock 기법으로 외부 상황을 통제한 채 테스트한다.
    만약 외부 상황을 고려하는 테스트라면, 이것은 단위테스트가 아니라 통합 테스트에서 수행되어야 한다.
    각 단계 별 테스트 순서는 V-model을 참고하자.
  4. 객체의 상태가 비결정적이면서, 무작위성을 기반으로 한다면(ex. 복권 번호 생성) 접근법이 다르다.
    먼저 단위 테스트에서 확인할 수 있는 부분은 결과값이 유효한지 확인하는 것이다.
    해당 객체의 무작위성 성능이나, 동작 방식에 대한 검증은 유효성이 아니라 성능의 영역에 해당한다. 이 검증은 벤치마킹으로 봐야한다.
  5. 유용한 함수들(utility functions)의 경우, 아마 코드 상의 중복 제거나, 문맥에 대한 가독성 향상을 위해 작성되었을 것이다.
    먼저, 순수함수와 같이 현재 상태와 관계없는 함수라면 해당 함수/기능을 묶어 다른 클래스로 분리하는 것이 좋을 것이다.
    만약 문맥에 대한 가독성 향상을 위해 작성된 함수 등 순수함수가 아니라면 원래 있던 복잡한 코드를 추상화한 것이므로 테스트 대상이 아니다.

위의 내용과는 별개의 질문인데, 자바를 사용한 실무 경험이 적어서 그런지 패키지(접근 한정자 없음) 권한은 어떤 때 써야 할지 잘 모르겠다. 그리고 테스트 코드가 같은 패키지에 있는 것이 적합한지도 잘 모르겠다. (구현 패키지와 테스트 패키지가 서로 분리되어 있어야 할 것 같다.)

클래스는 작아야 한다

클래스는 최대한 작게 만들어야 한다. 함수에서 비슷한 이야기를 했지만 작게 만드는 것이 기본 규칙이다. 그렇다면 함수에서는 라인 수로 크기를 측정했는데, 클래스는 무엇을 기준으로 크기를 측정할까?

클래스의 크기는 책임으로 그 기준을 측정한다.

예시로 나오는 SuperDashboard 클래스의 설계는 너무 많은 책임을 가지고 있다. 메서드 개수가 너무 많은 것은 딱 봐도 책임이 너무 많아 보인다. 메서드의 개수를 5개로 줄였음에도 불구하고 여전히 책임이 많다.

public class SuperDashboard extends JFrame implements MetaDataUser {
    public String getCustomizerLanguagePath()
    public void setSystemConfigPath(String systemConfigPath)
    public String getSystemConfigDocument()
    public void setSystemConfigDocument(String systemConfigDocument)
    public boolean getGuruState()
    public boolean getNoviceState()
    public boolean getOpenSourceState()
    public void showObject(MetaObject object)
    public void showProgress(String s)
    public boolean isMetadataDirty()
    public void setIsMetadataDirty(boolean isMetadataDirty)
    public Component getLastFocusedComponent()
    public void setLastFocused(Component lastFocused)
    public void setMouseSelectState(boolean isMouseSelected)
    public boolean isMouseSelected()
    public LanguageManager getLanguageManager()
    public Project getProject()
    public Project getFirstProject()
    public Project getLastProject()
    public String getNewProjectName()
    public void setComponentSizes(Dimension dim)
    public String getCurrentDir()
    public void setCurrentDir(String newDir)
    public void updateStatus(int dotPos, int markPos)
    public Class[] getDataBaseClasses()
    public MetadataFeeder getMetadataFeeder()
    public void addProject(Project project)
    public boolean setCurrentProject(Project project)
    public boolean removeProject(Project project)
    public MetaProjectHeader getProgramMetadata()
    public void resetDashboard()
    public Project loadProject(String fileName, String projectName)
    public void setCanSaveMetadata(boolean canSave)
    public MetaObject getSelectedObject()
    public void deselectObjects()
    public void setProject(Project project)
    public void editorAction(String actionName, ActionEvent event)
    public void setMode(int mode)
    public FileManager getFileManager()
    public void setFileManager(FileManager fileManager)
    public ConfigManager getConfigManager()
    public void setConfigManager(ConfigManager configManager)
    public ClassLoader getClassLoader()
    public void setClassLoader(ClassLoader classLoader)
    public Properties getProps()
    public String getUserHome()
    public String getBaseDir()
    public int getMajorVersionNumber()
    public int getMinorVersionNumber()
    public int getBuildNumber()
    public MetaObject pasting(
            MetaObject target, MetaObject pasted, MetaProject project)
    public void processMenuItems(MetaObject metaObject)
    public void processMenuSeparators(MetaObject metaObject)
    public void processTabPages(MetaObject metaObject)
    public void processPlacement(MetaObject object)
    public void processCreateLayout(MetaObject object)
    public void updateDisplayLayer(MetaObject object, int layerIndex)
    public void propertyEditedRepaint(MetaObject object)
    public void processDeleteObject(MetaObject object)
    public boolean getAttachedToDesigner()
    public void processProjectChangedState(boolean hasProjectChanged)
    public void processObjectNameChanged(MetaObject object)
    public void runProject()
    public void setAçowDragging(boolean allowDragging)
    public boolean allowDragging()
    public boolean isCustomizing()
    public void setTitle(String title)
    public IdeMenuBar getIdeMenuBar()
    public void showHelper(MetaObject metaObject, String propertyName)
    // ... many non-public methods follow ...
}
public class SuperDashboard extends JFrame implements MetaDataUser {
    public Component getLastFocusedComponent()
    public void setLastFocused(Component lastFocused)
    public int getMajorVersionNumber()
    public int getMinorVersionNumber()
    public int getBuildNumber()
}

책임이 적다면, 클래스를 짧은 문장으로 설명할 수 있을 것이다. 심지어 책에서는 만일(“if”), 그리고(“and”), 또는(“or”), 하지만(“but”) 같은 표현을 사용하지 말라고 하는데, 이는 위의 표현들이 아래 설명할 단일 책임 원칙을 위반하는 단어기 때문이다.

메서드의 이름으로 SuperDashboard 클래스의 책임을 분석해보자.

  1. 포커스되었던 Component를 설정하거나 가져오는 책임
    getLastFocusedComponent(), setLastFocused()
  2. 버전 정보를 가져오는 책임
    getMajorVersionNumber(), getMinorVersionNumber(), getBuildNumber()
  3. GUI를 관리하는 책임
    JFrame을 상속받으면서 생긴 책임

아마 책에서의 분석 내용으로 봤을때 1번 책임은 3번 책임을 기반으로 확장되어야 할 것이다. 1번 책임과 3번 책임을 묶더라도 여전히 2개의 책임을 가지고 있다. 이제 클래스의 책임을 줄이기 위한 원칙인 단일 채임 원칙을 확인해보자.

단일 책임 원칙

클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다.

위 문장에서는 책임의 정의클래스의 크기에 대한 가이드를 제공하고 있다. 책임의 정의를 클래스나 모듈을 변경할 이유라고 하고 있으며, 클래스나 모듈은 하나의 책임만 가져야 한다고 크기를 정하고 있다.

이전에 분석한대로, SuperDashboard는 두가지 책임을 갖고 있다. 이 중 버전 정보를 출력하는 기능은 이상적이라면 코드가 수정될 때마다 코드가 변경되어야 할 것이다. (버전 정보가 하드코딩된 경우에 국한된 상황이며, 오히려 관리 측면에선 좋지 않은 방법이지만 일단은 이 책의 이야기를 따르도록 하자.) SuperDashboard의 다른 책임(GUI 관련)을 수정할 때 버전 정보가 수정되어야 하는 것은 당연하지만, 반대로 버전 정보를 수정할 때마다 다른 책임의 코드도 수정해야 하는 것은 아니다.

public class SuperDashboard extends JFrame implements MetaDataUser {
    public Component getLastFocusedComponent()
    public void setLastFocused(Component lastFocused)
}

public class Version {
    public int getMajorVersionNumber()
    public int getMinorVersionNumber()
    public int getBuildNumber()
}

위와 같이 단일 책임 원칙은 객체지향 관점에서 중요하면서, 이해하기 쉽고, 지키기도 쉬운 원칙이다. 하지만 정작 잘 지켜지지 않는 편이며, 너무 많은 책임을 가진 클래스를 자주 보게 된다.

소프트웨어가 돌아가게 작성하는 것(요구사항을 만족하는 것)과 코드를 깨끗하게 작성하는 것(단일 책임 원칙 등을 지키는 것)은 다른 일이며, 대부분 돌아가게 하는 것에 더 집중하기 때문이다. 그리고 소프트웨어가 잘 동작하면 거기서 일이 끝났다고 생각하기 때문에 실패하는 것이다.

게다가 많은 프로그래머들은 여러 개의 단일 책임 클래스로 구성하는 것이 전체 구조를 이해하는데 더 어려울 것이라며 두려워한다는 것이다. (어떤 일을 하기 위해 여러 클래스들을 돌아다녀야 하기 때문에 일을 키운다고 생각한다.)

개인적인 의견으로, 이렇게 생각하는 사람이 진짜 있긴 한지 의문이 든다. 과거에는 개발환경이 불편해서 이런 생각을 했을 지 몰라도, 지금도 이런 생각을 하고 있다면 그 사람이 이상한 사람이라 생각한다. IDE에서 정의 찾아보기 기능은 기본이고, (가끔 Visual Studio에서는 선언만 찾아주고 정작 정의는 안 찾아주는 짜증나는 상황은 있지만, 이건 생략하도록 하자.) 듀얼모니터는 일반적이며, 개별 모니터의 해상도도 훨씬 크고, 여러 개의 탭도 쓸수 있고, 자동완성 기능도 잘 제공되는데, 정말 이런 생각을 하는 사람이 있는 지 궁금하다.

응집도Cohesion

  1. 클래스는 인스턴스 변수를 적게 가지고 있어야 좋다.
  2. 아마 클래스의 메서드는 인스턴스 변수를 1개 이상 사용할 것이다.
  3. 각 메서드에서 많은 인스턴스 변수를 쓸 수록 더 응집되어있다고 한다.

응집도가 높은 Stack 클래스를 보자. (size() 메서드를 제외한 모든 메서드가 모든 멤버 변수를 접근하고 있다.)

public class Stack {
    private int topOfStack = 0;
    List<Integer> elements = new LinkedList<Integer>();
    
    public int size() {
        return topOfStack;
    }

    public void push(int element) {
        topOfStack++;
        elements.add(element);
    }

    public int pop() throws PoppedWhenEmpty {
        if (topOfStack == 0)
            throw new PoppedWhenEmpty();
        int element = elements.get(--topOfStack);
        elements.remove(topOfStack);
        return element;
    }
}

함수의 크기를 작게 만들고, 매개변수의 개수를 줄이면서 클래스를 만들다보면 일부 메서드만이 사용하는 인스턴스 변수가 많아진다. 이는 관련 메서드들을 새로운 클래스로 쪼개야 한다는 신호다. (내부적인 연관성이 낮아지므로, 서로 다른 책임이 섞여 있을 가능성이 높다는 뜻이다.)

응집도를 유지하면 작은 클래스 여럿이 나온다

큰 함수를 작은 함수들로 쪼개는 행위만으로도 응집도를 높이는 클래스로 구성할 수 있다.

책에서는 예시로 도널드 커누스Literate Programming 책에 나온 PrintPrimes 프로그램의 코드를 자바로 변환했을 때의 코드를 원본으로 가정하고 있다.

큰 함수를 작은 함수로 쪼개는 방식, 그 과정에서 응집도를 높이기 위해 클래스를 분리하는 방식으로 리팩토링을 수행한 결과를 보여주고 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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;
            }
        }
    }
}
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); 
    }
}
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;
    }
}
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 leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor; 
        return candidate == leastRelevantMultiple;
    }

    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;
    } 
}

책에서 리팩토링 한 순서는 다음과 같다.

  1. 큰 함수를 작은 함수로 분리한다.
  2. 분리 과정에서 함수 당 매개변수 개수를 줄이기 위해 일부를 멤버변수로 승격시킨다.
  3. 이 과정에서 함수와 변수 사이의 관계가 줄어들면서 응집도가 낮아진다.
  4. 응집도가 낮은 것을 기준으로 클래스를 분리한다.

원본 코드의 main()함수를 의미 단위로 분리해보자.

  1. Line 29~49는 소수를 구하는 방법을 (에라토스테네스의 체 알고리즘을 사용) 표현하고 있다.
  2. Line 50~64는 구해진 소수들을 출력하는 방법을 표현하고 있다.
  3. Line 5~9는 구할 소수의 범위, 출력 조건 등을 설정하고 있다. (상수로 프로그램 동작을 위한 입력값 표현)

각 의미에 따라 최종적으로 3개의 클래스로 리팩토링 된 것을 볼 수 있다.

  1. PrimeGenerator: 소수를 구하는 책임
  2. RowColumnPagePrinter: 화면에 형식을 맞춰 출력하는 책임
  3. PrimePrinter: 전체 프로그램을 동작시키는 책임 (소수를 구해서 화면에 출력시키는 역할)

변경하기 쉬운 클래스

TODO: 남은 부분 마저 작성

public class Sql {
    public Sql(String table, Column[] columns)
    public String create()
    public String insert(Object[] fields)
    public String selectAll()
    public String findByKey(String keyColumn, String keyValue)
    public String select(Column column, String pattern)
    public String select(Criteria criteria)
    public String preparedInsert()
    private String columnList(Column[] columns)
    private String valuesList(Object[] fields, final Column[] columns) 
    private String selectWithCriteria(String criteria)
    private String placeholderList(Column[] columns)
}
abstract public class Sql {
    public Sql(String table, Column[] columns)
    abstract public String generate();
}

public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns)
    @Override public String generate()
}

public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns)
    @Override public String generate()
}

public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns, Object[] fields)
    @Override public String generate()
    private String valuesList(Object[] fields, final Column[] columns)
}

public class SelectWithCriteriaSql extends Sql {
    public SelectWithCriteriaSql(
            String table, Column[] columns, Criteria criteria)
    @Override public String generate()
}

public class SelectWithMatchSql extends Sql {
    public SelectWithMatchSql(
            String table, Column[] columns, Column column, String pattern)
    @Override public String generate()
}

public class FindByKeySql extends Sql {
    public FindByKeySql(
            String table, Column[] columns, String keyColumn, String keyValue)
    @Override public String generate()
}

public class PreparedInsertSql extends Sql {
    public PreparedInsertSql(String table, Column[] columns)
    @Override public String generate()
    private String placeholderList(Column[] columns)
}

public class Where {
    public Where(String criteria)
    public String generate()
}

변경으로부터 격리


JaeSang Yoo
글쓴이
JaeSang Yoo
the programmer

 
목차