오전에는 세 가지 내용의 강의를 들었다.
1. 우테코에 임하는 자세
우테코는 물고기를 잡을 수 있는 환경을 만들어 준다.
2. 단위 테스트
main method를 테스트 했을 때의 문제점, JUnit, assertion

  • 내가 단위 테스트를 작성하는 이유는 무엇인가?

->모든 기능이 잘 작동하는지 테스트하기 위해

  • 내가 작성한 좋은 단위 테스트는 어떠한 부분에서 좋은 단위 테스트라 느꼈는가?

-> 예외를 발생시킬 수 있는 값을 예상하여 테스트함

  • 위와 같은 좋은 단위 테스트를 작성하기 위해 어떠한 시도를 해볼 수 있는가?

-> 경계값 검증


3. 코드 품질

  • 코드 품질이 중요한 이유 중 가장 와닿는 이유는 무엇인가?

-> 보기 좋은 코드. 보기 싫은 코드를 봤을 때 이해를 하기 싫은 경험이 있다. 따라서 보기 좋은 코드가 가장 와닿았다.

  • 위 이유를 만족하기 위한 코드를 작성하기 위해 어떠한 노력을 해봤는가? 혹은 할 예정인가?

-> 다른 개발자가 내 코드를 본다는 관점에서 코드를 작성한다.

  • 코드 품질을 높은 코드를 작성하는 프로그래머가 훌륭한 프로그래머인가? 그렇게 생각한 이유는 무엇인가?

-> 훌륭한 프로그래머이다. 프로그램의 완성은 런칭이다. 즉, 이제 시작이기 때문에 앞으로 많은 유지보수 작업을 해야함. 앞으로의 작업을 하기 위해 좋은 품질의 코드에서 시작해야 다른 사람이 코드를 이해하고, 수정할 수 있고, 비용도 적어지기 때문


오후에는 페어프로그래밍을 시작했다. 요구사항을 먼저 정리하고 설계는 따로 하지 않고 프로그램의 실행의 흐름대로 프로그래밍하기 시작했다. 오랜만에 코딩을 하니 설계에 너무 무심했다는 생각이 들었다. "객체지향의 사실과 오해"를 다시 읽어보며 객체지향에 대해 다시 고민하는 시간을 가져야겠다.
테스트를 작성하는 과정에서 페어에게 @DisplayName과 @Nested의 사용법에 대해 알려줬다. 코딩을 오랜만에 해서 기억이 잘 나지 않았는데 알려주면서 나도 공부가 되었다.
내가 여러 String을 ,를 구분자로 연결하는 ( ex) a, b, c, d ) 메서드를 작성할 때 StringBuilder 를 사용해서 구구절절 코드를 작성하고 있을 때 페어가 String.join을 사용하는 것을 제안했다. 코드가 한줄로 줄어드는 것을 보고 매우 기분이 좋았다.
로직 오류가 발생했을 때 서로 고민하고 해결하는 과정이 재미있었다.
테스트를 작성할 때 private으로 사용하는 아주 작은 기능에 대한 테스트를 할 것인지, 랜덤 테스트는 어떻게 할 것인지 결정하는데에 어려움을 겪었다.
코딩에 대한 생각이 서로 다를 수 밖에 없으므로 상대에게 나의 생각을 타당한 근거를 기반으로 설득해야한다. 그 과정에서 내가 코딩하는 근거를 확실하게 함으로서 근거가 적절하면 확신이 들고, 그렇지 않으면 적절하지 않을 수 있다는 생각이 들었다.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.2.15 일일 회고  (0) 2023.02.15
2023.02.14 일일 회고  (0) 2023.02.15
2023.02.13 회고  (0) 2023.02.14
2023.2.10 회고  (0) 2023.02.10
2023.2.9 회고  (0) 2023.02.10

일급 컬렉션이란?

Collection을 Wrapping하면서, Wrapping한 Collection 외 다른 멤버 변수가 없는 상태를 일급 컬렉션이라 한다.

 

다음의 예를 살펴보자.

List<Integer> lotto = new ArrayList<>();
lotto.add(1);
lotto.add(2);
lotto.add(3);
lotto.add(4);
lotto.add(5);
lotto.add(6);

로또번호 6자리를 임의로 뽑는 간단한 코드를 작성했다. 여기서 Collection을 Wrapping 해보자.

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

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

이것이 일급 컬렉션이다. 일급 컬렉션을 왜 써야할까?

로또 번호를 뽑는 코드에 숫자를 검증하는 로직이 추가된다고 가정해보자.

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

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

    private void validate(List<Integer> numbers) {
        // 꺼내서 1~45인지 확인한다.
    }
}

일급 컬렉션 내에서 검증에 대한 로직을 수행하기 때문에 다른 추가 코드가 발생하더라도 Lotto를 사용하는 코드에서 수정되는 코드는 없을 것 이다. 즉, 중복을 줄여주고 로직이 사용되는 클래스를 가볍게 해준다.

 

또한, 컬렉션의 불변을 보장한다.

final List<Integer> lotto = new ArrayList<>();
lotto.add(1);
lotto.add(2);
lotto.add(3);
lotto.add(4);
lotto.add(5);
lotto.add(6);

final 키워드는 단순히 재할당만 금지할 뿐 위와 같은 코드는 정상적으로 동작할 것이다. 즉, 불변을 보장할 수 없다.

불변을 보장할 수 없게 되면 소프트웨어의 크기가 커질수록 코드를 이해하고 수정하는데 어려워질 수 있다.

일급 컬렉션의 불변을 보장하기 위해서는 위에서 작성했던 Lotto 클래스에 수정해야 할 것이 있다.

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

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

생성자 부분에서 List를 새로 재할당하여 저장하는 것이다. 재할당을 하지 않고 주소 값만 사용할 경우 전달받은 리스트에 수정을 가하면 불변을 보장할 수 없기 때문이다.

 

참고자료

[많은 시간을 투자한 부분]

 

피어 리뷰

이번 프리코스를 진행하면서 현업에서 프로젝트를 진행한다는 마인드로 프리코스에 임했습니다. 그 중 실천하고자 했던 것이 1회 피어 리뷰 실천하기이었습니다. 매 주 저의 PR링크를 올렸으나 3주 차 과제를 올렸을 때 처음으로 리뷰를 받았습니다. 리뷰의 내용과 공통 피드백을 비교하면서 공통 피드백의 내용도 실제로 어떻게 적용할 수 있을지 이해가 되기 시작했습니다. 리뷰를 바탕으로 리팩터링을 하는 과정에서 getter를 어떻게 줄일 것 인지, 객체의 책임 분배를 어떻게 할 것인지, 테스트 코드 리팩터링 등 많은 깨달음을 얻었습니다.

 

2. 컨트롤러 분리

처음 설계에서는 메인 컨트롤러 1개가 모든 control flow를 담당하고 있었습니다. 구현을 하는 과정에서 요구사항인 메서드 10 라인을 넘기지 않기가 매우 어려웠습니다. 컨트롤러 또한 분리가 필요하다고 생각해서 초기 설정을 담당하는 SettingController와 게임 플레이를 담당하는 PlayingController로 분리하고 이 두 가지를 제어하는 BridgeController 로 설계를 변경하니 컨트롤러의 책임이 더 명확해졌습니다.

 

3. 리팩터링

이번 과제는 완벽한 설계를 하려고 너무 많은 시간을 쏟지 않으며 구현을 하고, 리팩터링을 하는 방식으로 진행했습니다. 구현한 기능 중에서 이동한 칸을 선택해서 정답이고 다리의 끝에 도착하면~” 또는 이동한 칸이 오답이고 재시도 입력을 받으면~” 와 같은 기능을 의식의 흐름대로 중첩 if문과 반복문으로 작성했습니다. 완성된 코드를 보고 if문 줄이기, Enum 사용, 메서드 분리 등 하나씩 개선하면서 훨씬 가독성이 좋은 코드로 리팩터링에 성공했습니다.

 

[배운점]

 

1. 객체답게 사용하기

이전에는 필요한 값들에 대해 인스턴스 변수를 선언하고 이에 대해 get해서 사용하는 방식으로 구현을 했습니다. 피어 리뷰와 공통 피드백을 수용하여 값을 get해서 외부에서 계산하지 않고, 필요할 때 객체 내부에서 계산해서 결과만 돌려주는 형태로 구현했습니다. 이러한 과정에서 인스턴스 변수도 줄고, getter도 없어져서 더 깨끗한 코드를 작성할 수 있었습니다.

 

2. 명확한 예외 처리

예외처리를 할 때 단순히 Exception e; e.printStackTrace; 로 구현을 하곤 했습니다. 이렇게 구현하니 다른 사람은 나의 코드를 보고 어떤 예외가 발생할지 예측이 어려웠습니다. 이번 과제를 진행하면서 잘못된 입력에 대하여 예외를 명확하게 지정함으로써 처리한 예외의 범위가 확실해지고 가독성이 높아지는 것을 느낄 수 있었습니다.

 

[느낀점]

기능 목록 분리, 클래스 분리, 테스트, 리팩터링 등 처음에는 매우 어렵고 고민도 많이 했지만 조금씩 익숙해 지는 것을 느꼈습니다. “첫 술에 배부를 수 없다.” 라는 말처럼 꾸준히 적용하고, 피드백을 수용하며 학습을 하다 보면 저도 좋은 개발자가 될 수 있을 것 이라고 생각했습니다.

이번 프리코스를 진행하며 1주 마다 제출했던 과제를 보면서 1주일 마다 크게 성장한 것을 확인할 수 있었습니다. 이 과정을 모두 마치고 많은 것을 배우고 경험하며 성장했음에 매우 뿌듯합니다.

'회고 > 우테코 프리코스' 카테고리의 다른 글

3주 차 회고  (0) 2022.11.17
2주 차 회고  (0) 2022.11.10
1주 차 회고  (0) 2022.11.03

[많은 시간을 투자한 부분]

 

1) 클래스(객체) 분리

첫 단추가 잘 꿰어져야 그 이후가 순조롭게 진행된다고 생각하여 객체를 설계하는 데 시간을 많이 투자했습니다. 2주 차 미션까지는 단순히 기능을 먼저 정리하고 기능에 따라서 클래스를 분리했습니다. 이러한 과정에서 무언가 체계적이지 않고 확실한 기준 없이 내 마음대로 객체를 설계한다는 생각이 들었습니다. 따라서 3주 차 미션에서는 객체 설계 방법에 대해 학습했으며 그 중 도메인 모델을 사용해 설계하였습니다. 그림으로 직접 도메인 모델을 설계한 결과, 구현하기 위해 필요한 객체들과 그 책임이 명확해지고, 협력 또한 한 눈에 파악할 수 있었습니다.

 

도메인 모델

역시 과제를 하면서도 모호했던 것이 데이터를 꺼내서 사용하는 getter 였습니다. 꼭 필요했다고 생각했었지만 

 

2) 예외 처리

기본으로 제공된 ApplicationTest의 예외_테스트() 부분에서 어려움을 겪었습니다. 예외_테스트() 에서는 assertThatThrownBy() 으로 예외를 체크하는 것이 아니라 assertThat(output()).contains(ERROR_MESSAGE) 로 출력 메시지만 체크를 했기 때문에 단순히 throw new IllegalArgumentException 으로 구현하면 테스트에 실패했습니다. 제공된 기능 요구 사항에는 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.” 라고 되어 있었기 때문에 예외 처리를 하지 않고 메시지만 출력할 수도 없었습니다. 여러 가지 방법을 고민했고, 예외가 발생하는 부분을 throw하고, 구현된 기능들을 사용하여 전체 로직을 구성하는 매니저에서 catch하여 전체 로직을 멈추고 예외와 메시지를 출력, 종료하도록 구현했습니다.

 

3) 도메인 로직과 UI 분리

도메인 로직과 UI를 분리하기 위해 가장 널리 알려진 MVC 패턴에 대해 학습했습니다. 학습하는 과정에서 모델, , 컨트롤러로 분리한다는 것은 알겠는데, 내 코드를 실제로 어떻게 분리할 수 있을지에 대해 고민을 했습니다. 처음에 설계한 도메인 모델에 객체들을 사용하는 매니저 단을 추가하여 해결할 수 있었습니다. 매니저에서 전체 로직을 수행하며 필요한 UI 클래스와 도메인 클래스를 사용하는 방식으로 프로그램을 완성해 도메인 로직과 UI를 분리할 수 있었습니다.

 

[느낀 점]

추가된 요구 사항 중에서 함수의 길이를 15라인으로 제한하는 점이 좋았습니다. 길이가 긴 함수는 두 가지 이상의 기능을 할 가능성이 높기 때문에 분리되지 않은 기능을 더 세분화하여 분리할 수 있었습니다.

if-else 문을 사용하지 않음으로써 가독성이 매우 향상된 것을 느꼈습니다.

2주 차 미션에서는 도메인 로직과 UI 로직을 분리하지 않아서 테스트를 진행할 때 중복이 존재하는 등 애매한 부분이 있었습니다. 3주 차 미션을 진행하면서 도메인 로직과 UI을 분리하고 테스트를 진행해 보니 명확한 테스트를 진행할 수 있었습니다.

 

[배운 점]

[Enum]

enum은 다른 언어에서도 사용해본 적이 있지만 단순히 가독성만 늘려주는 정도였습니다. 하지만 JAVA에서의 enum은 훨씬 더 강력했습니다. 특히 enum클래스에 메서드를 구현할 수 있는 점이 인상 깊었습니다. 직접 예제를 만들어 보며 enum에 대해 학습하고 블로그에 글을 작성했는데, 그 과정이 매우 재미있었습니다.

 

작성한 블로그 : https://matouslescotousles.tistory.com/6

 

[SOLID]

클래스를 분리하기 위해 객체 설계에 대해 학습하던 중 SOLID에 대해 알게 되었습니다. Single Responsibility Principle 는 우테코 프리코스 1주 차 부터 강조했던 부분이여서 매우 반가웠습니다. 특히 Open/Closed Principle 원칙의 인터페이스에 의존하는 코드의 장점을 자주 사용하는 Collection 클래스를 통해 느꼈습니다.

 

작성한 블로그 : https://matouslescotousles.tistory.com/7

 

[immutable vs mutable]

테스트를 작성하던 중 에러가 생겨서 디버깅을 진행했습니다. ListretainAll() 연산을 수행하는 부분이 있었는데, 테스트 코드에서는 ListList.of()를 통해 immutablelist로 초기화하고 있었습니다. 따라서 retainAll() 연산을 진행할 새로운 mutable list를 생성하여 리턴하도록 구현했습니다. mutable listimmutable list가 나뉘어져 있다는 것을 처음 알았으며, 안정성을 위해 가능한 immutable을 사용해야 한다는 생각을 했습니다.

 

[도메인 로직과 UI 분리의 중요성]

도메인 로직과 UI를 분리하는 방법에 대해 학습하던 중 한 동영상에서 설명한 내용이 있었습니다. 우리가 지금 작성하는 프로그램의 입출력은 콘솔이지만 입출력을 윈도우, , 모바일로 변경했을 때를 생각해 보라는 내용 이었습니다. 도메인 로직과 UI 코드를 섞어서 구현할 경우 이를 전부 새로 구현해야 된다는 생각에 도메인 로직과 UI 분리의 중요성에 대해 깨달았습니다.

 

시청한 영상 : https://www.youtube.com/watch?v=efMRwQwxmJM

 

'회고 > 우테코 프리코스' 카테고리의 다른 글

4주 차 회고  (0) 2022.12.01
2주 차 회고  (0) 2022.11.10
1주 차 회고  (0) 2022.11.03

SOLID 란?

로버트 마틴이 2000년대 초반에 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 만들었다. SOLID는 이 원칙들의  앞글자만 따낸 결과이다.

SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스코드를 읽기 쉽고 확장하기 쉽도록 리팩토링 하기 위해 적용할 수 있는 원칙이다.

 

SOLID 5원칙

  • SPR - 단일 책임 원칙
  • OCP - 개방 폐쇄 원칙
  • LSP - 리스코프 치환 원칙
  • ISP - 인터페이스 분리 원칙
  • DIP - 의존관계 역전 원칙

각 원칙에 대해 하나씩 알아보자.

 

1. SPR (Single Responsibility Principle)

단일 책임 원칙. 작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 하나의 책임을 수행하는 데 집중되어 있어야 한다. SPR 원리를 적용하면 책임 영역이 하나로 확실해지기 때문에 책임이 변경되었을 때 수정해야할 영역이 한정되고 줄어든다. 책임을 분리함으로써 코드의 가독성이 향상되고 유지보수가 좋아진다.

 

2. OCP (Open/Closed Principle)

 

개방 폐쇄의 원칙. 소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에 열려있고,  변경에는 닫혀있어야 한다는 원리. 변경에 닫혀있어야 한다는 말은 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야 한다는 의미이다. 확장에 열려있어야 한다는 말은 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻이다. 따라서 변경을 위한 비용은 줄이고 확장을 위한 비용은 극대화 해야 한다는 의미다.

 

적용 방법

1. 변경될 것과 변경되지 않을 것을 구분한다.

2. 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.

3. 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드를 작성한다.

 

3. LSP (Liskov Substitution Principle)

 

리스코브 치환의 원칙. 서브타입은 언제나 기반 타입과 호환될 수 있어야 한다. ex) ArrayList와 List.

다형성과 확장성을 극대화 하기 위해 하위 클래스보다 상위 클래스(인터페이스)를 사용하는 것이 좋다.

일반적으로 선언은 기반 클래스로, 생성은 구체 클래스로 대입하는 방법을 사용한다. 아래는 그 예시다.

List<Integer> list = new ArrayList<>();

이러한 구조는 LSP가 OCP를 구성하는 구조가 되도록 한다. 

 

적용 방법

1. 만약 두 개체가 같은 일을 한다면 둘을 하나의 클래스를 표현하고 이들을 구분할 수 있는 필드를 둔다.

2. 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현한다.

3. 공통된 연산이 없다면 별개인 2개의 클래스를 만든다.

4. 만약 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 구현 상속을 사용한다.

 

4. ISP (Interface Segregation Principle)

 

인터페이스 분리의 원칙. 인터페이스를 분리하여 가능한 최소한의 인터페이스를 사용하도록 한다. 하나의 일반적인 인터페이스를  implement 하는 것 보다, 여러 개의 세부적인 인터페이스를 implement 하는 것이 낫다. 인터페이스에 클래스가 사용하지 않는 메서드가 포함되어 있다면 과감히 인터페이스를 분리한다. 

 

5. DIP (Dependency Inversion Principle)

 

의존성역전의 원칙. 구체화에 의존하지 않고 추상화에 의존한다.

고수준 모듈이 저수준 모듈의 구현에 의존하는 것이 아니라 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다. 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다.

 

 

참고자료

enum 이란?

enum 타입은 변수가 미리 정의된 상수 집합이 될 수 있게 하는 특수한 데이터 타입이다. 변수는 변수에 대해 미리 정의 된 값 중 하나와 같아야 한다. 흔한 예로 나침반 방향(NORTH, SOUTH, EAST, WEST)이나 요일이 있다.

상수이기 때문에 enum 타입의 필드 이름은 대문자로 표현한다.

Java 에서는 enum 키워드를 사용하여 enum 타입을 정의한다.

 

enum의 장점

  • 코드가 단순해지며 가독성이 좋아진다.
  • IDE를 이용할 때 컴파일러가 자동으로 오타를 찾아주는 등 편리하다.
  • 변경에 유연하다.
  • 데이터들의 관계를 표현하기 좋다.

 

enum 사용법

여러 사례들을 통해 enum 사용법에 대해 알아보자.

1. 단순 enum 패턴

다음은 요일에 대한 enum type을 정의한다.

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY 
}

 요일에 따라 switch-case 문을 동작시킴으로써 코드의 직관성을 높여준다.

public class EnumTest {
    Day day;
    
    public EnumTest(Day day) {
        this.day = day;
    }
    
    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;
                    
            case FRIDAY:
                System.out.println("Fridays are better.");
                break;
                         
            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;
                        
            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }
    
    public static void main(String[] args) {
        EnumTest firstDay = new EnumTest(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumTest fifthDay = new EnumTest(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumTest sixthDay = new EnumTest(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumTest seventhDay = new EnumTest(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}

결과는 아래와 같다.

Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.

 

2. 다른 행성에서 몸무게 구하기

행성에 따라서 나의 몸무게를 구하는 프로그램을 만들어보자.

만약 지구에서 몸무게가 56kg 나가는 사람이 있다고 하자. 각 행성별 중력은 다음의 표와 같이 비례한다고 가정하자.

행성 중력
지구 1
토성 2
목성 3

if문의 조건에 따라 행성에 따른 몸무게를 표현할 수 있을 것이다.

private void printWeight(String where) {
    int myWeight = 56;
    int result;
    if (where.equals("지구")) {
        result = getWeightOnEarth(myWeight);
        System.out.println("지구에서 나의 몸무게는 : " + result);
    }
    if (where.equals("토성")) {
        result = getWeightOnSaturn(myWeight);
        System.out.println("토성에서 나의 몸무게는 : " + result);
    }
    if (where.equals("목성")) {
        result = getWeightOnJupiter(myWeight);
        System.out.println("목성에서 나의 몸무게는 : " + result);
    }
}

이러한 방식은 당장은 직관적이고 괜찮아보이지만, 행성의 종류가 늘어났을 때 늘어나는 if문과 각 행성별로 구현해야하는 getWeightOn~메서드를 감당할 수 없을것이다. enum을 사용해서 이를 해결해보자.

public enum Planet {
    EARTH(value -> value * 1, "지구"),
    SATURN(value -> value * 2, "토성"),
    JUPITER(value -> value * 3, "목성");

    Planet(Function<Integer, Integer> expression, String where) {
        this.expression = expression;
        this.where = where;
    }

    private Function<Integer, Integer> expression;
    private String where;

    public int getWeight(int value) {
        return expression.apply(value);
    }

    public String getWhere() {
        return where;
    }
}

자바에서의 enum은 필드와 함수를 포함할 수 있다. 각 행성들이 함수형 인터페이스 Function을 구현하도록 함으로써 행성별로 몸무게를 구하는 함수를 한눈에 확인할 수 있다. 이렇게 하면 새로운 행성이 추가되더라도 수정할 내용이 매우 적어진다. 

참고 : enum 타입 생성자는 항상 private 이어야 하며 직접 호출할 수 없다.

다음으로 enum을 사용하는 코드를 확인해보자.

private void printWeight(String where) {
    int myWeight = 56;

    for (Planet p : Planet.values()) {
        if (p.getWhere().equals(where)) {
            System.out.println(where + "에서 나의 몸무게는 : " + p.getWeight(myWeight));
        }
    }
}

enum 클래스는 내부적으로 java.lang.Enum을 상속하도록 구현되어있다. java.lang.Enum 에는 values()가 구현되어있는데  enum 의 모든 원소를 배열로 리턴한다. (여기서 다른언어의 enum 보다 java의 enum이 강력한 기능을 보유한다는 것을 알 수 있다.) 

 

3. 데이터 그룹 관리하기

배달어플중에서 음식 카테고리를 구현한다고 해보자.

음식들의 카테고리와 카테고리에 속하는 음식들은 다음과 같다.

카테고리 음식
한식 김치찌개, 된장찌개, 삼겹살
중식 짜장면, 짬뽕
패스트푸드 치킨, 햄버거, 피자

이 관계를 enum 으로 다음과 같이 구현할 수 있다.

public enum KindOfFood {
    KOREAN_FOOD("한식", Arrays.asList("김치찌개", "된장찌개", "삼겹살")),
    CHINESE_FOOD("중식", Arrays.asList("짜장면", "짬뽕")),
    FAST_FOOD("패스트푸드", Arrays.asList("치킨", "햄버거", "피자"));
    private String kind;
    private List<String> foods;

    KindOfFood(String kind, List<String> foods) {
        this.kind = kind;
        this.foods = foods;
    }
}

 

이러한 경우, 파라미터로 전달받은 음식의 이름이 잘못되었을 경우 문제가 생기는 등 관리가 어렵다.

public enum Food {
    KIMCH_STEW("김치찌개"),
    SOYBEAN_PASTE_STEW("된장찌개"),
    PORK_BELLY("삼겹살"),
    JAJANGMYEON("짜장면"),
    CHAMPON("짬뽕"),
    CHICKEN("치킨"),
    PIZZA("피자"),
    HAMBURGER("햄버거");

    private String name;

    Food(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

이렇게 Food enum을 정의해 음식 데이터 그룹을 관리할 수 있다. 이제 문자열을 enum으로 바꿔주자.

public enum KindOfFood {
    KOREAN_FOOD("한식", Arrays.asList(Food.KIMCH_STEW, Food.SOYBEAN_PASTE_STEW, Food.PORK_BELLY)),
    CHINESE_FOOD("중식", Arrays.asList(Food.JAJANGMYEON, Food.CHAMPON)),
    FAST_FOOD("패스트푸드", Arrays.asList(Food.CHICKEN, Food.HAMBURGER, Food.PIZZA));
    private String kind;
    private List<Food> foods;

    KindOfFood(String kind, List<Food> foods) {
        this.kind = kind;
        this.foods = foods;
    }
}

 

여태까지는 데이터 그룹을 표현하기 위한 코드만 작성했다. 

KindOfFood 를 실제로 사용하기 위해 메서드를 약간 추가해보자.

public static KindOfFood findKindByFood(Food food) {
    return Arrays.stream(KindOfFood.values())
            .filter(KindOfFood -> KindOfFood.hasFood(food))
            .findAny()
            .get();
}

private boolean hasFood(Food food) {
    return foods.stream()
            .anyMatch(name -> name == food);
}

public String getKind() {
    return kind;
}

findKindByFood는 Food를 파라미터로 전달받아 해당 Food가 어느 카테고리(KindOfFood) 에 속하는지를 찾아주는 메서드이다. 이를 사용해보자.

public static void main(String[] args) {
    Food food = Food.PIZZA;
    KindOfFood kindOfFood = KindOfFood.findKindByFood(food);

    System.out.println(kindOfFood.getKind());
}

결과는 :

패스트푸드

 

[많은 시간을 투자한 부분]

2주차 야구 게임 미션의 목표는 함수를 분리하고 각 함수별로 테스트를 작성하는 것에 익숙해지는 것 이었습니다. 작은 단위의 테스트를 잘 하기 위해서는 함수를 기능별로 가장 작게 분리하는 것이 중요하고, 이를 달성하기 위해서는 객체지향 설계가 가장 중요하다고 생각했습니다. 따라서 책 객체지향의 사실과 오해에서의 설계 방법을 직접 적용해보았습니다. 가장 먼저 도메인 모델을 설정하고, 객체들이 해야 할 행동책임을 기준으로 객체를 설정했고, 객체가 하나의 책임만 담당하도록 하였습니다. 결과를 출력하는 객체는 결과를 검증하는 객체로부터 결과 값을 받아오는 협력을 하도록 설계했습니다. 객체들이 해야 할 행동들을 정했더니 객체별로 구현해야 할 함수들의 목록이 구체화되었습니다. 이에 따라 구현해야 할 기능 목록들을 정하고 구현했습니다.

과제에 구현되어 있는 테스트는 통합 테스트였습니다. 처음에는 구현되어 있는 테스트 코드에 테스트 케이스를 추가하는 방식으로 테스트 코드를 짰습니다. 테스트 케이스 추가를 ApplicationTest 파일에 했는데, 이렇게 하니 과제 제출의 예제 테스트에서 예기치 못한 오류로 인하여 실행에 실패하였습니다.”가 발생했습니다. 로컬 환경에서는 테스트가 잘 되었기 때문에 결과에 많은 의문을 품고 고민했습니다. 웹에서의 예제 테스트가 ApplicationTest를 기반으로 테스트한다고 예상했고, ApplicationTest 파일을 처음 상태로 복구하고, 테스트 파일을 따로 만들어서 테스트했습니다. 테스트 코드를 다시 분석을 해보니 추가한 테스트 케이스를 잘못 구현했을 수 있다는 생각을 했습니다.

 

[느낀 점]

과제를 완성하기 위해 학습을 진행하며 많은 내용들을 습득했습니다. 코드를 분석하는 과정, 새로운 지식들을 학습하면서 학습한 내용들을 정리해야겠다는 생각이 들었습니다. 어떻게 정리를 할지 고민하던 중 리처드 파인만 공부법에 대해 알게 되었습니다. “리처드 파인만 공부법은 자신이 학습한 내용을 다른 사람에게 가르침으로써 자신의 이해를 증명하는 공부법입니다. 따라서 제가 학습한 내용들을 다른 사람에게 가르침으로써 저의 확실한 학습을 하기 위한 블로그를 개설하였습니다.

블로그 주소 : https://matouslescotousles.tistory.com/

 

[배운 점]

이번 프로젝트를 진행하면서 테스트 코드의 중요성을 깨달았습니다. 이전에는 단순히 프로그램을 실행시켜서 값을 입력하고, 출력되는 결과를 보고 테스트를 손수 진행했습니다. 작은 프로그램이였기 때문에 그동안 문제가 없었지만, 프로젝트가 커지면 이러한 방식으로 테스트를 하기 어렵겠다는 생각이 들었습니다. 이번 과제를 진행하면서 함수를 작은 기능으로 나누고, 이에 따라 테스트를 진행하면서 각 기능에 대한 테스트를 명확하게 할 수 있었습니다.

[JUnit5AssertJ]

구현되어있는 테스트 코드를 분석하며 JUnit5AssertJ의 사용법을 학습했습니다. 코드를 분석하다 보니 mock 객체가 있었습니다. 실제 객체를 만들어 테스트를 하기에는 불필요한 요소들이 많기 때문에 mock 객체에 필요한 데이터만 담아 테스트를 하는 것이 흥미로웠습니다. 테스트를 진행하며 JUnit5AssertJ의 기능이 막강하고 가독성이 뛰어남을 직접 느꼈기 때문에 애용해야겠다는 생각을 했습니다.

'회고 > 우테코 프리코스' 카테고리의 다른 글

4주 차 회고  (0) 2022.12.01
3주 차 회고  (0) 2022.11.17
1주 차 회고  (0) 2022.11.03

이번 과제에서는 함수를 분리하고, 각 함수별로 테스트를 작성하는 것에 익숙해지는 것을 목표로 하고 있다. 따라서 테스트 도구를 학습하기 위해 작성되어 있는 테스트를 분석해보았다.

 

과제를 간단하게 설명하자면, 야구 게임을 시작해서 랜덤 3자리 숫자를 생성한 뒤에, 사용자의 입력을 통하여 랜덤 숫자를 맞추는 게임이다.

같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.

 

  • 예) 상대방(컴퓨터)의 수가 425일 때
    • 123을 제시한 경우 : 1스트라이크
    • 456을 제시한 경우 : 1볼 1스트라이크
    • 789를 제시한 경우 : 낫싱

 

먼저 과제에 작성되어 있는 ApplicationTest.java 파일을 분석해보기로 했다.

 

1) 게임종료_후_재시작 테스트

@Test
void 게임종료_후_재시작() {
    assertRandomNumberInRangeTest(
            () -> {
                run("246", "135", "1", "597", "589", "2");
                assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
            },
            1, 3, 5, 5, 8, 9
    );
}

run()은 Application.main()을 실행하고 System.setIn()을 통해 인자 값을 표준 입력 스트림으로 설정하도록 내부적으로 구현되어 있었다.

게임종료_후_재시작() 함수는 assertRandomNumberInRangeTest() 함수를 호출하는데, 이는 다음과 같이 구현되어 있었다.

public static void assertRandomNumberInRangeTest(
    final Executable executable,
    final Integer value,
    final Integer... values
) {
    assertRandomTest(
        () -> Randoms.pickNumberInRange(anyInt(), anyInt()),
        executable,
        value,
        values
    );
}

Executable은 잠재적으로 Throwable을 throws할 수 있는 코드 블럭을 구현하는데 사용될 수 있는 함수형 인터페이스다.

Runnable와 비슷하지만 Executable은 어느 종류의 exception이든 throw할 수 있다는점이 다르다.

 

assertRandomTest()의 구현은 다음과 같다.

 

private static <T> void assertRandomTest(
    final Verification verification,
    final Executable executable,
    final T value,
    final T... values
) {
    assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
        try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
            mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
            executable.execute();
        }
    });
}

mock은 가짜 객체를 의미한다. 실제 객체를 만들어 테스트를 하기에는 테스트를 하기엔 불필요한 요소들이 많기 때문에 우리가 필요한 부분만 포함하는 가짜 객체를 만들어 테스트하는 비용을 절약할 수 있다.

따라서, 위 코드에서는 Randoms의 mock 객체를 생성한다. mock객체는 verification을 호출하면 thenReturn() 괄호 안의 값을 리턴하도록 정의 되어있다. 그 이후 executable.execute()를 함으로써 assertRandomNumberInRangeTest의 executable이 실행되는 것이다. 아래의 코드는 assertRandomNumberInRangeTest의 executable 부분이다. 

run("246", "135", "1", "597", "589", "2");
assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");

즉, 맨 void 처음 게임종료_후_재시작() 테스트는 Application을 실행하여 입력값으로 run()내부의 인자를 입력하고, 그 결과의 출력이 올바른지 검증하는 테스트임을 알 수 있다.

 

 

2) 예외_테스트

@Test
void 예외_테스트() {
    assertSimpleTest(() ->
            assertThatThrownBy(() -> runException("1234"))
                    .isInstanceOf(IllegalArgumentException.class)
    );
}

Exception이 발생하는 경우를 테스트하기 위해서 assertThatThrownBy()를 사용한다.

protected final void runException(final String... args) {
    try {
        run(args);
    } catch (final NoSuchElementException ignore) {
    }
}

runException은 main을 실행하고 예외를 잡아낸다.

즉, 예외_테스트는 "1234"의 인자 값으로 main을 실행하고, 그 결과로 IllegalArgumentException을 throw했는지를 테스트한다.

+ Recent posts