노마드 코더에서 'DB고민 끝내드림' 이라는 영상을 보았다.
SQLite와 PostgreSQL에 대해 호기심이 생기게 되었고 먼저 스타트업에서 MySQL과 함께 많이 사용하는 SQLite에 대해 알아보려 한다.
📚각각의 Database들의 특징
📖MySQL
많은 기업, 사람들이 사용하는 많큼 문서화, 자료들이 많고 대부분의 프로그래밍 언어에서 지원한다.
읽기 전용 명령을 관리하는 데 선호된다.
대부분의 기능들을 지원하고, 초보자들이 사용하기에 좋다.
가장 유명한 database이기에 다음 DB로 빠르게 넘어가겠다!
📒PostgreSQL
MySQL보다 좀 더 전문적인 데이터베이스로, 더 많은 기능들이 존재한다
특징으로는 이러한 것들이 있다.
- 확장자를 지원한다
- 복잡한 쿼리에 탁월
- 대용량 데이터 관리에 적합
- Catalog 기반임으로 확장이 용이
- NoSQL 및 다양한 데이터 형식 지원 (JSON, hstore, XML 포함해서 등등)
- 높은 안정성과 신뢰성을 제공
가장 중요한 부분은 이것이다
🔎확장자를 지원한다
PostgreSQL에서는 extension이라는 기능을 제공한다. extension으로 외부 프로그램을 PostgreSQL에 연동하여 추가적인 기능들을 사용할 수 있다.
이를 통해 NoSQL, DB단위 테스트, JwtToken 생성 등등 훨씬 많은 기능들을 사용할 수 있다.
📑SQLite
많은 사람들이 잘 못 생각하고 있는 것이 있다. SQ'Lite'라는 이름을 보고 성을이 좋지 않고, 토이 프로젝트에나 사용하는 데이터베이스라 생각할 수 있는데 그것은 옳지 않다.
SQLite는 작다, 빠르다, 믿을 수 있다 는 의미이다.
특징으로는 이러한 것들이 있다.
- 적은 데이터 타입
NULL, INTEGER, REAL, TEXT, BLOB 이 5개의 유형만 존재하기에 편리하다. - SQLite는 하나의 데이터베이스를 하나의 파일로 관리한다. (임베디드 데이터베이스)
SQLite는 그저 하나의 디스크 파일일 뿐이기에 데이터베이스를 복사하여 붙여넣거나, 이메일로 DB를 전송하거나, USB에 넣을 수 있으며 데이터베이스를 백업하고 복원하는 것이 간단하다. - DB를 유지 관리 해줄 필요가 없다.
- 애플리케이션 코드가 있는 서버에 데이터 베이스가 있기에
- 데이터를 가져오기 위해 서버에 요청을 할 필요가 없다
- 여러 대의 서버가 있는 애플리케이션에는 적합하지 않다.
- 더 빠르고 저렴하다.
Redit에서 찾은 SQLite 단점들😅
- 별도의 BOOLEAN 데이터 유형 없음
- 별도의 DATETIME 데이터 유형 없음
- 기본적으로 외래 키 적용이 꺼져 있음
- PRIMARY KEY는 때때로 NULL을 포함할 수 있음
- 의심스러운 SQL은 오류나 경고 없이 허용된다.
- AUTOINCREMENT는 MySQL과 동일하게 작동하지 않는다.
Spring 적용시키기
dependencies 추가
implementation 'org.xerial:sqlite-jdbc:3.46.1.0'
SQLite는 hibernate에서 지원해주지 않아서 직접 SQLDialect를 생성해줘야한다. 😢
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.dialect.function.VarArgsSQLFunction;
import org.hibernate.type.StringType;
import java.sql.Types;
public class SQLDialect extends Dialect {
public SQLDialect() {
registerColumnType(Types.BIT, "integer");
registerColumnType(Types.TINYINT, "tinyint");
registerColumnType(Types.SMALLINT, "smallint");
registerColumnType(Types.INTEGER, "integer");
registerColumnType(Types.BIGINT, "bigint");
registerColumnType(Types.FLOAT, "float");
registerColumnType(Types.REAL, "real");
registerColumnType(Types.DOUBLE, "double");
registerColumnType(Types.NUMERIC, "numeric");
registerColumnType(Types.DECIMAL, "decimal");
registerColumnType(Types.CHAR, "char");
registerColumnType(Types.VARCHAR, "varchar");
registerColumnType(Types.LONGVARCHAR, "longvarchar");
registerColumnType(Types.DATE, "date");
registerColumnType(Types.TIME, "time");
registerColumnType(Types.TIMESTAMP, "timestamp");
registerColumnType(Types.BINARY, "blob");
registerColumnType(Types.VARBINARY, "blob");
registerColumnType(Types.LONGVARBINARY, "blob");
// registerColumnType(Types.NULL, "null");
registerColumnType(Types.BLOB, "blob");
registerColumnType(Types.CLOB, "clob");
registerColumnType(Types.BOOLEAN, "integer");
registerFunction("concat", new VarArgsSQLFunction(StringType.INSTANCE, "", "||", ""));
registerFunction("mod", new SQLFunctionTemplate(StringType.INSTANCE, "?1 % ?2"));
registerFunction("substr", new StandardSQLFunction("substr", StringType.INSTANCE));
registerFunction("substring", new StandardSQLFunction("substr", StringType.INSTANCE));
}
public boolean supportsIdentityColumns() {
return true;
}
public boolean hasDataTypeInIdentityColumn() {
return false; // As specify in NHibernate dialect
}
public String getIdentityColumnString() {
// return "integer primary key autoincrement";
return "integer";
}
public String getIdentitySelectString() {
return "select last_insert_rowid()";
}
public boolean supportsLimit() {
return true;
}
protected String getLimitString(String query, boolean hasOffset) {
return new StringBuffer(query.length() + 20).append(query).append(hasOffset ? " limit ? offset ?" : " limit ?")
.toString();
}
public boolean supportsTemporaryTables() {
return true;
}
public String getCreateTemporaryTableString() {
return "create temporary table if not exists";
}
public boolean dropTemporaryTableAfterUse() {
return false;
}
public boolean supportsCurrentTimestampSelection() {
return true;
}
public boolean isCurrentTimestampSelectStringCallable() {
return false;
}
public String getCurrentTimestampSelectString() {
return "select current_timestamp";
}
public boolean supportsUnionAll() {
return true;
}
public boolean hasAlterTable() {
return false; // As specify in NHibernate dialect
}
public boolean dropConstraints() {
return false;
}
public String getAddColumnString() {
return "add column";
}
public String getForUpdateString() {
return "";
}
public boolean supportsOuterJoinForUpdate() {
return false;
}
public String getDropForeignKeyString() {
throw new UnsupportedOperationException("No drop foreign key syntax supported by SQLiteDialect");
}
public String getAddForeignKeyConstraintString(String constraintName, String[] foreignKey, String referencedTable,
String[] primaryKey, boolean referencesPrimaryKey) {
throw new UnsupportedOperationException("No add foreign key syntax supported by SQLiteDialect");
}
public String getAddPrimaryKeyConstraintString(String constraintName) {
throw new UnsupportedOperationException("No add primary key syntax supported by SQLiteDialect");
}
public boolean supportsIfExistsBeforeTableName() {
return true;
}
public boolean supportsCascadeDelete() {
return false;
}
}
yml 파일
spring:
datasource:
url: jdbc:sqlite:practice.db
username: root
password: 1234
driver-class-name: org.sqlite.JDBC
jpa:
hibernate:
ddl-auto: create
database-platform: JavaProject.Sqlite.global.config.SQLDialect
- jpa.database-platform 에는 방금 만들어줬던 SQLDialect의 경로를 넣어준다.
이제 자신있게 실행!.. 하면 안된다..
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: org.hibernate.dialect.identity.IdentityColumnSupportImpl does not support identity key generation
문제가 무엇인지 보았는데 Sqlite는 Mysql과 Autoincrement가 달라서 INDENTITY에서 다른 전략으로 변경해줘야한다.

이제 회원가입 API를 만들어보자
@Service
@RequiredArgsConstructor
@Transactional
public class SignupService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public TokenResponse execute(SignupRequest signupRequest) {
if (userRepository.existsByAccountId(signupRequest.getAccountId())) {
throw UserAlreadyExistException.EXCEPTION;
}
String password = passwordEncoder.encode(signupRequest.getPassword());
userRepository.save(
User.builder()
.accountId(signupRequest.getAccountId())
.email(signupRequest.getEmail())
.password(password)
.name(signupRequest.getName())
.role(Role.STUDENT)
.build()
);
return jwtTokenProvider.createToken(signupRequest.getAccountId());
}
}
이런식으로 코드를 작성해줬다.
이제 postman을 실행 시키면??
또 에러가 뜬다..
[SQLITE_BUSY] The database file is locked (database is locked)
해당 에러를 에러를 찾아보니 아이디를 검증하는 메서드 문제였다.
SQLite는 원자성을 지키기 위해, 데이터베이스에 write작업이 발생할 때 File단위로 lock을 걸어서, lock을 걸린 상태에서는 현재 데이터베이스에 write/read 작업이 불가능 하다고 한다.
그렇다면 당연히 아이디를 검증하는 메서드를 삭제해주면 잘 작동은 하겠지만,
그렇다고 아이디 검증을 지울 수는 없는 일이다.
그 대신, 서비스 로직을 하나로 commit하지 않게 @Transactional을 제거해준다면 아래와 같이 DB에 스레드가 왔다갔다하며 잘 실행이 된다.
- 아이디 검증
- 유저 생성
회고
SQLite의 저장방식이 흥미로웠다.
MySQL과 많이 달라서 각각의 특징을 공부하며 코드를 작성하는게 재밌어서 기분 좋게 글을 작성 한 것 같다.
참고 👇👇
https://stackoverflow.com/questions/16113182/jpa-sqlite-no-such-table-sequence/16306718#16306718
https://www.reddit.com/r/programming/comments/vniiaw/sqlite_or_postgresql_its_complicated/
https://www.youtube.com/watch?v=ocZid4g4UpY
https://deep-jin.tistory.com/entry/Spring-Boot%EC%99%80-sqlite3-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0-Hibernate
https://makedotworld.tistory.com/55#google_vignette
'Database' 카테고리의 다른 글
| 데이터베이스 트랜잭션 격리수준(Isolation Level)와 문제점 예시 정리 (0) | 2025.01.09 |
|---|---|
| 슬로우 쿼리를 잡자! EXPLAIN, EXPLAIN ANALYZE (1) | 2024.11.13 |
| DBMS의 핵심? 옵티마이저 (1) | 2024.11.06 |
| VARCHAR와 TEXT의 저장방식 차이 (1) | 2024.10.22 |