코스 3주차 후기


 

드디어 4주차에 접어들면서 마무리를 향해 나아가고 있다. 3주차 동안의 주요 성과는 아래와 같다.

1. JAVA 언어에 대한 익숙함

 JAVA에 좀 더 익숙해지면서 여러 기능과 문법에 대해 익히는 기간이었다. 단순히 문법을 넘어 컬렉션의 특징에 대해서도 여러 공부하게 되었다.

2. 책임과 역할 분리에 대한 고민

 코드 작성 시 역할과 책임을 명확히 나누는 것에 대해 고민하는 시간이 되었다. 객체지향적 설계는 단순히 성능을 위한 것이 아니라, 협업과 코드 관리에서 큰 차이를 만든다고 생각한다. 역할이 분명해지니, 기능이 명확해지고 가독성 또한 높아졌다.(코드 리뷰 받을라고 더 신경쓰는게 더 크긴 함)

3. 테스트 코드 작성

 테스트 코드를 작성하는 것은 python언어 쓰면서도 하고는 있었다. 하지만 그 전에는 TDD보다는 '기능 -> 테스트' 이런 순서였다면 이번에는 '문서 -> 테스트 코드 -> 기능 구현' 순서로 작성하였다. 이런 순서로 하니 확실히 내가 해야할 일을 쉽게 정의하고 관리가 되는 느낌이다.

4. 코드 리뷰

 지금까지는 여러 프로젝트나 현업에서 코드 리뷰를 해본 적이 없어서, 내가 다른 사람 코드를 리뷰하거나 내 코드를 리뷰받는 경험이 이번이 처음이었다. 이번 코드 리뷰 과정에서 느낀 건, 같은 기능이라도 사람마다 접근 방식이 달라서 정답이 없다는 것이다. 내가 몰랐던 부분을 다른 사람이 조언해 주고, 또 내가 아는 걸 다른 사람에게 알려주면서 혼자 공부하는 것보다 훨씬 시너지가 난다는 느낌이 들었다. 확실히 서로 피드백 주고받는 게 학습에 도움이 된다.

 

 

1. 기능 요구사항

이번 프로젝트의 기능 요구사항은 로또 시스템을 만드는 것이다. 주요 흐름은 다음과 같다:

  1. 금액 입력
  2. 로또 번호 추첨
  3. 당첨 번호 및 보너스 번호 입력
  4. 결과 출력

이 흐름을 기반으로 필요한 기능들을 분리해보면 다음과 같다

 

 

  1. LottoMachine: 로또 발권 기능 담당 (1~45 사이의 중복되지 않는 번호 생성)
  2. LottoPrize: 당첨 금액 관리 기능
  3. LottoReport: 당첨 금액을 계산하고, 당첨 통계를 내는 기능

 

- 아쉬운 점

기능 분리의 어려움

처음에는 역할을 최대한 분리하려고 했지만, 실제 구현 단계에서 애매한 부분이 많았다. "이 기능을 여기에 두어야 하나, 아니면 분리해야 하나?" 같은 고민이 반복되었다. 또한 프로그램 요구사항을 따르다 보니 내가 생각한 구조와는 다른 방향으로 흘러가는 부분도 있어 어려움이 컸다.

Manage에 메소드 분리를 못함

 Manage 클래스에 너무 많은 기능이 몰리게 되었다. 급하게 구현하면서 돌아가도록 하는 데에 초점을 맞추다 보니 이 클래스에 많은 역할을 담게 되었는데, 나중에 수정하거나 개선할 필요가 있을 것 같다.

최대한 역할 분리를 유지하면서도 요구사항을 지켜내려는 과정에서 많은 고민이 있었다. 앞으로는 이 부분을 더 잘 정리해보는 게 목표다.

 

 

2. 리뷰

이번에는 코드 맞리뷰를 따로 진행하지 않았다. 이유는 시간 문제도 있었지만, 코드가 너무 방대해서 하루 정도는 잡아야 제대로 리뷰할 수 있을 정도였기 때문이다. 내 코드도, 다른 사람 코드도 꽤 많은 양이라 이번에는 직접 리뷰 대신, 다른 사람들의 코드를 눈으로만 몇 개 살펴보았다.

정리하자면, 대부분이 MVC 모델로 패키지를 관리하고 있었다. 이전보다 역할을 더 분리하고 체계적으로 관리하려는 흔적들이 눈에 띄었다. 역할이 분리되어 확실히 코드 구조가 정돈된 느낌을 받았다.

 

다음은 공통피드백을 정리한 내용이다. 그 다음 주차 미션에 반영해야 한다.

 

2.1. 객체는 객체답게: Lotto 클래스 개선하기

객체지향 설계에서 중요한 것 중 하나는 객체가 객체답게 동작하도록 만드는 것이다. 

public class Lotto {
    private final List<Integer> numbers;
   
    public Lotto(List<Integer> numbers) {
        this.numbers = numbers;
    }

    public List<Integer> getNumbers() {
        return numbers;
    }
}
 
위의 코드르 보면, Lotto 클래스는 numbers라는 숫자 리스트를 상태 값으로 가지고 있지만, 내부 로직 없이 단순히 데이터를 담고 getter 메서드로 반환하는 형태다. 이렇게 외부에서 데이터를 꺼내어(get) 사용하는 방식은 객체지향 설계 관점에서 바람직하지 않다. 객체가 자신의 데이터를 스스로 처리하지 않고, 외부에서 데이터를 끄집어내 사용하는 방식이 되기 때문이다.
 

 

public class LottoGame {
    public void play() {
        Lotto lotto = new Lotto(...);

        // 숫자가 포함되어 있는지 확인
        if (lotto.containsNumber(number)) {
            // 로직 수행
        }

        // 당첨 번호와 일치하는 개수 확인
        int matchCount = lotto.matchCount(winningNumbers);
    }
}
 

 이런 구조에서는 데이터 조작이 주로 외부에서 이루어지게 된다. 예를 들어 LottoGame 클래스에서 getNumbers() 메서드를 호출해 numbers 데이터를 가져오고, 숫자가 포함되어 있는지 확인하거나 당첨 번호와 일치하는 개수를 세는 작업을 모두 외부에서 처리하게 된다. 이래서 getter를 지양하라는 것 같다.

  

public class Lotto {
    private final List<Integer> numbers;

    public Lotto(List<Integer> numbers) {
        this.numbers = numbers;
    }

    public boolean containsNumber(int number) {
        return numbers.contains(number);
    }

    public int matchCount(List<Integer> winningNumbers) {
        return (int) winningNumbers.stream()
                                   .filter(numbers::contains)
                                   .count();
    }
}

 

개선된 버전은 위와 같다. Lotto 객체는 단순히 데이터 저장소로 사용될 뿐, 실제 로직을 처리하는 데 관여하지 않는다. 이처럼 객체에 있는 데이터를 외부로 꺼내서(get) 사용하는 대신, 해당 객체가 데이터 처리도 스스로 하도록 구조를 변경하는 것이 좋다. 이렇게 하면 Lotto 클래스는 단순 데이터 저장소가 아닌, 데이터에 대한 책임을 가진 "일하는 객체"가 된다.

 

Lotto 클래스가 직접 containsNumber와 matchCount 메서드를 가지게 하면, LottoGame 클래스에서는 Lotto 객체의 내부 데이터를 직접 꺼내올 필요가 없다. 대신 Lotto 객체에 메시지를 던져서, 객체가 알아서 데이터를 처리하게 하는 방식이다.

 

2.2. 테스트 가능한 코드 설계

 테스트하기 좋은 구조를 만드는 것에 대한 내용이다. 예를 들어, 랜덤한 값을 사용하게 되면 테스트가 어려워지는데, 이를 해결하기 위해 랜덤 값을 통제 가능한 구조로 만드는 것이다. 이렇게 설계하면 테스트 코드에서 다양한 시나리오를 시뮬레이션할 수 있고, 로직의 신뢰성을 높일 수 있다.

 

 참고한 글( https://tecoble.techcourse.co.kr/post/2020-05-07-appropriate_method_for_test_by_parameter/ )에서는 테스트가 어려운 상황에 대한 설명과 함께, 테스트 가능한 메서드를 설계하는 방법에 대해 다루고 있었다. 그 중 랜덤 값을 제어 가능하게 만들어야 한다는 점이다. 이 원칙을 기반으로 앞으로 더 많은 테스트 가능한 코드를 작성하려고 한다. 테스트 가능한 메서드 설계는 앞으로도 계속 신경 써서 반영할 계획이다.

 

2.3. private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다.

내가 이저 후기에서도 썼던 내용이다. private 메서드를 테스트하는 상황이라면 분리를 하는 것을 고려해야 한다는 것이다.

 

private 메서드는 클래스 내부에서만 접근할 수 있기 때문에, 외부에서 직접적으로 테스트할 수 없다. 보통 public 메서드를 통해 간접적으로 private 메서드를 테스트하게 되는데, 문제는 이 private 메서드가 단순히 가독성을 위해 분리된 것이 아니라 중요한 로직을 수행하는 경우다.

 

이럴 때는 클래스 분리를 고려해볼 필요가 있다. 중요한 역할을 수행하는 private 메서드를 새로운 클래스로 분리하면, 이 메서드를 public으로 노출하여 테스트할 수 있게 된다. 이 접근 방식은 단일 책임 원칙(Single Responsibility Principle, SRP)에도 부합한다.

 

SRP에 따라 하나의 클래스는 하나의 책임만 가지고, 하나의 목적에 집중해야 한다. 중요한 로직을 수행하는 private 메서드를 분리해 별도의 클래스로 관리하면, 각 클래스가 고유한 책임을 가지게 되어 응집도가 향상된다. 또한, 분리된 클래스는 개별적으로 테스트할 수 있어 테스트 가능성이 높아지고, 유지보수에도 유리하다.

 

따라서 private 메서드가 중요한 역할을 하고 있거나 복잡한 로직을 가지고 있다면, SRP를 준수하고 테스트 가능성을 높이기 위해 클래스를 분리하는 것을 고려해보는 것이 좋다.

'컴퓨터 > 기타' 카테고리의 다른 글

코스 4주차 후기  (1) 2024.11.18
코스 2주차 후기  (0) 2024.10.28
코스 1주차 후기  (0) 2024.10.23
Database - 데이터베이스 정규화  (0) 2024.04.18