UPDATE 쿼리 실행 시 WHERE 절을 PK로 지정해야 하는 이유

대부분의 데이터베이스에서 특정 row의 데이터를 변경하기 위해 UPDATE ... SET .. WHERE ... 쿼리를 이용하여 데이터를 변경합니다.

WHERE 절에는 row가 특정될 만한 조건을 넣어야 하는데, PK(Primary Key)를 조건으로 지정하는 것이 일반적입니다.

오래된 코드에서는 개발자의 성향이나 편의 등의 사유로 WHERE 절에 PK가 아닌 컬럼을 조건으로 지정하는 경우를 종종 볼 수 있습니다. 

예를 들어 아래와 같은 데이터가 있다고 가정해 보겠습니다.

id는 PK로 지정되어 있고, 나머지 컬럼은 모두 index가 생성되어 있다고 전제하겠습니다.

id code category_id name origin
200 P202309261343597462 3 Audra Kuhn soluta
153 P202309261343597056 3 Orlando Sipes recusandae
146 P202309261343591320 3 Brett Davis PhD quia
106 P202309261343583007 3 Orlando Shanahan sed
48 P202309261343585376 3 Larry Boyer quidem
38 P202309261343573938 3 Mossie Schumm dolore
31 P202309261343575130 3 Una Parisian consequatur

 

id 값이 146인 row의 name을 변경하려면 아래와 같이 SQL을 작성합니다

UPDATE tbl_name SET name = 'New Product' WHERE id = 146

아래와 같이 쿼리를 작성해도 동일한 결과를 얻을 수 있습니다.

UPDATE tbl_name SET name = 'New Product' WHERE category_id = 3 AND origin = 'quia'

첫 번째 쿼리와 두 번째 쿼리 모두 변경하고자 하는 row 만 제대로 특정된다면 크게 문제는 없어 보이는데요,

그럼에도 불구하고 두 번째 쿼리보다 첫 번째 쿼리를 사용하는 것이 좋은 이유는 무엇일까요?

 

바로 PK와 PK가 아닌 컬럼의 인덱스가 서로 다른 방식이기 때문입니다.

 

MySQL 기준, 테이블에 부여할 수 있는 인덱스의 종류로는 Clustered Index와 Non-Clustered Index 가 있습니다.

 

Clustered Index의 특징은 아래와 같습니다.

  • 테이블당 1개만 지정 가능
  • Primary Key 또는 Unique Index만 지정 가능하며, Primary Key가 있는 경우 PK가, 없는 경우 Unique Index가 Clustered Index 로 지정됨
  • 물리적인 정렬 방식을 이용하여 SELECT 속도가 빠름 (UPDATE/INSERT/DELETE 속도는 느림)
  • 생성시 데이터 재정렬이 필요하여 큰 부하 발생

반면에 Non-Clustered Index의 특징은 아래와 같습니다.

  • 테이블당 여러개 지정 가능
  • 페이지 단위로 인덱스가 저장되며, 랜덤한 순서로 저장 (뒤죽박죽)
  • SELECT 속도보다 UPDATE/INSERT/DELETE 속도가 빠름
  • 생성시 부하가 적어 비교적 부담 없이 생성 가능

 

트랜잭션 구간 내에서 UPDATE 쿼리 실행시 데이터베이스는 WHERE 절을 기준으로 레코드에 lock을 걸어 다른 트랜잭션에 의해 데이터가 오염되지 않도록 방지합니다.

Clustered Index를 기준으로 WHERE 절을 지정하면 단일 row만 lock이 걸리지만, Non-Clustered Index를 기준으로 WHERE 절을 지정하면 모든 WHERE 조건을 만족하는 1개 row만 lock이 걸리는게 아니라 WHERE 절의 첫 번째 조건을 만족하는 row 중 나머지 조건이 일치하는 row가 속한 인덱스 페이지 단위로 lock이 걸리게 됩니다.

 

아래는 Non-Clustered Index의 구조를 설명한 그림입니다.

MobileNo = 117인 row를 변경하려고 시도한다면, MobileNo = 117인 row 1개만 lock 이 걸리는 것이 아니라, 같은 인덱스 페이지인 115 ~ 118 범위의 row가 모두 lock이 걸리게 됩니다.

즉, 아래 쿼리로 UPDATE를 시도한다면, id = 146 인 row 1개만 lock이 걸리는 것이 아닌, category_id = 3인 row 중 랜덤한 row들이 lock이 걸리게 됩니다. (인덱스내 row는 정렬이 랜덤이라 페이지별 범위가 특정되지 않습니다)

UPDATE tbl_name SET name = 'New Product' WHERE category_id = 3 AND origin = 'quia'

실제로 변경하려는 row외 다른 row가 lock이 걸리는지 실험을 해보겠습니다.

DataGrip에서 Tx: Manual 로 설정 후 아래 쿼리를 실행하여 Lock을 겁니다.

SELECT * FROM products WHERE category_id = 3 AND origin = 'quia' FOR UPDATE

이 상태에서 id = 106 인 row의 데이터를 변경하려고 시도하면 Lock wait timeout exceeded 오류와 함께 변경에 실패하게 됩니다.

 

실제로 서비스중인 애플리케이션에 위와 같은 쿼리가 포함되었다면, Deadlock이 발생하여 전체 트랜잭션이 롤백되었을 것입니다. Non-Clustered Index 특성상 랜덤하게 페이지가 부여되기 때문에 항상 Deadlock이 발생하는 것이 아닌 '간헐적'으로 Deadlock이 발생하게 됩니다.

만약 운영중인 서비스에서 간헐적인 Deadlock이 발생하는데 원인파악이 어렵다면 위와 같은 케이스에 해당되지 않는지 면밀히 살펴보는 것이 좋습니다.

 

※ 본 포스팅을 할 수 있도록 조언과 도움을 아끼지 않았던  동료 개발자분께 감사드립니다.

Develop MySQL 데이터베이스

회사생활을 마무리하며.

조만간 현 직장에서 퇴직을 할 예정입니다.

퇴직은 작년 말부터 계획하고 있었는데, 인수인계나 여러가지 마무리 할 부분들이 있어서 최종 퇴직까지는 꽤 많은 시간이 걸렸던것 같네요.

 

2019년 2월 현 직장에서 개발자로 채용이 되어 현재까지 4년이 넘는 기간 동안 근무하였습니다.

제가 갑작스럽게 회사를 떠나게 되면, 서비스 장애가 생기지는 않을까... 이런 저런 부분에서 펑크가 나지는 않을까...

여러모로 걱정이 되었지만, 회사가 사람 한 두명 없다고 안 굴러가는 것도 아니고 처음 입사할 당시보다 개발과 운영 프로세스를 개선하며 여러 부분에서 체질 개선이 이루어졌기 때문에 큰 문제는 없을 것이라고 생각합니다.

(벌려 놓은 일이 많다 보니 퇴사 하고 나서도 한 동안은 연락을 받아야 할 것 같습니다)

 

처음 입사할 당시가 생각나네요.

당시 회사에는 제대로 된 개발 프로세스와 운영 시스템이 전혀 갖추어지지 않았습니다.

모든 서비스가 운영환경만 있는 상태에서 레거시 코드를 분석하며 각종 오류수정과 장애대응, 기능개발을 동시에 수행해야 하는 구조였습니다.

당시 충분한 역량이 없었던 저에게는 굉장히 어려운 일이 될 것이라는 생각이 들었고, '이대로 추노할까?' 라는 고민도 잠시 했었습니다.

그럼에도 회사의 서비스는 지속적으로 성장 중이었고 이용자와 트래픽도 제법 높은 편이었기 때문에 나름대로 현 상황을 이겨내면 좋은 경험이 될 것이라고 생각했습니다.

 

처음에는 회사의 시스템을 이해하려고 노력했습니다. 자랑은 아니지만 야근도 많이 했었고 주말에도 일을 했었습니다.

레거시 구조에서 발생하는 다양한 오류를 찾아서 수정하며 장애모니터링 시스템의 필요성을 절감하였고, 각종 시스템 오류나 지표의 변동이 있을 때마다 이를 개발자가 인지할 수 있는 시스템이 있으면 좋겠다는 생각이 들었습니다.

하여, 시스템 오류나 서비스 부하가 있을 때마다 이를 메신저로 알려주는 시스템을 추가하였습니다.

또한, 시스템에서 발생하는 모든 이벤트에 로깅 시스템을 구성하여 이벤트 발생 이력을 남기고 추적할 수 있는 기능을 만들었습니다.
(예를 들면 판매자가 배송비 정책을 변경하거나 고객이 회원정보를 변경하는 등의 행위)

서비스 이용자들이 본인이 잘못해놓고 서비스 장애가 있다고 주장하는 경우가 왕왕 있었는데요, 누가, 언제 어떤 이벤트를 발생시켰는지 명확하게 추적이 되기 때문에 서비스 운영에도 큰 도움이 되었습니다.

 

위기도 한 차례 찾아왔었는데요, 실수로 서비스 장애를 일으켰던 사건입니다.

창을 여러개 띄워놓고 테스트용 테이블을 생성하여 기능개발을 진행하고 있던 와중에, 개발용 테이블과 실제 테이블을 헷갈려 실제 테이블을 삭제해 버린 것이었습니다.

장애를 일으킬 당시에 큰 패닉을 겪었지만 정신을 차리고 대표님께 보고드린 후 전체 서비스를 내려서 데이터 정합성이 깨지지 않도록 조치하였습니다.

이전에 엑스플랜트 개발을 담당하셨던 분께 신속하게 연락을 드렸고, 그 분은 직전 백업에서 삭제된 테이블을 추출하여 복구하는 작업을,

저는 mysql binary log에서 백업 이후에 유입된 데이터들을 추출하여 복구하는 작업을 맡았습니다.

어찌어찌 해서 수습이 마무리 되었고, 이후 재발방지를 위해 개발환경과 운영환경을 완전히 분리하고, 모든 개발작업은 개발 환경에서만 이루어 질 수 있도록 시스템과 프로세스를 갖추었습니다.

개발자가 이용하는 SQL 클라이언트도 auto commit 설정을 수동으로 변경하고, 연결별 색상을 달리하는 등의 조치를 하여 더 이상 실수가 발생하지 않도록 설정하였습니다.

 

작업 후 소스를 배포하는 방식도 수동 배포 방식이었는데요, 이 부분도 Jenkins를 통해 자동으로 배포될 수 있도록 시스템을 갖추었습니다.

Jenkins 설정이 까다로워서 애를 먹었지만 열심히 구글링을 하며 하나씩 셋팅하였고, 팀내 배포 프로세스로 안착시키는데 성공하였습니다.

개발자는 더 이상 배포로 스트레스 받지 않고 개발 업무에 집중할 수 있게 되었습니다.

 

회사에서 이룬 가장 큰 성과는 라라벨 프레임워크를 도입하여 좀 더 모던한 형태로 서비스 개발이 가능해졌다는 부분입니다.

이미 회사의 서비스는 레거시로 운영이 되고 있었기 때문에 모든 부분을 모던 프레임워크로 변경하기는 어려운 상황이었습니다.

레거시를 유지하면서 모던 프레임워크를 연동하여 양쪽 모두 운영이 가능한 방법을 찾아야만 했었고, 레거시 인증(세션)과 라라벨 인증을 연동하는 드라이버와 서비스 프로바이더를 구성하였습니다.

기존에 이미 추가되어 있던 스키마도 라라벨 엘로퀀트 기반으로 엑세스가 가능하도록 모델도 추가하였고, 

노력 끝에 모던 프레임워크를 개발 할 수 있는 환경도 갖추게 되었습니다.

회사의 보안과 관련된 부분이라 어떤 기능이 라라벨로 개발되었는지 상세히 밝힐 수는 없지만, 라라벨의 설계 원칙과 다양한 기능을 활용한 개발이 가능해졌기에 개발자는 회사 업무를 통해 성장을 도모할 수 있고, 이용자들에게도 품질 높은 서비스를 제공할 수 있게 되었습니다.

 

이렇게 개선한 프로세스와 개발환경에서 개발이 이루어질 수 있도록 새로 합류하신 분께도 인수인계를 해드렸는데요,

다행히 제가 구성한 프로세스와 환경을 정확히 이해하고 따라주셔서 그 분께도 감사하게 생각하고 있습니다.

 

회사에서 이룬 여러가지 성과들이 아쉽긴 하지만 퇴사를 결정하게 된 이유는 아래와 같습니다.

ChatGPT의 등장으로 AI 시대가 도래하였고, 점차 개개인이 가진 능력은 퇴색될 것입니다.

이제 AI가 직원 대신 사업계획서나 제안서를 쓰고, 서비스 개발도 대신할 날이 올 것입니다. 결국 모두가 1인 기업가가 되어야 하고, 그렇지 못하면 살아남기 어려울 것인데 개발자에게 요구되는 능력은 기획력입니다.

자신만의 사업을 구상하고 서비스를 기획하여 성공시키기 위해서는 다양한 경험을 통해 인사이트를 축적하는 것이 필요합니다.

한 살이라도 어릴 때 많은 경험을 축적하는 것이 절실하다고 생각하고, 현실에 안주하기 보다는 새로운 도전이 필요할 때라고 생각합니다.

Develop 퇴사 AI 성장

Vue.js에서 Selectbox 컴포넌트 만들기

Vue.js에서 입력 폼을 만들 때 사용할 수 있는 Selectbox 컴포넌트를 만드는 방법을 소개하고자 한다.

SelectBox.vue (이름은 자유롭게 정한다) 파일을 하나 만들고 template를 아래와 같이 작성한다.

<template>
    <select
          v-model="this.value"
          class="custom-select"
          v-bind:id="input_id"
          v-on:input="updateValue($event.target.value)"
    >
        <option v-for="(item, index) in items" :value="index">{{ item }}</option>
    </select>
</template>

Vue.js 2.2 버전부터 v-model을 사용하는 컴포넌트는 value props를 가진다. (참고: #) 따라서 별도로 v-bind로 props를 내려줄 필요 없이 하위 컴포넌트에서 this.value라고만 명시해주면 상위 컴포넌트의 value 값을 하위 컴포넌트 값으로 가져올 수 있다.

그리고, v-on:input="updateValue($event.target.value)" 부분은 사용자가 selectbox의 값을 변경할 경우 변경된 값을 상위 컴포넌트로 전송하기 위한 코드이다. 이 코드가 없으면 상위 컴포넌트에 사용자가 변경한 값이 반영되지 않으니 주의하여야 한다.

script는 아래와 같이 작성한다.

<script>
    export default {
        name: "SelectBox",

        data() {
            return {
                //
            };
        },

        props: ['value', 'items', 'input_id'],

        methods: {
            updateValue: function (value) {
                this.$emit('input', value);
            }
        }

    }
</script>

updateValue 메서드는 사용자가 입력한 값을 input 이벤트로 내보내는 역할을 한다.

마지막으로, 컴포넌트를 가져다 쓰는 부분의 코드는 아래와 같다.

<SelectBox
       v-model="preselect_value"
       :items="somethings"
       :input_id="'my_selectbox'"
       @input="value => { preselect_value = value }"
></SelectBox>

 

Vue.js Component Develop