Kotlin과 함께 사용하는 Exposed는 SQL을 더 간결하게 작성할 수 있도록 도와주는 라이브러리입니다. Exposed의 여러 기능을 사용하려면 관련 의존성을 build.gradle 파일에 추가해야 합니다. 필요한 모듈을 선택적으로 추가할 수 있습니다.
// Core 기능: 기본적인 Exposed 기능
implementation("org.jetbrains.exposed:exposed-core:0.59.0")
// Crypt 관련 기능 (암호화)
implementation("org.jetbrains.exposed:exposed-crypt:0.59.0")
// DAO 기능 (Data Access Object)
implementation("org.jetbrains.exposed:exposed-dao:0.59.0")
// JDBC 기능 (JDBC 연결을 사용한 데이터베이스 작업)
implementation("org.jetbrains.exposed:exposed-jdbc:0.59.0")
// Java Time 기능 (Java 8의 날짜/시간 API 사용)
implementation("org.jetbrains.exposed:exposed-java-time:0.59.0")
// JSON 처리 기능
implementation("org.jetbrains.exposed:exposed-json:0.59.0")
// Money 관련 기능 (화폐 단위 처리)
implementation("org.jetbrains.exposed:exposed-money:0.59.0")
// Spring Boot Starter (Spring Boot와 통합)
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.59.0")
object UserTable : LongIdTable("user") {
val username = varchar("name", 255)
val email = varchar("email", 255).uniqueIndex()
val password = varchar("password", 255)
val createdAt = datetime("createdAt")
}
싱글톤 패턴을 이용한 테이블 정의
테이블을 object로 정의함으로써, 해당 테이블은 싱글톤 패턴을 따르게 됩니다. Exposed는 object로 정의된 테이블을 통해 테이블 객체를 한 번만 생성하고 관리할 수 있게 합니다. 이를 통해 중복된 테이블 객체 생성이 방지되고, 모든 쿼리에서 동일한 테이블 객체를 사용할 수 있습니다.
LongIdTable 사용
LongIdTable을 상속받으면 id 컬럼이 자동으로 추가되고, 이 컬럼은 Long 타입이며 기본키로 자동 증가합니다. 이렇게 하면 기본키를 명시적으로 지정하지 않아도 데이터베이스에서 자동으로 증가하는 값을 관리할 수 있습니다.
class User(id: EntityID<Long>):LongEntity(id) {
companion object : LongEntityClass<User>(UserTable)
var username by UserTable.username
var email by UserTable.email
var password by UserTable.password
var createdAt by UserTable.createdAt
}
DAO 방식
DAO 방식은 객체를 생성하고 이를 데이터베이스와 연결하여 CRUD 작업을 수행하는 방법입니다. Exposed에서는 테이블 정의에 대응하는 DAO 클래스를 작성하여 이를 통해 데이터를 관리할 수 있습니다. 이 방식은 Table 객체와는 별개로, DAO 객체를 사용하여 데이터베이스 작업을 더 객체 지향적으로 처리하는 방식입니다.
User 클래스 정의
User 클래스는 LongEntity를 상속받은 엔티티 클래스입니다. LongEntity는 Exposed의 DAO 클래스에서 Long 타입의 기본 키를 사용할 때 사용하는 부모 클래스입니다.
id: EntityID는 이 엔티티 객체의 기본 키 값을 담고 있는 EntityID를 나타내며, LongEntity에서 상속받은 id 속성은 데이터베이스에서 기본 키를 관리합니다.
companion object : LongEntityClass(UserTable)
이 부분은 Exposed에서 해당 User 엔티티 클래스가 데이터베이스 테이블인 UserTable과 연동된다는 것을 나타냅니다. LongEntityClass는 주어진 Table에 대해 CRUD 작업을 할 수 있게 하는 클래스를 생성합니다.
이 companion object를 통해 UserDAO 클래스를 사용한 쿼리 메소드(예: User.find, User.new, User.all 등)를 호출할 수 있게 됩니다.
속성 정의
username, email, password, createdAt은 UserTable에 정의된 테이블의 컬럼을 참조하는 속성들입니다. by UserTable.username와 같은 구문은 Exposed의 Delegated Property 문법을 사용하여, 테이블에서 각 컬럼을 참조하고 값을 자동으로 매핑하도록 설정합니다.
@Repository
class UserRepository {
fun findByEmail(email: String): User? {
return User.find { UserTable.email eq email }.singleOrNull()
}
fun findAll(): List<User> {
return User.all().toList()
}
}
UserRepository 클래스는 사용자 정보를 조회하는 메소드를 제공합니다. 이 클래스는 Exposed 라이브러리를 사용하여 데이터베이스와 상호작용합니다.
findByEmail(email: String): User?
목적: 주어진 이메일을 기준으로 사용자를 조회합니다.
설명:
User.find { UserTable.email eq email }로 이메일이 일치하는 사용자 레코드를 찾습니다.
singleOrNull()을 사용하여 결과가 없으면 null을 반환하고, 하나의 사용자만 있을 경우 그 객체를 반환합니다.
반환값: 이메일에 해당하는 User 객체 또는 null.
findAll(): List
목적: 모든 사용자 정보를 조회합니다.
설명:
User.all()로 User 테이블의 모든 레코드를 가져옵니다.
toList()를 사용하여 리스트 형식으로 반환합니다.
반환값: User 객체들의 리스트.
@Service
class UserService(
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
) {
@Transactional
fun signUp(request: SignUpRequest): SignUpResponse {
if (userRepository.findByEmail(request.email) != null) {
throw IllegalStateException("User already exists")
}
val user = User.new {
username = request.username
email = request.email
password = passwordEncoder.encode(request.password)
createdAt = LocalDateTime.now()
}
return SignUpResponse.from(user)
}
}
이메일 중복 체크: 먼저 userRepository.findByEmail(request.email)를 통해 주어진 이메일로 사용자가 이미 존재하는지 확인합니다. 이미 존재하면 IllegalStateException을 던져 회원가입을 막습니다.
새로운 사용자 생성: 이메일 중복이 없으면 User.new {} 블록을 통해 새로운 사용자를 생성합니다. new는 Exposed의 영속성 객체 생성 방식으로, JPA의 save와 같은 역할을 합니다.
패스워드 암호화: 패스워드는 passwordEncoder.encode(request.password)로 암호화하여 저장합니다.
SignUpResponse 반환: 생성된 User 엔티티를 SignUpResponse.from(user) 메소드를 사용해 DTO로 변환하여 반환합니다.
Exposed 사용기
Exposed의 두 가지 방식:
DAO 방식: 객체 지향적인 방식으로, User.new {}와 같이 Kotlin 객체를 사용해 데이터를 영속화하고 관리할 수 있습니다. JPA의 save()와 비슷한 역할을 합니다.
DSL 방식: SQL 문법을 Kotlin 코드로 작성할 수 있는 유연한 방법을 제공합니다. Exposed에서 제공하는 Kotlin DSL을 사용하면, join, select, update, delete 등의 쿼리를 쉽게 다룰 수 있습니다. 이는 QueryDsl과 유사하며, 복잡한 쿼리를 다룰 때 유용합니다.
결론