티스토리 뷰

728x90

목표

단일 책임 원칙 (single responsibility principle) 줄여서 SRP

KISS 원칙

CSV 파일을 읽어서 요구사항 구현하기

  • 은행 입출금 내역의 총 수입과 총 지출 구하기 결과가 양수? 음수?
  • 특정 달엔 몇 건의 입출금 내역 발생?
  • 지출이 가장 높은 상위 10건?
  • 돈을 가장 많이 소비하는 항목?
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

KISS (Keep it short and simple!) 원칙을 이용해 일단 만들어보자. 응용프로그램 코드를 한 개의 클래스로 구현

아직은 예외 처리에 신경쓰지 말자.

모든 거래 내역의 합 계산하기

public class BankStatementAnalyzerSimple {

    private static final String RESOURCES = "src/main/resources/";

    public static void main(final String[] args) throws Exception {
            final Path path = Paths.get(RESOURCES + "bank-data-simple.csv");
            final List<String> lines = Files.readAllLines(path);
            double total = 0;
            for(final String line: lines) {
                String[] columns = line.split(",");
                double amount = Double.parseDouble(columns[1]);
                total += amount;
            }

            System.out.println("The total for all transactions is " + total);
    }
}
  • 콤마로 열 분리
  • 금액 추출
  • 금액을 dobule로 파싱

다음과 같은 문제를 생각해보자.

  • 파일이 비어 있다면?
  • 데이터에 문제가 있어서 금액을 파싱할수 없다면?
  • 행의 데이터가 완벽하지 않다면?

프로그램을 개발하면서 언제나 이런 질문을 하는 습관을 가지는 것이 좋다.

 

월 입출금 내역 합계 계산하기

이전 코드를 그대로 복붙해서 붙여넣고 주어진 월을 선택하도록 로직을 바꾼다.

public class BankStatementAnalyzerProblematic {

    private static final String RESOURCES = "src/main/resources/";
    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy");

    public static void main(final String[] args) throws IOException {
        final Path path = Paths.get(RESOURCES + "bank-data-simple.csv");
        final List<String> lines = Files.readAllLines(path);
        double total = 0;
        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 transactions is " + total);
    }
}

코드 유지보수성과 안티 패턴

이전과 같은 예제를 만들 때 복붙이 좋은 방법인가? 당연이 아니라고 이야기 할 것이다. 비슷한 기능을 10개 만들려면 복붙을 10번 해야하는건데 그만큼 비용이 든다. 개발자는 항상 코드를 구현할 때는 코드 유지보수성을 높이기 위해 노력한다.

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

안티패턴 (갓 클래스, 코드 중복)

갓 클래스란?

한 개의 파일에 모든 기능이 다 들어가 있는 클래스. 한 클래스로 모든 것을 해결하는 패턴인데 이런 갓 클래스 안티 패턴이 나타나지 않도록 주의해야 한다.

코드 중복?

각 문제에서 입력을 읽고 파싱하는 로직이 중복. 만약 CSV가 아닌 JSON 파일로 입력형식이 바뀐다면?? 또는 다양한 형식의 파일을 지원해야 한다면? 구현은 한가지이지만 여러 곳에 코드가 중복되어 있어 새로운 버그가 발생할 가능성이 커진다.

단일 책임 원칙

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

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

SRP는 일반적으로 클래스와 메서드에 적용한다. SRP를 적용하면 코드가 바뀌어야 하는 이유가 한 가지로 제한되므로 더 튼튼한 코드를 만들 수 있다.

이전의 요구사항 기능을 개별로 분리해보면

1. 입력 읽기

2. 주어진 형식의 입력 파싱

3. 결과 처리

4. 결과 요약 리포트

첫번째. 다른 문제 구현에 이를 활용할 수 있도록 CSV 파싱 로직을 새로운 클래스로 분리

public class BankStatementCSVParser{

    private static final DateTimeFormatter DATE_PATTERN = DateTimeFormatter.ofPattern("dd-MM-yyyy");

    public BankTransaction parseFrom(final String line) {
        final String[] columns = line.split(",");

        final LocalDate date = LocalDate.parse(columns[0], DATE_PATTERN);
        final double amount = Double.parseDouble(columns[1]);

        return new BankTransaction(date, amount, columns[2]);
    }

    public List<BankTransaction> parseLinesFrom(final List<String> lines) {
        return lines.stream().map(this::parseFrom).collect(toList());
    }
}

여기서 BanskTransaction 클래스는 도메인 클래스로 입출금 내역을 표현. BankTransaction 클래스는 응용프로그램의 다른 부분에서 입출금 내역 부분이라는 의미를 공유할 수 있어 매우 유용하다.

기존 코드 리팩터링

 

public class BankStatementAnalyzerSRP {

    private static final String RESOURCES = "src/main/resources/";

    public static void main(final String[] args) throws Exception {

        final BankStatementCSVParser bankStatementParser = new BankStatementCSVParser();

        final Path path = Paths.get(RESOURCES + args[0]);
        final List<String> lines = Files.readAllLines(path);

        final List<BankTransaction> bankTransactions = bankStatementParser.parseLinesFrom(lines);

        System.out.println("The total for all transactions is " + calculateTotalAmount(bankTransactions));
        System.out.println("Transactions in January " + selectInMonth(bankTransactions, Month.JANUARY));
    }

    private static double calculateTotalAmount(final List<BankTransaction> bankTransactions) {
        return bankTransactions.stream().mapToDouble(BankTransaction::getAmount).sum();
    }

    private static List<BankTransaction> selectInMonth(final List<BankTransaction> bankTransactions, final Month month) {
        return bankTransactions.stream()
                .filter(bankStatement -> month.equals(bankStatement.getDate().getMonth()))
                .collect(Collectors.toList());
    }
}

구현 코드의 BankTransaction 객체에서 직접 정보를 추출하기 때문에 내부 파싱 방법을 알 필요가 없어졌다.

이제 메인 응용 프로그램에서 파싱 로직을 구현하는 부분이 사라졌다. 파싱 기능을 다른 클래스와 메서드에 위임했고 이 기능을 독립적으로 구현. -> BankStatementCSVParser 클래스

이제 새 요구사항이 들어오면 BankStatementCSVParser 클래스로 캡슐화된 기능을 재사용한다.

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
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
글 보관함