[회고] 우아한 테크코스 5기 4주차 회고 (with MVC, 싱글톤)

황준승·2022년 11월 22일
4
post-thumbnail

우테코 4주차까지 모두 완주하신 분들 다시 한 번 고생하셨습니다. 🎉🎉
이번 프리코스 동료분들과 코치분들이 있었기에 저도 4주차까지 모두 완주할 수 있었고 크게 성장할 수 있었습니다. 정말 감사드립니다!!

본 회고는 이번 4주차 미션을 진행하면서 어떤 걸 공부하고 가장 어려웠던 점과 이를 해결하기 위해 어떠한 방식으로 풀었는지에 대해 자세히 작성하였습니다.


미션을 수행하기 전에

객체를 객체답게 짜는 것이 무엇인지 공부하고, MVC패턴에 대해 학습하고 이번 프로젝트 잘 녹여내는 것을 목표로 삼고 이번 미션에 도전했습니다.

🤔 객체를 객체답게...?

이번 우테코 3주차 코수타에서 객체를 그냥 사용하기 보다는 객체를 객체답게 한번 써보라는 코치님의 말씀을 듣고 객체를 객체답게 사용하는 것이 무엇인가에 대해 제대로 고민을 해보았습니다.

객체라는 또 다른 세상

객체도 우리 인간 사회와 마찬가지로 각자의 역할에 충실하면서 다른 객체와 소통하기 위해 메세지를 보내고 협력하는 행위를 통해 사회(프로그램)을 이뤄야 객체 지향형 프로그래밍에 가깝다고 생각했다.

그래서 이번 미션에서는 이전 미션에서 학습한 객체의 유지보수성을 위해 의존성을 주입하고 분리하는 행위보단 객체 간 각자의 역할에 집중, 각 객체가 다른 객체에게 어떻게 메세지를 보내야하는가에 대해 고민하면서 코드를 구현하였습니다.

🤔 MVC 패턴...?

MVC 패턴 적용 이유

문제 내에서 UI와, 단위 로직을 구분해야한다는 요구사항과 요구사항 내 BridgeGame 클래스의 함수가 MVC 패턴의 Controller 기능과 유사하다고 판단하여 MVC패턴을 공부해보았습니다.

MVC패턴에 대해 공부하면서 Model, Controller, View 각자의 역할에 충실하면서 데이터를 저장하고 읽어오면서 메세지를 보내고 협력하는 행위가 이번 학습목표와 부합하다고 판단하여 적용하게 되었습니다.

MVC 적용 방법

MVC계층에서 많은 로직들이 Controller 에 부담된다고 판단, 이를 더 세분화하는 방법에 대해 고민하다가 MVC패턴 5계층에 대해 알게 되었고 이번 미션에서 View - Controller - Service - Repository - Model로 구성하였습니다.

뿐만 아니라 View - Controller - Model 간에 메시지를 통해 데이터를 저장하고 읽어오는 동작 구현하는 느낌으로 구현하려고 노력했습니다.

도메인 로직의 경우 각각에 맞는 클래스를 구현 Service 클래스에서 실행하도록 구현하였습니다.

📝 학습목표

앞서 공부한 두가지 개념을 가지고 다음과 같이 학습목표를 구현하였습니다.

  • 객체는 객체답게 구성하고 코드를 짠다는 것이 무엇인지 학습하고 이번 미션에 최대한 녹아내려고 노력한다.
    • 하나의 객체는 단일책임원칙을 지키도록 한다.
    • 객체 내 변수는 private 변수를 사용, 객체 간 메세지 전달을 위해 private 변수를 그대로 가져오는 getter를 지양한다.
  • 처음 사용해보는 MVC 패턴을 학습하고 이번 미션에 적용해보면서 MVC 패턴에 장단점에 대해 학습하고 MVC 패턴과 친해지기 위해 노력한다.
    • MVC 패턴에서 단위테스트TDD를 어떻게 효율적으로 작성할지 고민하고 이를 프로젝트에 적용해본다.

📌 MVC 패턴으로 설계한 Bridge Game 구조

😅 MVC패턴... 이거 맞아..?

이번 미션 프로젝트에서 MVC 패턴을 구현, 적용해보면서 MVC패턴의 장점보다는 단점 이 나한테 더 크게 다가왔다.

숨막히는 객체 간 의존성

MVC 패턴의 경우 View - Controller - Servcie - Repository - Model이 강하게 결합되어있다.
강하게 결속되어있는 객체 사이에서 하위 객체(작은 기능)부터 테스트하는 일은 너무나 어려웠습니다.

만약 Service클래스 내 데이터를 저장하는 기능(store)이 있다고 가정하보자.

Q. 그렇다면 store기능을 테스트하기 위해서는 어떻게 해야할까??

A. service클래스 내부의 repository로 들어가 그 repository의 모델의 값을 가져와야만 테스트 검증이 끝이 난다.
(처음 구현할 때 실제로 input값을 테스트하기 위해서 입력 기능, ouput기능을 모두 구현하고 테스트했습니다ㅜㅜㅜ)

숨막히는 의존성을 벗겨버리기 전에

나의 입장에서 MVC의 첫인상은 별로 안 좋지만, 과연 MVC는 왜 이렇게 숨막히는 의존성을 추구했는가에 대해 먼저 생각해보았다.

만약 내가 MVC 패턴의 의존성을 줄이기 위해 Model - Controller - View 의 관계를 의존성 주입(DI)을 통해서 구현했다고 가정해보자.

그렇게 할 경우 어쩌면 보안상 되게 중요할 수도 있는 Model의 정보가 외부로 유출, 이렇게 구현할 경우 개발하기는 쉬울지 몰라도 보안상의 문제는 어느 정도 존재할 수 있다고 판단하였다.

DI에 대해 자세히 알고 싶다면 JS로 알아보는 DI

그러면 작은 기능 테스트는 어떻게 할래??

첫번째 생각: DI기능 구현 + 보안 챙기기

View - Controller - Servcie - Repository - Model 객체를 setter를 통해서 의존성을 주입, 각각의 인스턴스를 private변수로 두어 보안을 증가시키는 방법이다.

class Controller {
  #service
  
  inject(mvcInstance) {
    this.#service = mvcInstance;
  }

  ...
}

class Service {
  #repository
  
  inject(mvcInstance) {
    this.#repository = mvcInstance;
  }
  
  ...
}

class Repository {
  #model
  
  inject(mvcInstance) {
    this.#model = mvcInstance;
  }

  ..
}

class Bean {
  #mvcInstance = {
	controller: new Controller();
	service: new Service();
    repository: new Repository(); 
	model: new Map();
  }
  
  .. 
  
  injectMVCDependency() {
    Object.values(this.#mvcInstance)
      .slice(0, 3)
      .forEach((instance, index, mvcArr) => {
        instance.inject(mvcArr[index + 1]);
    })    
  }

  ...
}

겉보기에는 문제가 없어보이지만 결국에는 종속적인 연결과 크게 다르지 않다.

결국에는 Bean객체에서도 model의 데이터를 가져오기 위해서는 private 변수에 getter를 사용해야한다.

두번째 생각: service 내부에서 생성되고 동작하는 domain 관련 클래스만 DI를 구현하자.

도메인 로직에 따라 작은 기능들을 각각의 클래스로 구현하고 Model에 대한 정보를 생성자 주입(DI)을 통해 domain 클래스에 전달한다면 테스트가 용이할 것이다.

뿐만 아니라 controller ~ model 사이의 service 내에서 DI를 구현하기 때문에 보안상에서도 크게 문제 되지 않을 것이다.

코드

class BridgeDirection {
  #input;

  #repo;

  constructor({ input, repo }) {
    this.#input = input;
    this.#repo = repo;
  }

  #updateUserBridge() {
    const oldData = this.#repo.read(MODEL_KEY.userBridge) || [];

    this.#repo.update(MODEL_KEY.userBridge, [...oldData, this.#input]);
  }

  store() {
    this.#updateUserBridge();
  }
}

테스트 코드


const makeRepo = () => {
  const repo = new BridgeRepository();
  repo.create();

  return repo;
};

const makeInput = ({ input, result }) => {
  return {
    input,
    repo: makeRepo(),
    result
  };

describe('(domain) BridgeDirection 클래스', () => {
  test.each([
    [makeInput({ input: 'U', result: ['U'] })],
    [makeInput({ input: 'D', result: ['D'] })]
  ])('U | D 키 입력 시 데이터 저장 확인', ({ input, repo, result }) => {
    const direction = new BridgeDirection({ input, repo });

    direction.store();

    expect(repo.read(MODEL_KEY.userBridge)).toEqual(result);
  });
});

(해결!!) 위의 코드처럼 new BridgeDirection 생성자에 대한 store함수만을 테스트할 수 있게 되었다.

📌 MVC패턴에 싱글톤을 적용하다

앞서 설계한 구조를 다시 한번 살펴보자

위 그림은 InputViewOutputView에서 동일한 Model에서 데이터를 가져오거나 입력하는 기능을 수행을 합니다.

문제 내에서 InputView와 OutputView는 반드시 분리되어야하고 만약 각각의 객체에서 new Controller()를 통해서 각자 다른 인스턴스에서 입출력을 구현했을 시 결과는 당연히 에러가 발생할 것이다. (한쪽은 입력만하고, 한쪽은 출력만하기 때문에)

싱글톤 에 대해 자세히 알고 싶다면 이 글 한번 보는 것 추천드립니다. singleton for javascript


const inputView = {
  controller: new Controller,
  
  store: (len) => {
    this.controller.inputLength(len);
  }
}

const OutputView = {
  controller: new Controller,
  
  show: () => {
    this.controller.getGameResult();
  }
}

// 실행
inputView.store('3'); 

inputView.getGameResult(); // undefined

이런 문제점을 해결하기 위해 하나의 인스턴스 생성만 허용하는 싱글톤을 적용시켜 이 문제를 해결해보았다.

Controller.js

let instance = null;

class Controller {
  #service;

  constructor() {
    if (instance) {
      throw new Error(ERROR_MESSAGE.singleton);
    }

    instance = this;

    this.#service = new Service();
  }

  ...
}

module.exports = Object.freeze(new Controller());

instance 변수: 새로운 인스턴스 생성이 되었는지 확인

새로운 인스턴스가 생성될 경우 에러를 발생한다.

좀 더 안정적인 구현을 위해 생성자에 Object.freeze() 라는 객체 동결함수를 사용해 새로운 객체 사용을 차단한다.

싱글톤의 장점

메모리적인 측면

하나의 인스턴스만을 사용하기 때문에 메모리 측면 에서 굉장히 효율적이다. 추가적으로 수많은 객체가 있을 시 이 객체를 생성하는 데도 시간복잡도적인 비용이 들어가는데 싱글톤을 사용할 경우 시간복잡도 적인 측면도 이점이 있다.

공유된 데이터

싱글톤 인스턴스전역으로 사용되는 인스턴스이기 때문에 다른 클래스의 인스턴스들이 접근하여 사용하기 쉽다.

싱글톤의 단점

동시성 문제

동일한 데이터에 여러 사람이 접근할 경우 데드락 문제를 초래할 수 있다. 이번 미션의 경우 Console를 통해 순차적으로 이루어지므로 싱글톤을 사용했다.

📌 아쉬운 점

MVC 패턴을 처음 사용하면서 완벽한 TDD를 구사하지 못한 점이 너무나 아쉽습니다.

View - Controller - Model 객체 간 관계가 너무 종속적이라 "이게 제대로 될까?"하면서 테스트를 구현하기 전 기능을 조금씩 구현해보고 테스트 코드를 작성했던 점이 가장 아쉬움이 많이 남습니다.


참고자료
MVC패턴 추상화
Spring MVC패턴 계층
싱글톤에 대해
javascript singleton

profile
다른 사람들이 이해하기 쉽게 기록하고 공유하자!!

0개의 댓글