๊ฐ์
Doctrine DBAL์ PHP ์ธ์ด๋ฅผ ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ถ์ํ ๊ณ์ธต์ด๋ค. PHP ์ํ๊ณ์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ถ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค ํ๋๋ก, ํนํ Symfony ํ๋ ์์ํฌ์ ๊ธด๋ฐํ๊ฒ ํตํฉ๋์ด ์๋ค. Laravel 11์์๋ DBAL์ ๋ํ ์์กด์ฑ์ด ์ ๊ฑฐ๋์๋ค
์ ์ฌ ๋๊ตฌ ๋น๊ต
๋ค๋ฅธ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ์ ์ฌํ ๋๊ตฌ๋ค:
- Java: Hibernate
- Python: SQLAlchemy
- Node.js: Sequelize
- Ruby: ActiveRecord
PHP ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ ๋ฐฉ์์ ๋ฐ์
timeline title PHP ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ ๋ฐฉ์์ ๋ฐ์ section ์ด๊ธฐ mysql_* ํจ์ : ์ด๊ธฐ MySQL ์ ์ฉ ํจ์ section ์ค๊ธฐ PDO : PHP Data Objects ๋์ section ํ์ฌ Doctrine DBAL : ๊ณ ๊ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ถ์ํ Doctrine ORM : ๊ฐ์ฒด ๊ด๊ณ ๋งคํ
1. ๊ฐ๋ ์ดํด
1.1 DBAL์ ์ ์์ ํ์์ฑ
DBAL(Database Abstraction Layer)์ ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์คํ ์ ์ผ๊ด๋ ๋ฐฉ์์ผ๋ก ๋ค๋ฃจ๊ธฐ ์ํ ์ถ์ํ ๊ณ์ธต์ด๋ค.
์ค์ํ ๋น์
์ฌํ์๊ฐ ๊ฐ ๋๋ผ๋ง๋ค ๋ค๋ฅธ ์ ๊ธฐ ์ฝ์ผํธ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ๋ฉํฐ์ด๋ํฐ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ฒ๋ผ, DBAL์ ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์คํ ์ ๋์ผํ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ๋ โ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๋ํฐโ์ด๋ค.
1.2 ์์คํ ์ํคํ ์ฒ
graph TB subgraph "Application Layer" A[PHP Application] B[Doctrine DBAL] end subgraph "Database Layer" C[Database Drivers] D[MySQL] E[PostgreSQL] F[SQLite] G[Oracle] end A -->|Uses| B B -->|Abstracts| C C -->|Connects| D C -->|Connects| E C -->|Connects| F C -->|Connects| G
2. ํ๊ฒฝ ์ค์ ๊ณผ ๊ธฐ๋ณธ ์ฌ์ฉ
2.1 ์ค์น ๋ฐ ์ด๊ธฐ ์ค์
// Composer๋ฅผ ํตํ ์ค์น
// $ composer require doctrine/dbal
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ค์
$connectionParams = [
'dbname' => 'my_database',
'user' => 'db_user',
'password' => 'db_password',
'host' => 'localhost',
'driver' => 'pdo_mysql',
// ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ์ถ๊ฐ ์ต์
'driverOptions' => [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'"
]
];
// Connection ๊ฐ์ฒด ์์ฑ
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);2.2 ๊ธฐ๋ณธ ๋ฐ์ดํฐ ์กฐ์
// 1. ๋ฐ์ดํฐ ์กฐํ
// ์๋ชป๋ ์์ - SQL Injection ์ทจ์ฝ์
$userId = $_GET['id'];
$wrongQuery = "SELECT * FROM users WHERE id = $userId"; // ์ ๋ ์ฌ์ฉํ๋ฉด ์ ๋จ
// ์ฌ๋ฐ๋ฅธ ์์ - ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์ฌ์ฉ
$queryBuilder = $connection->createQueryBuilder();
$result = $queryBuilder
->select('*')
->from('users')
->where('id = :id')
->setParameter('id', $userId)
->executeQuery()
->fetchAssociative();
// 2. ๋ฐ์ดํฐ ์ฝ์
$connection->insert('users', [
'name' => 'ํ๊ธธ๋',
'email' => 'hong@example.com',
'created_at' => new \DateTime()
]);
// 3. ๋ฐ์ดํฐ ์์
$connection->update('users',
['name' => '๊น์ฒ ์'], // ๋ณ๊ฒฝํ ๋ฐ์ดํฐ
['id' => 1] // WHERE ์กฐ๊ฑด
);
// 4. ๋ฐ์ดํฐ ์ญ์
$connection->delete('users', ['id' => 1]);3. ๊ณ ๊ธ ๊ธฐ๋ฅ ํ์ฉ
3.1 ํธ๋์ญ์ ์ฒ๋ฆฌ
sequenceDiagram participant A as Application participant T as Transaction Manager participant D as Database A->>T: beginTransaction() A->>D: Query 1 A->>D: Query 2 alt ์ฑ๊ณต A->>T: commit() T->>D: ๋ณ๊ฒฝ์ฌํญ ์ ์ฅ else ์คํจ A->>T: rollback() T->>D: ๋ณ๊ฒฝ์ฌํญ ์ทจ์ end
// ํธ๋์ญ์
์ฒ๋ฆฌ ์์
class OrderService {
private $connection;
public function processOrder(int $orderId, int $productId): void {
$this->connection->beginTransaction();
try {
// 1. ์ฌ๊ณ ํ์ธ
$stock = $this->checkStock($productId);
if ($stock <= 0) {
throw new \Exception('์ฌ๊ณ ๋ถ์กฑ');
}
// 2. ์ฃผ๋ฌธ ์ฒ๋ฆฌ
$this->updateOrder($orderId);
// 3. ์ฌ๊ณ ๊ฐ์
$this->decreaseStock($productId);
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
}
}3.2 ์ฑ๋ฅ ์ต์ ํ
๋ฐฐ์น ์ฒ๋ฆฌ ์์
class BatchProcessor {
private $connection;
private const BATCH_SIZE = 1000;
public function processBatch(array $items): void {
$this->connection->beginTransaction();
try {
foreach ($items as $i => $item) {
$this->connection->insert('items', $item);
if (($i + 1) % self::BATCH_SIZE === 0) {
$this->connection->commit();
$this->connection->beginTransaction();
}
}
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
}
}4. ๋ณด์ ๊ณ ๋ ค์ฌํญ
4.1 SQL Injection ๋ฐฉ์ง
// ์์ ํ์ง ์์ ์ฝ๋
$name = $_POST['name'];
$query = "SELECT * FROM users WHERE name = '$name'"; // ์ทจ์ฝ!
// ์์ ํ ์ฝ๋
$qb = $connection->createQueryBuilder();
$query = $qb
->select('*')
->from('users')
->where('name = :name')
->setParameter('name', $name);4.2 ๊ถํ ๊ด๋ฆฌ
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ์ ๊ถํ ์ค์ ์์
$connectionParams['user'] = 'read_only_user';
$connectionParams['password'] = 'read_only_pass';
// ์ฝ๊ธฐ ์ ์ฉ ์ฐ๊ฒฐ ์์ฑ
$readOnlyConnection = DriverManager::getConnection($connectionParams);5. ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋
5.1 ์ผ๋ฐ์ ์ธ ๋ฌธ์
Connection ๊ด๋ จ ๋ฌธ์
try {
$connection = DriverManager::getConnection($connectionParams);
} catch (\Doctrine\DBAL\Exception\ConnectionException $e) {
// ์ฐ๊ฒฐ ์คํจ ์ฒ๋ฆฌ
$logger->error('๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์คํจ: ' . $e->getMessage());
throw new DatabaseConnectionException('๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค.');
}6. ๋ชจ๋ํฐ๋ง๊ณผ ๋๋ฒ๊น
6.1 ์ฟผ๋ฆฌ ๋ก๊น
$configuration = new \Doctrine\DBAL\Configuration();
$logger = new \Doctrine\DBAL\Logging\DebugStack();
$configuration->setSQLLogger($logger);
$connection = DriverManager::getConnection($connectionParams, $configuration);
// ์ฟผ๋ฆฌ ์คํ ํ
foreach ($logger->queries as $query) {
echo sprintf(
"์คํ ์๊ฐ: %f์ด, SQL: %s\n",
$query['executionMS'],
$query['sql']
);
}7. ๊ฒฐ๋ก
Doctrine DBAL์ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์์ ํนํ ์ ์ฉํฉ๋๋ค:
- ๋ค์ค ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ง์์ด ํ์ํ ํ๋ก์ ํธ
- Type-safeํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์กฐ์์ด ํ์ํ ๊ฒฝ์ฐ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฒค๋ ๋ ๋ฆฝ์ฑ์ด ํ์ํ ๊ฒฝ์ฐ
7.1 Best Practices ์์ฝ
- ํญ์ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์ฌ์ฉ
- ํธ๋์ญ์ ๋ฒ์ ์ต์ํ
- ๋ฐฐ์น ์ฒ๋ฆฌ ํ์ฉ
- ์ ์ ํ ๋ก๊น ๊ณผ ๋ชจ๋ํฐ๋ง ๊ตฌํ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ๋ง ์ฌ์ฉ