- 이번 포스팅으론 Springboot 로 MongoDB 에 접근하여 사용하는 방법을 설명드리겠습니다.
- build.gradle.kts
저는 코틀린 언어를 사용하기에 build.gradle.kts 를 사용하는데, java 를 사용하신다면 적절히 수정을 하시면 됩니다.
종속성으로,
dependencies 태그 안에
// (MongoDB)
implementation("org.springframework.boot:spring-boot-starter-data-mongodb:3.3.0")
위와 같이 추가하면 됩니다.
- application.yml 파일 안에 데이터베이스 설정을 하겠습니다.
여기서는 하나의 프로젝트로 여러 데이터베이스에 접근이 가능하다는 것을 전제로 하기에 여러 데이터베이스에 대한 설정이 필요한데,
# MongoDB DataSource 설정
datasource-mongodb:
# MongoDB 추가
# 작명법은, 앞에 mdb{index}-{제목} 형식으로 하여, datasource 별 충돌을 피하기
# (주 사용 MongoDB)
# mongodb:// 뒤에 인증 아이디 : 인증 비밀번호를 넣어주고, @ 뒤에는 레플리카 접속 주소를 모두 넣어주며,
# ? 뒤의 replicaSet 은 레플리카 셋 이름을, authSource 는 사용자 정보가 저장된 데이터베이스를 설정하면 됩니다.
mdb1-main:
uri: mongodb://root:todo1234!@127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/db?replicaSet=rs0&authSource=admin
위와 같이 작성합니다.
이러한 설정 규칙은 제가 만들어낸 것으로, 설정명은 자유롭게 수정하시면 됩니다.
저의 경우 mongodb 의 설정명을 작성할 때에는, 앞에 mdb{index}-{제목} 형식으로 하여, datasource 별 충돌을 피하는 규칙을 사용하고 있습니다.
이러한 방식으로 여러 데이터소스를 설정하고, 추후 Springboot 설정에 추가하면 되는데, 일단 위와 같이 하나의 데이터소스만 설정해둡시다.
참고로 위에서 사용한 데이터 소스는 단일 mongodb 가 아닌 mongodb replicaset 을 가정하여 작성하였습니다.
- main 함수가 있는 클래스 파일 안에,
import com.raillylinker.springboot_mvc_template.data_sources.memory_const_object.ProjectConst
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.data.mongodb.config.EnableMongoAuditing
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import java.util.*
// [Main]
// Framework 의 시작점입니다.
@EnableScheduling // 스케쥴러 사용 설정
@EnableAsync // 스케쥴러의 Async 사용 설정
@EnableMongoAuditing // MongoDB 에서 @CreatedDate, @LastModifiedDate 사용 설정
@SpringBootApplication
class ApplicationMain {
@Bean
fun init() = CommandLineRunner {
// 서버 타임존 설정
TimeZone.setDefault(TimeZone.getTimeZone(ProjectConst.SYSTEM_TIME_ZONE))
}
}
fun main(args: Array<String>) {
// 서버 실행
runApplication<ApplicationMain>(*args)
}
위와 같이 @EnableMongoAuditing 어노테이션을 붙여주세요. 주석에서 설명한대로 MongoDB 제공 기능을 사용하기 위한 설정입니다.
- configurations/mongo_db_configs
라이브러리 임포트와 설정 정보 준비가 되었으므로 데이터베이스 설정을 해보겠습니다.
위와 같은 폴더에, Mdb1MainConfig.kt 라는 이름으로 클래스 파일을 만들어줍니다.
작명법은, 앞서 데이터베이스 설정 이름을 따와서 앞에 붙이고, 뒤에는 Config 라고 붙인 것 뿐입니다.
Config 클래스 코드는
import com.raillylinker.springboot_mvc_template.data_sources.memory_const_object.ProjectConst
import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.MongoTransactionManager
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
// [MongoDB 설정]
@Configuration
@EnableMongoRepositories(
basePackages = ["${ProjectConst.PACKAGE_NAME}.data_sources.database_mongo_db.${Mdb1MainConfig.MONGO_DB_DIRECTORY_NAME}.repositories"],
mongoTemplateRef = Mdb1MainConfig.MONGO_DB_DIRECTORY_NAME
)
class Mdb1MainConfig {
companion object {
// !!!application.yml 의 datasource-mongodb 안에 작성된 이름 할당하기!!!
const val MONGO_DB_CONFIG_NAME: String = "mdb1-main"
// !!!data_sources/mongo_db_sources 안의 서브 폴더(documents, repositories 를 가진 폴더)의 이름 할당하기!!!
const val MONGO_DB_DIRECTORY_NAME: String = "mdb1_main"
// Database 트랜젝션 이름 변수
// 트랜젝션을 적용할 함수 위에, @CustomMongoDbTransactional 어노테이션과 결합하여,
// @CustomMongoDbTransactional([MongoDbConfig.TRANSACTION_NAME])
// 위와 같이 적용하세요.
const val TRANSACTION_NAME: String =
"${MONGO_DB_DIRECTORY_NAME}_PlatformTransactionManager"
}
// ---------------------------------------------------------------------------------------------
@Value("\${datasource-mongodb.${MONGO_DB_CONFIG_NAME}.uri}")
private lateinit var mongoDbUri: String
private lateinit var mongoClientFactory: SimpleMongoClientDatabaseFactory
@PostConstruct
fun init() {
mongoClientFactory = SimpleMongoClientDatabaseFactory(mongoDbUri)
}
@Bean(name = [MONGO_DB_DIRECTORY_NAME])
fun mongoTemplate(): MongoTemplate {
return MongoTemplate(mongoClientFactory)
}
@Bean(TRANSACTION_NAME)
fun customTransactionManager(): MongoTransactionManager {
return MongoTransactionManager(mongoClientFactory)
}
}
위와 같습니다.
앞서 설정한 application.yml 의 설정에 맞게 MongoDB 의 Template 클래스 빈과, 트랜젝션 빈을 설정하여 등록한 것입니다.
위 설정에 따르면,
{프로젝트 경로}.data_sources.database_mongo_db.{MONGO_DB_DIRECTORY_NAME}.repositories
폴더 안에 MongoDB 의 레포지토리 클래스를 모아두도록 설정한 것입니다.
새로 데이터 소스를 늘리려면, 해당 코드를 복사하고, 주석상 !!! 로 표시된 상수값들을 변경시켜주면 됩니다.
- repositoires & documents
mongoDB 의 각 파일들을 저장하는 위치를 봅시다.
저는 위와 같이 각 데이터소스별, documents 와 repositories 파일들을 나누어 저장해두었습니다.
다큐먼트 파일은,
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.core.mapping.Field
import java.time.LocalDateTime
@Document(collection = "test")
data class Mdb1_Test(
@Field("content")
var content: String,
@Field("random_num")
var randomNum: Int,
@Field("nullable_value")
var nullableValue: String?,
@Field("row_activate")
var rowActivate: Boolean
) {
@Id
var uid: String? = null
@CreatedDate
@Field("row_create_date")
var rowCreateDate: LocalDateTime? = null
@LastModifiedDate
@Field("row_update_date")
var rowUpdateDate: LocalDateTime? = null
}
이런식으로,
repository 파일은,
import com.raillylinker.springboot_mvc_template.data_sources.database_mongo_db.mdb1_main.documents.Mdb1_Test
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.stereotype.Repository
@Repository
interface Mdb1_Test_Repository : MongoRepository<Mdb1_Test, String> {
}
이렇게 작성되어 있으며, MongoRepository 는 JpaRepository 처럼 사용이 가능합니다.
- 이제 설정의 마지막으로, 멀티 데이터 소스에서 Transaction 을 구현하겠습니다.
위와 같이 MongoDB 의 트랜젝션 어노테이션과 그 처리를 위한 AOP Aspect 클래스 파일을 만들었습니다.
어노테이션 코드는,
// [MongoDB 트랜젝션 어노테이션]
// MongoDB 에서 트랜젝션을 사용하려면 MongoDB Replica Set 설정을 하여 실행시키는 환경에서만 가능합니다.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class CustomMongoDbTransactional(
val transactionManagerBeanNameList: Array<String>
)
AOP 코드는,
import com.raillylinker.springboot_mvc_template.annotations.CustomMongoDbTransactional
import com.raillylinker.springboot_mvc_template.data_sources.memory_const_object.ProjectConst
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.reflect.MethodSignature
import org.springframework.context.ApplicationContext
import org.springframework.data.mongodb.MongoTransactionManager
import org.springframework.stereotype.Component
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.DefaultTransactionDefinition
// [MongoDB @CustomMongoDbTransactional 어노테이션 함수 처리 AOP]
@Component
@Aspect
class MongoDbTransactionAnnotationAspect(
private val applicationContext: ApplicationContext
) {
companion object {
// MongoDb 트랜젝션용 어노테이션인 CustomMongoDbTransactional 파일의 프로젝트 경로
const val MONGO_DB_TRANSACTION_ANNOTATION_PATH =
"@annotation(${ProjectConst.PACKAGE_NAME}.annotations.CustomMongoDbTransactional)"
}
// ---------------------------------------------------------------------------------------------
// <AOP 작성 공간>
// (@CustomTransactional 를 입력한 함수 실행 전후에 JPA 트랜젝션 적용)
@Around(MONGO_DB_TRANSACTION_ANNOTATION_PATH)
fun aroundMongoDbTransactionAnnotationFunction(joinPoint: ProceedingJoinPoint): Any? {
val proceed: Any?
// transactionManager and transactionStatus 리스트
val transactionManagerAndTransactionStatusList =
ArrayList<Pair<PlatformTransactionManager, TransactionStatus>>()
try {
// annotation 에 설정된 transaction 순차 실행 및 저장
for (transactionManagerBeanName in ((joinPoint.signature as MethodSignature).method).getAnnotation(
CustomMongoDbTransactional::class.java
).transactionManagerBeanNameList) {
// annotation 에 저장된 transactionManager Bean 이름으로 Bean 객체 가져오기
val platformTransactionManager =
applicationContext.getBean(transactionManagerBeanName) as MongoTransactionManager
// transaction 시작 및 정보 저장
transactionManagerAndTransactionStatusList.add(
Pair(
platformTransactionManager,
platformTransactionManager.getTransaction(DefaultTransactionDefinition())
)
)
}
//// 함수 실행 전
proceed = joinPoint.proceed() // 함수 실행
//// 함수 실행 후
// annotation 에 설정된 transaction commit 역순 실행 및 저장
for (transactionManagerIdx in transactionManagerAndTransactionStatusList.size - 1 downTo 0) {
val transactionManager = transactionManagerAndTransactionStatusList[transactionManagerIdx]
transactionManager.first.commit(transactionManager.second)
}
} catch (e: Exception) {
// annotation 에 설정된 transaction rollback 역순 실행 및 저장
for (transactionManagerIdx in transactionManagerAndTransactionStatusList.size - 1 downTo 0) {
val transactionManager = transactionManagerAndTransactionStatusList[transactionManagerIdx]
transactionManager.first.rollback(transactionManager.second)
}
throw e
}
return proceed // 결과 리턴
}
// ---------------------------------------------------------------------------------------------
// <공개 메소드 공간>
// ---------------------------------------------------------------------------------------------
// <비공개 메소드 공간>
// ---------------------------------------------------------------------------------------------
// <중첩 클래스 공간>
}
위와 같이 작성하였습니다.
어노테이션이 붙은 함수의 코드 실행 전에 앞서 설정한 MongoDB 트랜젝션 빈을 가져와서 실행시키고,
코드 실행 후에 에러가 발생했다면 rollback, 에러가 발생하지 않았다면 commit 을 하도록 한 것입니다.
여기서 주의해야하는 점으로는, MongoDB 에서 트랜젝션을 실행시키려면 단일 노드 형태로는 불가능하고, Replicaset 을 적용해야만 트랜젝션 기능이 지원이 된다는 것입니다.
- 마지막으로, 트랜젝션 어노테이션 사용 및 mongoDB 사용 예시를 보겠습니다.
@CustomMongoDbTransactional([Mdb1MainConfig.TRANSACTION_NAME]) // ReplicaSet 환경이 아니면 에러가 납니다.
fun api12TransactionRollbackTest(
httpServletResponse: HttpServletResponse
) {
mdb1TestRepository.save(
Mdb1_Test(
"test",
(0..99999999).random(),
null,
true
)
)
throw Exception("Transaction Rollback Test!")
httpServletResponse.setHeader("api-result-code", "")
httpServletResponse.status = HttpStatus.OK.value()
}
fun api13NoTransactionRollbackTest(
httpServletResponse: HttpServletResponse
) {
mdb1TestRepository.save(
Mdb1_Test(
"test",
(0..99999999).random(),
null,
true
)
)
throw Exception("No Transaction Exception Test!")
httpServletResponse.setHeader("api-result-code", "")
httpServletResponse.status = HttpStatus.OK.value()
}
위와 같은 함수로, 트랜젝션 어노테이션 사용 한 경우와 하지 않은 경우에 대해 처리가 가능합니다.
repository 사용법은 jpa repository 와 동일하며, 위와 같이 트랜젝션 테스트를 했을 때,
트랜젝션 어노테이션이 붙어있다면 롤백되고, 아니라면 롤백되지 않는 것을 확인할 수 있습니다.
- 이상입니다.
'Springboot' 카테고리의 다른 글
Springboot JavaMailSender 로 이메일 보내기 (PlainText 발송, 첨부파일 발송, Thymeleaf HTML 이메일 발송) (2) | 2024.10.08 |
---|---|
Springboot 에서 AWS S3 다루기 (1) | 2024.10.08 |
Springboot JPA 설정하기 (멀티 소스 데이터베이스 접속 및 트랜젝션 Annotation 작성) (2) | 2024.10.08 |
Springboot 프로젝트 Docker 로 배포하기 (1) | 2024.09.30 |
Springboot JPA Timezone 설명 (데이터베이스와 Springboot 간의 타임존 처리 방식) (0) | 2024.09.29 |