티스토리 뷰

시작하며

node.js로 개발되어 있는 42서울 체크인 시스템을 spring으로 클론코딩하는 과제를 진행하게 되었다. 멘토님은 DB(Database) 설계부터 배포까지 한 사이클을 모두 경험하는 게 중요하다고 하셨다.  첫 과제는 DB 스키마 설계였다. 직접 체크인 시스템이 어떻게 돌아가는지 파악해서 DB를 설계한 후, 피드백받았던 과정을 남긴다.

 

현재 42서울 체크인 시스템

42서울의 각 클러스터(개포, 서초)의 입장 인원 수를 파악하기 위해 개발된 서비스

 

- 42OAuth를 통한 로그인

- 실물카드를 받아 카드번호를 입력해서 체크인

- 해당 클러스터 아이피로만 체크인 가능 (외부에서 조작 불가)

- 이미 사용중인 카드로는 체크인 불가능

- 체크아웃 후 실물카드 제출

- 개포, 서초 클러스터 인원수 각각 파악

 

위 정보들을 바탕으로 DB를 설계했다.

 

DB 설계

회원, 체크인아웃, 카드, 클러스터 모델을 설정해 DB 테이블에 1:1 로 매핑시켰다.

 

<회원>

  • 42 API(Application Programming Interface) 에서 모든 유저의 '회원번호(intra)'와 '회원명'을 받아오고, 새로운 '회원번호(new)'를 PK(Primary Key)로 부여한다.
  • 새로운 번호를 부여하는 이유: 만약 intra에서 회원번호를 잘못 관리하는 경우 우리 시스템에도 문제가 생김, 이 PK를 다른 테이블에서 FK(Foreign Key)로 사용하기 때문에 나한테 관리권한이 있어야 함

 

<체크인아웃>

  • 이 테이블을 로그라고 생각했고, 모든 출입 기록이 누적되는 방향으로 설계했다.
  • 나간 사람 순서를 알고 싶을 때, A가 들어오기 직전에 누가 나갔는지 알고 싶을 때...등의 케이스를 추적하려면, 모든 기록이 시간 순으로 쌓여야 한다고 생각했다.
  • 그래서 아래처럼 체크인시간/체크아웃시간을 동시에 갖고 있는 설계는 버려졌다.
    버려진 버전

 

<카드>

  • 사용중여부(bool) : 이미 사용중인 카드는 사용할 수 없기 때문에 필요하다.
  • location : 해당 카드로 체크인을 했을 때 자동으로 클러스터를 구분해서 인원수가 카운팅되어야 하기 때문에 필요하다.

 

<클러스터>

  • max(전체 인원수) 외에 current(현재 인원수) 컬럼도 필요할까 고민이 있었다.
  • 지금은 인원수를 파악할 때 <카드> 테이블에서 해당 location의 사용중(true)인 데이터를 가져와야 하고 / current 컬럼을 추가한다면 <체크인아웃> 기록이 쌓일 때마다 값을 +1 해주는 방법이 있다.
  • 출입때마다 <클러스터> 테이블을 건드리지 않기 위해, 컬럼을 추가하지 않았다. 

 


멘토링과 피드백

 

위 내용처럼 DB를 설계할 때 "각 테이블에 어떤 컬럼들이 필요한지"를 많이 고민했다. 반면에 테이블 자체를 깊게 생각해보지는 않았다. 이후 코드에 유저, 카드... 모델이 있으니까 단순히 해당 객체에 맞춰 테이블 설계했다. "<카드> 테이블에 무엇이 들어가야 할까?"를 생각했지, "왜 테이블이 4개가 필요한데?"라는 질문에는 "객체지향에서 객체와 테이블이 매핑되어야 한다고 배워서"라고 답변할 수밖에 없었다. 한마디로 이미 테이블이 존재한다고 가정하고, 컬럼만 고민했던 것이다.

 

멘토링을 통해 테이블 자체를 고찰하는 시간이 있었고 정규화와 역정규화DB index에 대해 알 수 있었다.

 

 

 

<체크인아웃> 테이블

이 테이블은 로그 기록용으로 사용되고 있다. 그래서 데이터가 점점 쌓이면 검색 비용, 나아가 서버 비용 이슈가 발생한다. 하지만 설계 당시 '로그 서버를 따로 두고, 그 서버를 무한으로 늘리면 되지...ㅎ' 라는 안일한 생각으로 해당 문제를 깊게 생각하지 않았다. 하지만 '출입누적기록'은 항상 필요한 게 아니라 문제가 발생했을 때 추적을 위한 데이터인데, 성능과 비용을 감수하면서까지 이 데이터로 보존해야 할 이유가 부족하다. 

 

이렇게 로그를 DB로 관리하는 것은 데이터가 누적될수록 성능과 비용문제가 발생해 불가능하다. 실제로도 로그는 로그파일을 따로 두고, 필요 시 해당 파일을 분석해서 추적을 한다고 한다.

 

 

<카드> 테이블

카드의 사용여부를 판단하기 위해서 + 해당 카드가 어느 클러스터에 속해있는지 알기 위해서 테이블을 생성했다. 특히 관리자 입장에서 카드의 현황 파악을 위해, 전체 카드번호 목록과 각 카드의 using/no using 여부를 알아야 한다고 생각했다.

 

하지만 멘토님이 "전체 카드를 왜 알아야 하는지, 그게 왜 DB에 저장되어야 하는지" 계속 의문을 던지셨고, 나는 '관리하려면 당연히 필요한 건데...? 도서관에서 책 정보도 전부 DB에 저장되어 있잖아, 없으면 이 책이 대출중인지 아닌지, 도서관에 있는지 없는지 어떻게 알아' 라며 이해를 하지 못했다. 

 

그것이 꼭 'DB'에 저장이 되어야 하는지가 핵심이었다. 어딘가에 (엑셀?) 전체 카드 번호가 기록되어 있고, <회원> 테이블에 [카드번호] 컬럼을 추가해 value(체크인)과 null(체크아웃)으로 구분하는 방법도 있었다. 사용중이지 않은 카드번호 목록을 알고 싶을 땐 이 <회원> 테이블에서 [카드번호]가 not null인 데이터를 뽑아와서 엑셀에서 제외시키면 된다.

 

테이블을 추가할 경우 해당 테이블을 관리해야 하는 책임이 생겨버린다. 직접 출입구에서 카드를 관리하는 사람은 DB가 어떤지 관심이 없는데도, 만약에 카드가 10개 늘어나면 그걸 다 개발자한테 보내고 개발자는 굳이 DB에 10개를 추가하는 번거로움이 생기는 것. 번거로움 뿐만 아니라 DB에도 부담이 된다. 관리상의 편의성이 없다.

 

다시 도서관 책과 비교해보자.

책은 ISBN부터 많은 정보들이 있고 모든 책들을 담당자 선에서 관리할 수 있는 사이즈가 아니기 때문에 DB로 남기지만, 카드에는 단순히 카드번호라는 정보만 있다. 그거 하나 때문에 테이블을 추가해서 DB 관리의 수고로움을 감당할 필요가 없다.

 

 

<클러스터> 테이블

어떻게 특정 카드를 입력하면 특정 클러스터에 카운트가 될 수 있을까?

기존에 구현했던 방식은 카드에 클러스터 정보를 넣고 <카드> 테이블과 <클러스터> 테이블을 관계시키는 것. 하지만 지금은 <카드> 테이블이 없어져 <카드>와 연결되어 있던 <클러스터> 테이블은 이제 동떨어진 섬이 되었다.

 

다른 접근이 필요했고 생각해낸 방법은 아이피로 구별하는 것이다. 기존에 갖고 있던 '클러스터 아이피로 체크인을 제한한다' 라는 true/false를 발전시키면 1/2/3으로 클러스터를 구별하는 것도 가능하다. 이제 개포 문이 닫히고 서초만 문이 열려도 DB에 개포로 등록된 카드정보를 전부 서초로 변경할 필요없이, 그냥 카드 전부 들고 서초로 오면 된다.

 

+추가) 위 방법은 각 클러스터의 IP가 다르다는 전제가 있었다. 하지만 실제로는 클러스터의 모든 IP는 동일하고, 카드 번호로 구분한다고 한다. 어쨌거나 핵심은 카드의 클러스터 정보가 DB에 저장될 필요가 없다는 점이다. 쿼리나 코드로 충분히 클러스터를 구분할 수 있다.


<회원> 테이블

설계할 때부터 "42API에서 회원 정보를 언제, 어떻게 받아오는가"에 대해 고민했고, 두 가지 방법이 있었다.

  1. 사전에 42 API에서 all users 정보를 전부 받아와서 내 <회원>에 저장을 한다.
  2. 첫 체크인이라면 <회원>에 저장을 하고, 그 다음부터 체크인을 시도할 때마다 <회원>에서 해당 유저를 검색해서 없으면 추가한다.

1번으로 진행할 경우 all users 정보를 어느 주기마다 업데이트 해야하는지에 대해 결정이 필요했다. 새로운 기수가 들어올 때마다 업데이트 한다, 42에서 유저정보가 변경될 때마다 업데이를 한다... 등의 후보군이 있었고, 현실적으로 전자가 적당했다.

 

2번으로 진행할 경우 출입마다 반복적으로 DB를 검색하는 것이 불필요하고, 데이터가 쌓일수록 검색 비용 문제가 발생할 것이라고 생각했다. 그리고 출입을 할 때마다 <회원> 테이블을 건드리지 않기 위해서 1번을 선택했다.

 

 

여기까지 <체크인아웃> 테이블은 로그파일로 대체되었고, <카드>와 <클러스터> 테이블은 필요성이 사라져 <회원> 테이블만 남게 되었다. 

<회원> 테이블에 필요한 정보를 추가해서 기존 설계와 비교해보자.

1번: 기존 설계, 2번: 수정 설계

1번)

나는 '출입할 때 <체크인아웃> 외에 다른 테이블에 최대한 영향이 없어야 한다' 라는 원칙(?)을 갖고 있었다. 그래서 <클러스터> 테이블에도 current 컬럼을 넣지 않았고, <회원> 정보도 42 API를 통해 사전에 전부 받아왔다. 이 원칙은 "A 객체를 변경했을 때, A와 관계된 B 객체에 주는 영향이 적어야 한다(결합을 낮춰야 한다)"라는 나의 객체지향적 무의식에서 나온 것 같다. 왜 무의식이냐면 별 의심 없이 당연하다고 생각하고 있었으니까.

 

2번)

하지만 다른 테이블을 전부 제거하고 <회원> 테이블만 남기면 이런 원칙은 필요없어진다. 테이블 간의 관계가 없기 때문에 '누가 언제 어떤 카드로 출입을 했는지' 같은 정보는 <회원>에서 전부 확인할 수 있다. 출입 누적기록을 알고 싶으면 로그 파일을 확인하면 된다.

 

1번이 정규화, 2번이 역정규화다.

 


정규화, 역정규화

정규화

정규화의 가장 큰 목적은 데이터 중복 제거다. 중복되는 데이터가 없어 데이터의 용량이 최소화되고, 입력/수정/삭제 시 이상현상이 발생하지 않아 데이터 무결성을 보장할 수 있다. 정규화를 통해 데이터를 분리해 테이블 간의 관계를 설정하면 필요한 정보가 여러 테이블에 산재하게 된다. 따라서 데이터를 조회할 때 테이블의 join이 필요하다. 이렇게 테이블 개수가 증가하면서 발생하는 과도한 join 연산은 시간과 비용을 증가시키고, 서비스가 진화하다보면 결국 All Tables Join이 일어나게 된다.

 

역정규화

정규화된 데이터에서 읽기 성능을 개선하기 위한 시도가 역정규화다. 약간의 데이터 중복을 허용하고, join 연산을 줄여 전체적인 시스템의 속도를 향상시킬 수 있다. 데이터 중복 때문에 무결성, 저장공간, 쓰기(CUD) 속도에 문제가 생긴다는 단점이 있다.

 

 

정규화와 역정규화 중 정답은 없다. 상황에 따라서 정규화 테이블이나 비정규화 테이블이 필요하다. 설계에 대한 이유를 설명할 수 있고, 그게 요구사항을 만족한다면 된다. (나처럼 무의식적으로 정규화를 선택하는 것만 안하면 된다..)

이 프로젝트에서는 데이터의 중복도 없고, 좀더 고민하면 테이블을 줄여 join 없이 사용할 수 있기 때문에 굳이 불필요한 연산을 발생시킬 필요가 없다. 따라서 역정규화도 좋은 방법이다.

 


멘토링에서 나왔던 질문과 답변

Q(나): 객체와 테이블이 1:1 매핑되어야 한다고 배웠고 정규화가 좀 더 객체지향적인 설계라고 생각합니다. 역정규화는 객체지향에 반대되는데 괜찮나요?

 

A: 유지보수를 쉽게 하기 위해서 객체지향을 쓰는 건데, 그 객체지향을 그대로 모델링에 적용해서 유지보수가 더 어려워졌다. 객체지향을 지키기 위해 테이블을 결합시킨 상태에서는 그 문제를 해결 할 수 없으니까 틀(정규화)을 깨는 것이다. 실 서비스에서는 객체지향을 원칙을 고수하는 게 핵심이 아니고, 일부는 그 틀을 깨서라도 문제를 극복하는 것이 핵심이다. 

 

A2: 정규화 때문에 읽기 성능이 떨어지면 캐시나 Read Table로 역정규화 테이블을 만들어서 쓸 수 있다. 또한 임피던스 불일치를 해결하기 위해 ORM이 나왔고 이제 테이블이 아닌 객체를 보고 개발한다. 객체만 생각해서 개발하면 테이블은 ORM이 매핑해준다 (1:1 매핑을 신경쓸 필요가 없다)

 

 

Q(멘토님): 1번과 2번 설계의 장단점이 무엇인가요?

 

A(나): 답변할 때 어떤 상황을 가정해서 비교를 했었다.

1번) 출입시 <회원> 테이블에 변화가 없다.

2번) 출입시 <회원>테이블이 실시간으로 업데이트 되어야 하지만, 해당 유저가 어떤 상태인지 해당 테이블만으로 빠르게 확인 가능하다.

 

1번) 어떤 카드의 using/no using 여부를 <카드> 테이블을 통해 확인할 수 있다

2번) <회원> 테이블을 통해 체크인 여부로 필터링하고 카드를 확인해야 해서 시간이 더 걸릴 것 같다.

 

지금 시점에서 생각해보니, 이 질문은 정규화의 역정규화를 비교해보라는 질문이었던 것 같다.


[DB설계] 2. 인덱스로 이어진다

 

'프로젝트' 카테고리의 다른 글

프로젝트 회고  (0) 2021.11.28
AWS - 클라우드 서버 구조 분석  (0) 2021.10.07
AWS - VPC 관련  (0) 2021.10.06
URL Shortener - DB 인덱스  (0) 2021.10.01
42 API 사용법 (OAuth)  (0) 2021.09.28
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함