Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[이현희] : 사다리타기 요구사항 3 + 리팩토링 #14

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

nonaninona
Copy link
Contributor

@nonaninona nonaninona commented Oct 8, 2024

6, 7장 읽어보고 아 도메인 모델부터 다시 해봐야지 싶어서 처음부터 다시 짰습니다.

요구사항

  1. 사다리의 가로, 세로 길이를 지정해서 만들 수 있다.
  2. 사다리의 특정 위치에 선을 그을 수 있다.
    1. 양 옆에 같은 높이에 선이 없어야 한다.
    2. x, y 위치에 선을 그린다는 의미는 x번째 사다리의 y높이에서 x+1번째 사다리의 y높이로 선을 긋는다는 의미이다.
  3. 사다리의 시작 위치를 지정하면 종료 위치를 계산한다.
  4. 사다리의 현재 모양을 출력할 수 있다.
  5. 사다리의 현재 모양에서 시작 위치를 지정했을 때, 종료 위치를 계산하는 과정을 출력할 수 있다.
  6. 사다리를 랜덤으로 생성할 수 있다.

생각의 흐름 - 1번 요구사항

1. 도메인 객체를 식별하자

객체를 구분하는 특정한 방법이 있는지 궁금합니다.
책에서 배운 내용대로라면, 그냥 알잘딱깔센으로 하라는 것 같네요(멘탈모델 = 니 생각)
그래서 그냥 진짜 구분되는 객체가 무엇일지 고민해봤습니다.
결과적으로는 사용자, 사다리게임, 사다리, 사다리 가로줄, 사다리 세로줄이라고 생각했습니다.

그래서 제가 설계한 최초의 도메인 모델은 아래 그림과 같습니다.
IMG_C9D64BA00220-1

2. 첫번째 유스케이스 식별

책에 있는 템플릿 그대로 작성해보았습니다.

유스케이스 이름 : 사다리 게임 플레이
일차 액터 : 사용자

  1. 사용자가 사다리 생성을 요청한다.
  2. 시스템이 사다리를 만든다.
  3. 사용자가 사다리의 선 긋기를 요청한다.
  4. 시스템이 사다리의 선을 긋는다.
  5. 사용자가 사다리 게임의 시작 지점을 선택한다.
  6. 시스템이 종료 지점을 계산한다.

3. 유스케이스를 통해 협력(책임, 기능) 도출

유스케이스 작성 결과 총 3개의 시나리오(=기능, 책임, 협력)이 식별되었습니다.

  1. 사다리 만들기, 2. 가로줄 그리기, 3. 시작지점에 대한 종료기능 계산하기

4. 각 협력에 대한 인터페이스 설계

3개의 협력에 대한 인터페이스 설계를 진행했습니다.

  1. 사다리 만들기 / 초기 메시지 : 사다리를 만들어라(너비, 높이)
    IMG_25E2518875A3-1

  2. 가로줄 그리기 / 초기 메시지 : 가로줄을 그려라(가로 위치, 세로 위치)
    IMG_88C48D02075F-1

  3. 시작지점에 대한 종료기능 계산하기 / 초기 메시지 : 결과를 계산해라(시작지점 위치)
    IMG_7687DF34D3FC-1

4. 구현 중 수정

앞서 메시지 1, 3번에 대한 인터페이스를 먼저 구현하다가 도메인 모델의 수정 필요성을 느꼈습니다.
가로 막대기를 세로 막대기 안에 포함관계로 구성해야겠다 싶더군요.
그래서 2번과 같은 도메인 모델로 수정했습니다.

그 밖에도 빼먹은 인자나 리턴 값, 아니면 인터페이스 자체를 추가하거나 수정했습니다.

가로 막대기를 세로 막대기가 포함하는 관계로 변경(메시지 1, 메시지 3)
IMG_A950D44460EB-1
IMG_2A7BA962B89F-1

포함관계이기 때문에 생성에 대한 리턴 제거(메시지 1)
IMG_942CD565192E-1

결과 계산 시 세로 막대기, 가로 막대기에도 협력을 요청해야 함(메시지 3)
IMG_7DFF25CFD8A8-1

그런데 막상 구현을 해보니까 결과적으로는 세로 막대기 -> 가로 막대기로 구분할 게 아니라
가로 행 -> 가로 막대기로 구분해야겠더라고요.

  1. 가로 행을 이어 붙이는 확장성이 좋음
  2. 세로 막대기에 가로 막대기가 포함되어있다고 따지니까 가로 막대기의 자율성이 침해됨.
    세로 막대기가 메시지 3에 대한 책임을 수행할 때, 가로 막대기 혼자 계산할 수 있는 게 없으므로 자꾸 가로 막대기의 상태를 물어보게 됨.

그래서 결과적으로는 아래와 같이 인터페이스 설계와 구현이 진행되었습니다.

메시지 1.
IMG_28E111524C9C-1

메시지 2.
IMG_9F1FFC6EB9DF-1

메시지 3.
IMG_28D288A2E34C-1

생각의 흐름 - 2번 요구사항

1. 두번째 유스케이스 작성 (사실 안함)

는 사실 굳이 텍스트로 작성하지 않았습니다
그냥 딱 봐도 사다리 출력이라는 기능(=시나리오, 협력, 책임)만 추가되는 것 같았거든요.

2. 협력 도출

네, 바로 "사다리 출력" 기능을 식별했습니다.

3. 사다리 출력 협력에 대한 인터페이스 설계

아 그런데 작성하다 보니까 제가 요구사항을 잘 못 파악했다는 게 보이네요...
여기서는 출력에 대한 요구사항을 "4. 사다리의 현재 모양을 출력할 수 있다."만 있다고 인식하고 진행했습니다.
"5. 사다리의 현재 모양에서 시작 위치를 지정했을 때, 종료 위치를 계산하는 과정을 출력할 수 있다."를 빼먹었네요

IMG_8CE8416FD35E-1

4. 구현 중 수정

큰 수정사항이 없어 생략합니다.

생각의 흐름 - 3번 요구사항

1. 세번째 유스케이스는… 아니고 기존 첫번쨰 유스케이스의 확장

이번에 추가된 요구사항(=책임, 기능, 협력)은 새로운 유스케이스에서 다룰 게 아니고
기존의 첫번째 유스케이스에서 확장하면 된다고 판단했습니다.

2. 인터페이스 추가 및 구현

얘는 사실 기존의 유스케이스에서 확장된 부분이라 따로 설계를 할 필요가 별로 없었습니다.
이게 협력의 재활용이라는 장점인가 보네요.

3. 구현 중 수정

큰 수정사항이 없어 생략 합니다.

생각의 흐름 - 3번 요구사항의 리팩토링

1. Game에 LadderCreator 의존성 주입해 변경 가능하게 하기

우선 현재 1. 빈 사다리 만들기 2. 랜덤으로 사다리 만들기
2가지의 기능은 game의 메소드로 구현이 되어있습니다.
user가 요청하면 game에서 Ladder의 makeLadder, makeLadderRandomly 2가지의 메소드를 구분해서 호출하고 있죠.

여기서

  1. Ladder의 생성을 LadderCreator에게 위임
  2. LadderCreator를 외부에서 주입받음
  3. 주입받은 LadderCreator의 makeLadder 메소드 호출
    을 진행해주면 됩니다.

기본적으로 LadderCreator라는 인터페이스가 필요하고,
이를 구현하는 LadderBasicCreator와 LadderRandomCreator 클래스를 만들어 줍니다.
그리고 인터페이스의 메소드를 구현하는 부분에다가 기존에 game에 위치한 로직을 옮겨주면 됩니다.

//LadderCreator
public interface LadderCreator {
    Ladder makeLadder(int width, int height);
}

//LadderBasicCreator
public class LadderBasicCreator implements LadderCreator{
    public Ladder makeLadder(int width, int height) {
        Row[] rows = new Row[height];
        for (int i = 0; i < height; i++) {
            rows[i] = new Row(width);
        }

        return new Ladder(rows);
    }
}

//LadderRandomCreator
public class LadderRandomCreator implements LadderCreator{
    public Ladder makeLadder(int width, int height) {
        Row[] rows = new Row[height];
        for (int i = 0; i < height; i++) {
            rows[i] = new Row(width);
        }
        Random random = new Random();
        System.out.println(width*height*0.3);
        for(int i=0;i<width*height*0.3-1;i++) {
            int row = random.nextInt(1, height);
            int column = random.nextInt(1, width);
            if(!drawHorizontalLine(rows, row, column)) {
                i--;
                continue;
            }
        }

        return new Ladder(rows);
    }
    private static boolean drawHorizontalLine(Row[] rows, int row, int column) {
        return rows[row-1].drawLine(column);
    }
}

//Game
//...
    private LadderCreator ladderCreator;
    public Game(LadderCreator ladderCreator) {
        this.ladderCreator = ladderCreator;
    }
    public void makeLadder(int width, int height) {
        ladder = ladderCreator.makeLadder(width, height);
    }
//...

2. 정적 팩토리 메서드 활용하기

개념 정리

우선 팩토리 패턴, 심플 팩토리, 추상 팩토리, 팩토리 메서드, 정적 팩토리 메서드
너무 다양한 이름이 있어서 내용부터 정리해봤습니다.
(팩토리와 추상팩토리 모두 추상 클래스나 인터페이스 형태이고, 구상 클래스가 구체적인 클래스를 의미합니다.)

  • 팩토리 패턴 : 구체적인 정의를 찾은 것은 아니지만 아래 여러 패턴을 묶어서 부르는 이름이라고 생각하면 될 것 같습니다.
  • 심플 팩토리 : 객체의 생성을 다른 클래스에 위임하는 패턴입니다. 아마 사다리타기 요구사항에서 예시로 제시해준 것이 이것 같습니다.
  • 팩토리 메서드 : 객체의 생성을 담당하는 클래스(=팩토리)가 있는데, 그 클래스의 자식 클래스(=구상 팩토리)에게 생성을 또 위임하는 패턴입니다.
  • 추상 팩토리 : 여러 객체의 생성을 담당하는 클래스(=추상 팩토리)가 존재하는 패턴입니다. 물론 구체적인 생성을 진행하는 클래스는 그 자식 클래스(=팩토리)입니다.
  • 정적 팩토리 메서드 : 객체의 생성을 생성자가 아닌 정적 메소드로 진행하는 패턴입니다.

일단 팩토리 패턴은 범주의 의미를 가지고,
심플 팩토리 - 팩토리 메서드 - 추상 팩토리는 객체 생성의 단계를 얼마나 가져갈지에 대한 내용인 것 같습니다.
정적 팩토리 메서드는 다른 애들과 좀 구분되는 스킬인 것 같습니다.

정적 팩토리 메서드는 생성자를 제한해서 좀 더 객체지향적인 목표들을 달성할 수 있게 해줍니다.
객체 생성의 이름을 명확하게 지어줄 수 있다거나, 객체 생성을 통제할 수 있다거나(eg. 싱글톤)

심플 팩토리는 객체 생성을 외부의 클래스에 위임해서, 책임을 분리해줄 수 있게 해줍니다.

팩토리 메서드는 객체 생성을 담당하는 외부의 클래스(=팩토리)가 직접 객체를 생성하는 것이 아니고, 인터페이스만 제공합니다.
이를 구현한 자식들이 객체를 생성하는 역할을 맡습니다.
굳이 이렇게 하는 이유는, OCP가 가능하기 때문입니다.
팩토리 메서드에서 객체 생성을 담당하는 외부 클래스(=팩토리)가 생성하는 객체는 특정 interface 타입이어야 합니다.
그래야 확장이 가능하니까요(새롭게 추가된 객체가 그 interface를 상속해야, 자식 클래스를 추가해서 메소드 오버라이딩 가능)

추상 팩토리 메서드는 "여러" 객체 생성을 담당하는 클래스(=추상 팩토리)와 이를 구현하는 자식 클래스들로 이루어지는 패턴입니다.
위의 팩토리 메서드와 큰 구조 차이는 없는데 굳이 쓰는 이유는, "여러" 객체를 일관적으로 생성할 수 있기 때문입니다.

예를 들어 의자, 책상이라는 2개의 제품이 있다면,
그 두 제품을 생성하는(진짜 생성하는 건 아니고 interface가 있다는 것) 추상 팩토리가 있고,
이를 구현하는 빨간구상팩토리(빨간의자, 빨간 책상 생성)와 파란구상팩토리(파란의자, 파란 책상 생성)가 있는 것입니다.

이름 떄문에 헷갈렸는데
팩토리 메서드에 비해서 무슨 추상화 단계가 증가하는 게 아니고 그냥 제품 종류가 많아진 것 뿐입니다.

적용

계속 헷갈리긴 하는데 GPT와 검색의 콜라보로 낸 결론은 이렇습니다.

  • 정적 팩토리 메서드 = 객체 생성을 static 메서드로 처리하는 것. 보통 객체 내부에 있는 그 static 메서드를 말함.
  • 심플 팩토리 = 객체 생성을 외부의 다른 클래스에게 위임하는 것.
    그 클래스가 객체를 생성하는 방식은 static 메서드여도 되고 그냥 메서드여도 됨.
    정적 팩토리 메서드처럼 객체의 생성을 통제할 수 있음(eg. 싱글톤이나 캐싱)

아무튼 심플 팩토리 패턴을 적용해보겠습니다.

  1. Game 객체를 생성하는 외부 클래스(GameFactory) 생성
  2. static 메서드로 Game 객체 생성 구현(LadderCreator 구현체를 주입해줌)
public class GameFactory {
    public static Game createRandomLadderGame() {
        return new Game(new LadderRandomCreator());
    }
    public static Game createBasicLadderGame() {
        return new Game(new LadderBasicCreator());
    }
}

프로젝트 폴더가 하나 더 있음
1번 메시지 - 사다리를 만들어라
3번 메시지 - 가로줄을 그어라
자꾸 ladder가 column의 자율성을 보장해주지 못함. calcResult에서 column의 상태값을 조회함
LadderCreator의 메소드가 Ladder를 만들어 Ladder를 리턴하도록 함
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant