Skip to main content

PHP 코드의 예측 불가능한 동작

· 7 min read
Bundaberg Man
Maintainer of Docusaurus

도전: PHP 코드의 예측 불가능한 동작 🎯

10년차 개발자인 저도 코딩을 하다가 자주 문제에 부딪힙니다.

이번에는 엑셀 파일에서 데이터를 처리하는 PHP 스크립트가 예측 불가능하게 동작하는 문제가 발생했다.

심사 마감이 얼마 남지 않은 상황에서 오늘 오전 9시에 들어온 요청인데 적어도 오후 2-3시 사이에 문제를 파악해서 수정하는 코드를 배포하는 것이 중요한데 와 오늘 오후 마감인데 이제 요청이...?

이 문제의 원인을 찾는 것이 중요 🔍

엑셀에 업로드된 파일로 데이터를 입력하는 기능인데 어떨 때는 정상적으로 업로드 되고 어떨 때는 정상적으로 업로드 되지 않는 문제가 생겼다. 프로그래머로 이런 문제를 만나면 정말 당황스럽다 상황에 따라 되고 안되고 한다니? 똑같은 코드에서..?? 😱

# 같은 프로세스인데 저장 된 값이 아래 부분과 윗 부분이 다르다.
# 거의 숨은그림 찾기 수준 잘 찾아보면 다른점이 보인다.
# a:5:{i:1;a:2:{s:5:"check";i:0;s:4:"text";s:107:"정량지표는 보통 계량화된 수치의 형태로 나타날 수 있는 생산성 지표이다.";}i:2;a:2:{s:5:"check";i:0;s:4:"text";s:130:"정성지표는 비계량화된 형태로 나타나므로 역량을 기반으로 평가를 하는 것이 일반적이다.";}i:3;a:2:{s:5:"check";i:0;s:4:"text";s:150:"조직의 성과평가는 객관적이고 공정해야 하며 가급적이며 평가자의 주관적 요소가 개입되어서는 안 된다.";}i:4;a:2:{s:5:"check";i:1;s:4:"text";s:48:"정성적인 지표를 최대화해야 한다.";}i:5;a:2:{s:5:"check";i:0;s:4:"text";s:0:"";}}

# a:5:{i:1;a:2:{s:5:"check";s:1:"0";s:4:"text";s:107:"정량지표는 보통 계량화된 수치의 형태로 나타날 수 있는 생산성 지표이다.";}i:2;a:2:{s:5:"check";s:1:"0";s:4:"text";s:130:"정성지표는 비계량화된 형태로 나타나므로 역량을 기반으로 평가를 하는 것이 일반적이다.";}i:3;a:2:{s:5:"check";s:1:"0";s:4:"text";s:150:"조직의 성과평가는 객관적이고 공정해야 하며 가급적이며 평가자의 주관적 요소가 개입되어서는 안 된다.";}i:4;a:2:{s:5:"check";s:1:"1";s:4:"text";s:48:"정성적인 지표를 최대화해야 한다.";}i:5;a:2:{s:5:"check";s:1:"0";s:4:"text";s:0:"";}}

문제가 된 코드는 아래와 같다.

        $rows[10] = @explode(",", $rows[10]);

$row['exb_answers'] = array(
1 => array('check' => (in_array(1, $rows[10])) ? '1' : '0', 'text' => sql_escape_string($rows[5])),
2 => array('check' => (in_array(2, $rows[10])) ? '1' : '0', 'text' => sql_escape_string($rows[6])),
3 => array('check' => (in_array(3, $rows[10])) ? '1' : '0', 'text' => sql_escape_string($rows[7])),
4 => array('check' => (in_array(4, $rows[10])) ? '1' : '0', 'text' => sql_escape_string($rows[8])),
5 => array('check' => (in_array(5, $rows[10])) ? '1' : '0', 'text' => sql_escape_string($rows[9])),
);
$row['exb_answers'] = serialize($row['exb_answers']);

처음에는 데이터베이스 설정에서 문제가 발생했을 거라고 생각했는데

하지만 서비스 간 설정을 비교해본 결과, 데이터베이스 구성이 일관되게 유지되고 있었고,

문제는 PHP 코드 내에서 문자열 이스케이핑을 처리하는 sql_escape_string 함수의 사용 방식에 있었다.

분명히 얼마전에는 문제가 없었는데 코드 수정도 안했는데 갑자기? 이런 문제가 생기네요??? 🤔

문제의 주범: sql_escape_string 함수 ⚠️

function sql_escape_string($str) {
global $connect_db;
return ($connect_db, $str);
}

이 함수는 데이터베이스 연결에 문자 인코딩을 명시적으로 설정하지 않았고, 이는 비-ASCII 문자들, 특히 한글 문자나 특수 수학기호 처리에 결정적

해결책: 올바른 문자 인코딩 설정

문제를 해결하기 위해 sql_escape_string 함수를 수정하여 데이터베이스 연결 시 UTF-8 인코딩을 명시적으로 설정하도록 변경. 모든 문자열이 올바르게 인코딩되도록 보장하는 것이 중요헀음 다음은 업데이트된 함수

function sql_escape_string($str) {
global $connect_db;
$connect_db->set_charset('utf8');
return mysqli_escape_string($connect_db, $str);
}

해결책 선택 이유 💡

  • 일관된 인코딩: 각 호출마다 문자 집합을 utf8으로 설정함으로써 특히 한글,특수문자 데이터 처리의 일관성을 보장
  • 유연성과 보안: 이 함수는 mysqli_escape_string을 계속 사용하지만(mysqli_real_escape_string 사용을 더 권장합니다), SQL 인젝션에 대한 보안을 제공
  • 전역 적용: 이 변경은 한글 문자 문제 해결뿐만 아니라 다른 비-ASCII 문자 처리 능력을 향상시켜 수학기호 등의 특수기호 사용에 용의

결론: 특수문자를 저장하고 코드는 문자 인코딩이 매우 중요하고 오류를 수정할 때나 기능을 구현할 때 제일 먼저 고려해야 함 🎓

혹시나 같은 문제를 가지고 있는 사람에게 도움이 될까하여 올려 봅니다.

현재 개발되고 있는 새로운 python 기반 시스템이 심사가 잘 통과될때까지 잘 고쳐서 써봐야지 휴... 🐍

🎨 Fullpage.js + Next.js + TypeScript로 멋진 웹페이지 만들기

· 4 min read
Bundaberg Man
Maintainer of Docusaurus

Fullpage.js + Next.js + TypeScript로 멋진 웹페이지 만들기 🚀

안녕하세요! 오늘은 Fullpage.js, Next.js, 그리고 TypeScript를 함께 사용하여 멋진 웹 페이지를 구현하는 방법을 공유하려고 합니다. 인터넷에서 이 세 가지 기술을 함께 사용하는 예제를 찾기 어려워 이렇게 블로그에 작성하게 되었습니다.

📚 참고 자료

🛠️ 준비물

먼저, 프로젝트에 필요한 패키지들을 설치해야 합니다:

npx create-next-app@latest --typescript
npm install react-bootstrap bootstrap
npm install @fullpage/react-fullpage

📦 package.json

{
"name": "fullpage_nextjs_typescript",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"license": "gpl-3.0",
"dependencies": {
"@fullpage/react-fullpage": "^0.1.38",
"@types/node": "18.15.11",
"@types/react": "18.0.34",
"@types/react-dom": "18.0.11",
"bootstrap": "^5.2.3",
"next": "13.3.0",
"react": "18.2.0",
"react-bootstrap": "^2.7.2",
"react-dom": "18.2.0",
"typescript": "5.0.4"
}
}

💻 구현하기

components/fullpageExample.tsx라는 파일을 생성하고 아래의 코드를 입력합니다.

[이전 코드와 동일한 TypeScript 코드 블록]

import React, { useState } from "react";
import ReactFullpage, { fullpageOptions } from "@fullpage/react-fullpage";
import { Navbar, Nav, Button } from "react-bootstrap";

interface Section {
text: string;
id?: number;
}

const originalColors = [
"blue",
"#0798ec",
"#fc6c7c",
"#435b71",
"orange",
"blue",
"purple",
"yellow",
];

type Credits = {
enabled?: boolean;
label?: string;
position?: "left" | "right";
};

const pluginWrapper = () => {
/*
* require('../static/fullpage.scrollHorizontally.min.js'); // Optional. Required when using the "scrollHorizontally" extension.
*/
};

const FullpageJsExample = () => {
const [sectionsColor, setSectionsColor] = useState([...originalColors]);
const [fullpages, setFullpages] = useState<Section[]>([
{
text: "Section 1",
},
{
text: "Section 2",
},
{
text: "Section 3",
},
]);

const onLeave = (origin: any, destination: any, direction: any) => {
console.log("onLeave", { origin, destination, direction });
};

const handleChangeColors = () => {
const newColors =
sectionsColor[0] === "yellow" ? [...originalColors] : ["yellow", "blue", "white"];
setSectionsColor(newColors);
};

const handleAddSection = () => {
setFullpages((prevFullpages) => [
...prevFullpages,
{
text: `section ${prevFullpages.length + 1}`,
id: Math.random(),
},
]);
};

const handleRemoveSection = () => {
setFullpages((prevFullpages) => {
const newPages = [...prevFullpages];
newPages.pop();
return newPages;
});
};

if (!fullpages.length) {
return null;
}

const Menu = () => (
<Navbar bg="light" expand="lg" fixed="top">
<Navbar.Brand href="#home" className="mx-2">
Fullpage.js + Next.js + Typescript
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Item>
<Button onClick={handleAddSection} className="mr-2, mx-1">
ADD SECTION
</Button>
</Nav.Item>
<Nav.Item>
<Button onClick={handleRemoveSection} className="mr-2, mx-1">
REMOVE SECTION
</Button>
</Nav.Item>
<Nav.Item>
<Button onClick={handleChangeColors}>CHANGE SECTION</Button>
</Nav.Item>
</Nav>
</Navbar.Collapse>
</Navbar>
);

const credits: Credits = {
enabled: true,
label: "my custom",
position: "left",
};

return (
<div className="App">
<Menu />
<ReactFullpage
licenseKey={"OPEN-SOURCE-GPLV3-LICENSE"}
navigation
onLeave={onLeave}
sectionsColor={sectionsColor}
pluginWrapper={pluginWrapper}
debug={false}
credits={credits}
render={(comp: any) => (
<ReactFullpage.Wrapper>
{fullpages.map(({ text }) => (
<div key={text} className="section">
<h1>{text}</h1>
</div>
))}
</ReactFullpage.Wrapper>
)}
/>
</div>
);
};

export default FullpageJsExample;

코드 설명은 다음과 같습니다:

  1. React 및 필요한 모듈을 import 합니다.
  2. 각 섹션의 정보와 색상을 저장하는 상태를 생성합니다.
  3. 섹션을 추가하고 삭제하거나 색상을 변경하는 함수를 구현합니다.
  4. React-Bootstrap을 사용하여 상단 메뉴를 구현합니다.
  5. Fullpage.js의 옵션을 설정하고, 각 섹션을 렌더링합니다.

실행

이제 터미널에서 npm run dev 명령어를 입력하여 개발 서버를 실행시키세요. 웹 브라우저에서 http://localhost:3000에 접속하면 작성한 웹 페이지를 확인할 수 있습니다.

이상으로 Fullpage.js, Next.js, TypeScript를 사용하여 웹 페이지를 구현하는 방법입니다.

🚀 PHP 레거시 코드 리팩토링 - 300배 성능 개선 사례

· 5 min read
Bundaberg Man
Maintainer of Docusaurus

📝 개요

레거시 PHP 시스템의 성능 개선 사례를 공유합니다. 300분이 걸리던 데이터 처리 작업을 5초로 단축한 리팩토링 과정을 설명합니다.

🔍 문제 상황

❌ 기존 시스템의 문제점

  • 300명의 데이터 처리에 약 300분 소요 (1인당 1분)
  • 운영팀의 업무 효율성 저하
  • 데이터가 증가할수록 기하급수적으로 처리 시간 증가

🤔 성능 저하의 주요 원인

  1. 비효율적인 데이터 조회 방식

    // 기존: 단건 조회 반복
    for($i = 0; $i < count($list); $i++) {
    $sql = "SELECT id, member_info FROM 고객테이블 WHERE id = '$user_id'";
    // 매 반복마다 DB 조회 발생
    // 고객이 입과 되어 있는 데이터 조회 1
    // for문 안에서 고객 정보가 있는 또다른 데이터 조회
    // 고객 정보가 있는 또다른 데이터 조회

    // 이러한 방식으로 데이터 조회가 반복되어 성능 저하
    }
  2. 트랜잭션 관리 미흡

    • 건별 처리 및 커밋으로 인한 오버헤드
    • 부분 실패 시 데이터 정합성 문제

⚡ 개선 방안

1. 💾 데이터 조회 최적화

// 개선: IN 절을 활용한 일괄 조회
$user_ids = array_column($list, 'user_id');
$sql = "SELECT * FROM 고객테이블 WHERE id IN (" . implode(',', $user_ids) . ")";
$result = sql_query($sql);

// 메모리 캐싱 구현
$user_map = [];
while($row = sql_fetch_array($result)) {
$user_map[$row['id']] = $row; // 사용자 ID를 키로 하는 연관 배열 생성
}

// 캐시된 데이터 활용 예시
foreach($list as $item) {
$user_id = $item['user_id'];
if (isset($user_map[$user_id])) {
$user_info = $user_map[$user_id]; // DB 조회 없이 메모리에서 즉시 조회
// 추가 처리 로직...
}
}

메모리 캐싱 효과

  • 🚀 DB 조회 횟수: 300회 → 1회로 감소
  • 💡 조회 속도: O(n) → O(1) 복잡도로 개선
  • 🔄 데이터 재사용: 동일 데이터 반복 조회 제거

2. 📦 Bulk Insert 구현

function bulk_insert($tableName, $columns, $values) {
$sql = "INSERT INTO {$tableName} (" . implode(',', $columns) . ") VALUES ";
$valueStrings = [];

foreach($values as $row) {
$valueStrings[] = "(" . implode(',', array_map('sql_escape', $row)) . ")";
}

$sql .= implode(',', $valueStrings);
return sql_query($sql);
}

3. 🔄 트랜잭션 처리 개선

sql_begin_transaction();
try {
// 데이터 검증
$validated_data = processValidate($input_data);

// Bulk Insert 실행
bulk_insert('score_table', $columns, $validated_data);
bulk_insert('history_table', $columns, $validated_data);

sql_commit();
} catch (Exception $e) {
sql_rollback();
log_error($e->getMessage());
return ['success' => false, 'message' => '처리 중 오류가 발생했습니다.'];
}

📊 개선 결과

구분처리 시간메모리 사용량DB 부하
개선 전300분높음높음
개선 후5초최적화낮음

✨ 주요 개선 포인트

  1. 🔋 데이터베이스 호출 최소화

    • 300 * 10회 → 10회로 감소
    • 메모리 캐싱 활용
  2. ⚡ 일괄 처리 도입

    • Bulk Insert로 Insert 쿼리 최소화
    • 트랜잭션 단위 최적화
  3. 📈 코드 품질 향상

    • 모듈화 및 함수 분리
    • 에러 처리 강화
    • 데이터 검증 로직 체계화

💫 마치며

이번 리팩토링을 통해 얻은 교훈:

  • 🎯 레거시 코드도 적절한 리팩토링으로 큰 성능 개선이 가능
  • 💡 데이터베이스 작업 최적화의 중요성
  • 🏗️ 체계적인 코드 구조의 필요성

앞으로도 지속적인 코드 개선과 학습을 통해 더 나은 시스템을 만들어가보겠습니다.

Legacy..Legacy..Legacy.. 내가 만들땐 진짜 이런 문제가 안생기도록 노력해야 할 것 같습니다.

💡 참고

실제 프로덕션 환경에서 검증된 사례입니다. 유사한 성능 이슈가 있다면 이러한 접근 방식을 고려해보세요.