6장. 아키텍처 관점에서의 DB 접근 계층
지금까지 데이터베이스 접근 방식의 흐름을 살펴보았다.
- Raw Query
- Query Builder
- ORM
이제 중요한 것은
👉 이들을 “어떻게 코드 구조 안에 배치할 것인가”이다.
1. 왜 구조가 필요한가
서비스 규모가 커질수록 데이터 접근 코드는 빠르게 증가한다.
이 과정에서 자연스럽게 다음과 같은 문제가 발생한다.
- 쿼리가 여러 곳에 흩어진다
- 동일한 로직이 반복된다
- 수정 시 영향 범위를 파악하기 어렵다
👉 즉, 데이터 접근을 체계적으로 관리할 구조가 필요하다
2. 데이터 접근 계층 구조
일반적으로 데이터 접근은 다음과 같은 계층 구조로 구성된다.
[ Controller ]
↓
[ Service ]
↓
[ Repository ]
↓
[ Database ]
각 계층은 명확한 역할을 가진다.
3. 계층별 역할
Controller
app.get('/users/:id', async (req, res) => {
const user = await userService.getUser(req.params.id);
res.json(user);
});
- 요청/응답 처리
- 비즈니스 로직 없음
Service
class UserService {
async getUser(userId: number) {
return await userRepository.findById(userId);
}
}
- 비즈니스 로직 담당
- 데이터 접근 방식에 의존하지 않음
Repository
class UserRepository {
async findById(userId: number) {
return await prisma.user.findUnique({
where: { id: userId }
});
}
}
- 데이터 접근 전담
- ORM / Query Builder / Raw Query 사용
Database
- 실제 데이터 저장소
- MySQL, PostgreSQL 등
4. 데이터 흐름
계층 간 데이터는 역할에 맞게 형태가 달라진다.
Repository → Entity
type UserEntity = {
id: number;
email: string;
name: string;
};
👉 데이터베이스 구조 그대로 표현
Service → DTO 변환
type UserDTO = {
id: number;
name: string;
};
class UserService {
async getUser(userId: number): Promise<UserDTO> {
const user = await userRepository.findById(userId);
return {
id: user.id,
name: user.name
};
}
}
👉 외부에 필요한 데이터만 전달
5. 이 구조가 가지는 장점
이 구조는 단순한 계층 분리가 아니라
👉 실제 개발 과정에서 발생하는 문제를 해결하기 위한 설계 방식이다.
1) 유지보수성 – 변경 영향 최소화
❌ 구조가 없는 경우
app.get('/users/:id', async (req, res) => {
const user = await db.query("SELECT * FROM users WHERE id = 1");
res.json(user);
});
👉 문제:
- SQL이 여러 곳에 퍼짐
- 테이블 변경 시 전부 수정
⭕ 구조가 있는 경우
class UserRepository {
async findById(id: number) {
return db.query("SELECT * FROM users WHERE id = ?", [id]);
}
}
👉 변경 발생:
// users → members 변경
return db.query("SELECT * FROM members WHERE id = ?", [id]);
👉 Repository만 수정하면 끝
👉 핵심:
변경이 한 곳에 집중된다
2) 테스트 용이성 – DB 없이 테스트
❌ 구조가 없는 경우
class UserService {
async getUser(id: number) {
return db.query("SELECT * FROM users WHERE id = ?", [id]);
}
}
👉 문제:
- DB 필요
- 테스트 느림
⭕ 구조가 있는 경우
interface UserRepository {
findById(id: number): Promise<UserEntity>;
}
class MockUserRepository implements UserRepository {
async findById(id: number) {
return { id, name: "test-user", email: "test@test.com" };
}
}
const service = new UserService(new MockUserRepository());
const user = await service.getUser(1);
👉 DB 없이 테스트 가능
👉 핵심:
비즈니스 로직을 독립적으로 검증할 수 있다
3) 데이터 보호 – Entity vs DTO
❌ 구조가 없는 경우
res.json(user);
👉 문제:
- email 등 민감 정보 그대로 노출
⭕ DTO 적용
return {
id: user.id,
name: user.name
};
👉 핵심: 외부로 나가는 데이터는 통제된다
4) 기술 변경 유연성
상황
ORM → Raw Query 변경
⭕ 구조가 있는 경우
// 기존
return prisma.user.findUnique(...);
// 변경
return db.query("SELECT * FROM users WHERE id = ?", [id]);
👉 Service / Controller 수정 없음
👉 핵심:
기술 변경이 전체 코드에 영향을 주지 않는다
5) 혼합 전략 적용
class UserRepository {
async findById(id: number) {
return prisma.user.findUnique({ where: { id } });
}
async searchUsers(...) {
return knex('users')...;
}
async getStatistics() {
return db.query("SELECT ...");
}
}
👉 하나의 구조 안에서
- ORM
- Query Builder
- Raw Query
공존 가능
👉 핵심:
상황에 맞는 기술 선택이 가능하다
6. 핵심 정리
이 구조의 핵심은 다음과 같다.
- 데이터 접근은 반드시 분리해야 한다
- 각 계층은 명확한 역할을 가진다
- 내부 구현은 자유롭게 변경 가능하다
그리고 가장 중요한 점은
👉 이 구조는 코드를 나누기 위한 것이 아니라
변화에 대응하기 위한 설계다