회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
About
Service
Q&A
PR리그
자유게시판N
갤러리
포인트게임
공지사항N
통계
로그인 회원가입
고객센터
3.6 데이터 처리 구조

DB 접근 방식

A Administrator
2026.04.21 00:54(수정됨) 93 0

1. 개요

DX 미니 프레임워크의 DB 접근은 두 개의 계층으로 구성됩니다. 하위 계층인 Database 클래스는 PDO를 직접 감싸는 얇은 래퍼(thin wrapper)로, PHP 5.6부터 MySQL•MariaDB 환경에서 안전한 파라미터 바인딩과 준비된 구문(Prepared Statement)을 제공합니다. 상위 계층인 QueryBuilder 클래스는 Laravel 스타일의 메서드 체이닝 인터페이스를 제공하며, Database 위에서 동작합니다.

두 클래스 모두 싱글턴 혹은 경량 팩토리 패턴으로 동작하며, 개발자는 SQL을 직접 쓸 수도 있고, 메서드 체이닝 방식으로 쿼리를 조립할 수도 있습니다.
 
계층 클래스 역할 요약
하위 (PDO 래퍼) Database PDO 연결 관리, Prepared Statement 실행, 편의 CRUD 메서드, 트랜잭션
상위 (쿼리 빌더) QueryBuilder WHERE/JOIN/ORDER 등 메서드 체이닝으로 SQL 조립, 페이지네이션, 집계 함수
전역 헬퍼 dx_db() QueryBuilder 인스턴스를 one-liner로 생성하는 전역 함수


2. Database 클래스 — PDO 래퍼


2.1 설계 원칙

  • 싱글턴 패턴: 요청 전체에서 하나의 PDO 인스턴스만 유지합니다.
  • Prepared Statement 전용: 모든 쿼리는 pdo->prepare() + execute() 경로를 거칩니다.
  • PHP 5.6 호환: ATTR_EMULATE_PREPARES = false로 네이티브 준비된 구문을 활성화하고, 32bit PHP 환경에서의 정수 오버플로우를 방지합니다.
  • ERRMODE_EXCEPTION: PDO 오류는 예외(PDOException)로 던져지며, execute() 내부의 try/catch에서 포착해 dx_error()로 처리합니다.
  • utf8mb4 기본 캐릭터셋: 이모지•유니코드 4바이트 문자를 지원합니다.


2.2 연결 초기화 (connect)

data/config.php에서 $db->connect()가 호출되며, DSN•PDO 옵션•캐릭터셋을 설정합니다.
 
$db = Database::getInstance();
$db->connect(
    $host,         // DB 호스트
    $dbName,       // 데이터베이스명
    $dbUser,       // 사용자명
    $dbPass,       // 비밀번호
    'utf8mb4',     // 캐릭터셋 (기본값)
    'dx_'          // 테이블 접두사 (기본값)
);

연결 시 설정하는 PDO 옵션은 다음과 같습니다.
 
PDO 옵션 설정값 및 효과
ATTR_ERRMODE ERRMODE_EXCEPTION — 모든 오류를 예외로 변환
ATTR_DEFAULT_FETCH_MODE FETCH_ASSOC — 결과를 연관 배열로 반환
ATTR_EMULATE_PREPARES false — 네이티브 Prepared Statement 사용 (SQL 인젝션 방어 강화)
MYSQL_ATTR_INIT_COMMAND SET NAMES 'utf8mb4' — 커넥션 캐릭터셋 명시 보장


2.3 핵심 실행 메서드

모든 SELECT•INSERT•UPDATE•DELETE는 내부 private 메서드 execute()를 통해 처리됩니다.
 
// 내부 실행 흐름
private function execute($sql, $params = array())
{
    $this->queryCount++;
    try {
        $stmt = $this->pdo->prepare($sql);  // Prepared Statement
        $stmt->execute((array)$params);      // 바인딩 실행
        return $stmt;
    } catch (PDOException $e) {
        dx_error('DB 오류: ' . $e->getMessage());
        return false;
    }
}
개발자가 직접 호출하는 공개 메서드는 아래와 같습니다.
 
메서드 반환 타입 용도
row($sql, $params) array|null 단일 행 SELECT — 첫 번째 행만 반환
rows($sql, $params) array 전체 행 SELECT — fetchAll()
value($sql, $params) mixed 단일 컬럼 값 SELECT — 첫 번째 행 첫 번째 컬럼
query($sql, $params) int INSERT•UPDATE•DELETE — 영향받은 행 수 반환
insert($sql, $params) int|string INSERT + lastInsertId() 반환


2.4 편의 CRUD 메서드

원시 SQL 작성 없이 간단한 CRUD를 처리할 수 있는 고수준 편의 메서드를 제공합니다.


2.4.1 조회 — find / findAll

// 단일 행 조회
$board = $db->find('boards', array('board_key' => 'free', 'status' => 1));
// → SELECT * FROM `dx_boards` WHERE `board_key` = ? AND `status` = ? LIMIT 1

// 다중 행 조회 (정렬•제한 포함)
$posts = $db->findAll('posts', array('board_id' => 3), '*', 'id DESC', '10');
// → SELECT * FROM `dx_posts` WHERE `board_id` = ? ORDER BY id DESC LIMIT 10


2.4.2 삽입 — insertRow

// 배열 데이터를 그대로 INSERT
$db->insertRow('survey_answers', array(
    'survey_id'   => $surveyId,
    'question_id' => $qId,
    'member_id'   => $uid,
    'answer'      => $answer,
    'created_at'  => $now,
));
// → INSERT INTO `dx_survey_answers` (`survey_id`, ...) VALUES (?, ...)


2.4.3 수정 — updateRow

// 상태 변경
$db->updateRow(
    'posts',
    array('status' => 0, 'updated_at' => $now),    // SET
    array('id' => $pid, 'board_id' => $bid)        // WHERE
);
// → UPDATE `dx_posts` SET `status`=?, `updated_at`=? WHERE `id`=? AND `board_id`=?


2.4.4 삭제 — deleteRow

$db->deleteRow('post_files', array('post_id' => $postId));
// → DELETE FROM `dx_post_files` WHERE `post_id` = ?


2.4.5 존재 확인 / 행 수 — exists / count

$isExists = $db->exists('members', array('email' => $email));
// → SELECT COUNT(*) FROM `dx_members` WHERE `email` = ?  → bool

$total = $db->count('posts', array('board_id' => 3, 'status' => 1));
// → SELECT COUNT(*) FROM `dx_posts` WHERE `board_id` = ? AND `status` = ?


2.4.6 테이블 접두사 — table()

모든 편의 메서드는 내부에서 table() 메서드를 호출해 접두사를 자동 추가합니다. 원시 SQL 작성 시에도 아래와 같이 사용합니다.
 
$tbl = $db->table('posts');    // "dx_posts"
$sql = "SELECT * FROM `{$tbl}` WHERE id = ?";
$post = $db->row($sql, array($id));

📋  편의 메서드 vs. 원시 SQL 선택 기준
단순 조건 조회•삽입•수정: find() / insertRow() / updateRow() 사용 권장
복잡한 JOIN•서브쿼리•집계: 원시 SQL 또는 QueryBuilder 사용
원시 SQL은 반드시 파라미터 바인딩(? 플레이스홀더)을 사용해야 합니다.


3. microtime ID 생성 전략

DX CMS의 posts•comments 테이블은 AUTO_INCREMENT 대신 microtime 기반 BIGINT PK를 사용합니다. 이 방식은 분산 환경에서의 ID 충돌을 최소화하고, 정렬 시 시간 순서를 자연스럽게 반영합니다.


3.1 ID 구조 (16자리 이하)

구성 자릿수 내용
타임스탬프(초) 10자리 microtime(true)의 정수부 — Unix 타임스탬프
밀리초 3자리 microtime(true)의 소수부를 밀리초로 변환
난수 3자리 rand(0, 999) — 같은 밀리초 내 충돌 방지

// 예시: 1745123456789012
//  1745123456 → Unix 타임스탬프(초)
//  789         → 밀리초
//  012         → 랜덤 3자리


3.2 32bit PHP 안전 처리

32bit PHP에서 13자리 이상의 정수를 (int)로 캐스팅하면 오버플로우가 발생합니다. DX CMS는 이를 문자열로 조작하고, PDO가 BIGINT 컬럼에 바인딩할 때 문자열로 전달합니다.
 
// (int) 캐스팅 절대 금지 — 오버플로우 발생
// 올바른 처리: 문자열 그대로 PDO에 전달
$sec   = floor(microtime(true));
$ms    = round((microtime(true) - $sec) * 1000);
$id    = sprintf('%010d%03d%03d', $sec, $ms, rand(0,999));
// $id 는 문자열 "1745123456789012" — 32bit 안전


3.3 충돌 방지 로직 (최대 10회 재시도)

public function generateMicrotimeId($table, $retry = 10)
{
    for ($i = 0; $i < $retry; $i++) {
        $id = sprintf('%010d%03d%03d', $sec, $ms, rand(0,999));
        // 중복 검사
        $exists = $db->value("SELECT COUNT(*) FROM `{$tbl}` WHERE id=?", [$id]);
        if (!$exists) return $id;   // 중복 없음 → 반환
        usleep(1000);               // 1ms 대기 후 재시도
    }
    // fallback: 추가 3자리 랜덤
    return sprintf('%010d%03d%03d', $sec, $ms, rand(0,999));
}


3.4 insertWithMicrotimeId 사용법

// ID를 자동 생성하여 INSERT
$postId = $db->insertWithMicrotimeId('posts', array(
    'board_id'   => $boardId,
    'member_id'  => $uid,
    'title'      => $title,
    'content'    => $content,
    'created_at' => $now,
));
// $postId 는 "1745123456789012" 형태의 문자열

QueryBuilder에서도 동일 기능을 사용할 수 있습니다.

$postId = dx_db('posts')->insertWithId(array(
    'board_id'  => $boardId,
    'title'     => $title,
    'content'   => $content,
));

⚠️  AUTO_INCREMENT와의 차이
posts•comments 테이블은 AUTO_INCREMENT PK 대신 microtime ID를 사용합니다.
일반 보조 테이블(boards, members 등)은 AUTO_INCREMENT를 그대로 사용합니다.
insertWithMicrotimeId는 posts•comments 전용 메서드입니다.


4. 트랜잭션 처리

Database 클래스는 PDO 트랜잭션을 begin() / commit() / rollback() 세 메서드로 감쌉니다. 여러 쿼리가 하나의 논리적 단위로 처리되어야 할 때 사용합니다.


4.1 트랜잭션 API

메서드 역할
$db->begin() PDO::beginTransaction() — 트랜잭션 시작
$db->commit() PDO::commit() — 모든 변경 확정
$db->rollback() PDO::rollBack() — 모든 변경 취소


4.2 실제 사용 예 — 설문 응답 저장

boards/handler.php의 설문 응답 처리 코드입니다. 설문 답변 INSERT와 참여 로그 INSERT가 하나의 트랜잭션으로 묶여 원자성을 보장합니다.
 
try {
    $db->begin();

    // 1) 문항별 답변 INSERT
    foreach ($surveyQuestions as $q) {
        $db->insertRow('survey_answers', array(
            'survey_id'   => $survey['id'],
            'question_id' => $q['id'],
            'member_id'   => $uid,
            'answer'      => $answer,
            'created_at'  => $now,
        ));
    }

    // 2) 참여 로그 INSERT (중복 방지)
    $db->query(
        "INSERT IGNORE INTO `{$db->table('survey_votes')}` ..."),
        array($survey['id'], $uid, $ip, $now)
    );

    $db->commit();    // 모두 성공 → 확정

} catch (Exception $e) {
    $db->rollback();  // 하나라도 실패 → 전체 취소
    dx_log('[Survey] vote error: ' . $e->getMessage(), 'error');
}

💡  트랜잭션 사용 원칙
여러 테이블에 걸쳐 데이터 정합성이 필요한 경우 반드시 트랜잭션을 사용합니다.
try { begin → 쿼리들 → commit } catch { rollback } 패턴을 일관되게 적용합니다.
PDO ERRMODE_EXCEPTION 설정으로 PDOException이 자동으로 catch 블록으로 전달됩니다.


5. QueryBuilder — 메서드 체이닝 쿼리 조립

QueryBuilder는 v6.2.0에 추가된 Laravel 스타일 쿼리 인터페이스입니다. Database 위에서 동작하며, 기존 Database 메서드와 100% 하위 호환됩니다.


5.1 전역 헬퍼 — dx_db()

dx_db() 함수는 QueryBuilder 인스턴스를 one-liner로 생성합니다. 테이블 접두사는 자동 적용됩니다.
 
// functions.php
function dx_db($table) {
    return new QueryBuilder(Database::getInstance(), $table);
}

// 사용 예
$posts = dx_db('posts')->where('status', 1)->orderBy('id', 'desc')->get();


5.2 SELECT 메서드

5.2.1 기본 조회

// 전체 행 반환
$rows = dx_db('posts')->get();

// 첫 번째 행 반환
$post = dx_db('posts')->where('id', $id)->first();

// 단일 컬럼 값
$title = dx_db('posts')->where('id', $id)->value('title');

// 특정 컬럼 전체 배열
$ids = dx_db('posts')->where('board_id', 3)->pluck('id');


5.2.2 WHERE 조건

메서드 설명 및 예시
where($col, $val) 단순 등호 — where('status', 1)
where($col, '>=', $val) 비교 연산자 지정 — where('level', '>=', 5)
orWhere($col, $val) OR 조건 — orWhere('is_admin', 1)
whereIn($col, $arr) IN 조건 — whereIn('id', [1,2,3])
whereNotIn($col, $arr) NOT IN 조건
whereNull($col) IS NULL 조건
whereNotNull($col) IS NOT NULL 조건
whereBetween($col, $s, $e) BETWEEN 조건 — whereBetween('created_at', $start, $end)
whereRaw($raw, $bindings) 임의 SQL 조건 (바인딩 포함)

// 복합 조건 예시
$result = dx_db('posts')
    ->where('board_id', 3)
    ->where('status', 1)
    ->where('created_at', '>=', '2024-01-01')
    ->whereIn('category_id', array(1, 2, 5))
    ->orderBy('id', 'desc')
    ->limit(20)
    ->get();


5.2.3 JOIN

메서드 설명
join($tbl, $col1, $op, $col2) INNER JOIN
leftJoin($tbl, $col1, $op, $col2) LEFT JOIN
rightJoin($tbl, $col1, $op, $col2) RIGHT JOIN

// JOIN 예시 — posts + members
$list = dx_db('posts')
    ->select(array('posts.*', 'members.name AS member_name'))
    ->leftJoin('members', 'posts.member_id', '=', 'members.id')
    ->where('posts.status', 1)
    ->orderBy('posts.id', 'desc')
    ->get();


5.2.4 정렬 / 그룹 / 페이지

메서드 설명
orderBy($col, $dir) 정렬 — orderBy('id', 'desc')
orderByRaw($raw) 임의 정렬 표현식
groupBy($col) GROUP BY
having($raw, $bind) HAVING 절
limit($n) LIMIT
offset($n) OFFSET
forPage($page, $per) 페이지•행 수 → LIMIT/OFFSET 자동 계산


5.3 집계 메서드

메서드 반환 및 설명
count() int — SELECT COUNT(*) 실행
max($col) mixed — SELECT MAX(col)
min($col) mixed — SELECT MIN(col)
sum($col) mixed — SELECT SUM(col)
avg($col) mixed — SELECT AVG(col)
exists() bool — count() > 0 여부
 
$total = dx_db('posts')->where('board_id', 3)->count();
$maxId = dx_db('posts')->max('id');
$isAdmin = dx_db('members')->where('id', $uid)->where('is_admin', 1)->exists();


5.4 페이지네이션 — paginate()

paginate()는 COUNT와 데이터 조회를 한 번에 처리하며, 페이지 정보가 담긴 배열을 반환합니다.
 
$result = dx_db('posts')
    ->where('board_id', 3)
    ->where('status', 1)
    ->orderBy('id', 'desc')
    ->paginate(20);  // 페이지는 $_GET['page'] 자동 참조

// 반환 배열 구조
array(
    'data'         => array(...),  // 현재 페이지 데이터
    'total'        => 100,         // 전체 행 수
    'per_page'     => 20,
    'current_page' => 1,
    'last_page'    => 5,
    'from'         => 1,
    'to'           => 20,
)


5.5 쓰기 메서드

5.5.1 INSERT

// 일반 INSERT (auto_increment PK)
$id = dx_db('boards')->insert(array(
    'board_key' => 'notice',
    'name'      => '공지사항',
));

// microtime ID INSERT (posts•comments 전용)
$postId = dx_db('posts')->insertWithId(array(
    'board_id' => 1,
    'title'    => $title,
    'content'  => $content,
));


5.5.2 UPDATE / DELETE

// UPDATE — WHERE 없으면 자동 차단
dx_db('posts')->where('id', $id)->update(array(
    'title'      => $newTitle,
    'updated_at' => $now,
));

// DELETE — WHERE 없으면 자동 차단
dx_db('post_files')->where('post_id', $postId)->delete();

⚠️  WHERE 없는 UPDATE/DELETE 자동 차단
update()와 delete()는 WHERE 조건 없이 호출되면 경고 로그만 남기고 실행을 거부합니다.
전체 행 업데이트•삭제가 필요하다면 원시 SQL $db->query()를 명시적으로 사용해야 합니다.


5.5.3 UPSERT (ON DUPLICATE KEY UPDATE)

// INSERT 또는 UPDATE
dx_db('visits')->upsert(
    array('visit_date' => $date, 'visit_count' => 1),  // INSERT 데이터
    array('visit_count' => $db->value("SELECT visit_count+1 ..."))  // UPDATE 데이터
);

5.5.4 increment / decrement
// 카운터 증가
dx_db('posts')->where('id', $postId)->increment('view_count');
dx_db('posts')->where('id', $postId)->increment('like_count', 1);

// 카운터 감소
dx_db('posts')->where('id', $postId)->decrement('like_count');


5.6 SQL 디버깅 — toSql()

체이닝으로 조립된 최종 SQL과 바인딩 값을 실행 전에 확인할 수 있습니다.
 
$qb = dx_db('posts')
    ->where('board_id', 3)
    ->where('status', 1)
    ->orderBy('id', 'desc')
    ->limit(10);

list($sql, $bindings) = $qb->toSql();
// $sql      = "SELECT * FROM `dx_posts` WHERE `board_id` = ? AND `status` = ? ORDER BY `id` DESC LIMIT 10"
// $bindings = array(3, 1)


6. 원시 SQL 작성 및 PDO 직접 접근


6.1 원시 SQL with 파라미터 바인딩

복잡한 JOIN•서브쿼리는 원시 SQL을 직접 작성합니다. 반드시 ? 플레이스홀더와 파라미터 배열을 함께 전달해야 합니다.
 
$tbl  = $db->table('posts');
$mTbl = $db->table('members');

// COUNT (복합 조건)
$total = (int)$db->value(
    "SELECT COUNT(*) FROM `$tbl` p LEFT JOIN `$mTbl` m ON p.member_id=m.id WHERE $where",
    $params
);

// 목록 조회 (JOIN + LIMIT + OFFSET)
$posts = $db->rows(
    "SELECT p.*, m.name AS member_name
     FROM `$tbl` p
     LEFT JOIN `$mTbl` m ON p.member_id = m.id
     WHERE $where
     ORDER BY $orderBy
     LIMIT $perPage OFFSET $offset",
    $params
);


6.2 PDO 직접 접근 — pdo()

SHOW TABLES처럼 PDO placeholder 바인딩이 지원되지 않는 시스템 쿼리, 또는 복잡한 특수 작업에서 PDO 객체에 직접 접근합니다.
 
// PDO 직접 접근 예 (delete_helper 내부)
$pdo = $db->pdo();

// SHOW TABLES — placeholder 바인딩 불가 → 직접 쿼리
public function tableExists($tableName)
{
    $safeName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);
    $stmt = $this->pdo->query("SHOW TABLES LIKE '" . $safeName . "'");
    return ($stmt && $stmt->rowCount() > 0);
}

⚠️  PDO 직접 접근 시 주의사항
pdo()로 취득한 PDO 객체를 사용할 때는 반드시 준비된 구문(prepare/execute)을 사용하세요.
테이블명•컬럼명은 바인딩이 불가하므로, preg_replace로 화이트리스트 정규화 후 사용합니다.
가능하면 Database 클래스의 메서드를 우선 사용하고, PDO 직접 접근은 최소화합니다.


7. 디버그 및 모니터링


7.1 쿼리 카운터

Database는 실행한 쿼리 수를 내부적으로 집계합니다. 예제 플러그인의 실행 시간 위젯에서도 활용합니다.
 
$count = $db->getQueryCount();
// 현재 요청에서 실행된 총 쿼리 수


7.2 쿼리 로그

DX_DEBUG 상수가 true로 정의된 경우 쿼리 로그를 배열에 축적합니다.
 
// data/config.php 또는 extend/top/*.php
define('DX_DEBUG', true);

// 쿼리 로그 조회
$log = $db->getQueryLog();
// array(
//   array('sql' => "SELECT ...", 'params' => array(1, 2)),
//   ...
// )

// 실행 시간 + 쿼리 수 출력 (example-plugin)
$ms  = round((microtime(true) - DX_START_TIME) * 1000, 2);
echo "⚡ {$ms}ms | DB: {$db->getQueryCount()}쿼리";


7.3 에러 처리 방식

상황 처리 방식
DX_DEBUG = true 오류 메시지 + SQL 문자열을 화면에 출력 (개발 환경)
DX_DEBUG = false "데이터베이스 오류가 발생했습니다." 메시지만 표시 (운영 환경)
연결 실패 dx_error('DB 연결 실패: ...', 500) → 즉시 종료
쿼리 실패 PDOException catch → dx_error() → exit


8. 전체 DB 접근 구조 요약

DX CMS의 DB 접근 레이어는 아래와 같은 3단 계층 구조로 정리됩니다.
 
+-------------------------------------------------------------+
|  개발자 코드 / 플러그인 / extend 스크립트                   |
|                                                             |
|  dx_db('posts')->where(...)->get()   <- QueryBuilder 사용   |
|  $db->find() / findAll() / row()     <- 편의 메서드 사용    |
|  $db->query($sql, $params)           <- 원시 SQL 사용       |
+-------------------------------------------------------------+
|  QueryBuilder (core/db/QueryBuilder.php)                    |
|  메서드 체이닝 -> toSql() -> Database::rows() / row() 위임  |
+-------------------------------------------------------------+
|  Database (core/db/Database.php)                            |
|  execute() -> pdo->prepare() -> stmt->execute(params)       |
|  모든 쿼리는 Prepared Statement 경유 (SQL 인젝션 방어)      |
+-------------------------------------------------------------+
|  PDO (PHP Data Objects)                                     |
|  MySQL / MariaDB | utf8mb4 | ERRMODE_EXCEPTION              |
+-------------------------------------------------------------+
 
사용 시나리오 권장 방식
단순 조건 조회•삽입•수정•삭제 Database 편의 메서드 (find, insertRow, updateRow, deleteRow)
복잡 조건•정렬•페이지네이션 QueryBuilder (dx_db() 헬퍼)
복잡한 JOIN / 서브쿼리 원시 SQL + $db->rows() / $db->row()
시스템 쿼리 (SHOW TABLES 등) pdo() 직접 접근 (최소화)
다중 쿼리 원자성 보장 트랜잭션 (begin / commit / rollback)
posts•comments INSERT insertWithMicrotimeId() / insertWithId()
 

댓글0

로그인 후 댓글을 작성할 수 있습니다.
3.6 데이터 처리 구조 공통 함수 활용 2026.04.21 3.6 데이터 처리 구조 데이터 흐름 상세 기술 2026.04.21 3.6 데이터 처리 구조 DB 접근 방식 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록