Kotlin Exposed ORM - 2

구워먹는 삼겹살·2025년 2월 16일
0

1. Exposed의 의존성 추가

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")

2. SpringBoot 와 Exposed

Table

object UserTable : LongIdTable("user") {
    val username = varchar("name", 255)
    val email = varchar("email", 255).uniqueIndex()
    val password = varchar("password", 255)
    val createdAt = datetime("createdAt")
}
  1. 싱글톤 패턴을 이용한 테이블 정의
    테이블을 object로 정의함으로써, 해당 테이블은 싱글톤 패턴을 따르게 됩니다. Exposed는 object로 정의된 테이블을 통해 테이블 객체를 한 번만 생성하고 관리할 수 있게 합니다. 이를 통해 중복된 테이블 객체 생성이 방지되고, 모든 쿼리에서 동일한 테이블 객체를 사용할 수 있습니다.

  2. LongIdTable 사용
    LongIdTable을 상속받으면 id 컬럼이 자동으로 추가되고, 이 컬럼은 Long 타입이며 기본키로 자동 증가합니다. 이렇게 하면 기본키를 명시적으로 지정하지 않아도 데이터베이스에서 자동으로 증가하는 값을 관리할 수 있습니다.

  • email: varchar("email", 255).uniqueIndex()는 이메일을 저장하는 컬럼으로, 고유 인덱스를 설정하여 중복을 방지합니다.

DAO Class

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 객체를 사용하여 데이터베이스 작업을 더 객체 지향적으로 처리하는 방식입니다.

  1. User 클래스 정의
    User 클래스는 LongEntity를 상속받은 엔티티 클래스입니다. LongEntity는 Exposed의 DAO 클래스에서 Long 타입의 기본 키를 사용할 때 사용하는 부모 클래스입니다.
    id: EntityID는 이 엔티티 객체의 기본 키 값을 담고 있는 EntityID를 나타내며, LongEntity에서 상속받은 id 속성은 데이터베이스에서 기본 키를 관리합니다.

  2. companion object : LongEntityClass(UserTable)
    이 부분은 Exposed에서 해당 User 엔티티 클래스가 데이터베이스 테이블인 UserTable과 연동된다는 것을 나타냅니다. LongEntityClass는 주어진 Table에 대해 CRUD 작업을 할 수 있게 하는 클래스를 생성합니다.
    이 companion object를 통해 UserDAO 클래스를 사용한 쿼리 메소드(예: User.find, User.new, User.all 등)를 호출할 수 있게 됩니다.

  3. 속성 정의
    username, email, password, createdAt은 UserTable에 정의된 테이블의 컬럼을 참조하는 속성들입니다. by UserTable.username와 같은 구문은 Exposed의 Delegated Property 문법을 사용하여, 테이블에서 각 컬럼을 참조하고 값을 자동으로 매핑하도록 설정합니다.

Repository

@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 라이브러리를 사용하여 데이터베이스와 상호작용합니다.

  1. findByEmail(email: String): User?
    목적: 주어진 이메일을 기준으로 사용자를 조회합니다.
    설명:
    User.find { UserTable.email eq email }로 이메일이 일치하는 사용자 레코드를 찾습니다.
    singleOrNull()을 사용하여 결과가 없으면 null을 반환하고, 하나의 사용자만 있을 경우 그 객체를 반환합니다.
    반환값: 이메일에 해당하는 User 객체 또는 null.

  2. findAll(): List
    목적: 모든 사용자 정보를 조회합니다.
    설명:
    User.all()로 User 테이블의 모든 레코드를 가져옵니다.
    toList()를 사용하여 리스트 형식으로 반환합니다.
    반환값: User 객체들의 리스트.

  • 이 두 메소드는 User 테이블에서 데이터를 조회하는 기본적인 CRUD 기능을 제공합니다. findByEmail은 특정 이메일에 맞는 사용자를, findAll은 모든 사용자의 데이터를 반환합니다.

Service

@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)
    }
}
  1. 이메일 중복 체크: 먼저 userRepository.findByEmail(request.email)를 통해 주어진 이메일로 사용자가 이미 존재하는지 확인합니다. 이미 존재하면 IllegalStateException을 던져 회원가입을 막습니다.

  2. 새로운 사용자 생성: 이메일 중복이 없으면 User.new {} 블록을 통해 새로운 사용자를 생성합니다. new는 Exposed의 영속성 객체 생성 방식으로, JPA의 save와 같은 역할을 합니다.
    패스워드 암호화: 패스워드는 passwordEncoder.encode(request.password)로 암호화하여 저장합니다.

  3. SignUpResponse 반환: 생성된 User 엔티티를 SignUpResponse.from(user) 메소드를 사용해 DTO로 변환하여 반환합니다.

  • Exposed에서 new는 JPA의 save와 같은 역할을 합니다.
    SignUpResponse.from(user)는 엔티티를 DTO로 변환하는 companion object 메소드입니다.

3. What I leanred

Exposed 사용기

  • Exposed는 DAO 방식뿐만 아니라 DSL 방식도 지원하는 Kotlin 라이브러리로, SQL 쿼리를 직접 작성할 수 있어 유연하게 데이터를 처리할 수 있습니다. JPA를 주로 사용하던 중 Exposed를 도입하면서 익숙하지 않았지만, 그 강력한 특징에 익숙해지니 매우 유용하게 느껴졌습니다.

Exposed의 두 가지 방식:

  • DAO 방식: 객체 지향적인 방식으로, User.new {}와 같이 Kotlin 객체를 사용해 데이터를 영속화하고 관리할 수 있습니다. JPA의 save()와 비슷한 역할을 합니다.

  • DSL 방식: SQL 문법을 Kotlin 코드로 작성할 수 있는 유연한 방법을 제공합니다. Exposed에서 제공하는 Kotlin DSL을 사용하면, join, select, update, delete 등의 쿼리를 쉽게 다룰 수 있습니다. 이는 QueryDsl과 유사하며, 복잡한 쿼리를 다룰 때 유용합니다.

결론

  • DAO 방식 + Kotlin DSL 방식을 결합해 사용하는 것이 Exposed의 핵심입니다. JPA + QueryDsl처럼 Exposed에서도 두 방식을 함께 사용하여 다양한 쿼리 요구사항을 처리할 수 있습니다. 이를 통해 SQL 작성의 유연성을 높이고, Kotlin과 SQL을 효율적으로 결합할 수 있게 되었습니다.

0개의 댓글