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의 특징은 아래와 같습니다.
반면에 Non-Clustered Index의 특징은 아래와 같습니다.
트랜잭션 구간 내에서 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이 발생하는데 원인파악이 어렵다면 위와 같은 케이스에 해당되지 않는지 면밀히 살펴보는 것이 좋습니다.
※ 본 포스팅을 할 수 있도록 조언과 도움을 아끼지 않았던 동료 개발자분께 감사드립니다.
]]>
<?php
$arr = [1, 3, 55, 88];
if (in_array(55, $arr)) { ... }
위 방식은 사용이 간편하지만 회사에서 대량의 데이터를 생성하는 배치를 개발하던 도중에 심각한 퍼포먼스 저하 현상을 겪었습니다.
특정 기간 내의 데이터를 조회하여 일/월별로 중복제거 또는 합산을 처리하는 코드를 작성 중이었는데, 중복제거를 위한 배열에 값을 넣어두고 루프를 돌 때마다 값이 있는지 확인하는 로직이었는데요,
in_array() 대신 아래와 같은 방식을 활용하여 퍼포먼스 개선을 할 수 있었습니다.
<?php
$arr = [
1 => true,
3 => true,
55 => true,
88 => true
];
if(isset($arr[55])) { ... }
위 현상에 대한 ChatGPT 답변은 아래와 같습니다.
두 코드 조각은 모두 주어진 배열에서 특정 요소가 존재하는지 확인하는 방법을 보여주고 있지만, 두 번째 코드 조각이 성능 면에서 더 효율적인 이유는 다음과 같습니다. 첫 번째 코드 조각에서 in_array 함수를 사용하면 배열을 순회하면서 특정 요소를 찾아야 합니다. 배열의 크기에 따라서 성능이 저하될 수 있습니다. 이 함수의 시간 복잡도는 O(n)입니다. 따라서 배열의 크기가 크면 성능 저하가 발생할 수 있습니다. 두 번째 코드 조각에서는 연관 배열(associative array)을 사용하여 각 요소를 키로 가지고 있는데, 이렇게 하면 배열 내에서 특정 요소를 바로 접근할 수 있습니다. 연관 배열에서 요소를 접근하는 시간 복잡도는 O(1)이므로 매우 빠릅니다. 따라서 특정 키의 값에 접근하는 작업은 상수 시간만큼 걸리며, 배열의 크기와 무관합니다. 따라서 두 번째 코드 조각은 특정 키의 값에 접근하는 과정에서 상대적으로 더 빠르며, 큰 배열에서도 성능을 유지할 가능성이 높습니다. 하지만 이러한 성능 차이는 배열의 크기와 사용하는 컴퓨터 환경에 따라 달라질 수 있으므로, 실제 사용 시에는 성능 테스트를 통해 확인하는 것이 좋습니다. |
PHP로 데스크탑 어플리케이션을 개발할 수 있는 프레임워크가 출시되었다고 하길래, 냅다 설치해봤습니다.
현재 맥/우분투 환경만 지원하며, 맥 장비가 없는 관계로 우분투 22.04 환경에서 설치를 시도해봤습니다.
NativePHP는 단독으로 구성할 수 없으며, 사전에 Laravel 설치가 필요합니다.
아래와 같은 절차로 진행합니다.
$ composer create-project laravel/laravel test-app
$ cd test-app
$ composer require nativephp/electron
$ php artisan native:install
하지만, php artisan native:install 실행시 아래와 같은 오류가 발생합니다.
오류 내용으로 미루어 볼 때, 리눅스 환경임에도 불구하고 Mac용 PHP 바이너리를 참조하려고 시도하는것으로 보입니다.
해당 디렉토리에는 x64 디렉토리도 존재하지 않는것을 알 수 있습니다.
아직 우분투에서는 개발할 수 있는 환경이 갖추어지지 않은 것으로 보입니다.
나중에 Mac 장비를 구하게 되면 다시 시도해보거나, 우분투 대응 업데이트를 해주길 기다리는 방법밖에 없을듯 합니다.
]]>npm run build를 했음에도 불구하고 npm run dev 환경의 주소를 바라보는 오류가 발생할 수 있습니다.
$ php artisan optimize
$ php artisan cache:clear
$ php artisan view:clear
위 명령으로 캐시를 삭제하여도 마찬가지이며,
특이사항으로 npm run dev를 한번 실행 후 build를 하면 이후부터는 문제가 해결됩니다.
원인은 npm run dev 시 public 경로에 hot 파일이 생성되는데, npm dev run 상태에서 (vite 서버가 동작중인 상태에서) phpstorm을 강제종료하는 경우 해당 파일이 남아 있어 발생하는 것으로 추정됩니다.
해결방법은 public/hot 파일을 삭제해주기만 하면 됩니다.
아래 게시글의 도움을 받았습니다.
https://laracasts.com/discuss/channels/vite/laravel-vite-deployment-not-working
]]>
20대 시절 해킹대회 운영 솔루션 개발을 진행하면서 늘 마음속에 품고 있었던 오픈소스 프로젝트를 드디어 시작하였습니다.
당시에도 오픈소스 사이트를 구성해서 배포 준비까지 마쳤지만, 군 입대 등으로 결국 진행하지 못했는데 늦었지만 이번에는 제대로 시작해 보고자 합니다.
오픈소스라는 것이 혼자만 해서는 의미가 없고 마음 맞는 분들과 함께하는 것이 의미가 있기 때문에 함께 개발할 분들도 모셨습니다.
주제는 '라라벨 기반 이커머스 솔루션' 입니다.
무료로 사용할 수 있는 이커머스 솔루션은 영카트, 고도몰 등 이미 시장에 많이 나와 있습니다. 하지만 대부분 낡은 레거시 코드에 기반하고 있습니다.
창업 초창기에는 이런 솔루션이 운영에 큰 도움이 되지만, 비즈니스가 성장하며 새로운 기능을 추가하고 인프라도 확장해야 할 때 기술부채가 발목을 잡는 경우가 많습니다.
PHP 오픈소스라는 장점을 살리면서도 기술부채가 적은 솔루션이 있으면 좋지 않을까라는 고민 끝에 시작한 프로젝트입니다.
라라벨은 아시다시피 PHP 생태계에서 가장 모던한 프레임워크입니다.
의존성 주입, IoC 컨테이너, MVC 패러다임 등 최신 개발 기법과 기능이 두루 적용되어 있기 때문에 이런 기능과 설계원칙을 잘 활용하여 개발을 진행한다면 확장성있는 솔루션을 개발하는데 큰 도움이 될 것입니다.
사족이지만, 얼마전에 새로운 회사로 이직을 하게 되었습니다.
새로운 회사는 5시에 업무가 끝나고 연차 사용이 자유롭기 때문에 업무 외 자기계발을 하기 좋은 환경이 되었습니다.
남은 시간 활용을 잘 하여 프로젝트를 성공적으로 마무리할 수 있기를 기대합니다.
]]>퇴직은 작년 말부터 계획하고 있었는데, 인수인계나 여러가지 마무리 할 부분들이 있어서 최종 퇴직까지는 꽤 많은 시간이 걸렸던것 같네요.
2019년 2월 현 직장에서 개발자로 채용이 되어 현재까지 4년이 넘는 기간 동안 근무하였습니다.
제가 갑작스럽게 회사를 떠나게 되면, 서비스 장애가 생기지는 않을까... 이런 저런 부분에서 펑크가 나지는 않을까...
여러모로 걱정이 되었지만, 회사가 사람 한 두명 없다고 안 굴러가는 것도 아니고 처음 입사할 당시보다 개발과 운영 프로세스를 개선하며 여러 부분에서 체질 개선이 이루어졌기 때문에 큰 문제는 없을 것이라고 생각합니다.
(벌려 놓은 일이 많다 보니 퇴사 하고 나서도 한 동안은 연락을 받아야 할 것 같습니다)
처음 입사할 당시가 생각나네요.
당시 회사에는 제대로 된 개발 프로세스와 운영 시스템이 전혀 갖추어지지 않았습니다.
모든 서비스가 운영환경만 있는 상태에서 레거시 코드를 분석하며 각종 오류수정과 장애대응, 기능개발을 동시에 수행해야 하는 구조였습니다.
당시 충분한 역량이 없었던 저에게는 굉장히 어려운 일이 될 것이라는 생각이 들었고, '이대로 추노할까?' 라는 고민도 잠시 했었습니다.
그럼에도 회사의 서비스는 지속적으로 성장 중이었고 이용자와 트래픽도 제법 높은 편이었기 때문에 나름대로 현 상황을 이겨내면 좋은 경험이 될 것이라고 생각했습니다.
처음에는 회사의 시스템을 이해하려고 노력했습니다. 자랑은 아니지만 야근도 많이 했었고 주말에도 일을 했었습니다.
레거시 구조에서 발생하는 다양한 오류를 찾아서 수정하며 장애모니터링 시스템의 필요성을 절감하였고, 각종 시스템 오류나 지표의 변동이 있을 때마다 이를 개발자가 인지할 수 있는 시스템이 있으면 좋겠다는 생각이 들었습니다.
하여, 시스템 오류나 서비스 부하가 있을 때마다 이를 메신저로 알려주는 시스템을 추가하였습니다.
또한, 시스템에서 발생하는 모든 이벤트에 로깅 시스템을 구성하여 이벤트 발생 이력을 남기고 추적할 수 있는 기능을 만들었습니다.
(예를 들면 판매자가 배송비 정책을 변경하거나 고객이 회원정보를 변경하는 등의 행위)
서비스 이용자들이 본인이 잘못해놓고 서비스 장애가 있다고 주장하는 경우가 왕왕 있었는데요, 누가, 언제 어떤 이벤트를 발생시켰는지 명확하게 추적이 되기 때문에 서비스 운영에도 큰 도움이 되었습니다.
위기도 한 차례 찾아왔었는데요, 실수로 서비스 장애를 일으켰던 사건입니다.
창을 여러개 띄워놓고 테스트용 테이블을 생성하여 기능개발을 진행하고 있던 와중에, 개발용 테이블과 실제 테이블을 헷갈려 실제 테이블을 삭제해 버린 것이었습니다.
장애를 일으킬 당시에 큰 패닉을 겪었지만 정신을 차리고 대표님께 보고드린 후 전체 서비스를 내려서 데이터 정합성이 깨지지 않도록 조치하였습니다.
이전에 엑스플랜트 개발을 담당하셨던 분께 신속하게 연락을 드렸고, 그 분은 직전 백업에서 삭제된 테이블을 추출하여 복구하는 작업을,
저는 mysql binary log에서 백업 이후에 유입된 데이터들을 추출하여 복구하는 작업을 맡았습니다.
어찌어찌 해서 수습이 마무리 되었고, 이후 재발방지를 위해 개발환경과 운영환경을 완전히 분리하고, 모든 개발작업은 개발 환경에서만 이루어 질 수 있도록 시스템과 프로세스를 갖추었습니다.
개발자가 이용하는 SQL 클라이언트도 auto commit 설정을 수동으로 변경하고, 연결별 색상을 달리하는 등의 조치를 하여 더 이상 실수가 발생하지 않도록 설정하였습니다.
작업 후 소스를 배포하는 방식도 수동 배포 방식이었는데요, 이 부분도 Jenkins를 통해 자동으로 배포될 수 있도록 시스템을 갖추었습니다.
Jenkins 설정이 까다로워서 애를 먹었지만 열심히 구글링을 하며 하나씩 셋팅하였고, 팀내 배포 프로세스로 안착시키는데 성공하였습니다.
개발자는 더 이상 배포로 스트레스 받지 않고 개발 업무에 집중할 수 있게 되었습니다.
회사에서 이룬 가장 큰 성과는 라라벨 프레임워크를 도입하여 좀 더 모던한 형태로 서비스 개발이 가능해졌다는 부분입니다.
이미 회사의 서비스는 레거시로 운영이 되고 있었기 때문에 모든 부분을 모던 프레임워크로 변경하기는 어려운 상황이었습니다.
레거시를 유지하면서 모던 프레임워크를 연동하여 양쪽 모두 운영이 가능한 방법을 찾아야만 했었고, 레거시 인증(세션)과 라라벨 인증을 연동하는 드라이버와 서비스 프로바이더를 구성하였습니다.
기존에 이미 추가되어 있던 스키마도 라라벨 엘로퀀트 기반으로 엑세스가 가능하도록 모델도 추가하였고,
노력 끝에 모던 프레임워크를 개발 할 수 있는 환경도 갖추게 되었습니다.
회사의 보안과 관련된 부분이라 어떤 기능이 라라벨로 개발되었는지 상세히 밝힐 수는 없지만, 라라벨의 설계 원칙과 다양한 기능을 활용한 개발이 가능해졌기에 개발자는 회사 업무를 통해 성장을 도모할 수 있고, 이용자들에게도 품질 높은 서비스를 제공할 수 있게 되었습니다.
이렇게 개선한 프로세스와 개발환경에서 개발이 이루어질 수 있도록 새로 합류하신 분께도 인수인계를 해드렸는데요,
다행히 제가 구성한 프로세스와 환경을 정확히 이해하고 따라주셔서 그 분께도 감사하게 생각하고 있습니다.
회사에서 이룬 여러가지 성과들이 아쉽긴 하지만 퇴사를 결정하게 된 이유는 아래와 같습니다.
ChatGPT의 등장으로 AI 시대가 도래하였고, 점차 개개인이 가진 능력은 퇴색될 것입니다.
이제 AI가 직원 대신 사업계획서나 제안서를 쓰고, 서비스 개발도 대신할 날이 올 것입니다. 결국 모두가 1인 기업가가 되어야 하고, 그렇지 못하면 살아남기 어려울 것인데 개발자에게 요구되는 능력은 기획력입니다.
자신만의 사업을 구상하고 서비스를 기획하여 성공시키기 위해서는 다양한 경험을 통해 인사이트를 축적하는 것이 필요합니다.
한 살이라도 어릴 때 많은 경험을 축적하는 것이 절실하다고 생각하고, 현실에 안주하기 보다는 새로운 도전이 필요할 때라고 생각합니다.
]]>매월 납부하는 AWS 비용이 아까워서라도 글을 쓰고자 마음먹었습니다.
2022년에도 회사 서비스의 많은 부분 개선이 있었습니다.
기존 영카트로 운영되고 있던 낡은 상품관리자를 라라벨로 완전히 교체하였습니다.
가장 눈에 띄는 부분은 서비스에 '큐(Queue)' 시스템을 도입했다는 것인데요,
큐란, 특정 작업을 원하는 타이밍에 백그라운드로 처리할 수 있는 기능입니다.
기존 레거시 시스템에서는 비동기 작업을 처리할 때 크론탭이라는 원시적인 방법을 사용하여 대기열에 작업이 쌓이면 처리 속도가 늦어지거나, 작업이 끝날 때 까지 애플리케이션 작동이 멈추는 치명적인 단점이 있었는데요,
이번 큐 도입으로 인해 사용자에게 보다 빠른 서비스를 제공할 수 있게 되었습니다.
특히, 상품등록 시 업로드한 이미지와 동영상을 처리하는 부분에 큐를 적용함으로서 입점사 사장님들께서 보다 빠르게 상품을 등록하고 본연의 업무에 집중할 수 있게 되었습니다.
이외에도 해외수출을 위한 해외배송비 정책 설정기능과 본인인증시스템을 라라벨로 개발하였고, Event & Listener도 도입하여 기능간의 결합도를 낮추는 작업도 함께 진행하였습니다.
(보안상 밝히기는 어렵지만, 그 외에도 많은 부분이 모던하게 바뀌었습니다.)
이렇게 환경을 갖추어 둔 덕분인지 작년 말에 팀에 새로 합류한 주니어분도 자연스럽게 라라벨 기반에서 기능개발을 진행하고 계시답니다. (뿌듯)
기존에는 작업한 코드를 실 서비스에 배포할 때 FTP 업로드 방식을 사용하였습니다. (매우 낡은 방식)
이렇게 배포하다보니 배포 도중 실수가 발생하기도 하고, 배포가 제대로 되었는지도 확실하지 않고, 불필요한 파일들이 정리되지 않는 등 여러가지 이슈가 발생하였습니다.
개발자가 배포하는데 귀한 시간을 낭비하여 (개발자 시급이 얼만데!) 기능 개발에 집중하지 못해 업무 효율성이 떨어지는 문제도 발생했습니다.
이를 개선하고자 코드 배포시 Jenkins라는 CI/CD 툴을 도입하였고, Git에 merge만 되면 이를 Jenkins가 감지하여 자동으로 변경된 부분만을 서버에 반영하도록 구성하였습니다.
이제 저희팀 개발자는 더 이상 코드 배포에 스트레스 받지 않고 온전히 본연의 기능 구현에만 집중할 수 있게 되었습니다.
드디어 회사 서비스에도 카카오페이, 네이버페이, 페이팔 결제를 지원하게 되었습니다.
간편결제가 시장에 활성화된지 꽤 오랜시간이 지났지만 뒤늦게나마 간편결제를 지원하게 되어 다행이라고 생각합니다.
또한, 해외 고객에게도 기존의 불편한 신용카드 결제 방식이 아닌 페이팔 결제를 지원하게 됨으로서, 해외 매출 향상에도 큰 기여를 할 수 있게 되었습니다. (페이팔 결제연동은 저희팀 주니어분이 고생해주셨습니다)
뿐만 아니라, 타사에서 지원하는 '브랜드페이'를 지원하게 되었다는 점인데요,
쿠팡이나 배달의민족 앱에서 카드를 등록하기만 하면 클릭 한번에 자동결제가 이루어지는 것처럼,
회사 서비스에도 자체 페이를 지원하게 되었습니다.
(이부분도 저희팀 에이스인 주니어님이 멋지게 연동해주셨습니다)
이외에도 많은 기능을 개발하고, 개선하였습니다.
2023년 올해에도 파이팅 넘치는 한해가 되기를 소망합니다.
]]>밀린 업무를 처리하느라 눈코뜰새 없던 2021년 이었던것 같습니다. 중간중간 번아웃이 올 뻔한적도 있었지만 어찌어찌 극복하여 올 한 해도 잘 마무리 한 것 같습니다.
체감상으로는 시간이 몇 달 정도 밖에 지나지 않은 것 같은데 나이를 먹어갈 수록 시간이 정말 빨리 자나간다는 것을 몸소 체험하고 있는 요즘입니다.
각설하고, 작년에는 올해 Morden PHP 도입을 소망했었는데, 올해 얼마나 목표를 달성했을까요?
우선, Morden PHP 도입은 성공적이었습니다. 2021년 6월부터 라라벨 프레임워크 기반 백엔드 API를 설계하여 8월부터 실 서비스에 적용하기 시작했습니다.
오래된 서비스에 라라벨을 적용하는 작업은 쉽지 않았습니다. 라라벨은 기존 쿠키 방식의 세션을 사용하지 않기 때문에 레거시 세션을 연동하는 드라이버를 개발해야 했으며, 인증 드라이버 역시 라라벨의 것을 이용할 수 없어 그누보드용 인증 드라이버를 개발하여야 했습니다. 이 과정에서 에반스님이 올려주신 gnu-migration 프로젝트를 많이 참고하였습니다. (이 자리를 빌어 에반스님께 감사드립니다)
이후 주문서, 장바구니, 상품 등 주요 기능을 하나씩 라라벨 API로 이전하였습니다. 그리고 신규로 프로젝트의 대부분은 라라벨 기반 API에서 진행하였습니다.
그 결과 2021년 12월 현재 회사 서비스의 약 10% 비즈니스 로직이 Morden PHP로 구현이 되었습니다.
10% 정도라면 매우 적은 수치일 수 있겠지만, 오랜 기간 동안 유지보수가 이루어지던 서비스였고 대부분 Model 1의 낡은 코드에 기반한 서비스를 이정도로 개선했다는 것만으로 놀라운 성과라고 생각합니다.
대부분의 개발자는 낡은 레거시 코드를 유지보수하기 싫어합니다. 레거시 코드 기반으로 신기능을 추가하고, 유지보수를 한다는 것은 개발자 입장에서 괴로운 일이고 커리어에도 도움이 되지 않는 일입니다. 개발자 커뮤니티에는 새로 출근한 회사가 10년이 넘은 레거시 코드로 운영되는 모습을 보고 3일만에 추노했다는 도시전설(?)까지 있을 정도니 말 다한 셈입니다.
하지만, 개발자가 항상 신기술만 다루며 일을 할 수 있을까요? 아무리 최신 프레임워크와 패러다임을 이용하여 서비스를 만들었다고 해도 그 서비스로 뚜렷한 성과나 매출을 기대하기 힘들다면 기술이 아무런 소용이 없는 일입니다. 레거시 코드로 10년 이상 유지가 되었다는 것은 그 서비스의 업력과 실적이 그만큼 탄탄하다는 반증입니다.
이 글을 보시는 개발자분들께서도 레거시를 만난다면 회피하기보다는 기회를 봐서 과감하게 환경을 바꾸어 보라고 말씀을 드리고 싶습니다. 길가에 떨어진 쓰레기를 치우는 시민의식이 있다면 거리가 늘 깨끗해지듯이, 레거시를 맞닥뜨리게 되면 과감하게 개선하는 문화가 자리잡는다면 개발자들이 일하기 좀 더 편해지지 않을까요?
아무튼, 2022년부터는 새로운 API에 더 많은 기능을 추가하고 기존 레거시에 구현되어 있던 기능들도 마이그레이션이 진행될 예정입니다. 그렇게 된다면 저희 회사에 새로운 인원이 합류하였을 때 백엔드 구조를 이해할 필요 없이 가져다 쓰기만 하면 되기 때문에 업무 효율이 극대화되게 됩니다. 앱 개발자는 백엔드 기술에 대한 이해 필요 없이 앱 개발에만 집중할 수 있으며, 프론트엔드 개발자는 PHP나 SQL을 할 줄 몰라도 디자이너가 작업한 멋진 디자인을 프론트 기술을 활용하여 빠르게 실서비스에 적용할 수 있습니다.
탄탄한 기초 위에 멋진 건물이 지어질 수 있듯이, API가 탄탄한 기초 역할을 하여 저희 회사가 멋진 플랫폼 기업으로 성장할 수 있는 기반이 되기를 소망합니다.
]]>
회사가 운영하고 있는 서비스에 많은 기능들이 추가되고 이용 고객들이 날로 늘어나다 보니 서버 업그레이드에 대한 소요가 지속적으로 제기되었습니다.
기존에 운영하고 있던 스토리지 서버도 훌륭한 스펙을 갖추고 있었지만 상기의 문제들로 점점 한계에 부딪혔습니다.
그래서 몇 달 전쯤 새로운 스토리지 서버를 추가적으로 도입하였고, 이전까지 성공적으로 마무리하였습니다.
회사 보안상 자세하게 밝힐 수는 없지만 대략적인 과정을 블로그를 통해 공유하고자 합니다. ^^
먼저 기존에 운영하고 있던 스토리지 서버는 다음과 같은 문제를 내포하고 있었습니다.
회사가 운영중인 서비스에는 상품 판매를 위한 이미지, 사이즈별 썸네일, 첨부파일 등 자잘한 파일이 수십TB를 이루고 있었습니다.
그리고 모든 서버가 해당 스토리지에 연결되어 하루에도 수십GB 분량의 파일을 썼다, 지웠다 등의 작업이 발생하고 있는 상황이었습니다.
그 상황에서 대용량 SSD가 탑재된 신규 스토리지로 이전 작업을 해야 하는데, 요구조건은 아래와 같습니다.
서버 이전을 위해 실제로 서비스중인 서버를 종료하고 수 시간에 걸쳐 데이터를 신규 서버로 복사하고 서버를 부팅하는 방식으로 작업을 진행한다면, 이는 서비스의 중단으로 이어지고 곧 매출에 부정적인 영향을 미칠 수밖에 없습니다.
따라서, 대규모 서비스에서 서버를 업그레이드하거나 이전하는 작업은 무중단 작업이 필수불가결한 부분일 수밖에 없습니다.
절차는 아래와 같습니다.
기존 스토리지 서버에는 더 이상 필요가 없어진 파일을 자동으로 삭제하거나, 자동으로 백업하는 batch job이 있었습니다. 이러한 부분이 있다면 사전에 중단하여 신규 서버 이전 도중 데이터 불일치가 발생할 여지를 최소화 하도록 합니다.
1차적으로 기존 스토리지에 있던 파일을 신규 스토리지 서버로 복사합니다. 복사는 리눅스의 rsync를 사용하였습니다.
$ rsync -aPvz --delete --bwlimit=4096 /복제할소스 사용자명@신규서버주소:/복제할목적지
bwlimit 옵션을 주면 일정 대역폭 이내에서 파일을 전송하게 됩니다. 기존 스토리지가 하드디스크 드라이브였기 때문에 부하를 최소화하기 위함입니다.
만약 rsync 복제가 실패한다면 873번 포트가 허용되어 있는지 확인하고, 차단되어 있다면 이를 허용해주도록 합니다.
$ firewall-cmd --permanent --zone=dmz --add-port 873/tcp
$ firewall-cmd --reload
rsync로 파일 전체 복제를 1차적으로 마무리 하였더라도 기존 스토리지에는 지속적으로 파일 쓰기/삭제가 발생하고 있기 때문에 결과적으로 기존 스토리지와 신규 스토리지의 데이터는 달라질 수밖에 없습니다.
불일치한 데이터를 맞추기 위해 lsyncd를 활용할 것입니다.
lsyncd는 우리가 흔히 사용하는 원드라이브, 구글드라이브 등의 동기화 프로그램을 PC에 설치하면 PC의 내용을 클라우드 서버로 자동을 전송하는 것처럼 양 서버간의 불일치한 데이터를 동기화 시켜주는 강력한 도구입니다.
lsyncd는 양방향/단방향 동기화가 가능하며, 여기서는 단방향 동기화 설정을 하여 서버 이전작업을 진행할 것입니다.
먼저 기존 스토리지 서버에 lsyncd가 설치되어 있지 않다면 면저 설치해 주도록 합니다. (기존 스토리지 서버가 동기화 서버가 될 것입니다)
$ yum install lsyncd
그리고 /etc/lsyncd.conf 설정파일을 편집하여 아래와 같이 설정해줍니다.
...
sync {
default.rsync,
delete=true, # 원본(소스) 파일이 삭제될 경우 목적지 서버의 파일도 함께 삭제됩니다.
source="복제할소스디렉토리",
target="복제할타겟디렉토리::타겟서버rsync공유이름",
exclude={"제외할디렉토리"},
rsync = {
archive = true,
compress = false,
whole_file = false
},
delay=0
}
그리고 lsyncd 서비스를 시작해 줍니다.
$ systemctl start lsyncd
동기화 서버 설정이 끝났다면 동기화 클라이언트 (복제 대상 목적지) 설정을 할 차례입니다.
신규 스토리지 서버에서 /etc/rsyncd.conf 를 아래와 같이 설정합니다.
...
[rsync공유이름]
# destination directory
path = /공유할디렉토리
# Hosts you allow to copy (specify source Host)
hosts allow = 허용할IP
use chroot = yes
uid = root
gid = root
read only = false
설정이 완료 되었다면 서비스를 기동합니다.
$ systemctl start rsyncd
이제 lsyncd는 기존 스토리지 서버의 파일과 신규 스토리지 서버의 파일을 서로 비교하여 불일치한 부분만 자동으로 동기화 할 것입니다.
동기화 진행 상황은 로그파일 /var/log/lsyncd.log 에서 확인할 수 있습니다.
(최초에는 양 서버간 불일치 파일을 확인하기 때문에 시간이 오래 걸립니다)
3번까지 진행했다면 대부분의 이전 작업은 마무리 된 것입니다.
이제 가장 서비스 이용량이 적은 시간대 (주로 새벽)에 서비스를 잠시 내리고 (1분 이내) mount point 등 기존 서버에 연결되어 있던 경로를 신규 서버로 맞추어주면 되겠습니다.
이 때, 각 서버들의 서비스 설정파일, 서비스 코드 내 경로를 콘솔이나 에디터로 미리 변경해놓고 동시다발적으로 배포 및 서비스 재기동을 실시하여 다운타임을 최소화 할 수 있도록 합니다.
그리고 기존 서버에 동작하고 있던 Batch job도 신규 서버에 맞게 수정하여 다시 등록하면 되겠습니다.
이상으로 대용량 스토리지 서버 이전 작업에 필요한 사항을 간략하게나마 적어 보았습니다.
대부분 위와 같은 시나리오대로 진행하면 되지만, 실무에서는 예기치 못한 상황이 언제든지 발생할 수 있습니다.
예를 들면 이전한 서버가 예상대로 동작하지 않는 경우가 있을 수 있는데, 이를 대비해서 언제든지 이전을 중단하고 기존 서버로 복구할 수 있는 절차도 마련해 두시는 것을 권장합니다.
물론 작업에 필요한 사항은 보고서, 계획서로 정리하여 사전에 충분한 검토를 거친 후 시행하는 것이 좋겠습니다.
감사합니다.
]]>눈 감았다 떠보니 2021년이 되어 있을 정도로 정신없이 일했던 것 같습니다.
전 직장에서는 보안장비에 올라가는 관리자 화면을 개발했습니다.
제품의 특성상 회사의 핵심적인 업무가 아니었기 때문에 웹 개발자로서 성장의 한계를 체감하였습니다.
신기술 도입에도 인색하여 변변한 사내 업무관리 시스템이나 버전 관리 시스템 조차도 없어서
이메일과 공유폴더로 업무를 관리하다 보니 업무 효율도 시대에 뒤떨어지는 부분이 많았습니다.
반면에 현재 다니는 회사에서는 Notion으로 모든 프로젝트와 일정, 문서를 관리하고 있습니다.
참고: 스타트업, 더 좋은 문서도구가 필요해요! Notion을 만나다. | by seapy | 당근마켓 팀블로그 | Medium
기존에는 Jira+Confluence 로 협업시스템이 구성되어 있었습니다만, 최근 업데이트 이후 전체적으로 굼뜨고 무거워지는 문제가 생겼습니다.
그 바람에 대체할 솔루션을 찾고 있던 도중 Trello를 알게 되었고, 잠시 Trello로 일정관리를 하다 이번에 최종적으로 Notion에 안착하였습니다.
협업 툴을 변경하자는 제안을 대표님께서 혼쾌히 수락해주시고, 동료들도 새로운 도구를 익히는데 불편함을 감수해 준 덕분입니다.
회사가 운영하고 있는 서비스에도 많은 기능들이 추가되었습니다.
그 중 주요한 것을 뽑자면 아래와 같습니다.
뿐만 아니라, 서비스 아키텍처 구성에도 큰 변화가 있었습니다.
2019년과 2020년은 실무에서 좌충우돌하는 성장의 기간이였다면,
2021년에는 그동안 계획했던 Modern PHP의 도입, CI/CD 등 서비스/개발프로세스 고도화의 한 해가 되기를 소망합니다.
]]>원인은 여러가지일 수 있다. 몇 가지를 꼽자면,
등이 있다.
하지만, 서버 리소스나 네트워크에 별 다른 문제가 없고, Slow Query 로그에도 별다른 이상징후가 없다면,
원인분석을 해야 하는데, 이 경우 Web Application에서 어느 로직에서 문제가 발생하는지 원인을 찾아야 한다.
개발환경이라면 Xdebug를 활용 수 있겠지만, 실 서비스 중인 서버에는 적용할 수 없는 방법이다.
이 때는 php-fpm의 slowlog를 찍어서 확인하는 방법이 있다.
php-fpm.conf 파일을 열어보면 아래와 같은 설정을 확인할 수 있다.
......
request_slowlog_timeout = 30s
slowlog = /var/log/php-fpm/slow.log
......
여기서 request_slowlog_timeout을 3~5s 정도로 낮추고, php-fpm 서비스를 재시작한다.
그 후 tail 명령으로 /var/log/php-fpm/slow.log 를 확인 후 응답속도가 느린 페이지에 접속을 계속 시도해보면 다음과 같은 로그가 찍힌다.
[10-Nov-2018 11:30:22] [pool www] pid 8260
script_filename = ******************
[0x00007f1a47de79a8] fopen() ****.php:422
[0x00007f1a47de6500] +++ dump failed
이 로그에는 응답속도 저하의 원인이 되는 파일과 해당 function까지 추적해주기 때문에 원인을 금방 파악할 수 있다.
]]>가끔 기술 관련된 글도 올리고 해야 하는데 여유가 별로 없었던 듯... ㅡ,.ㅡ
얼마전에 Laravel 6.0 LTS도 출시되었는데 업데이트도 하고 기능 추가도 할 겸 블로그 손도 봐야 할 것 같다.
]]>기존에는 /etc/network/interfaces 파일에서 설정을 하였지만, 18.04 버전 부터는 netplan을 통한 설정 방식으로 바뀌었다.
VirutalBox의 경우 네트워크 카드 하나를 추가하고 다음에 연결됨을 [호스트 전용 어댑터]로 설정한다.
가상머신을 재부팅 한 후, ifconfig -a 명령을 통해 추가된 네트워크 인터페이스를 확인한다.
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.2.15 netmask 255.255.255.0 broadcast 10.0.2.255
inet6 fe80::a00:27ff:fe13:c615 prefixlen 64 scopeid 0x20<link>
ether 08:00:27:13:c6:15 txqueuelen 1000 (Ethernet)
RX packets 342 bytes 411198 (411.1 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 111 bytes 9792 (9.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.56.100 netmask 255.255.255.0 broadcast 192.168.56.255
inet6 fe80::a00:27ff:fe9a:198e prefixlen 64 scopeid 0x20<link>
ether 08:00:27:9a:19:8e txqueuelen 1000 (Ethernet)
RX packets 136 bytes 11940 (11.9 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 394 bytes 39229 (39.2 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 120 bytes 8836 (8.8 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 120 bytes 8836 (8.8 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
추가된 인터페이스 카드 명이 enp0s8 임을 확인할 수 있다.
Ubuntu 18.04 버전부터는 /etc/netplan/*.yaml 파일을 수정하여야 한다.
가상머신에서 다음 명령을 입력하여 해당 파일을 vi 에디터로 편집한다.
sudo vi /etc/netplan/*.yaml
다음과 같이 ipconfig -a 명령으로 찾은 신규 네트워크 카드 명을 기재하고, IP를 설정해준다.
# This file is generated from information provided by
# the datasource. Changes to it will not persist across an instance.
# To disable cloud-init's network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
ethernets:
enp0s3:
addresses: []
dhcp4: true
enp0s8:
addresses: [192.168.56.100/24]
gateway4: 192.168.56.1
nameservers:
addresses: [8.8.8.8,8.8.4.4]
dhcp4: no
dhcp6: no
version: 2
변경한 설정을 적용하려면 다음 명령을 입력한다.
sudo netplan apply
]]>
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>
]]>
다음 코드는 Post 모델에 Category 모델의 name 컬럼을 Post 모델의 category_name 프로퍼티로 포함하는 예제이다.
class Post extends Model
{
protected $appends = [
'category_name',
];
public function category()
{
return $this->belongsTo(Category::class);
}
public function getCategoryNameAttribute()
{
return $this->category->name;
}
}
하지만, 중간중간 해당하는 Category 모델이 없는 경우 (카테고리가 삭제 된 경우) "laravel Trying to get property 'name' of non-object" 에러가 발생한다.
당연히 해당 모델이 없기 때문에 $this->category는 null이 되고, $this->user->name 프로퍼티에 접근할 수 없기 때문에 발생하는 문제이다.
이럴 경우 다음과 같이 null 여부를 체크하면 된다.
public function getCategoryNameAttribute()
{
return $this->user !== null ? $this->user->name : null;
}
뿐만 아니라, 개발에 있어서 이와 같은 방어적 코딩을 습관화 하는 것이 좋다.
]]>
[Illuminate\Database\QueryException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table users add unique users_email_unique(email))
[PDOException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes
이럴 경우 AppServiceProvider.php의 boot() 메서드에 다음 구문을 추가하여 해결할 수 있다.
use Illuminate\Support\Facades\Schema;
public function boot()
{
Schema::defaultStringLength(191);
}
]]>
대표적으로 순번 필드가 있는데, 고유번호(id) 필드와는 별도로 item마다 순번을 부여하여 취급하고자 하는 경우이다.
간단한 필드의 경우 Mutators를 이용할 수 있지만, 순번과 같이 아이템의 개수와 페이지에 따라 동적으로 값이 부여되어야 할 경우 Controller에서 생성해주어야 한다.
다음은 User 모델의 항목들을 10개 단위로 Pagination 하는 예제이다.
public function index()
{
$users = User::paginate(10);
$response = [
'pagination' => [
'total' => $users->total(),
'per_page' => $users->perPage(),
'current_page' => $users->currentPage(),
'last_page' => $users->lastPage(),
'from' => $users->firstItem(),
'to' => $users->lastItem()
],
'data' => $users
];
return response()->json($response);
}
여기서 number라는 순번 필드를 $users에 추가하고자 하는 경우 다음과 같이 코드를 수정한다.
public function index()
{
$users = User::paginate(10);
$total = $users->total();
$per_page = $users->perPage();
$current_page = $users->currentPage();
$count = 0;
// 순번 필드를 추가하여 transform
$users->getCollection()->transform(function ($user) use ($total, $per_page, $current_page, &$count) {
$user->number = ($total - ($per_page * ($current_page - 1))) - $count;
$count++;
return $user;
});
$response = [
'pagination' => [
'total' => $total,
'per_page' => $per_page,
'current_page' => $current_page,
'last_page' => $users->lastPage(),
'from' => $users->firstItem(),
'to' => $users->lastItem()
],
'data' => $users
];
return response()->json($response);
}
이렇게 하면 고유번호와는 별개로 순번 필드인 number가 각 item마다 추가된다. 페이지를 이동하여도 각 페이지에 맞게 순번이 올바르게 부여되는 것을 확인할 수 있다.
view에서 출력할 때 해당 필드를 적절히 이용하면 된다.
]]>AdminLTE는 무료 오픈소스 대시보드 템플릿이다. 본 포스트에서는 AdminLTE를 Laravel과 통합하는 방법을 설명하고자 한다.
Laravel 프로젝트에서 다음 명령어를 입력하여 AdminLTE 템플릿을 npm으로 설치한다.
(bower로 설치하는 방법도 있지만 Laravel Mix와 연계를 위해 가급적 npm을 사용하는 것이 좋다고 생각한다.)
npm install admin-lte --save-dev
resources/sass 경로에 admin.scss (이름은 자유롭게 한다.) 파일을 하나 생성하고, 코드를 다음과 같이 입력한다.
// Fonts
@import url('https://fonts.googleapis.com/css?family=Nunito');
// Variables
@import 'variables';
// Font awesome
@import '~font-awesome/scss/font-awesome';
// Bootstrap3
@import '~admin-lte/bower_components/bootstrap/dist/css/bootstrap.css';
// Admin LTE
@import '~admin-lte/dist/css/AdminLTE.css';
@import '~admin-lte/dist/css/skins/_all-skins.css';
@import '~admin-lte/bower_components/Ionicons/scss/ionicons';
참고로 AdminLTE 2.x 버전은 Bootstrap 3에 의존성을 가진다. 2018년 12월 기준 현재 Laravel 5.7 버전은 Bootstrap 4 버전을 내장하고 있기 때문에 기존의 app.scss 파일에 있는 Bootstrap 4를 그대로 사용하면 스타일이 깨져 나온다. admin-lte에 내장된 Bootstrap 3를 사용하도록 한다.
resources/js 경로에 admin.js 파일을 하나 생성하고 코드를 다음과 같이 입력한다.
require('admin-lte');
require('admin-lte/bower_components/chart.js');
webpack.min.js 파일은 다음과 같이 수정한다.
mix.js('resources/js/app.js', 'public/js')
.js('resources/js/admin.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.sass('resources/sass/admin.scss', 'public/css')
.extract([
'jquery'
])
.autoload({
jquery: ['$', 'jQuery', 'jquery'],
})
.version();
중간에 autoload 메소드에 jquery를 로드하는 부분 없이 그냥 jquery만 vendor에 통합하면 Error가 발생한다.
다음으로, DashboardController를 생성한다.
php artisan make:controller DashboardController
app/Http/Controllers/DashboardController.php를 아래와 같이 편집한다.
class DashboardController extends Controller
{
public function index()
{
return view('dashboard');
}
}
routes/web.php를 아래와 같이 편집한다.
Route::get('/admin', 'DashboardController@index')->name('dashboard');
resources/views/layouts 에 다음과 같이 레이아웃 파일을 생성한다. 레이아웃 형식은 자유롭게 작성한다.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title></title>
<link href="{{ asset('css/admin.css') }}" rel="stylesheet">
</head>
<body class="hold-transition skin-blue sidebar-mini">
<div id="app">
@yield('content')
</div>
<!-- Scripts -->
<script src="{{ mix('js/manifest.js') }}"></script>
<script src="{{ mix('js/vendor.js') }}"></script>
<script src="{{ mix('js/admin.js') }}"></script>
<script src="{{ mix('js/app.js') }}"></script>
@yield('script')
</body>
</html>
이제 resources/views에 dashboard.blade.php 뷰 파일을 생성하고 AdminLTE 데모 페이지의 소스를 참고해 내용을 채워넣는다.
js와 sass파일을 컴파일한다.
npm run prod
]]>
Laravel Dusk에서 로그인 테스트를 하고자 할 경우 다음과 같이 코드를 작성 할 수 있다.
/** @test */
public function 로그인한다()
{
$this->browse(function (Browser $browser) {
$browser->visit(route('login'))
->type('email', 'myemail@address.com')
->type('password', 'password')
->press('Login')
->assertRouteIs('home');
});
}
/** @test */
public function 사용자_입력값이_유효하지_않으면_오류난다()
{
$this->browse(function (Browser $browser) {
$browser->visit(route('login'))
->type('email', 'test@testdomain.com')
->type('password', '123456')
->press('Login')
->assertSee(trans('auth.failed'));
});
}
첫번째 로그인한다()는 정상적으로 작동하지만, 사용자_입력값이_유효하지_않으면_오류난다() 메서드는 오류가 발생한다. 이전 로그인 세션이 남아있기 때문이다.
이를 해결하기 위해서는 DuskTestCase.php에 다음과 같이 setUp() 메서드를 오버라이딩하고, 쿠키 삭제 코드를 추가한다.
protected function setUp()
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
}
]]>