김예빈Yebeen Kim

포트폴리오 · 학부 수업 프로젝트 · 데이터베이스 원리와 응용

배달의 한양 — 화면을 그리기 전에 개체와 관계를 먼저 정한 배달 웹서비스

같은 주문 한 벌을 점주·구매자·배달대행자 셋이 다르게 봐야 했다. 화면 대신 ERD부터 그리고 MySQL 스키마와 Flask 화면까지 내려갔다

기간
2019.11 – 2019.12
역할
개념 모델·논리 모델 설계부터 MySQL 스키마와 Flask 웹 구현까지 단독 수행
발표·산출
데이터베이스 원리와 응용 과제 2·3 및 텀프로젝트 제출 (한양대학교 산업공학과)
날짜
2019.12
목차 14개 절
목차
  1. 문제 — 데이터는 한 벌인데 보는 사람이 셋이었다
  2. 개념 모델 — 6개 개체와 5개 관계
  3. 키를 고른 근거를 문서에 적어 두었다
  4. 다중값 속성은 릴레이션으로 뗐다
  5. 메뉴는 가게 안에서만 유일하다 — 약한 개체와 복합키
  6. 다대다는 주문_메뉴 하나로 풀었다
  7. 참여 제약 — 정하기 전에 가정을 적었다
  8. 논리 모델 전문 — 개체 6개가 테이블 10개가 되기까지
  9. 역할별 기능 — 하나의 모델 위에 세 개의 화면
  10. 구현 — 모델을 실제 데이터베이스로 옮기며
  11. 돌아간 화면 — 표가 곧 주문 폼이다
  12. 마지막까지 남은 것 — 화면에 결제수단이 숫자로 나왔다
  13. 같은 수업의 나머지 절반 — B+ 트리와 회복 로그
  14. 남은 결론
요약
  • 배달 대행 웹서비스를 만드는 과제였는데, 다루는 데이터는 사실상 '주문' 한 종류였고 그것을 보는 사람이 점주·구매자·배달대행자 셋이었다. 화면부터 만들면 같은 주문이 세 벌로 흩어질 것이 분명해서, 개체와 관계를 먼저 정의하고 화면을 그 위에 올렸다.
  • 개념 모델에서 6개 개체와 5개 관계를 정의했고, 다중값 속성 분해(전화번호·카드·계좌)와 다대다 해소(주문_메뉴)를 거쳐 논리 모델에서 정확히 10개 테이블이 되었다.
  • 설계 판단마다 근거를 문서에 적었다. 점주의 기본키를 이메일이 아니라 아이디로 정한 이유는 '데이터 길이가 더 적을 것으로 보여서'였고, 이메일은 대신 유일성 제약을 건 대체키로 따로 지정해 중복을 막았다.
  • 요구사항에 답이 없는 대목은 가정을 적고 넘어갔다. '구매를 한 사람만 구매자'라고 가정해 구매자–주문 관계를 필수 참여로 두었고, 배달대행자는 배달을 아직 안 한 사람도 있을 수 있으므로 선택 참여로 두었다.
  • 구현 기록에는 성공만 남아 있지 않다. 테스트 데이터를 손으로 넣다가 기본키가 겹쳐 삽입 한 건이 거부된 로그가 그대로 있고, 학기 마지막 수정 문서에는 결제수단 코드값이 화면에 숫자로 노출되던 버그가 적혀 있다.
주요 수치
10개논리 모델 테이블개체 6 + 다중값 분해 3(전화번호·결제수단_카드·결제수단_계좌) + 다대다 해소 1(주문_메뉴)
5개개념 모델 관계소유 · 서비스 · 주문_메뉴 · 구매 · 배달. 이 중 다대다는 주문_메뉴 하나뿐
3종사용자 역할점주 · 구매자 · 배달대행자. 로그인 후 권한별로 화면이 갈린다
hanyang물리 스키마명워크벤치 캡처에서 확인. 한글 개체명을 영문 테이블·칼럼으로 다시 명명해 구현했다
4건제출 자료개념모델 PPT · 개체/관계 설명 DOCX · 과제3 DOCX · 텀프로젝트 HTML 변경분 DOCX

문제 — 데이터는 한 벌인데 보는 사람이 셋이었다

배달 대행 서비스에서 다루는 데이터는 사실상 '주문' 하나다. 문제는 그것을 보는 각도가 셋이라는 데 있었다.

학부 데이터베이스 원리와 응용 수업의 텀프로젝트로 배달 대행 웹서비스 배달의 한양을 만들었다. 요구사항을 읽고 나서 가장 먼저 눈에 들어온 것은 기능의 개수가 아니라, 같은 주문 데이터를 세 부류의 사용자가 각자 다른 각도로 읽고 쓴다는 점이었다.

역할같은 '주문'에서 읽는 것그 역할이 바꾸는 것
점주(판매자)주문 수, 구매자 연락처, 주문 시각, 결제 수단메뉴명·가격의 변경·추가·삭제, 배달대행자 배정, 구매 취소
구매자총 메뉴 수, 결제 수단, 주문 시각, 배달 완료 여부가게 검색, 장바구니 담기, 주문 생성
배달대행자자신에게 배정된 주문배달 완료 여부 갱신

여기서 판단을 하나 내렸다. 화면부터 만들면 데이터가 세 벌로 흩어진다. 점주 화면을 만들며 점주용 테이블을, 구매자 화면을 만들며 구매자용 테이블을 각각 만들게 되고, 그러면 같은 주문이 세 곳에 중복 저장되어 어느 쪽이 진짜인지 알 수 없게 된다. 배달 완료 여부처럼 두 역할이 동시에 건드리는 값에서는 특히 그렇다.

그래서 순서를 뒤집었다. 개체와 관계를 먼저 정의하고, 화면과 기능은 그 위에 얹기로 했다. 이 글은 그 설계 과정과, 설계대로 만들었을 때 실제로 무엇이 쉬워지고 무엇이 여전히 문제로 남았는지에 대한 기록이다.

개념 모델 — 6개 개체와 5개 관계

구현 언어를 고르기 전에 종이 위에서 개체·속성·대응수를 먼저 확정했다.

첫 산출물은 코드가 아니라 개념 모델 다이어그램이었다. 개체 6개와 그 사이의 관계 5개, 그리고 각 개체가 가질 속성을 먼저 적었다.

개체속성메모
가게surrogate key, 상호, 주소(시·군·구·상세주소), 업종정보, 개점시간, 폐점시간, 오픈날짜, 전화번호, 점주 아이디주소는 복합 속성, 전화번호는 다중값 속성으로 표시
점주아이디, 이메일, 비밀번호, 이름가게를 소유하는 쪽
메뉴가게 key, 메뉴번호, 이름, 가격정보, 할인율가게에 종속된 약한 개체
구매자아이디, 이메일, 비밀번호, 이름, 전화번호, 배송주소, 결제수단 번호(카드·계좌)결제수단 번호가 다중값 속성
주문주문번호, 구매시간, 결제방법, 배송완료 여부, 구매자 아이디, 배달대행자 아이디세 역할이 공유하는 중심 개체
배달대행자아이디, 이메일, 비밀번호, 배달영역, 배송가능여부, 남은배송건수, 수수료 정보배정 조건이 여기서 속성으로 들어간다

이 단계에서 이미 배달대행자 배정 기능의 형태가 결정되었다. 점주가 아무 배달대행자에게나 주문을 넘길 수 있는 것이 아니라 배송가능여부와 배달영역을 만족하는 사람만 후보가 되어야 하는데, 그 두 조건이 화면 로직이 아니라 배달대행자 개체의 속성으로 들어가 있으면 배정은 그냥 조건 조회가 된다. 남은배송건수까지 두었기 때문에 '얼마나 밀려 있는 기사인가'도 데이터로 판단할 수 있다.

주소를 시 · 군 · 구 · 상세주소로 쪼갠 것도 같은 이유다. 구매자가 지역으로 가게를 찾는 기능이 문자열 부분 일치가 아니라 속성 조건으로 성립한다.

키를 고른 근거를 문서에 적어 두었다

기본키를 무엇으로 할지는 취향이 아니라 이유가 있어야 한다고 보고, 개체마다 선택 이유를 적었다.

가게에는 자연키가 마땅치 않았다. 상호는 중복될 수 있고 주소만으로도 유일성을 보장하기 어렵다. 그래서 surrogate key를 기본키로 두고, 점주 아이디는 외래키로 참조하게 했다.

점주에서는 아이디와 이메일 중 하나를 골라야 했다. 둘 다 유일하게 만들 수 있는 값이었는데, 당시 적어 둔 선택 이유는 이것이다 — 아이디와 이메일 중에서 데이터 길이가 더 적을 것으로 보이는 쪽을 기본키로 선택했다. 대신 이메일도 중복되면 안 되므로 유일성 제약을 건 대체키로 따로 지정했다. 구매자와 배달대행자에도 같은 방식을 적용했다.

지금 다시 보면 이 근거는 절반만 맞다. 저장 길이는 기본키 선택의 한 가지 고려 사항일 뿐이고, 실제로 더 중요한 것은 그 값이 바뀌지 않는가다. 다만 이 시점에 남겨 둔 것은 판단의 정확성보다 판단에 이유를 붙이는 습관이었다고 생각한다.

점주 릴레이션의 키 설정 — 기본키와 별도로 이메일에 대체키를 만들고 유일성 제약을 걸어 중복 가입을 막았다.
점주 릴레이션의 키 설정 — 기본키와 별도로 이메일에 대체키를 만들고 유일성 제약을 걸어 중복 가입을 막았다.

다중값 속성은 릴레이션으로 뗐다

가게의 전화번호와 구매자의 결제수단은 한 칸에 담길 수 없는 값이었다.

가게는 전화번호를 여러 개 가질 수 있다. 이걸 가게 테이블의 한 칼럼으로 두면 값을 콤마로 이어 붙이거나 전화번호1·전화번호2 식의 칼럼을 미리 만들어 두어야 한다. 둘 다 나중에 값을 늘리거나 검색할 때 문제가 된다. 그래서 전화번호를 별도 릴레이션으로 분리하고, 가게의 surrogate key와 전화번호를 함께 기본키로 두었다.

구매자의 결제수단도 같은 문제였고, 여기에는 종류가 두 가지라는 조건이 더 붙었다. 카드와 계좌를 각각 별도 릴레이션으로 나누고, 각각 surrogate key를 기본키로, 구매자 아이디를 외래키로 두었다. 대응수는 1:n이다.

참여 제약은 비대칭으로 두었다. 카드나 계좌 행은 반드시 어떤 구매자에게 속해야 하므로 필수 참여지만, 구매자는 카드만 등록하고 계좌는 등록하지 않는 경우가 있을 수 있으므로 선택 참여다. 이 비대칭을 적어 두지 않으면 나중에 '계좌가 없는 구매자'가 오류로 오해된다.

가게의 다중값 속성인 전화번호를 별도 릴레이션으로 분리하고 가게 surrogate key와 전화번호를 함께 키로 둔 구조.
가게의 다중값 속성인 전화번호를 별도 릴레이션으로 분리하고 가게 surrogate key와 전화번호를 함께 키로 둔 구조.
구매자의 결제수단을 카드·계좌 두 릴레이션으로 나눈 구조 — 각각 구매자 아이디를 외래키로 참조하며 여러 개를 등록할 수 있다.
구매자의 결제수단을 카드·계좌 두 릴레이션으로 나눈 구조 — 각각 구매자 아이디를 외래키로 참조하며 여러 개를 등록할 수 있다.

메뉴는 가게 안에서만 유일하다 — 약한 개체와 복합키

같은 음식이라도 가게마다 이름과 가격이 다르다는 사실이 키 설계를 바꿨다.

메뉴를 독립 개체로 두면 곤란해진다. 같은 김치찌개라도 가게마다 이름 표기와 가격이 다르고, 어느 가게의 메뉴인지가 빠지면 주문을 복원할 수 없다. 그래서 메뉴를 가게에 대한 약한 개체로 두고, 기본키를 가게의 surrogate key와 메뉴번호를 묶은 복합키로 잡았다. 메뉴 이름은 중복될 수 있으므로 키에 넣지 않았다.

이 결정이 두 가지를 자동으로 해결했다. 첫째, 존재하지 않는 가게에 속한 메뉴는 만들어질 수 없다(필수 참여). 둘째, 점주가 메뉴명과 가격을 바꾸거나 추가·삭제할 수 있는 범위가 자기 가게 안으로 제한된다. 권한을 화면에서 막은 것이 아니라 키 구조가 막는다.

가게–메뉴 관계는 1:n이고, 메뉴가 하나도 없는 가게는 성립하지 않는다고 보아 양쪽 모두 필수 참여로 두었다. 메뉴에는 할인 행사를 상정한 할인율 속성도 함께 넣었다.

가게와 메뉴의 서비스 관계 — 메뉴가 가게의 surrogate key를 자기 키의 일부로 물려받는 식별 관계로 그려져 있다.
가게와 메뉴의 서비스 관계 — 메뉴가 가게의 surrogate key를 자기 키의 일부로 물려받는 식별 관계로 그려져 있다. (세로가 긴 전체 캡처 — 상자 안에서 스크롤하거나 눌러서 크게 보세요)

다대다는 주문_메뉴 하나로 풀었다

한 주문에 여러 메뉴가 들어가고, 같은 메뉴를 여러 번 담을 수도 있어야 했다.

주문과 메뉴는 다대다다. 한 주문에 여러 메뉴가 들어가고, 한 메뉴는 여러 주문에 나타난다. 이걸 물리적으로 표현하기 위해 주문_메뉴 릴레이션을 만들고 임의의 surrogate key를 기본키로 두었다.

키를 '주문번호 + 메뉴' 조합으로 잡지 않고 별도 surrogate key를 준 데에는 이유가 있다. 조합을 키로 삼으면 같은 음식을 두 개 담는 주문이 불가능해진다. surrogate key를 기본키로 두고 주문번호의 중복을 허용했기 때문에, 한 주문에 여러 메뉴를 넣는 것도 같은 메뉴를 여러 번 넣는 것도 가능하다.

참여 제약은 이렇게 두었다. 모든 주문_메뉴 행은 반드시 어떤 주문과 어떤 메뉴에 속해야 한다(필수 참여). 반대로 아직 아무도 주문하지 않은 메뉴는 존재할 수 있으므로 메뉴 쪽은 선택 참여다. 또 주문_메뉴가 가게의 surrogate key와 메뉴번호를 함께 들고 있기 때문에, 특정 주문이 어느 가게에서 나왔는지는 조인으로 언제든 복원된다.

다대다 관계를 해소한 주문_메뉴 릴레이션 — 자체 키를 갖고 주문번호와 메뉴 복합키를 외래키로 물고 있다.
다대다 관계를 해소한 주문_메뉴 릴레이션 — 자체 키를 갖고 주문번호와 메뉴 복합키를 외래키로 물고 있다.

참여 제약 — 정하기 전에 가정을 적었다

요구사항이 답하지 않는 대목이 있었고, 그럴 때는 임의로 정하지 않고 가정을 명시했다.

대응수와 참여 제약은 요구사항만으로 결정되지 않는 경우가 많다. 특히 두 군데가 그랬다.

  • 구매자–주문. 회원가입만 하고 아직 주문하지 않은 사람을 구매자로 볼 것인가. 요구사항에 언급이 없었다. '구매자'라는 단어를 구매를 한 사람으로 해석한다는 가정을 적고 필수 참여로 두었다. 회원가입 직후 상태를 허용하려면 선택 참여로 바꿔야 한다는 것도 함께 적었다.
  • 주문–배달대행자. 주문은 반드시 배달 관계에 참여해야 하므로 필수 참여지만, 아직 한 건도 배달하지 않은 배달대행자가 있을 수 있으므로 배달대행자 쪽은 선택 참여로 두었다.

가게–점주 관계에서는 다른 종류의 판단을 했다. 가게가 이미 surrogate key를 기본키로 갖고 있으므로 점주 아이디를 키에 포함시킬 필요가 없어 비식별자 관계로 두었다. 이 선택은 자신이 없어서 별도로 찾아본 뒤 결정했고, 그 과정도 문서에 남겨 두었다.

관계대응수참여
가게 – 점주 (소유)n : 1양쪽 모두 필수, 비식별자 관계
가게 – 메뉴 (서비스)1 : n양쪽 모두 필수, 메뉴는 약한 개체
주문 – 메뉴 (주문_메뉴)m : n주문 쪽 필수, 메뉴 쪽 선택
구매자 – 주문 (구매)1 : n가정에 따라 양쪽 필수
배달대행자 – 주문 (배달)1 : n주문 쪽 필수, 배달대행자 쪽 선택
가게와 점주의 소유 관계 — 점주 아이디를 가게의 외래키로만 두고 키에는 넣지 않은 비식별자 관계.
가게와 점주의 소유 관계 — 점주 아이디를 가게의 외래키로만 두고 키에는 넣지 않은 비식별자 관계.
구매자와 주문의 구매 관계 — 한 구매자가 여러 주문을 갖는 1:n이며, 한 주문에 여러 구매자가 대응하지 않는다.
구매자와 주문의 구매 관계 — 한 구매자가 여러 주문을 갖는 1:n이며, 한 주문에 여러 구매자가 대응하지 않는다.

논리 모델 전문 — 개체 6개가 테이블 10개가 되기까지

개념 모델에서 논리 모델로 내려오면서 테이블이 넷 늘었고, 늘어난 이유가 각각 다르다.

개념 모델의 개체는 6개였는데 논리 모델은 10개 테이블이 되었다. 늘어난 넷은 전부 이유가 다르다 — 전화번호(가게의 다중값), 결제수단 카드와 계좌(구매자의 다중값, 종류 둘), 주문_메뉴(다대다 해소)다.

릴레이션기본키나머지 속성
점주점주 아이디이메일(대체키), 비밀번호, 이름
가게가게 surrogate key점주 아이디(외래키), 상호, 시, 군, 구, 상세주소, 업종정보, 개점시간, 폐점시간, 오픈날짜
전화번호가게 surrogate key(외래키) + 전화번호
메뉴가게 surrogate key(외래키) + 메뉴번호이름, 가격정보, 할인율
구매자구매자 아이디이메일(대체키), 비밀번호, 이름, 배송주소, 전화번호
결제수단번호_카드자체 surrogate key구매자 아이디(외래키), 카드번호
결제수단번호_계좌자체 surrogate key구매자 아이디(외래키), 계좌번호
주문주문번호구매자 아이디(외래키), 배달대행자 아이디(외래키), 구매시간, 결제방법, 배송완료여부
주문_메뉴자체 surrogate key주문번호(외래키), 가게 surrogate key + 메뉴번호(외래키)
배달대행자배달대행자 아이디이메일(대체키), 비밀번호, 배달영역, 배송가능여부, 남은배송건수, 수수료정보

표에서 세 가지가 눈에 띈다. 첫째, 배정은 별도 관계 테이블이 아니라 주문 행의 외래키를 채우는 행위다. 주문 한 행이 '누가 주문했고 누가 배달하는가'를 함께 담는다. 둘째, 배달 완료 여부가 배송완료여부 속성 하나다. 구매자가 읽는 값과 배달대행자가 쓰는 값이 같은 자리이므로 두 화면이 어긋날 여지가 없다. 셋째, 대부분의 문자열 속성을 같은 길이로 통일해 두었다. 이름·주소·전화번호·비밀번호의 실제 길이는 전혀 다른데 모두 같은 길이로 잡혀 있으니, 이 부분은 지금 기준으로는 명백히 대충 넘어간 대목이다.

논리 모델 전체 — 점주·가게·전화번호·메뉴·구매자·결제수단번호_카드·결제수단번호_계좌·주문·주문_메뉴·배달대행자 10개 테이블과 외래키 연결.
논리 모델 전체 — 점주·가게·전화번호·메뉴·구매자·결제수단번호_카드·결제수단번호_계좌·주문·주문_메뉴·배달대행자 10개 테이블과 외래키 연결.

역할별 기능 — 하나의 모델 위에 세 개의 화면

모델이 정해진 뒤에는 기능을 붙이는 일이 '어떤 관계를 어떻게 읽고 쓰는가'로 바뀌었다.

세 역할의 기능을 정리하면 다음과 같다. 각 기능이 어느 릴레이션의 어떤 속성에 기대는지를 함께 적으면, 앞 절의 설계 판단이 어디에서 제값을 했는지가 드러난다.

역할기능기대는 구조
점주가게 정보 · 메뉴 정보 조회가게 – 메뉴 1:n
점주주문 조회 (주문 수 · 구매자 연락처 · 주문 시각 · 결제 수단)주문에서 구매자 외래키를 따라간 조회
점주메뉴명과 가격의 변경 · 추가 · 삭제메뉴의 복합키가 범위를 자기 가게로 제한
점주조건을 만족하는 배달대행자에게 주문 배정배달대행자의 배송가능여부 · 배달영역 조회 후 주문의 배달대행자 외래키 갱신
점주구매 취소 처리주문 · 주문_메뉴 삭제
구매자지역 또는 상호명으로 가게 검색가게의 시 · 군 · 구 분리 저장
구매자장바구니에 담아 구매주문_메뉴가 같은 메뉴의 중복 담기를 허용
구매자주문 화면에서 결제 수단 · 주문 시각 · 배달 완료 여부 확인주문 한 행
배달대행자배정된 주문 확인 및 배달 완료 상태 갱신주문의 배송완료여부 하나
전체본인 정보 수정역할별 릴레이션의 갱신

배정 기능이 가장 까다로울 것이라고 예상했는데 실제로는 그렇지 않았다. 배달 가능 여부와 담당 지역이 이미 속성으로 있었기 때문에 조건 조회 한 번과 외래키 갱신 한 번으로 끝났다. 화면부터 만들었다면 이 조건은 화면 안의 임시 로직으로 흩어져, 점주 화면과 배달대행자 화면 사이에서 서로 다르게 판단했을 것이다.

구현 — 모델을 실제 데이터베이스로 옮기며

한글로 그린 논리 모델을 영문 스키마로 다시 명명해 올리고, 그 위에 화면을 붙였다.

구현 단계에서 이름이 한 번 바뀐다. 논리 모델은 한글 개체명으로 그렸지만, 실제 데이터베이스는 영문으로 다시 명명했다. 한글로 그린 개체·속성명이 전부 영문 이름으로 다시 붙었고, 문자열·시각 같은 자료형을 이 단계에서 처음 정해야 했다.

실행 로그에는 성공만 남아 있지 않다. 테스트 데이터를 손으로 넣다가 아이디를 겹치게 넣어 삽입 한 건이 기본키 중복으로 거부된 기록이 그대로 있고, 바로 아래 줄에서 같은 형태의 삽입이 한 행 반영으로 성공한다. 기본키 제약이 실제로 작동했다는 뜻이기도 하다.

웹 계층은 요청을 받아 질의를 직접 실행하고 결과를 화면 템플릿에 넘기는 구조로 붙였고, 객체 매핑 계층은 두지 않았다. 수업에서 배운 것은 질의까지였고 웹 계층은 따로 익혀서 붙였다.

데이터베이스 작업 화면 — 논리 모델을 옮겨 만든 스키마와 테이블, 주문 테이블을 만드는 질의문, 그리고 기본키 중복으로 실패한 삽입이 남은 실행 로그.
데이터베이스 작업 화면 — 논리 모델을 옮겨 만든 스키마와 테이블, 주문 테이블을 만드는 질의문, 그리고 기본키 중복으로 실패한 삽입이 남은 실행 로그.
웹 계층 코드 — 요청을 받아 질의를 실행하고 결과를 화면 템플릿에 넘기는 구조로, 객체 매핑 계층을 두지 않았다.
웹 계층 코드 — 요청을 받아 질의를 실행하고 결과를 화면 템플릿에 넘기는 구조로, 객체 매핑 계층을 두지 않았다.

돌아간 화면 — 표가 곧 주문 폼이다

구매자 화면 캡처 한 장이 모델과 화면의 대응 관계를 그대로 보여 준다.

실행된 구매자 화면이 캡처로 남아 있다. 좌측에 역할 이름(구매자)을 그대로 붙인 내비게이션 블록이 있고 그 안에 구매자 정보 화면 · 주문 화면 · 구매 화면 세 개가 들어 있다. 로그인 후 권한별로 페이지가 갈린다는 설계가 실제로는 역할 이름을 그대로 붙인 화면 그룹으로 구현되었다.

화면 영역내용대응하는 릴레이션
가게 정보 표가게 이름, 가게 주소(서울특별시 성동구 사근동), 가게 전화번호, 오픈시간 11:00, 마감시간 19:00가게 + 전화번호
메뉴 정보 표메뉴이름 열 + 행마다 수량 입력칸과 담기 버튼메뉴

주목할 점은 장바구니가 별도 화면이 아니라는 것이다. 메뉴 표의 각 행에 수량 입력칸과 담기 버튼이 붙어 있어 표 자체가 주문 폼이다. 메뉴 릴레이션의 한 행이 화면의 한 행으로 그대로 나가고, 담기 버튼이 주문_메뉴에 한 행을 넣는다. 설계가 화면 구조를 결정한 가장 알기 쉬운 예다.

테스트 데이터도 실제 값으로 채워 넣었다. 가게 주소가 학교 인근인 서울특별시 성동구 사근동이고 영업시간이 11:00~19:00로 들어가 있으며, 메뉴 표에는 실제 한식 메뉴명이 열 몇 줄 이상 채워져 있다. 더미 문자열이 아니라 실제로 있을 법한 값을 넣어야 화면이 깨지는 지점이 보인다.

구현된 구매자 화면 — 좌측 역할 내비게이션, 가게 정보 표, 그리고 행마다 수량 입력칸과 담기 버튼이 붙어 표 자체가 주문 폼이 된 메뉴 정보 표.
구현된 구매자 화면 — 좌측 역할 내비게이션, 가게 정보 표, 그리고 행마다 수량 입력칸과 담기 버튼이 붙어 표 자체가 주문 폼이 된 메뉴 정보 표.

마지막까지 남은 것 — 화면에 결제수단이 숫자로 나왔다

학기 마지막 제출물(2019.12.14)에 남은 수정 기록. 설계가 아니라 표현 계층의 문제였다.

제출 자료 중 마지막 문서는 텀프로젝트 HTML 변경부분이다. 첫 항목이 결제 내역 화면의 버그다.

결제 방법을 데이터베이스에는 코드값으로 저장해 두고, 화면에서는 그 값을 그대로 출력하고 있었다. 그 결과 화면에 결제수단이 1, 2, 3 같은 숫자로 나왔다. 저장 형식은 맞았지만 사람이 읽을 수 있는 형태로 바꾸는 단계가 빠져 있었다.

수정은 화면 쪽에서 조건 분기로 처리했다. 1이면 페이, 2면 신용카드, 3이면 계좌이체, 그 외에는 현금으로 출력하도록 했다. 코드값과 표시 문자열의 대응을 화면에 박은 것이라 좋은 해법은 아니다. 코드 대응표를 따로 두거나 화면에 넘기기 전에 변환했어야 한다.

이 문서는 항목 번호 2만 적힌 채 끝난다. 두 번째 이후의 수정 항목은 문서에 남아 있지 않다.

같은 수업의 나머지 절반 — B+ 트리와 회복 로그

텀프로젝트와 같은 학기, 같은 수업이 요구한 다른 과제도 함께 남아 있다.

이 프로젝트는 수업 하나 안에 있었고, 같은 수업은 서비스를 만드는 일과 별개로 저장 구조와 회복을 손으로 따라가게 했다. 두 과제가 자료로 남아 있다.

하나는 B+ 트리 구축이다. 차수 4인 빈 트리에 (2, 3, 5, 7, 11, 17, 19, 23, 29, 31)을 순서대로 삽입하며 매 단계의 트리를 그리는 문제였다. 뿌리도 잎도 아닌 노드는 1~4개의 자식을, 잎 노드는 2~3개의 값을 갖는다는 조건 아래 삽입할 때마다 노드가 분할되고 상위 키가 올라간다. 열 개를 다 넣으면 높이 3의 트리가 되고, 잎 노드끼리 오른쪽으로 연결된 형태가 남는다. 최종 트리의 뿌리에는 19 하나만 남고, 그 아래 내부 노드가 5 · 11과 29로 갈리며, 잎은 (2,3) · (5,7) · (11,17) · (19,23) · (29,31) 다섯 개다.

다른 하나는 회복 로그 레코드 채우기다. 두 트랜잭션 T1·T2가 start – 갱신 – commit 순으로 남기는 열 개의 로그 레코드에서 각 갱신의 이전 값과 이후 값을 채워 넣는 문제였다. T1이 B를 300에서 400으로, A를 100에서 540으로 바꾸고 커밋한 뒤 T2가 A를 540에서 550으로, E를 80에서 480으로, D를 60에서 530으로 바꾼다. 앞 트랜잭션이 남긴 이후 값이 다음 트랜잭션의 이전 값이 되는 연결을 손으로 따라가게 하는 문제다. 서비스를 만들 때 신경 쓰지 않았던 것들 — 갱신 전 값을 어디에 남기는가, 커밋 전에 죽으면 무엇을 되돌리는가 — 이 여기서 나온다.

이 두 과제를 이 글에 함께 두는 이유는, 텀프로젝트에서 내가 실제로 한 일이 데이터베이스 관리 시스템이 알아서 해 주는 층 위에서 스키마를 설계한 것이었다는 점을 분명히 하기 위해서다. 삽입이 기본키 중복으로 거부되고 트랜잭션이 안전하게 커밋되는 동안, 그 아래에서 인덱스와 로그가 무엇을 하고 있었는지는 별도의 과제로 배웠다.

삽입 중간 단계(7) Insert 29) — 뿌리가 5 · 11 · 19로 가득 차고 오른쪽 잎이 19 · 23 · 29로 채워진 시점. 여기서 31을 더 넣으면 잎 분할이 위로 전파되어 뿌리가 한 단 더 생긴다.
삽입 중간 단계(7) Insert 29) — 뿌리가 5 · 11 · 19로 가득 차고 오른쪽 잎이 19 · 23 · 29로 채워진 시점. 여기서 31을 더 넣으면 잎 분할이 위로 전파되어 뿌리가 한 단 더 생긴다.
차수 4의 B+ 트리에 열 개의 키를 순서대로 삽입한 최종 형태 — 루트 19 아래로 내부 노드가 갈리고 잎 노드가 오른쪽으로 연결된다.
차수 4의 B+ 트리에 열 개의 키를 순서대로 삽입한 최종 형태 — 루트 19 아래로 내부 노드가 갈리고 잎 노드가 오른쪽으로 연결된다.
회복 로그 레코드 과제 — 두 트랜잭션의 start·갱신·commit 순서에서 각 갱신의 이전 값과 이후 값을 채워 넣은 답안.
회복 로그 레코드 과제 — 두 트랜잭션의 start·갱신·commit 순서에서 각 갱신의 이전 값과 이후 값을 채워 넣은 답안.

남은 결론

이 프로젝트에서 확인한 것과, 지금 보면 부족한 것.

확인한 것은 하나다. 데이터의 구조를 먼저 정하면 화면과 기능은 그 위에 놓인다. 로그인 후 권한별 화면 분기도, 배달대행자 배정 조건도, 배달 완료 상태 갱신도 결국 하나의 모델 위에서 어떤 관계를 어떻게 읽고 쓰는지의 문제였다. 반대로 말하면, 화면을 먼저 만들었다면 그 조건들은 화면마다 흩어진 임시 로직이 되었을 것이다.

부족한 것도 분명하다. 문자열 속성을 전부 같은 길이로 통일한 것, 기본키를 고르며 '저장 길이가 짧을 것 같아서'를 근거로 든 것, 결제수단 코드값을 화면 직전에서야 문자열로 바꾼 것은 모두 설계를 끝까지 밀지 않은 흔적이다. 인덱스를 어디에 걸어야 하는지, 데이터가 많아질 때 어떤 조회가 느려지는지는 이 프로젝트에서 전혀 다루지 못했다.

그럼에도 이 과제에서 얻은 순서 — 요구사항에서 개체를 뽑고, 관계와 참여 제약을 정하고, 애매한 대목에는 가정을 적어 두고, 그다음에 화면을 그린다 — 는 이후 작업에서 계속 썼다.

여기서 배운 것

  1. 같은 데이터를 여러 역할이 다르게 봐야 하는 문제는 화면 문제가 아니라 모델 문제다. 배달 완료 여부를 한 칼럼으로 두었기 때문에 구매자가 읽는 값과 배달대행자가 쓰는 값이 어긋날 여지가 없었다.
  2. 기능의 제약 조건은 화면 로직이 아니라 속성으로 넣어야 한다. 배송가능여부·배달영역이 열로 있었기 때문에 배정 기능이 조건 조회 한 번으로 끝났다.
  3. 요구사항이 답하지 않는 대목에서는 임의로 정하는 대신 가정을 적어 두어야 나중에 그 판단을 되짚을 수 있다. '구매를 한 사람만 구매자'라는 가정이 그런 경우였다.
  4. 판단에 이유를 붙이는 습관이 판단의 정확성보다 먼저 온다. 기본키를 고른 근거는 지금 보면 절반만 맞지만, 적어 두었기 때문에 무엇이 틀렸는지 알 수 있다.
  5. 설계가 맞아도 표현 계층에서 무너질 수 있다. 코드값으로 저장한 결제수단이 화면에 숫자로 나온 버그가 그 예이고, 저장 형식과 표시 형식은 별도로 설계해야 한다.