본문 바로가기
카테고리 없음

클린코드 - 클래스

by HaningYa 2021. 9. 1.
728x90

더 높은 단계의 클린코드

클래스 체계

클래스를 정의하는 표준 자바관례

  • 변수 목록
    • 정적 공개 상수 (public static)
    • 비공개 변수 (private static)
    • 비공개 인스턴스 변수 (private)
  • 공개함수
  • 비공개 함수 (자신을 호출하는 공개함수 직후)

캡슐화

  • 변수와 유틸리티 함수는 가능한 공개하지 X
  • 비공개 상태를 유지해야 하고 같은 패키지 안에서
    테스트 코드가 함수를 호출하거나 변수를 사용해야 할 경우 protected로 선언하거나 패키지 전체 공개

클래스는 작아야 한다.

  • 함수와 마찬가지로 "작게" 가 기본 규칙이다.
  • 함수는 "작게" 의 개념을 행 수로 크기를 측정했다.
  • 클래스는 맡은 책임을 기준으로 세운다.
  • 예제의 superDashboard와 같은 70개의 공개 메서드 -> 딱봐도 책임이 너무 많다.
protocol SuperDashboard : JFrame, MetaDataUser {
    func getCustomerizerLanguagePath() -> String
    func setSystemConfigPath(systemConfigDocument: String)
    func getSystemConfigDocument() -> String
    func setSystemConfigDocument(systemConfigDocument: String)
    func getGuruState() -> Bool
    func getNoviceState() -> Bool
    func getOpenSourceState() -> Bool
    func showObject()
    ///... etc
}
  • 메서드 몇개만 포함한다고 하면 괜찮은가? 정답은 아니다. 왜 아닌가 생각해보자
protocol SuperDashboard : JFrame, MetaDataUser {
    func getLastFocusedComponent() -> Component
    func setLastFocused(lastFocused: Component)
    func getMajorVersionNumber() -> Int
    func getMinorVersionNumber() -> Int
    func getBuilderNumber() -> Int
}
더보기
  • 메서드 수는 많지만 책임이 너무 많다.
  • 책임이 많은건 어떻게 아느냐?!
  • 간결한 이름이 떠오르지 않으면, 클래스의 책임이 너무 많아서다.
  • Processor, Manager, Super등과 같은 단어가 있다면 그 클래스는 여러 책임을 떠안았다는 증거다.
  • 이걸 네이밍으로 판단 조져버리네

 

여튼 SuperDashboard의 책임은 두개로 나눌 수 있다.

  • 마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며
  • 버전과 빌드 번호를 추적하는 메커니즘을 제공한다.

단일 책임 원칙

  • 클래스나 모듈을 변경할 이유가 단 하나 뿐이여야 한다는 원칙
  • 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시해 준다.

그래서 겉보기에 작아 보이는 SuperDashboard 도 변경할 이유가 두가지 있다.

더보기
  • SuperDashBoard는 소프트웨어 버전 정보를 추적한다. 그런데 버전 정보는 소프트웨어 출시 때마다 달라진다.
  • SuperDashBoard는 자바 스윙 컴포넌트를 관리한다. 그런데 스윙 코드를 변경할 때 마다 버전 번호가 달라진다.

그러니까 스윙 관련해서 코드 작업을 할때도 SuperDashboard가 변경될 수 있고 버전 정보가 바뀔 때마다도 SuperDashboard가 변경될 수 있으니 변경될 이유가 두가지라는 거고 이걸 따로 분리하라는 말인가?

책임 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화 하기도 쉬워진다

protocol Version {
    func getMajorVersionNumber() -> Int
    func getMinorVersionNumber() -> Int
    func getBuilderNumber() -> Int
}
  • SRP는 클래스 설계자가 가장 무시하는 규칙 중 하나이다.
  • 깨끗하고 체계적인 소프트웨어보다 "돌아가는 소프트웨어"에 초점을 맞추기 때문
  • 프로그램이 돌아가면 다시 되돌아가 만능 클래스를 단일 책임 클래스로 여럿 분리하는 대신 다음 피쳐로 넘어감
  • 자잘한 단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려함 (타고 또 타고 들어가야되서)
  • 결론은 작은 클래스 여럿으로 이뤄진 시스템이 바람직하다!

응집도

  • 클래스는 인스턴스 변수가 작아야 한다.
  • 각 클래스 메서드는 클래스 인스턴스 변수를 하나이상 사용해야 한다.
  • 메서드가 변수를 더 많이 사용할 수록 응집도가 높다.
  • 모든 인스턴스 변수를 메서드마다 사용하는 클래스도 응집이 가장 높다.

클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 것!

// 응집도 높은 예시

public class Stack {
    private var topOfStack = 0
    var elements = [Int]()

    public func size() -> Int {
        return topOfStack
    }

    public func push(element: Int) {
        topOfStack += 1
        elements.append(element)
    }

    public func pop() throws -> Int {
        if topOfStack == 0 {
            throw StackErrors.whatTheFuck
        }
        let element = elements.popLast()
        topOfStack -= 1
        return element!
    }
}
enum StackErrors: Error {
    case whatTheFuck
}

 

  • 함수를 작게, 매개변수 목록을 짧게 하다보면 몇몇 메서드만이 사용하는 인스턴스 변수가 많아진다 -> 쪼개라
  • 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.

큰 함수의 일부를 작은 함수 하나로 빼내고 싶다.
빼내려는 코드가 함수에 정의된 변수 네개를 사용한다.

  1. 파라미터로 넘길 것인가 -> 애바다
  2. 인스턴스 변수로 뺄것인가 -> 응집력을 잃는다

정답: 독자적인 클래스로 분리해라

 

실전연습

package clean.code.chapter10.original;

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;
      
      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;
          } 
      }
    • 행과 열에 맞춰 출력
    • 더보기
      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;
          } 
      }

 

  • 좀 더 서술적인 변수 이름을 사용
  • 주석을 대신할 수 있을만한 함수 선언과 클래스 선언
  • 공백 추가와 형식 맞춤

변경하기 쉬운 클래스

시스템은 언젠간 기필코 변경되기 때문에 변경이 있을 때 마다 버그가 생길 가능성이 있다면 스트레스 만땅

예를들어


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

위 sql 코드는 5법칙 중 무엇을 위반하는가??

더보기
  • 새로운 SQL문을 지원할 때 손대야 하고, 
  • 기존 SQL문을 수정할 때도 손대야 하므로 

 

SRP 위반한다.

클래스 일부에서만 사용되는 비공개 메서드를 보면 코드를 개선의 가능성을 알 수 있다. 
(클래스 일부에서만 사용하니까 쪼개라는 건가?)

  • 공개 인터페이스를 전부 SQL 클래스에서 파생되는(SQL 클래스 상속하는) 클래스로 만듬
  • 비공개 메서드는 사용되는 해당 클래스로 옮김
  • 공통된 인터페이스도 따로 클래스 분리 (Where, CoulumList)

위 작업을 통해 새로운 sql문 (update) 추가시에 기존의 클래스를 수정할 필요가 없다.

	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()
	}
	
	public class ColumnList {
		public ColumnList(Column[] columns) public String generate()
	}

템플릿 메소드 패턴?

결론은 위와 같이 잘 짜여진 시스템은 추가와 수정이 있을떄 수정해야할 코드가 최소이다.

  • concrete 클래스에 의존하는 클래스는 구현이 바뀌면 버그가 발생할 수 있다. 
  • 그래서 abstract 클래스를 사용해 수정에 영향에서 격리시켜야 한다.
  • 추상화를 통해 시스템의 결합도를 낮춘다고 표현할 수 있다.
  • 유연성과 재사용성도 높아진다.

예제

상세한 구현에 의존하는 코드

[문제상황]
Portfolio 클래스를 테스트 하려고 하는데
Portfolio 클래스는 외부 ToykoStockExchangeAPI를 사용해서 값을 계산한다.
api는 5분마다 값이 달라진다.

[해결방법]
Portfolio에서 직접 api를 호출하지 않고 StockExchange라는 인터페이스를 생성한 후 메서드를 선언한다.

public interface StockExchange { 
	Money currentPrice(String symbol);
}

인터페이스를 구현하는 TokyoStockExchange 클래스를 구현하고난 뒤
Portfolio 생성자에 StockExchange 레퍼런스를 파라미터로 받는다.

public Portfolio {
	private StockExchange exchange;
	public Portfolio(StockExchange exchange) {
		this.exchange = exchange; 
	}
	// ... 
}

TokyoStockExchange Mock class를 만들 수 있다.

public class PortfolioTest {
	private FixedStockExchangeStub exchange;
	private Portfolio portfolio;
	
	@Before
	protected void setUp() throws Exception {
		exchange = new FixedStockExchangeStub(); 
		exchange.fix("MSFT", 100);
		portfolio = new Portfolio(exchange);
	}

	@Test
	public void GivenFiveMSFTTotalShouldBe500() throws Exception {
		portfolio.add(5, "MSFT");
		Assert.assertEquals(500, portfolio.value()); 
	}
}

 

인터페이스를 활용하여 구현 클래스와의 격리를 통해 실제 구현이 달라져도 영향받지 않게 만들 수 있다.

 

728x90

댓글