반응형

개념

ORM?

  • Object-Relational Mapping (객체와 관계형 데이터베이스 매핑, 객체와 DB의 테이블이 매핑을 이루는 것)
  • 객체가 테이블이 되도록 매핑 시켜주는 프레임워크이다.
  • 프로그램의 복잡도를 줄이고 자바 객체와 쿼리를 분리할 수 있으며 트랜잭션 처리나 기타 데이터베이스 관련 작업들을 좀 더 편리하게 처리할 수 있는 방법

JPA?

  • Java Persistence API (자바 ORM 기술에 대한 API 표준 명세)
  • 한마디로 ORM을 사용하기 위한 인터페이스를 모아둔 것 이라고 볼 수 있다.
  • 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다.
  • MyBatis는 ORM이 아니고 SQL Mapper임

Hibernate?

  • JPA를 사용하기 위해서 JPA를 구현한 ORM 프레임워크중 하나.
  • JPA 인터페이스의 실제 구현부를 담당함

JPA의 장점

  • 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 더 집중할 수 있게 도와줌
  • 객체 지향적으로 데이터를 관리할 수 있기 때문에 전체 프로그램 구조를 일관되게 유지할 수 있음
  • SQL을 직접 작성하지 않고 객체를 기준으로 동작하기 때문에 유지보수가 쉬움 (ex. 컬럼 수정시 해당 모델 객체 필드만 수정해주면 끝)
  • 쿼리 문법 오류를 런타임시점이 아닌 컴파일시점에 미리 알 수 있음
  • DBMS에 대한 코드 종속성이 줄어듬

JPA의 단점

  • 학습 비용이 높음
  • 통계와 같은 복잡한 쿼리 사용시 불리함 (실시간 처리용 쿼리에 최적화)
  • 잘못 사용할 경우 실제 SQL문을 직접 작성하는 것보다 성능이 떨어질 수 있음

Spring 기본 예제

docker-compose.yml

version: "3.3"
volumes:
  mysql01_data: { }
services:
  mysql01:
    image: mysql:8.3.0
    ports:
      - "13306:3306"
    environment:
      TZ: Asia/Seoul
      MYSQL_DATABASE: example
      MYSQL_ROOT_PASSWORD: abcde12345!@
    volumes:
      - mysql01_data:/var/lib/mysql/
    command:
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"

MySQL 실행

docker-compose up -d

테이블 생성

DROP TABLE IF EXISTS Student;
CREATE TABLE Student
(
    studentId    BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name         VARCHAR(200),
    age          INTEGER,
    registerDate TIMESTAMP(6) — milliseconds 단위까지 저장
);

build.gradle.kts

plugins {
    // ...
    kotlin("plugin.jpa") version "1.9.24"
}

dependencies {
    // ...
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.mysql:mysql-connector-j")
}

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:13306/example
    username: root
    password: abcde12345!@

  jpa:
    open-in-view: false
    properties:
      hibernate:
        hbm2ddl:
          auto: none              # 애플리케이션 실행시 테이블 자동 생성 타입
        show_sql: true            # 하이버네이트가 실행하는 모든 SQL문을 콘솔로 출력.
        format_sql: false         # 콘솔에 출력되는 JPA 실행 쿼리를 가독성있게 포맷팅.
        use_sql_comments: false   # 디버깅이 용이하도록 SQL문 이외에 추가적인 정보를 출력.

        # 변수명을 그대로 칼럼명으로 지정 가능하도록 설정(변수명이 카멜케이스이면 필드명도 카멜케이스)
        implicit_naming_strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
        physical_naming_strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

logging:
  level:
    org.hibernate.orm.jdbc.bind: trace    # SQL문에 바인딩된 파라미터값을 로그로 출력하기 위한 설정.

Student

@Entity
data class Student(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val studentId: Long = 0L,
    var name: String,
    var age: Int,
    val registerDate: LocalDateTime,
)

StudentRepository

@Repository
interface StudentRepository : JpaRepository<Student, Long> {
    fun findByName(name: String): List<Student>
}

StudentRepositoryTest

@SpringBootTest
class StudentRepositoryTest {
    @Autowired
    private lateinit var jdbcTemplate: JdbcTemplate

    @Autowired
    private lateinit var studentRepository: StudentRepository

    @BeforeEach
    fun before() {
        jdbcTemplate.execute("TRUNCATE TABLE Student")
    }

    @Test
    fun testInsert() {
        studentRepository.save(Student(0L, "john", 25, LocalDateTime.now()))
        studentRepository.save(Student(0L, "tom", 30, LocalDateTime.now()))

        val result = studentRepository.findAll()

        assertEquals(2, result.size)
        assertEquals(1L, result[0].studentId)
        assertEquals(2L, result[1].studentId)
    }

    @Test
    fun testUpdate() {
        studentRepository.save(Student(0L, "john", 25, LocalDateTime.now()))
        studentRepository.save(Student(1L, "john2", 25, LocalDateTime.now()))

        val result = studentRepository.findAll()

        assertEquals(1, result.size)
        assertEquals("john2", result[0].name)
    }

    @Test
    fun testDelete() {
        studentRepository.save(Student(0L, "john", 25, LocalDateTime.now()))
        studentRepository.deleteById(1L)

        val result = studentRepository.findAll()

        assertEquals(0, result.size)
    }

    @Test
    fun testSelect() {
        studentRepository.save(Student(0L, "john", 25, LocalDateTime.now()))
        studentRepository.save(Student(0L, "tom", 30, LocalDateTime.now()))

        val result1 = studentRepository.findAll()
        val result2 = studentRepository.findByName("john")
        val result3 = studentRepository.findByIdOrNull(2L)

        assertEquals(2, result1.size)
        assertEquals("john", result1[0].name)
        assertEquals("tom", result1[1].name)

        assertEquals(1, result2.size)
        assertEquals("john", result2[0].name)

        assertEquals("tom", result3?.name)
    }
}

다중 DataSource 예제

docker-compose.yml

version: "3.3"
volumes:
  mysql01_data: { }
  mysql02_data: { }
services:
  mysql01:
    image: mysql:8.3.0
    ports:
      - "13306:3306"
    environment:
      TZ: Asia/Seoul
      MYSQL_DATABASE: example
      MYSQL_ROOT_PASSWORD: abcde12345!@
    volumes:
      - mysql01_data:/var/lib/mysql/
    command:
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"

  mysql02:
    image: mysql:8.3.0
    ports:
      - "23306:3306"
    environment:
      TZ: Asia/Seoul
      MYSQL_DATABASE: example
      MYSQL_ROOT_PASSWORD: abcde12345!@
    volumes:
      - mysql02_data:/var/lib/mysql/
    command:
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"

MySQL 실행

docker-compose up -d

application.yml

spring:
  datasource:
    primary:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:13306/example
      username: root
      password: abcde12345!@
    secondary:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:23306/example
      username: root
      password: abcde12345!@

  jpa:
    open-in-view: false
    properties:
      hibernate:
        hbm2ddl:
          auto: create            # 애플리케이션 실행시 테이블 자동 생성 타입
        show_sql: true            # 하이버네이트가 실행하는 모든 SQL문을 콘솔로 출력.
        format_sql: false         # 콘솔에 출력되는 JPA 실행 쿼리를 가독성있게 포맷팅.
        use_sql_comments: false   # 디버깅이 용이하도록 SQL문 이외에 추가적인 정보를 출력.

        # 변수명을 그대로 칼럼명으로 지정 가능하도록 설정(변수명이 카멜케이스이면 필드명도 카멜케이스)
        implicit_naming_strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
        physical_naming_strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

logging:
  level:
    org.hibernate.orm.jdbc.bind: trace    # SQL문에 바인딩된 파라미터값을 로그로 출력하기 위한 설정.

Student

package com.example.demo.entity.primary // primary용 패키지 경로

// ...

@Entity
data class Student(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val studentId: Long = 0L,
    var name: String,
    var age: Int,
    val registerDate: LocalDateTime,
)

StudentRepository

package com.example.demo.entity.primary // primary용 패키지 경로

// ...

interface StudentRepository : JpaRepository<Student, Long> {
    fun findByName(name: String): List<Student>
}

PrimaryDatabaseConfig

@EnableJpaRepositories(
    basePackages = ["com.example.demo.entity.primary"],
    entityManagerFactoryRef = "primaryEntityManagerFactory",
    transactionManagerRef = "primaryTransactionManager",
)
@Configuration
class PrimaryDatabaseConfig {
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    @Bean
    fun primaryDataSource(): DataSource {
        return DataSourceBuilder.create().type(HikariDataSource::class.java).build()
    }

    @Bean
    fun primaryEntityManagerFactory(@Qualifier("primaryDataSource") primaryDataSource: DataSource, jpaProperties: JpaProperties): LocalContainerEntityManagerFactoryBean {
        val entityManager = LocalContainerEntityManagerFactoryBean()
        entityManager.setDataSource(primaryDataSource)
        entityManager.setPackagesToScan("com.example.demo.entity.primary")
        entityManager.setJpaPropertyMap(jpaProperties.properties as Map<String, Any>)
        entityManager.jpaVendorAdapter = HibernateJpaVendorAdapter()
        return entityManager
    }

    @Bean
    fun primaryTransactionManager(@Qualifier("primaryEntityManagerFactory") primaryEntityManagerFactory: EntityManagerFactory): PlatformTransactionManager {
        return JpaTransactionManager(primaryEntityManagerFactory)
    }
}

Logging

package com.example.demo.entity.secondary // secondary용 패키지 경로

// ...

@Entity
data class Logging(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val loggingId: Long = 0L,
    var message: String,
    val registerDate: LocalDateTime,
)

LoggingRepository

package com.example.demo.entity.secondary // secondary용 패키지 경로

// ...

interface LoggingRepository : JpaRepository<Logging, Long>

SecondaryDatabaseConfig

@EnableJpaRepositories(
    basePackages = ["com.example.demo.entity.secondary"],
    entityManagerFactoryRef = "secondaryEntityManagerFactory",
    transactionManagerRef = "secondaryTransactionManager",
)
@Configuration
class SecondaryDatabaseConfig {
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    @Bean
    fun secondaryDataSource(): DataSource {
        return DataSourceBuilder.create().type(HikariDataSource::class.java).build()
    }

    @Bean
    fun secondaryEntityManagerFactory(@Qualifier("secondaryDataSource") secondaryDataSource: DataSource, jpaProperties: JpaProperties): LocalContainerEntityManagerFactoryBean {
        val entityManager = LocalContainerEntityManagerFactoryBean()
        entityManager.setDataSource(secondaryDataSource)
        entityManager.setPackagesToScan("com.example.demo.entity.secondary")
        entityManager.setJpaPropertyMap(jpaProperties.properties as Map<String, Any>)
        entityManager.jpaVendorAdapter = HibernateJpaVendorAdapter()
        return entityManager
    }

    @Bean
    fun secondaryTransactionManager(@Qualifier("secondaryEntityManagerFactory") secondaryEntityManagerFactory: EntityManagerFactory): PlatformTransactionManager {
        return JpaTransactionManager(secondaryEntityManagerFactory)
    }
}

DemoService

@Service
class DemoService(
    private val studentRepository: StudentRepository,
    private val loggingRepository: LoggingRepository,
) {
    @Transactional("primaryTransactionManager") // transactionManager 지정
    fun saveDemo() {
        // studentRepository는 primaryTransactionManager를 사용하므로 primaryTransactionManager에 포함됨
        // studentRepository.save()와 동시에 DB에 반영되지 않고 saveDemo()가 완료되어야 DB에 반영
        studentRepository.save(Student(0, "john", 35, LocalDateTime.now()))

        // loggingRepository는 secondaryTransactionManager를 사용하기 때문에 primaryTransactionManager에 포함되지 않음
        // loggingRepository.save()와 동시에 DB에 반영
        loggingRepository.save(Logging(0, "Hello", LocalDateTime.now()))
    }
}

DemoServiceTest

@SpringBootTest
class DemoServiceTest {
    @Autowired
    private lateinit var demoService: DemoService

    @Test
    fun demoServiceTest() {
        demoService.saveDemo()
    }
}
반응형

'Development > JPA' 카테고리의 다른 글

[JPA] Dynamic Query  (0) 2024.11.23

+ Recent posts