Language/Java-Book

Real-World Software Development : Chapter02 : KISS, Code Maintainability, SRP, Cohension, Coupling

j4ko 2024. 8. 26. 20:02
728x90
반응형

 

목차

     

    개요

    최근 Java를 사용하고 익히고자 책을 몇 권 구매하게되었다. 교보문고에 가서 Project 기반으로 Java를 학습할 수 있는 책을 찾고 있던 도중 “실전 자바 소프트웨어 개발”이란 책이 눈에 띄었는데 이 책의 예제 코드와 학습한 내용을 정리해보고자한다.

     

    이 글에서 정리하는 범위는 Chapter 02이며 page로는 21page ~ 48page 까지이다. Chapter02 ~ 03은 “입출금 내역 분석기” 이다.

     

    또한 SOLID 원칙 중 단일 책임 원칙을 다룬다.

     

    입출금 내역 분석기 - 요구사항

    책에서는 가상 인물인 “마크 버그저커”라는 인물을 등장시킨다. 이 인물은 여러 군데에서 많은 돈을 소비하는데 소비 내역을 자동으로 요약할 수 있는 소프트웨어가 필요한 상황이라는 맥락을 제공한다. 더 구체적인 부분은 “마크 버그저크”라는 인물이 자신의 입출금 목록이 담긴 텍스트 파일을 읽어 분석할 것을 요구하는 것을 가정한다.

     

    이 텍스트 파일은 다음과 같은 형식이다.

    30-01-2017,-100,Deliveroo
    30-01-2017,-50,Tesco
    01-02-2017,6000,Salary
    02-02-2017,2000,Royalties
    02-02-2017,-4000,Rent
    03-02-2017,3000,Tesco
    05-02-2017,-30,Cinema
    

    위 형식에 대해 구현해야될 문제들은 다음과 같다.

    • 은행 입출금 내역의 총 수입과 총 지출은 각각 얼마인가? 결과가 양수인가 음수인가?
    • 특정 달엔 몇 건의 입출금 내역이 발생했는가?
    • 지출이 가장 높은 상위 10건은 무엇인가 ?
    • 돈을 가장 많이 소비하는 항목은 무엇인가 ?

     

    Keep it Short and Simple, 간단하게 시작하자

    요구사항을 통해 먼저 시작할 수 있는 부분에 대해 책에서는 첫번째 문제인 “은행 입출금 내역의 총 수입과 총 지출은 각각 얼마인지” 부터 구현을 시작한다. 복잡하게 시작하지 말고 KISS 원칙을 이용해 한 개의 클래스로 간단한 구현을 시작한다.

    // 책의 예제에서는 BankTransactionAnalyzer 이지만 책의 내용에서는 BankStatementAnalyzer 라고도 하는 듯
    public class BankTransactionAnalyzer {
        private static final String RESOURCES = "";
    
        public static void main(final String... args) throws IOException {
            final Path path = Paths.get(RESOURCES);
            final List<String> lines = Files.readAllLines(path);
    
            double total = 0d;
            for (final String line : lines) {
                final String[] columns = line.split(",");
                final double amount = Double.parseDouble(columns[1]);
                total += amount;
            }
    
            System.out.println("The total for all transaction is " + total);
        }
    }
    

    위 코드는 파일의 모든 행을 가져온 다음 “,”로 열을 분리한 뒤 금액을 추출하고 금액을 double로 파싱한다음 총합에 더한다. 이렇게 하여 최종적으로 전체 금액의 합계를 얻는다. 

     

    두 번째 문제인 “특정 달엔 몇 건의 입출금 내역이 발생했는가?”는 앞선 코드를 조금 복사 붙여넣기 하여 조금 수정하는 정도로 구현한다.

    public static void main(final String... args) throws IOException {
        final Path path = Paths.get(RESOURCES);
        final List<String> lines = Files.readAllLines(path);
    
        double total = 0d;
        final DateTimeFormatter DATE_PATTERN = DateTimeFormatter.ofPattern("dd-MM-yyyy");
    
        for (final String line : lines) {
            final String[] columns = line.split(",");
    
            final LocalDate date = LocalDate.parse(columns[0], DATE_PATTERN);
            if (date.getMonth() == Month.JANUARY) {
                double amount = Double.parseDouble(columns[1]);
                total += amount;
            }
        }
    
        System.out.println("The total for all transaction in January " + total);
    
    } 
    

     

    final, 값을 재할당하지 않도록

    예제 코드에서는 final 키워드를 사용해 변수들을 정의했는데 이에 대해 책에서는 final 키워드를 사용할 때는 다음과 같은 내용들을 알려준다.

     

    “final” 사용에 따른 장점

    • “final”은 지역 변수나 필드를 정의할 때 사용하는 것이므로 “final”로 선언된 변수에 값을 재할당할 수 없다.
    • “final”을 사용하면 어떤 객체의 상태가 바뀔 수 있고, 어떤 객체의 상태가 바뀔 수 없는지 명확하게 구분할 수 있다.

    “final” 사용에 따른 단점

    • final 키워드를 적용한다고 해서 객체가 바뀌지 못하도록 강요하는 것은 아니다. final 필드로 가리키는 객체라도 가변 상태를 포함한다.
    • final로 인해 더 많은 코드가 추가된다. 어떤 팀에서는 메서드 파라미터에에 final를 사용해 지역 변수도 아니며 다시 할당할 수 없음을 명시하기도 한다.

    “final” 키워드가 쓸모없는 상황

    • 추상 메서드의 메서드 파라미터에 final을 사용하는 상황
      • 실제 구현이 없으므로 final 키워드의 의미가 무력화된다. 또한 java 10에서 var 키워드가 등장하면서 final 유용성이 크게 감소되었다.

    기타

    • “final” 사용에 따른 장단점이 존재하므로 팀과 프로젝트에 따라 달라진다.

     

    Code Maintainability, 코드 유지보수성

    앞서 KISS 원칙으로 1번 문제를 먼저 코드로 작성한뒤 2번 문제는 복사/붙여넣기를 통해 작성했다. 책에서는 이 방법이 “좋은 방법이라고 생각하는가?”라는 물음을 던진다. 단축하여 “코드 유지보수성”을 높여한다고 말하고 있으며 구현하는 코드가 가졌으면 하는 속성에 대해 다음과 같이 나열한다.

    • 특정 기능을 담당하는 코드는 쉽게 찾을 수 있어야한다.
    • 코드가 어떤 일을 수행하는 쉽게 이해할 수 있어야한다.
    • 새로운 기능을 쉽게 추가하거나 기존 기능을 쉽게 제거할 수 있어야 한다.
    • Encapsulation이 잘 되어 있어야 한다. 즉, 세부 구현 내용이 감춰져 있으므로 사용자가 쉽게 코드를 이해하고 , 기능을 바꿀 수 있어야 한다.

    요점은 “궁극적으로 개발자의 목표는 현재 만들고 있는 응용 프로그램의 복잡성을 관리하는 것이며 새로운 요구 사항이 생길때마다 복사/붙여넣기로 이를 해결한다면 거대한 클래스가 생기고, 중복 코드가 만들어지면서 코드가 불안정하고 변화에 쉽게 망가진다” 라는 점을 시사한다.

     

    즉, 복사/붙여넣기는 “갓 클래스”, “코드 중복”이라는 안티 패턴을 발생시키기에 KISS 원칙을 남용하지 말라고 한다.

    결론적으로는 응용 프로그램의 설계를 되돌아보고, 한 문제를 작은 개별 문제로 분리해 더 쉽게 관리할 수 있는지 파악해야 한다고 말하고 있으며 이 과정을 통해 이해하기 쉬우며, 유지보수하기 쉽고, 새로운 요구 사항도 쉽게 적용하는 결과물을 만들 수 있어야 한다고 말한다.

     

    Single Responsibility Principle,  단일 책임 원칙

    SOLID 원칙은 객체지향 관련 개념을 보다보면 한 번쯤은 접하게되는 개념인데 이 책에서 SOLID 원칙이 등장한다. Chapter02는 “단일 책임 원칙”이 중점이다. 이 책에서 SRP에 대해 다음과 같이 소개하고 있다.

    “단일 책임 원칙은 쉽게 관리하고 유지보수하는 코드를 구현하는데 도움을 주는 포괄적인 소프트웨어 개발 지침이다”

    다음 두 가지를 보완하기 위해 SRP를 적용한다.

    • 한 클래스는 한 기능만 책임진다.
    • 클래스가 바뀌어야 하는 이유는 오직 하나여야한다.

    SRP는 일반적으로 “클래스”와 “메서드”에 적용한다. SRP는 한 가지 특정 동작, 개념, 카테고리와 관련되며 SRP 를 적용하면 코드가 바뀌어야 하는 이유가 한 가지로 제한되므로 더 튼튼한 코드를 만들 수 있다

     

    “입출금 내역 분석기”에 적용할 SRP는 다음과 같이 구분할 수 있다.

    • 입력 읽기
    • 주어진 형식의 입력 파싱
    • 결과 처리
    • 결과 요약 리포트

    CSV 파싱 로직 분리

    책의 예제에서는 SRP를 적용하여 다른 문제 구현에 활용할 수 있도록 CSV 파싱 로직을 새로운 클래스로 분리하는 방법에 대해 다음과 같은 코드를 작성하였다.

    public class BankStatementCSVParser {
    
        private static final DateTimeFormatter DATE_PATTERN = DateTimeFormatter.ofPattern("dd-MM-yyyy");
    
        private BankTransaction parseFormCSV(final String line) {
            final String[] columns = line.split(",");
    
            final LocalDate date = LocalDate.parse(columns[0], DATE_PATTERN);
            final double amount = Double.parseDouble(columns[1]);
            final String description = columns[2];
    
            return new BankTransaction(date, amount, description);
        }
    
        public List<BankTransaction> parseLineFromCSV(final List<String> lines) {
            final List<BankTransaction> bankTransactions = new ArrayList<>();
            for (final String line : lines) {
                bankTransactions.add(parseFormCSV(line));
            }
            return bankTransactions;
        }
    }
    
    

    위 코드에서 사용된 BankTransaction Class는 “입출금 내역” 부분이라는 의미를 공유할 수 있는 도메인 클래스로 표현되었다. 위 입출금 내역 CSVParser 클래스를 이용하여 기존에 작성된 코드를 다음과 같이 리팩터링 할 수 있다.

    final BankStatementCSVParser bankStatementCSVParser = new BankStatementCSVParser();
    
    final Path path = Paths.get(RESOURCES);
    final List<String> lines = Files.readAllLines(path);
    
    final List<BankTransaction> bankTransactions = bankStatementCSVParser.parseLineFromCSV(lines);
    
    System.out.println("The total for all transaction is " + calculateTotalAmount(bankTransactions));
    System.out.println("Transaction is January" + selectInMonth(bankTransactions, Month.JANUARY));
    

    앞서 구현했던 방법과의 차이는 BankTransaction 객체에서 직접 정보를 추출하기 떄문에 내무 파싱 방법을 알 필요가 없다는 점이다. 또한 리팩터링 덕분에 파싱 로직을 구현하는 부분이 사라졌다. 파싱 기능을 다른 클래스와 메서드에 위임했기 때문이다. 즉 기능을 독립적으로 구현한 것이다.

     

    다양한 문제(책에서는 여러 Parsing을 뜻하는 듯 하다)를 처리해야하는 새 요구 사항에 대해 클래스로 캡슐화된 기능을 재사용해 구현한다.

     

    메서드를 구현할 때는 “놀람 최소화 원칙(Principle of least surprise)”를 따라야 한다고 한다. 그래야 코드를 보고 무슨 일이 일어나는지 명확이 이해할 수 있기 때문이다.

     

    “놀람 최소화 원칙”에 대해 책의 옮긴이는 다음과 같이 말한다.

    • 어떤 메서드가 다른 메서드와 달리 예상치 못한 방법으로 동작한다면 코드를 이해하기가 어려울 것이다. 따라서 누군가 놀라지 않도록 일관성을 유지하는 범위에서 코드를 구현할 것을 강조하는 원칙이다.

    Cohension, 응집도

    응집도는 클래스나 메서드의 책임이 서로 얼마나 강하게 연결되어있는지를 측정한다. 즉 어떤 것이 여기저기에 모두 속해있는지를 말한다.

    응집도는 소프트웨어의 복잡성을 유추하는데 도움을 준다. 높은 응집도는 개발자의 목표이고, 누구나 쉽게 코드를 찾고, 이해하고, 사용할 수 있도록 만들고 싶어 한다

    32page

     

     

    프로그램의 진입점인 BankTransactionAnalyzer 클래스는 파싱, 계산, 화면으로 결과 전송 등 다양한 부분을 연결한다. 계산 작업을 수행하는 로직만을 따로 떼어내 BankStatementProcessosr라는 별도의 클래스로 추출해서 응집도를 높일 수 있다.

    public class BankStatementProcessor {
    
        private final List<BankTransaction> bankTransactionList;
    
        public BankStatementProcessor(List<BankTransaction> bankTransactionList) {
            this.bankTransactionList = bankTransactionList;
        }
    
        public double calculateTotalAmount() {
            double total = 0;
            for (final BankTransaction bankTransaction : bankTransactionList) {
                total += bankTransaction.getAmount();
            }
            return total;
        }
    
        public double calculateTotalInMonth(final Month month) {
            double total = 0;
            for (final BankTransaction bankTransaction : bankTransactionList) {
                if (bankTransaction.getDate().getMonth() == month) {
                    total += bankTransaction.getAmount();
                }
            }
            return total;
        }
    
        public double totalCalculateForCategory(final String category) {
            double total = 0;
            for (final BankTransaction bankTransaction : bankTransactionList) {
                if (bankTransaction.getDescription().equals(category)) {
                    total += bankTransaction.getAmount();
                }
            }
            return total;
        }
    }
    

     

    Coupling, 결합도

    Chapter 02에서는 “코드 유지보수성”, “응집도” 외에 코드 구현 시 고려할 또 다른 중요 특성으로 “결합도(Coupling)”라는 특성도 존재함을 알려준다.

    응집도는 클래스, 패키지, 메서드 등의 동작이 얼마나 관련되어 있는가를 가리키는 반면, 결합도는 한 기능이 다른 클래스에 얼마나 의존하고 있는지를 가늠한다.

    결합도는 어떤 클래스를 구현하는데 얼마나 많은 지식을 참조했는가로 설명할 수 있다. 더 많은 클래스를 참조했다면 기능을 변경할 때 그만큼 유연성이 떨어진다. 어떤 클래스의 코드를 바꾸면 이 클래스에 의존하는 모든 클래스가 영향을 받는다.

    39page

     

     

    결합도를 제거하기 위해서는 인터페이스를 이용할 수 있으며 인터페이스를 이용하면 요구 사항이 바뀌더라도 유연성을 유지할 수 있다고 한다. 예시에서는 “입출금 내역 파서”를 어떻게 사용하는지 정의하는 인터페이스를 만드는 것으로 시작한다.

    public interface BankStatementParser {
    		BankTransaction parseForm(String line);
    		List<BankTransaction> parseLinesFrom(List<String> lines);
    }
    

    인터페이스가 생김에 따라 CSVParser를 다음과 같은 방식으로 구현한다.

    public class BankStatementCSVParser implementes BankStatementParser { ... }
    

    위와 같은 예제가 어떻게 “결합도를 제거할 수 있다”고 하는 걸까? 이에 대해서는 다음 코드가 예시이다.

    public class BankStatementAnalyzer {
    
    	public vouid analze(final String fileName, final BankStatementParser bankStatementParser) {
    			...
    			final List<BankTransaction> bankTransactions = bankStatementParser.parseLineFrom(lines);	
    	
    	}
    }
    

    위 예시는 BankStatementParser Interface를 사용함으로써 특정 구현에 종속되지 않도록 클래스를 개선시킨 예제라고 한다. 위 코드에서는 BankStatementAnalyzer가 BankStatementCSVParser를 사용하는 부분이 드러나지 않는 부분이 핵심인 듯 하다.

     

    Result

    결과적으로 메인 응용 프로그램은 다음과 같은 모양의 코드를 사용한다.

    public static void main(String... args) throws IOException {
    
      final BankStatementAnalzyer bankStatementAnalyzer = new BankStatementAnalzyer();
      final BankStatementParser bankStatementParser = new BankStatementCSVParser();
    
      bankStatementAnalyzer.analyze(args[0], bankStatementParser);
    }
    

     

    마치며

    Chapter02에서는 SRP 원칙, 코드 유지보수성, 응집도, 결합도 에 대한 키워드로 “입출금 내역 분석”라는 주제를 다뤘다.

    KISS 원칙으로 시작했던 구현 방식은 “파싱”, “계산”, “화면으로 결과전송” 등 프로그램의 진입점에서 구현했다.

     

    그러나 이는 코드 유지보수성 관점에서 “갓 클래스”, “코드 중복” 등의 안티패턴으로 다뤄질 수 있는 문제를 내포하고 있기에 단일 책임 원칙을 통해 각각의 책임을 가지는 클래스로 분리를 시도했다. 이 과정에서 “응집도”, “결합도”라는 특성으로 더 세분화하여 파싱을 담당하는 Parser, 계산을 담당하는 Processor, 거래 내역의 개념을 담는 도메인 클래스(BankTransaction) 로 분리헀다.

     

     

    Chapter 03에서는 개방/폐쇄 원칙 (OCP)를 주제로 Chapter02에서 다룬 “입출금 내역 분석기”를 조금 더 확장시킨다.

    728x90
    반응형