티스토리 뷰

우아한테크코스 레벨1의 장기 미션을 돌아보며, 학습한 기술 키워드를 아래 10가지 질문으로 회고하였다.

 

1. 어느 범위까지 영속화 할 것이며, 어느 범위까지 메모리에서 재구성할 것인가?

  • 어떤 정보를 메모리 상에서 재구성(재계산)할 수 있을지, 어떤 정보가 재구성 할 수 없을지 등을 기준으로 영속화 할지 안할지에 대한 기준은 현재 어플리케이션 환경과 요구사항에 맞게 세우는 것이 좋다고 생각한다. 장기 미션에서는 히스토리 추적을 위해 현상태가 아닌 이벤트를 저장하는 이벤트 소싱을 선택하였고, 이 덕분에 메모리 재구성 영역에 대해 고민해 볼 수 있었다. 구체적인 사항은 다른 질문에서 설명한다.

2. 객체 사이의 양방향 의존성은 허용되는가?

  • 최대한 지양해야 한다. Board의 내부 상태를 제공하는 쿼리 메소드를 Piece에 전달하기 위해 메서드 인자로 넘겨주는 것을 선택하였기에 양방향 의존성이 발생하게 되었다. 이에 대한 차선책으로 Board와 Piece는 서로 쿼리 메서드에만 의존하도록 구성하였지만, 추후 다른 개발자가 개발하는 과정에서 의존 관계를 읽기 어려울 수 있다고 생각한다.

3. API Contract는 무엇이며, 어떤 관점에서 설계하는 것이 중요한가?

  • API 명세를 의미한다. API Contract가 중요한 이유는 무엇일까? API 명세는 외부에 노출되는 퍼블릭 인터페이스이다. 설계 측면에서 외부에 공개되는 데이터 필드의 구체적인 속성이 무엇인지가 중요한 이유는 서버 내부 정보를 어느 정도, 어디까지 노출 시킬 것인지를 결정하기 때문이다. 너무 많은 구체적인 정보를 외부에 제공한다면, 이는 추후 변경 지점 발생시 해당 명세에 의존이 짙기 때문에 유연하게 대처할 수 없다.
  • 이번 장기 미션과 같은 간단한 어플리케이션내에서도, 이동 기물을 입력하거나 사용자 편의를 위해 현재 게임 상태를 제공하는 이러한 요청, 응답값을 어디까지 노출시킬 것인가에 대한 고민이 따라왔다. 예시로, 사용자가 기물을 이동시킬 때 출발, 도착 위치만 입력 받아 경로를 계산할 것인지, 아니면 출발 위치에 있는 기물의 타입도 함께 입력받아 구체적인 검증을 더 수행할 것인지 고민하였다. 요청/응답 API의 명세 변경은 내부 변경 지점을 초래하기 때문에 API Contract를 심도있게 결정하는 것은 중요하다. 또한 이 변경 가능성에  대비해 설계하는 것도 필요하다.

4. 이벤트 소싱 과정에서 어플리케이션에 있는 “초기 상차림 배치도”를 DB에 메타데이터로 저장한 이유는 무엇인가?

  • 시간이 지난 뒤에도 과거 게임 상태를 정확하게 복원하기 위함이다. 이벤트 소싱 환경에서는 단순히 어떤 전략만 선택했는지만 기록한다면, 추후 기존 배치가 어플리케이션 환경에서 수정되었을 때 추적할 수 있는 방법이 사라진다. 따라서 수정된 배치도를 기준으로 복원을 시도하고, 이 과정에서 논리적 오류가 발생하여 복구에 실패하거나 전혀 다른 복원 값이 생성된다.
  • 메타데이터를 저장함으로서, 이미 확정된 결과 상태를 기준으로 복원을 시킬 수 있기 때문에 시스템 안정성과 재현 가능성을 모두 보장할 수 있다.

5. A테이블에서 B테이블에 다른 컬럼으로 2번 의존하는 것을 어떻게 표현하는가?

  • game 테이블은 formation_template 테이블을 향하는 외래키(FK) 2개를 가진다. 한나라와 초나라의 templete을 각각 보관해야 하기 때문이다. 이런 관계를 같은 테이블을 다른 역할로 두번 참조하는 관계라고 표현한다 .

6. 식별관계와 비식별관계는 무엇인가?

  • 식별 관계는 부모의 PK가 자식의 FK임과 동시에 PK로 포함되는 것을 의미한다. 반대로 비식별 관계는 부모의 PK는 자식의 PK가 아닌 오직 FK키로만 존재하는 것을 의미한다.
  • 즉, 식별 관계는 자식이 부모 없이는 자기를 정의할 수 없는 경우에 사용한다. 예를 들어, 주문과 주문 상품의 관계가 식별관계의 대표적인 예시이다. 즉, 자식이 부모한테 완전히 종속된 개념일 때 사용을 고려할 수 있다.
Order
- order_id (PK)

OrderItem //해당 객체는 부모가 없다면 의미가 없는 객체이다. ㅈ
- order_id (PK, FK)
- product_id (PK)
  • 실무에서는 비식별관계를 더 많이 사용한다. 식별관계로 구성하는 경우 PK가 복합키로 사용되며, 이는 JOIN이나 ORM을 통해 코드로 매핑하는 과정에서 어려움이 발생한다. 또한 부모의 PK가 수정되는 경우, 해당 키가 자식의 PK이기도 하기 때문에 자식 PK도 모두 수정해줘야 하는 문제가 발생한다.

7. 객체 내부 상태를 DB 엔티티로 변환하는 과정에서 발생한 Getter를 대거 추가하는 것은 어쩔 수 없는 타협인가?

  • 이 문제가 발생한 이유는, 도메인 모델과 영속성 모델이 본질적으로 다른 관심사를 가지기 때문이다.
  • 도메인 모델은 내부 프로퍼티를 외부에 직접적으로 노출시키고 싶지 않다. 내부 프로퍼티에 대한 모든 책임은 도메인 내부로 캡슐화하고 싶다. 하지만 인프라 영역이 들어온다면, 디비에 저장하기 위해 도메인 내부의 모든 값을 꺼내오는 과정이 필요하다.
  • 결국, 이 저장을 위한 프로퍼티 인터페이스 공개는 불가피하다. 하지만, 최소한으로 줄일 수는 있다. 그리고 무의미한 getter를 추가하기보다는 도메인 의미가 있는 형태로 메서드를 정의할 수 있다. 예를 들어, GameSnapshot라는 DTO로 DB테이블에 저장할 정보들만 담아 제공할 수 있다. 이와 같이 저장이나 전송에 필요한 표현 객체를 도메인이 만들어서 외부에 제공해주는 것도 사실상 캡슐화의 한 종류이다.
  • 이벤트를 외부에 제공하는 것도 하나의 방법이다. 도메인의 내 상태를 다 꺼내는 형태가 아닌, 이번 실행에서 이런 이벤트가 발생했음만을 infra 레이어에 전달하는 방식으로도 해당 간극을 해결할 수 있다.
  • 결론적으로 어쩔 수 없는 타협이 아니라는 점이다. 이렇게 매핑 편의용으로 getter로 전면 공개하지 말고, 의미있는 퍼블릭 인터페이스로 DB에 반영할 정보만 외부에 제공하는 것이 더 건강하다.

8. SQL에서 ON DELETE / ON UPDATE 키워드는?

  • 해당 키워드는, 외래키 제약 조건에서 ON DELETE와 ON UPDATE는 부모 테이블의 데이터가 변경될 때 자식 테이블에 어떤 영향을 줄지를 정의하는 옵션이다.
  • ON DELETE RESTRICT는 부모 데이터가 자식에서 참조되고 있는 경우 삭제를 막는 역할을 하며, 이는 데이터 정합성을 유지하기 위해 필요하다. 부모 레코드가 없어진다면, 자식 레코드에 논리적 오류가 발생한다. 이를 정합성이 깨졌다고 표현한다.
  • ON UPDATE CASCADE는 부모의 키 값이 변경될 경우 이를 참조하는 자식 데이터도 함께 자동으로 변경되도록 한다. 일반적으로 마스터 데이터(혹은 메타데이터)와 같이 삭제되면 안 되는 경우에는 RESTRICT를 사용하고, 키 변경에 대한 일관성을 유지하기 위해 CASCADE를 함께 설정하는 것이 실무에서 많이 사용되는 방식이다.
  • 한가지 주의할 점은, 해당 키워드는 특정 컬럼에 개별적으로 적용되는 것이 아니라, “부모와 자식 사이의 관계”에 적용되는 규칙이다. 따라서 ON 옵션은 컬럼 단위가 아니라 외래키 관계 단위로 이해하는 것이 정확하다.

9.  어플리케이션 내부 규칙이 변경되는 경우, DB 테이블에 저장된 메타데이터와의 간극 해결

  • DB 테이블에 초기 배치 전략을 별도 메타데이터 테이블로 분리한 이유는, 추후 규칙 변동시 기존 게임들을 재구성하는 과정에서 논리적 오류를 막기 위함이다.
  • 어플리케이션 내부 규칙이 변경되는 경우, 어플리케이션과 DB 사이의 정합성을 해결하기 위해 사용할 수 있는 방법은 다음과 같다.
  1. 기존의 전략 배치를 어플리케이션에서 수정할 때, 영속 영역은 DB테이블에 새 전략으로 추가하는 방식을 사용한다.
    • 이 경우, 기존의 V1 배치를 따라가던 작년의 게임은 재구성시에 V1배치도를 따라 갈 것이며, 새롭게 생성된 게임은 이번에 수정된 배치 전략 V2를 따라갈 것이다.
  2. 게임 시작 시점의 최초 스냅샷을 “이벤트 형식”으로 기록한다.
    • 게임이 시작되는 순간의 스냅샷을 이동 이벤트의 첫번째 기록으로 남기는 방식이다. 즉, 다른 메타 데이터가 필요하지 않으며 모든 게임은 0번째 초기 상태를 기준으로 재구성을 시작한다.
    • 이 경우에 이벤트를 여러 타입으로 구분할 수 있을 것이다. 예를 들어, 0번째 이벤트는 INIT_EVENT로 지정하고, 나머지 이동 이력은 MOVE_EVENT로 표현할 수 있을 것이다.

10. Query, Command의 혼용의 문제점과 실제 오해 사례

  • 기물을 옮기고 어떤 기물을 옮겼는지 출력하기 위해, movePiece와 getMoveStatus를 별도의 메서드로 구성하였다.
  • 그 이유는, movePiece는 command의 성격이기에 void 반환값으로 유지하기 위함이엇고, getMoveStatus는 단순 함수로 상태 반환의 성격이었기 때문이다.
  • 결국 controller의 메서드에서 한 트랜젝션으로 묶기는 했지만, manager 내부에 move와 getMoveStatus가 별도 메서드로 구성되있음이 어색하게 느껴졌다. 그 이유는 getMoveStatus는 무조건 move 메서드가 선행되야 함을 보장해야하는 메서드로 독립적인 메서드로 보기 어려웠기 때문이다.
  • 결론적으로, command와 query가 섞었느냐 자체보다는, 그로 인해 책임이 흐려지고 개발자에게 혼동을 주는냐가 더 중요한 문제이다. 특히, movePiece의 반환값이 단순 조회의 반환값이 아닌, 수행한 행위에 대한 결과 값이라면, 오히려 자연스럽다. 즉, 이런 경우는 조회 기능을 덤으로 붙였다기 보다는 명령 수행의 결과 보고에 가깝다. 즉, 모든 상태를 반환하지 않고, 행위의 결과물로 보인다면 이정도는 괜찮다고 판단했다.
  • 이 고민 과정의 결과물로, Query와 Command의 혼용 허용 여부 판단 기준을 아래 3가지로 결정했다.
    1. 반환값이 단순 조회인지 아니면 명령(Command)의 결과인지 따라 혼용을 결정한다.
    2. 반환값이 없으면, 호출하는 클라이언트가 불편해지는가? 명령 이후 자연스럽게 바로 알아야하는 정보라면 반환해도 괜찮다.
    3. 객체의 캡슐화를 깨지는 않는가? 반환값이 의미있는 결과값만 제공해주는가? 아니면 내부 자료구조 자체를 노출하는가? 후자라면 지양하자.
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday