호우동의 개발일지

Today :

article thumbnail

우테코
우테코


Repository 주소

https://github.com/howudong/java-baseball-6/tree/howudong

 

GitHub - howudong/java-baseball-6

Contribute to howudong/java-baseball-6 development by creating an account on GitHub.

github.com

 


다이어그램

domain 다이어그램 설계

전체 도메인 설계
도메인 설계

 


domain과 view의 연관관계 다이어그램

도메인과 뷰의 연관관계
domain과 View의 연관관계

domain과 view과 서로 연관관계없이 잘 만들어진 모습을 확인할 수 있다.
컨트롤러만 빼고 보면 서로 모르게 잘 만들진 것 같은데..

컨트롤러를 포함해서 생각해야 하나?
컨트롤러를 포함하면 그림이 좀 복잡해져서 연관관계를 파악하기 어려워진다.

전체 관계도
View Controller Domain 관계도

일단 내가 할 수 있는 최선으로 결합도를 낮추고, MVC를 나누었다.

서론이 좀 많이 길었는데, 아래에서부터 본격적인 회고를 시작하겠다!!

 

 


집중적으로 구현한 점


1. 기본기부터 다지기

  • Java 컨벤션 지키기
    • 의미 없는 공백 남기지 않기
    • 패키지, 클래스, 메서드(camelCase), 상수(SNAKE_CASE) 명명 규칙 지키기
  • Git 커밋 컨벤션 지키기
    • docs, style, refactor, feat, fix 등등..
  • ReadMe와 개발과정을 항상 동기화(ReadMe 최신화)

README.md에 작성한 기능 명세사항

 


2. 협업을 위한 설계(가독성 좋은 코드 작성하기)

1. 메서드 분리하기

public Map<HintType, Integer> compareNumbers(BaseBallNumber playerNumber) {
    Map<HintType, Integer> hintMap = new HashMap<>();

    for (int i = 0; i < BaseBallNumber.NUMBER_LENGTH; i++) {
    	
        HintType type = compareToAnswer(playerNumber, i); // 메서드 분리
        hintMap.merge(type, 1, increaseHintCount());
    }
    trimHint(hintMap);
    return hintMap;
}

// 가독성을 위해 메서드를 분리
private HintType compareToAnswer(BaseBallNumber number, int index) {
    boolean isMatch = answerNumber.isSameNumberOfIndex(number, index);
    boolean hasContain = answerNumber.hasContainNumberOfIndex(number, index);

    if (isMatch) {
        return HintType.STRIKE;
    }
    if (hasContain) {
        return HintType.BALL;
    }
    return HintType.NOTHING;
}

메서드 분리를 통해 한 함수의 라인이 길어지지 않게 됨으로써 보기가 더 편해졌다.

또한 분리한 메서드 이름을 통해 세부 구현 사항을 보지 않고도,
어떤 일을 하는 메서드인지 유추가 가능하다.

2. 들여 쓰기(indent) 최대 2번 (되도록이면 1번)

3. 메서드명으로 해당 메서드가 무슨 일 하는지 알 수 있도록 하기(메서드명 축약 X)

// 함수 이름만으로 역할을 알 수 있겠금 네이밍
public boolean isSameWithAnswer(BaseBallNumber other) {
    String otherNumber = other.getNumber().toString();

    return answer.toString().equals(otherNumber);
}

메서드 이름을 축약해서 쓰지 않음으로써, 해당 메서드가 가진 역할을 유추할 수 있다!

 


3. 최대한 객체지향적으로 설계하기

  • 캡슐화중요한 내용 유출되지 않도록 정보 은닉
    • SOLID 원칙 중 하나인 캡슐화(은닉성)
package baseball.domain.numbers;

import java.util.List;

public final class AnswerNumber {
	// 정답 번호 private으로 외부에서 확인할 수 없음
    private final BaseBallNumber answer;

    public AnswerNumber(BaseBallNumber answer) {
        this.answer = answer;
    }

    public static AnswerNumber createAnswerNumbers(NumberGenerator numberGenerator) {
        List<Integer> generateNumbers = numberGenerator.generate(BaseBallNumber.NUMBER_LENGTH);
        BaseBallNumber answer = new BaseBallNumber(generateNumbers);
        return new AnswerNumber(answer);
    }
    
    /*
    * 아래의 메서드들로 계산 결과만을 확인할 수 있다.
    */
    public boolean isSameWithAnswer(BaseBallNumber other) {
        String otherNumber = other.getNumber().toString();
        return answer.getNumber().toString().equals(otherNumber);
    }

    public boolean isSameNumberOfIndex(BaseBallNumber other, int index) {
        int targetNumber = other.getNumber().get(index);
        return answer.getNumber().get(index).equals(targetNumber);
    }

    public boolean hasContainNumberOfIndex(BaseBallNumber other, int index) {
        int targetNumber = other.getNumber().get(index);
        return answer.getNumber().contains(targetNumber);
    }
}

숫자 야구 게임에서 가장 중요한 정보는 '정답 번호'라고 생각한다.

이런 정답 번호 정보를 다른 domain이 안다면 보안성에 문제가 생길 수 있다고 생각했다.

그래서 정답 번호인 answer를 priavte으로 접근을 제한하게 했다.
그리고 필요한 필요한 정보만을 얻을 수 있도록 하는 메서드를 정의해 줬다.

 

  • 상속보단 조합(Composition)을 사용하여 결합도를 낮춤

AnswerNumber은 BaseBallNumber가 필요
AnswerNumber은 BaseBallNumber가 필요

원래는 AnswerNumber와 BaseBallNumber가 같은 '숫자 번호'를 포함하고 있기 때문에 상속 관계로 정의해 줬다.
하지만 객체지향적으로 생각해 보면 상속 관계는 결합성을 강하게 만들고, 유연한 변화를 방해한다.

무엇보다도 AnswerNumber에는 BaseBallNumber 클래스와는 같지 않기 때문에
'숫자 번호'라는 같은 공통점이 있다고 표현하는 것이 맞다.

이런 경우 상속보단 합성(Composition)이 더 어울린다는 것을 찾아냈다.

상속의 경우, 두 객체가 'Is-A' 관계인 경우,
조합인 경우, 두 객체가 'has-A' 관계인 경우 사용해 주면 더 좋다고 한다.

https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/

 

상속보다는 조합(Composition)을 사용하자.

tecoble.techcourse.co.kr

자세한 사항은 테코톡에서도 다뤘기 때문에 해당 링크를 참조하는 것이 제일 깔끔하다!

public final class AnswerNumber {
    private final BaseBallNumber answer;
	
    // BaseBallNumber를 필드로 가지게 하여 조합한다!
    public AnswerNumber(BaseBallNumber answer) {
        this.answer = answer;
    }
   ...

하는 방법은 위와 같이 간단한데,
그냥 AnswerNumber에 BaseBallNumber 필드를 가지도록 하게 했다.

이런 식으로 하면 BaseBallNumber의 기능을 그대로 사용할 수 있는 것이다.

  • 불변인 객체들 final 붙여서 잘 관리하기

불변 객체를 쓰는 데는 다양한 이유가 있는데,
주된 이유는 안전성, 부수 효과 방지, 성능 향상(가비지 컬렉터 관련)이라고 생각한다.

일단 안전성은 불변 객체가 되면 생성 이후부턴 다른 사람이 수정 불가능하기 때문에, 오용을 방지할 수 있다.
부수효과 방지는 멀티스레드 환경에서 동시성 문제를 방지할 수 있다는 것에 큰 장점이 있는 것 같다.

마지막으로는 성능 향상은 내가 클래스 레벨이 final을 붙이는 이유이다.
클래스를 불변으로 만들면, 가비지 컬렉터가 컨테이너 객체 하위의 불변 객체들은 Skip 한다.

왜냐하면 해당 컨테이너 객체(ImmutableHolder)가 살아있다는 것은 
하위의 불변 객체들(value) 역시 처음에 할당된 상태로 참조되고 있음을 의미하기 때문이다.

즉, 가비지 컬렉터 성능 향상에 도움이 된다는 것이다!

그래서 나는 상속이나 확장되지 않는 경우에는 의도적으로 클래스에 final을 붙이는 습관을 들이고 있다.

 


4. MVC 패턴 적용하기

  • domain, view , controller를 역할별로 나누어서 설계

 

도메인이 바뀌면 어디까지 영향을 받을까?

  • 변경을 해야 할 때, 최대한 덜 변경할 수 있도록(독립적으로) 설계

 

외부에서 이 도메인을 무슨 목적으로 쓸까?

  • 해당 도메인이 정말 존재할 필요가 있는 도메인일까?
    그러니까 꼭 저장되어야 하는 부분일까?
    → 도메인은 프로그램에서 핵심적인 부분만이 들어가야 한다.

 

만약 요구사항이 추가되거나 변경된다면 이에 잘 대응할 수 있는 설계인가?

  • 메서드 이름을 지을 때 List나 Set 같은 특정 컬렉션 이름을 사용하지 않는다.
  • 하드 코딩된 값을 사용하지 않는다.
  • 역할별로 클래스 분리를 잘 이뤄내야 한다.
  • 일반화된 유형을 사용하지 않는다.

 

 


고민했던 점


해당 클래스(객체)를 안다는 의미

가장 기본적인 MVC 패턴에서는 크게 Model, Controller, View 이렇게 3가지로 나뉜다.

Model은 Controller와 View를 알아서는 안된다.

그리고 View 또한 단지 화면에 보여주는 역할을 할 뿐,
Model과 Controller에 관한 정보를 알고 있으면 안 된다고 한다.

Controller만이 Model과 View를 알아서 이 둘 사이의 매개체로써 둘을 관리한다고 할 수 있다.

여기서 ‘해당 클래스(객체)를 안다’는 것은 어떤 의미일까?

이번 숫자야구 과제로 예로 들어보자.

HinType이라는 클래스에 “스트라이크”, “볼”, “낫싱”이 포함되어 있고,
OutputView라는 출력을 담당하는 클래스가 있다고 가정해 보자.

출력 명세 사항으로 스트라이크, 볼, 낫싱을 출력하게 하기 위해서는
OutputView 클래스가 HintType 클래스를 참조해야 한다.

이러할 때 OutputView가 HintType을 안다고 할 수 있다.

이렇게 직접적으로 참조되어있지 않아도,
간접적으로(개념적으로) 되어 있는 경우에도 안다고 하는 경우가 있다고 한다.

이 부분은 나도 추후에 프리코스를 진행하면서 알아가 봐야겠다.

 


View가 Model을 모르는 것이 가능한가?

현재 요구사항에서는 가능할지 모르겠는데, 아래와 같은 요구사항이 추가된다고 예상해 보자.

"게임이 종료될 때, 정답 번호를 출력한다."

정답 번호는 확실히 domain에 있다.
이를 요구사항대로 출력하기 위해서는 domain에서 가져와서 View가 알아야 한다.

근데 MVC에서는 View는 Controller과 Model을 알면 안 된다는데…
내가 아는 지식상에서는 모순된다.

알아보니 위에서 말하는 원칙은 전통적인 MVC에서 말하는 것이다.

지금은 View에서 입력을 주는 것이기 때문에, 현재는 [Cocoa MVC 패턴]를 사용한다.

이 방식에서는 View가 순수 도메인 데이터에 의존하는 것은 가능하다.
(출력을 위해) 이를 바탕으로 설계하였다.

 


숫자 번호 계산(스트라이크, 볼, 낫싱)을 어떻게 할 것인가?

이 부분에서 고민한 이유는,
어디서 숫자 번호 계산을 해야지 모듈화가 잘될 수 있을까?
즉, 다른 클래스에 영향을 최대한 덜 줄 수 있을까?

 

1. 정답번호를 가지고 있는 AnswerNumber 클래스에서 계산하기

해당 방식을 사용했을 때의 장점은 뭘까?

AnswerNumber 클래스 내에서 모든 계산이 끝나기 때문에
정답번호를 외부에 노출하지 않을 수 있다는 것이다.

단점은 힌트(스트라이크, 볼, 낫싱)에 대한 정보를 알고 있어야 한다는 점이다.

즉, 만약 힌트를 담고 있다는 클래스가 있다면, AnswerNumber는 힌트 클래스를 알고 있어야 한다.

즉, 힌트 클래스를 고친다면 AnswerNumber 클래스도 영향을 받게 된다.

 

2. 계산 로직만을 수행하는 Service 클래스 만들기

비즈니스 로직만을 수행하는 Service 클래스를 새로 하나 만드는 것이다.

계산 결과는 어디 저장될 필요도 없기 때문에 사실상 쓰고 버린다.
그래서 서비스로 만들어도 상관없다고 판단했다.

해당 방식을 사용했을 때의 장점은 새로운 도메인을 만들 필요가 없다는 것이다.

입력을 통해 만든 BaseBallNumber와 AnswerNumber만을 이용해서 계산해 줄 수 있다.

하지만 나는 해당 방식으로 코드를 짜다가 갈아엎었는데,
오직 이 클래스를 위해서 서비스 레이어를 만들기는 과하다고 생각했기 때문이다.

게다가 이렇게 되면 Controller가 생성자 주입으로 InputView와 OutputView, Service
이렇게 총 3개를 받아야 해서 매개변수가 너무 많다고 판단했다.

 

3. 계산을 하는 클래스를 따로 만들기(HintProvider)

최종적으로 선택한 방법인데,
도메인에 AnswerNumber과 사용자의 입력을 받아 계산을 하는 새로운 클래스를 만들었다.

해당 방식을 사용했을 때의 장점은
AnswerNumber과 BaseBallNumber 객체가 힌트클래스를 전혀 몰라도 된다는 것이다.

이런 식으로 의존성을 낮출 수가 있다.

해당 방식의 단점은 힌트 클래스가 AnswerNumber과 BaseBallNumber를 둘 다 알아야 한다는 것

하지만 HintProvider 수정되었을 때 영향을 생각해 보면,
AnswerNumber와 BaseBallNumber에서는 HintProvider를 모르기 때문에 영향이 없다.

반대로 AnswerNumber나 BaseBallNumber가 수정될 일은
숫자의 수가 늘어나는 것밖에 없기 때문에 HintProvider 로직 상에서는 크게 문제가 없다.

여담으로 AnswerNumber와 BaseBallNumber는
구성(Combination) 관계이기 때문에 함께 움직인다고 생각하면 된다.

 

 


의문점


과도한 추상화는 어디까지일까?

소프트웨어 아키텍처 설계에서 “인터페이스로 과도한 추상화를 하지 말라”라는 중요한 원칙이 있다.

그런데 MVC 패턴으로 객체 지향적으로 설계하는 것이
결국엔 확장 가능성을 고려하기 위함이지 않은가? 그래서 뭔가 모순된다고 생각한다.

숫자 야구 프로그램을 만들 때, 처음에는 dto와 service로 만들어보고,
inputview와 outputview도 인터페이스로 설계하여 그 구현체를 만들었었다.

이 부분은 어느 정도 프로그램 규모가 작기 때문에 이렇게 나누는 것까지 과하다고 느껴졌다.

하지만 추가되는 요구사항에 대비하는 것 또한 과도한 것인지가 의문이다.

위에서 언급했듯 요구사항에서 마지막에 정답을 출력하라던가,
몇 번 만에 맞췄는지를 출력하라던가,
숫자가 3개에서 5개로 늘어나게 정책이 바뀔 수도 있다.
혹은 1과 2번밖에 없던 선택지에 3번과 4번 이런 식으로 더 늘어날 수도 있다.

어디까지가 과도한 추상화이고, 적절한 대비인지가 의문이다.

 


“스트라이크”,”볼”,”낫싱”은 domain 인가? view인가?

이 부분이 끝까지 애매했다.

View가 모델에 의존가능하다고 하더라도 그래도 최대한 덜 의존하는 것이 나을 텐데,
출력되는 “스트라이크”, “볼”, “낫싱” 문구는 도메인 명세로 봐야 하는가?
아니면 출력 명세로 봐야 하는가? 아직까지 애매한 부분이다.

 

 


배운 점

  • 여기서 Map 컬렉션에서 써보지 않았던 기능(put, putIfAbsent, putIfPresent, merge)을 배웠다.
  • 상속과 합성(Composition)의 차이를 파악하고, 이를 적용할 수 있었다.
  • 응집도, 결합도의 차이를 인지
  • 객체지향 SOLID 원칙, 객체지향 생활체조 원칙
  • 전통적인 MVC와 현대 MVC의 차이

 

 


아쉬운 점


View 설계

여기서는 InputView나 OutputView나 구현해야 할 것이 크게 다르지 않아 한 클래스에 구현했다.

하지만 이렇게 해도 될까?
프로그램이 커진다면, 이런 식으로 하면 안 될 것 같다.

그런데 뭔가 좋은 방법이 크게 떠오르지가 않았다.

파라미터로 넣자니 그것도 결국엔 하드코딩된 값이고, 그냥 뭔가 이렇게 하는 게 최선인 것 같았다.
전체 코드 리뷰 때 답을 찾았으면 좋겠다.

 

 

참고자료

https://mangkyu.tistory.com/199

 

[OOP] 코드의 재사용, 상속(Inheritance)보다 합성(Composition)을 사용해야 하는 이유

객체지향 프로그래밍에서 코드를 재사용하기 위한 방법으로 크게 상속과 합성이 있습니다. 대부분의 경우 상속보다 합성을 이용하는 것이 좋은데, 이번에는 왜 합성을 사용해야 하는지에 대해

mangkyu.tistory.com

https://mangkyu.tistory.com/195

 

[OOP] 객체지향 캡슐화(Encapsulation), 응집도(Cohension)와 결합도(Coupling)

프로그래밍을 하다보면 추상화를 이용하고, 응집도가 높고 결합도는 낮은 애플리케이션을 개발해야 한다는 얘기를 많이 듣습니다. 그래서 이번에는 객체 지향 프로그래밍의 핵심 특징들인 캡

mangkyu.tistory.com

책 [좋은 코드, 나쁜 코드] : 7.3 지나치게 일반적인 데이터 유형은 피해라
책 [좋은 코드, 나쁜 코드] : 7.5 데이터에 대한 진실의 원천을 하나만 가져야 한다.
책 [좋은 코드, 나쁜 코드] : 7.5 로직에 대한 진실의 원천을 하나만 가져야 한다.