본문 바로가기
Projects/Tetris

[Tetris] 회고록 1 - 프로젝트 시작부터 1인용 구현까지

by Kloong 2022. 6. 21.

https://github.com/Kloong1/Tetris

 

GitHub - Kloong1/Tetris: Tetris

Tetris. Contribute to Kloong1/Tetris development by creating an account on GitHub.

github.com

서론

 최근에 고등학교 친구에게 오랜만에 연락이 왔다. 자신이 컴퓨터공학 부전공을 하는데, 자료구조 과제로 B-tree 구현 과제가 나왔다고 한다. 그런데 너무 어려워서 도움을 청할 컴퓨터공학 전공생을 찾다가 나에게 연락을 했다는 것이었다. 나는 DB 과제로 B+ tree를 직접 구현한 적이 있는데, 구글링을 해도 제대로 된 정보가 안나와서 도서관에서 전공 서적을 빌려서 겨우겨우 코딩했던 기억이 있다. 그 때의 슬픈 기억 때문인지 친구를 도와줘야겠다는 마음이 들었다.

 그런데 문제는 구현한지 거의 1년이 지나서 알고리즘을 전부 잊어버렸다는 것이었다. 과제를 겨우 제출한 뒤 시간 날 때 리팩토링과 문서화 작업을 해야지~ 해놓고 1년동안 방치해둔 것이다. 그러니 기억에 남아 있을리가 없다. 뭐 어찌저찌 해서 친구를 도와줄 순 있었지만, 문서화의 중요성을 다시 한번 깨닫게 되었다.

 테트리스도 1인용 구현을 한지 몇 달 되었지만 아직 문서화를 하거나 회고록을 쓰지 않았었기에, 부랴부랴 회고록을 남긴다.

프로젝트 동기

 Spring core와 Java의 여러가지 문법에 대해서 공부를 하면서, 간단한 토이 프로젝트를 통해 객체 지향적 프로그래밍을 직접 해보면 좋겠다는 생각이 들었다. 프로젝트의 목적을 특정 기능 구현에 두는 것이 아니라, 프로그램의 전체적인 구조와 세부 코드가 객체 지향적이도록 하는 데 목적을 두기로 했다.

 테트리스를 만들기로 한 건 그냥 내가 게임을 좋아해서다. 그리고 다른 학교 컴퓨터공학과 수업 과제로 테트리스를 구현하는 데가 있는 것 같길래 나도 한 번 구현해 보고 싶었는데, 마침 기회가 되어서 테트리스를 구현하기로 했다. GUI를 구현해야 한다는 부담감이 있긴 했지만 그래도 GUI가 있어야 다 만들면 뿌듯할 것 같아서 귀찮음을 이겨내고 테트리스를 선택했다.

구현 과정

 크게 다음과 같은 순서로 구현을 했다.

  1. 테트리스 게임을 구성하는 핵심 객체 구현
    1. Point: 테트로미노의 각 블록을 구성한다.
    2. Tetromino: 테트로미노 객체. 길이가 7인 Point의 배열 형태로 자신의 형태와 위치를 가지고 있다. 테트로미노 자기 자신의 회전 및 이동에 관한 메서드가 구현되어있다.
    3. TetrisBoard: 테트로미노가 움직이고 쌓이는 보드에 대한 객체.
  2. 테트리스 게임의 핵심 로직을 담당하는 컨트롤러 객체 구현
    1. TetrisController: 핵심 로직을 담당하는 컨트롤러. 활성화 되어있는 테트로미노의 회전 및 이동 메소드를 호출해주고, 그에 관련된 다른 작업들(화면 그리는 객체 호출, 점수 관리하는 객체 호출 등)을 담당하는 핵심적인 역할을 한다.
    2. ScoreManager: 점수와 콤보를 관리하는 객체
    3. TetrominoGenerator: 새로운 테트로미노를 넘겨주는 객체
  3. GUI 구현 - Swing 사용
    1. JPanel, JFrame 객체들
    2. 중간중간 테스트를 위해 컨트롤러 객체를 수정하며 진행
  4. 중간 리팩토링
    1. JPanel과 JFrame의 개수와 객체간의 의존관계가 매우 복잡해지면서 개발에 어려움 발생
    2. 가독성을 높이기 위해 객체간 의존관계를 최대한 단순화시킴
    3. SRP를 고려하여 기능 재분배
  5. 플레이어 조작을 위한 키 바인딩
    1. PlayerKeyAction
  6. 테트로미노가 자동으로 하강하는 기능 구현
    1. TetrominoMoverThread
  7. 최종 리팩토링
    1. main 메소드에서 객체 의존관계 주입을 함
    2. 하나의 객체가 여러 객체와 의존관계가 있는 경우(ex. TetrisController)가 많아서, main에서 객체를 생성하고 다른 객체들에 주입함

세부 구현 과정

1. Tetromino 구현

  테트로미노는 총 7개가 존재하므로 상속관계를 이용해야만 했다. 모든 테트로미노가 동일한 방식으로 움직이고 회전하지만, 테트로미노의 형태만 서로 다르다. 따라서 핵심 메소드들은 전부 부모 클래스에서 구현하고, 자식 클래스에서는 각 테트로미노 종류에 맞는 형태에 대한 Point 배열을 초기화하는 방식으로 구현했다.

//package, import 생략

//Tetromino 상속
public class TetrominoI extends Tetromino{

    private final static Point[][] BLOCKS_I;

    static {
        BLOCKS_I = new Point[2][4];
        initBlocksI();
    }

    public TetrominoI(TetrisBoard tetrisBoard) {
        super(tetrisBoard);
        blocks = BLOCKS_I;
        color = Color.CYAN;
        initPoints();
    }

    private static void initBlocksI() {
       //BLOCKS_I 초기화
    }
}

 

 또 게임을 하다보면 같은 종류의 테트로미노가 여러개 생성되는데, 같은 종류의 테트로미노라는 것은 형태가 동일하므로 Point 배열의 값이 동일하다. 따라서 메모리 낭비와 객체 생성에 대한 오버헤드를 막기 위해 static 변수로 만들었다.

private final static Point[][] BLOCKS_I;

static {
    BLOCKS_I = new Point[2][4];
    initBlocksI();
}

2. GUI 구현과 리팩토링

 Swing을 이용해서 GUI를 구현했다. 화면을 구성하기 위해 JPanel이 여러개의 JPanel을 가지고 있고, 또 그 JPanel들이 다시 여러 개의 JPanel을 가지는 방식으로 구현을 해야했다. 그런데 구현을 하다 보니 객체간의 의존관계가 너무 복잡해졌다.

Ex) PlayerScorePanel의 의존관계
TetrisFrame - (has a) - PlayerPanel - (has a) - PlayerStatusPanel - (has a) - PlayerScorePanel
TetrisFrame이 PlayerPanel을 가지고 있고, PlayerPanel은 PlayerStatusPanel을 가지고 있고, PlyerStatusPanel은 PlayerScorePanel을 가지고 있는 상황.

 맨 처음에 GUI를 구현할 때는 JPanel 객체 내부에서 초기화를 통해 new 연산자로 다른 JPanel 객체를 생성해서 가지고있게 구현을 했었다. 그런데 문제는 TetrisController 객체 같은 경우, 이벤트가 발생하면 방금 예시로 든 PlayerScorePanel 객체에 접근해서  화면을 그리는 메서드를 호출해야 한다는 것이었다. 그런데 이런 식으로 GUI를 구현하면 PlayerScorePanel 객체를 TetrisController에 주입하는 것이 너무 복잡해졌다.

 TetrisController 객체를 주입 받는 TetrisFrame이 다른 객체를 거쳐서 PlayerScorePanel에게 TetrisController 객체를 넘겨주기에는 코드의 직관성이 너무 떨어졌다. TetrisController의 전달책 역할을 하는 객체인 PlayerPanel과 PlayerStatusPanel은 TetrisController에 의존하지 않기 때문이다. GUI를 구현하는 과정에서는 이런 구조를 유지하면서 가독성을 최대화 하기 위해 노력했지만 애초에 이 구조 자체가 문제라는 것을 깨닫고, 막바지에는 main 메소드에서 객체들을 조립하는 역할을 담당하게 만들었다.

public static void main(String[] args) {
    TetrisBoard playerTetrisBoard = new TetrisBoard();
    TetrisBoardPanel playerTetrisBoardPanel = new TetrisBoardPanel(playerTetrisBoard);
    PlayerStatusPanel playerStatusPanel = new PlayerStatusPanel();
    PlayerPanel playerPanel = new PlayerPanel(playerTetrisBoardPanel, playerStatusPanel);

    TetrisBoard enemyTetrisBoard = new TetrisBoard();
    TetrisBoardPanel enemyTetrisBoardPanel = new TetrisBoardPanel(enemyTetrisBoard);
    EnemyPanel enemyPanel = new EnemyPanel(enemyTetrisBoardPanel);

    PanelDrawingManager playerPanelDrawingManager = new PanelDrawingManager(playerTetrisBoardPanel, playerStatusPanel);

    TetrisController tetrisController = new TetrisController(playerPanelDrawingManager, playerTetrisBoard);

    new TetrisFrame(tetrisController, playerPanel, enemyPanel);

    new TetrominoMoverThread(tetrisController).start();
}

 또 TetrisController가 여러 JPanel의 메소드를 호출하고 있어서 의존 관계도 복잡해지고 코드도 길어졌다. 그래서 화면을 그리는 메소드를 호출하는 PanelDrawingManager 객체를 만들고 TetrisController에 주입하는 방식으로 리팩토링을 하였다.

돌아보며

1. 느낀점

 생각보다 GUI 구현에 품이 제일 많이 들었다. Swing이 사용하기 힘들다는 부분도 있었고, 특히 화면 배치에서 애를 먹어서 Swing 공부와 GUI 구현에 절반 이상의 시간을 소요한 것 같다. 그래도 오히려 구현해야 할 JPanel 클래스들이 많아지면서 객체 간의 의존관계에 대해 고민해보고, 리팩토링을 할 수 있어서 좋은 경험이 된 것 같다.

 아쉬운 점은 이런 아주 작은 크기의 토이 프로젝트로는 객체 지향적 프로그래밍에 대한 고민을 충분히 할 수 없었다는 것이다. 처음에 설계 과정에서 객체 지향적으로 설계를 확실히 하고, 거기에 맞춰서 개발을 하는 방식으로 진행을 했으면 소프트웨어 설계적인 측면에서 더 많이 배웠을 것 같다는 생각이 들었다. 구현을 하면서 설계대로 잘 안되거나, 설계가 잘못된 부분을 찾아가고 고치는 과정이 있었으면 내 몸은 더 힘들어도 배울점은 많았겠구나 싶어서 아쉽기도 하다.

 또 기능적으로 유지보수나 확장을 할 부분이 많지 않아서(GUI 개선은 Swing을 추가적으로 공부해야 하는데 Swing 공부는 너무 시간 낭비라 제외한다) 객체 지향적 코드의 장점 중 하나인 확장성을 제대로 느껴보지 못한다는 점도 아쉽다. 내가 작성한 이 코드들이 정말로 객체 지향적인 관점에서 좋은 코드라면 확장성이 높을 것이고, 아니라면 수정이나 확장이 매우 어려울 텐데, 만약 유지보수나 확장을 실제로 해볼 기회가 있었으면 내 코드를 다양한 관점에서 평가해 보는 경험이 되었을 것 같다. 하지만 여기서 게임적인 기능(예를 들어 아이템이라던가, T spin 이라던가...)을 추가하다 보면 내가 배우는 것보다 로직 구현 자체에 너무 시간을 쏟아야 할 것 같아서 더이상 확장은 하지 않기로 했다.

 여러모로 재미있는 프로젝트인 동시에 아쉬움도 많이 남는 프로젝트였다. 빠른 시일 내에 네트워킹 기능을 추가해서 온라인 1:1도 가능하게 만들어야겠다.

2. 객체 지향적 설계 - 인터페이스의 중요성

  객체 지향적 프로그래밍을 해보고 싶다는 프로젝트에서 나는 귀찮다는 이유로 객체 지향적 설계 단계를 거치지 않았다. 변명을 좀 하자면, 개발을 시작하기 전에 핵심 게임 로직은 상대적으로 매우 짧고, GUI에서 코드가 길어지겠다는 것을 어느정도 예상을 하고 있었다. 그런데 문제는 내가 Swing을 사용해본 게 고등학교 때가 마지막이기 때문에, 이 설계를 하려면 Swing을 미리 공부를 해야지 클래스 다이어그램을 만들 수 있다는 것이 문제였다.

 공부를 위해 시작한 프로젝트이긴 하지만 그래도 엄연히 나의 흥미가 중요한 토이 프로젝트이기 때문에, 이거 때문에 Swing 공부를 하고, 클래스 다이어그램을 그리고, 어쩌고 하는 과정에서 흥미를 전부 잃을 것 같아서 이 설계 과정을 뛰어 넘었다. 그러다보니 테트리스를 이루는 기본적인 객체들(Tetromino, TetrisBoard, TetrisController 등)은 물론 미리 구상을 하고 개발에 들어가긴 했지만, 그 과정에서 필요한 클래스가 생기면 그때 그때 추가해서 구현하고, 리팩토링을 하는 식으로 개발을 했던 것 같다.

 이렇게 하다보니 인터페이스 없이 전부 클래스로 구현을 하게 되었다. 그런데 인터페이스 없이 개발을 하니까 여러 클래스를 동시에 개발해야만 하는 문제가 발생했다. 다른 클래스들을 의존하는 어떤 클래스를 개발을 하는 과정에서, 의존하는 클래스의 메소드를 호출을 하는 부분이 생기는데, 아직 의존하는 클래스는 개발을 시작도 안한 상태이다 보니 그 클래스가 어떤 메소드를 가지고 있는지 정해진 것이 전혀 없었다. 그래서 개발하던 클래스의 개발을 잠시 멈추고 의존하는 클래스를 조금 개발을 한 다음 다시 개발하던 클래스로 돌아오고, 이런 과정을 반복하며 개발을 할 수밖에 없었다.

 물론 설계를 아예 안한 것은 아니라서 동시에 3개, 4개의 클래스를 개발하고 그러지는 않았지만, 아주 많은 클래스에 의존하는 TetrisController를 개발하면서 이런 문제가 계속 발생했던 것 같다. 만약 설계를 확실히 하고, 설계에 맞춰서 인터페이스를 미리 작성을 한 뒤, 구현체를 하나씩 만들어나가는 식으로 개발을 했다면, 설계 과정에서 힘이 많이 들더라도 개발 과정은 훨씬 편해지지 않았을까 하는 생각이 들었다. 그리고 그렇게 개발하면 DIP와 OCP도 만족하는 코드가 나오지 않았을까? 하는 생각도 든다.

3. Spring core 도입...?

 위에서 언급한 DIP와 OCP를 완벽하게 만족하기 위해서는 DI Container가 필요하다. 최근에 배운 Spring core까지 도입해서 개발을 빡세게 했으면 객체 지향적으로 정말 좋은 코드가 나오지 않았을까 하는 생각이 든다. 물론 배보다 배꼽이 더 큰 것 같긴 하다 ㅎㅎ...

'Projects > Tetris' 카테고리의 다른 글

[Tetris] 기획서  (1) 2022.04.15

댓글