반응형
개념
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 |
---|