ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring boot + MongoDB Multi-Document Transactions
    DB 2022. 7. 22. 17:21
    반응형

    MongoDB 4.0 이전 버전에선 Single-Document Transactions만을 지원하였습니다.

    RDB는 데이터들을 정규화해서 서로 다른 테이블에 저장하는 반면 MongoDB는 정규화를 거치지 않고 하나의 Document에 몽땅 저장 하기 때문에 MongoDB의 지향점을 생각하면 Single-Document Transactions이면 충분했겠지만 세상은 우리의 뜻(?)대로만 살 수 없기에 4.0 버전부터 Multi-Document Transactions이 지원되게 되었습니다. 여담이지만 4.0 이전 버전에서 Multi-Document Transactions을 위해선 2-Phase-Commits과 같은 방법을 개발자가 직접 처리해야 했습니다.

    그럼 버전별로 달라진 점을 간략 설명하자면

    v4.0

    • Multi-Document Transactions 지원 시작. 단 replica sets 구성이여야 함
    • MongoDB driver 4.0 이상 버전 사용

    v4.2

    • Multi-Document Transactions 지원 범위가 sharded clusters까지 확장
    • MongoDB driver 4.2 이상 버전 사용

    v4.4

    • Transactions에서 컬렉션과 인덱스 생성 가능. 단 샤드 간 쓰기 Transactions이 아니여야 함
    • MongoDB driver 4.4 이상 버전 사용

    그럼 Multi-Document Transactions을 지원하는 MongoDB를 설치하고 Spring Boot에서 정상적으로 동작하는지 확인 해보도록 하겠습니다.

    MongoDB

    • docker-compose 활용
    • Multi-Document Transaction을 위한 MongoDB 3대 replica sets 구성
      • 1대 Master, 2대 Sencondary
    • Transactions에서 컬렉션 생성을 위해 4.4 버전 사용

    예제 docker-compose은 해당 게시물 예제 Git 프로젝트에 docker-compose 폴더 안에

    • mongo-4.2 MongoDB 4.2.5 버전
    • mongo-4.4 MongoDB 4.4 버전

    버전별 테스트를 위해 위 폴더별로 docker-compose yml 파일을 생성해두었습니다. 폴더 구조는 아래를 참고하시면 되겠습니다.

    +--- mongo-4.2
    |   +--- docker-entrypoint-initdb.d
    |   |   +--- rs-init.sh
    |   +--- mongo4_2-docker-compose.yml
    +--- mongo-4.4
    |   +--- docker-entrypoint-initdb.d
    |   |   +--- rs-init.sh
    |   +--- mongo4_4-docker-compose.yml

    docker-compose.yml

    version: '3.8'
    
    services:
      mongodb-4.4-1:
        image: mongo:4.4
        container_name: mongodb-4.4-1
        extra_hosts:
          - "host.docker.internal:host-gateway"
        ports:
          - 27017:27017
        volumes:
          - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
        healthcheck:
          test: ["CMD", "/docker-entrypoint-initdb.d/rs-init.sh"]
        entrypoint: ["/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs"]
        depends_on:
          - mongodb-4.4-2
          - mongodb-4.4-3
        links:
          - mongodb-4.4-2
          - mongodb-4.4-3
      mongodb-4.4-2:
        image: mongo:4.4
        container_name: mongodb-4.4-2
        extra_hosts:
          - "host.docker.internal:host-gateway"
        ports:
          - 27018:27017
        entrypoint: ["/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs"]
      mongodb-4.4-3:
        image: mongo:4.4
        container_name: mongodb-4.4-3
        extra_hosts:
          - "host.docker.internal:host-gateway"
        ports:
          - 27019:27017
        entrypoint: ["/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs"]

    rs-init.sh

    #!/bin/bash
    
    mongo <<EOF
    var config = {
        "_id": "rs",
        "version": 1,
        "members": [
            {
                "_id": 1,
                "host": "host.docker.internal:27017",
                "priority": 3
            }
            , {
                "_id": 2,
                "host": "host.docker.internal:27018",
                "priority": 2
            }
            , {
                "_id": 3,
                "host": "host.docker.internal:27019",
                "priority": 1
            }
        ]
    };
    rs.initiate(config, { force: true });
    rs.status();
    EOF
    
    mongo <<EOF
    use admin; 
    db.createUser( { user: "user", pwd: "password", roles: ["root"] });
    EOF

    Master(mongodb-4.4-1)에서 추가적으로 Shell을 실행하여 replica set 설정 및 관리자 계정을 생성합니다. 정상적으로 replica sets 구성이 되려면 몇 초 정도의 시간이 소요 됩니다.

    Spring boot

    • 2.7.1 버전
    • spring-boot-starter-data-mongodb 사용
    • 2.7.1 버전 사용 시 mongodb driver 버전은 4.6.1 사용

    https://i.ibb.co/N1rLwn4/mongodb-driver.png

    application.yml

    mongodb:
      master:
        host: localhost
        port: 27017
        database: board
        auth-database: admin
        username: user
        password: password
      sencondary:
        - host: localhost
          port: 27018
        - host: localhost
          port: 27019

    replica set 접속을 위한 접속 정보

    Config

    MongoDB 접속을 위해 Java Config을 사용하였습니다.

    @Configuration
    public class MongoDBConfig {
        private final MongoDBProperties mongoDBProperties;
    
        public MongoDBConfig(MongoDBProperties mongoDBProperties) {
            this.mongoDBProperties = mongoDBProperties;
        }
    
        @Bean
        public MongoClient mongoClient() {
            Master master = mongoDBProperties.getMaster();
    
            MongoCredential mongoCredential = MongoCredential.createCredential(master.getUsername()
                    , master.getAuthDatabase()
                    , master.getPassword().toCharArray());
    
            List<ServerAddress> serverAddresses = new ArrayList<>();
            serverAddresses.add(new ServerAddress(master.getHost(), master.getPort()));
    
            List<Sencondary> sencondarys = mongoDBProperties.getSencondary();
            for(Sencondary sencondary : sencondarys) {
                serverAddresses.add(new ServerAddress(sencondary.getHost(), sencondary.getPort()));
            }
    
            MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
                    .credential(mongoCredential)
                    .applyToClusterSettings(b -> b.hosts(serverAddresses))
                    .build();
    
            return MongoClients.create(mongoClientSettings);
        }
    
        @Bean
        public MongoDatabaseFactorySupport<MongoClient> mongoDatabaseFactorySupport(MongoClient mongoClient) {
            return new SimpleMongoClientDatabaseFactory(mongoClient, mongoDBProperties.getMaster().getDatabase());
        }
    
        @Bean
        public MongoTransactionManager mongoTransactionManager(MongoDatabaseFactorySupport<MongoClient> mongoDatabaseFactorySupport) {
            return new MongoTransactionManager(mongoDatabaseFactorySupport);
        }
    
        @Bean
        public MongoTemplate mongoTemplate(MongoDatabaseFactorySupport<MongoClient> mongoDatabaseFactorySupport) {
            MongoTemplate mongoTemplate = new MongoTemplate(mongoDatabaseFactorySupport);
            ((MappingMongoConverter) mongoTemplate.getConverter()).setTypeMapper(new DefaultMongoTypeMapper(null));
    
            return mongoTemplate;
        }
    }

    Entity

    Multi-Document Transaction 테스트를 위한 목적으로 게시물 엔티티를 생성하였습니다.

    /**
     * <pre>
     *     MongoDB Multi-Document Transaction 테스트를 위해 생성한 게시물 엔티티
     * </pre>
     */
    @Document(collation = "posts")
    public class Posts {
        private final String memberId;
        private final String text;
        private final LocalDateTime registrationDate;
    
        public Posts(String memberId, String text) {
            this.memberId = memberId;
            this.text = text;
            this.registrationDate = LocalDateTime.now();
        }
    
        public String getMemberId() {
            return memberId;
        }
    
        public String getText() {
            return text;
        }
    
        public LocalDateTime getRegistrationDate() {
            return registrationDate;
        }
    }

    Service

    Transaction 테스트를 위해 작성된 간단한 테스트 Service 클래스 입니다.

    /**
     * <pre>
     *     MongoDB Multi-Document Transaction 테스트를 위한 구현체
     * </pre>
     */
    @Service
    public class PostsServiceImpl {
        private final MongoTemplate mongoTemplate;
    
        public PostsServiceImpl(MongoTemplate mongoTemplate) {
            this.mongoTemplate = mongoTemplate;
        }
    
        /**
         * <pre>
         *     테스트 목적을 위한 게시물 저장 메서드
         * </pre>
         * @param posts
         * @param isTransactionsTest 예외 발생 시 롤백이 수행되는지 확인 하기 위한 변수
         */
        @Transactional(rollbackFor = Exception.class)
        public void savePosts(Posts posts, boolean isTransactionsTest) {
            this.mongoTemplate.insert(Objects.requireNonNull(posts), "posts");
            this.mongoTemplate.insert(Objects.requireNonNull(posts), "posts_transactions");
    
            if(isTransactionsTest) {
                throw new IllegalStateException("트랜잭션 테스트를 위한 임시 예외 발생");
            }
        }
    }

    두 번째 매개변수인 isTransactionsTest 를 보시면 해당 변수의 값이 true인 경우 임의의 예외를 발생 시킵니다. 예외가 발생되면 성공적으로 Rollback이 수행되어야 합니다.

    Test

    정상 Insert 케이스

    먼저 정상적으로 Insert가 되는지 확인해보겠습니다. 테스트 코드는 아래를 참고하시면 되겠습니다.

    @Test
    void mongodb_게시물_정상_케이스() {
        Posts posts = new Posts("success_id", "mongodb Insert!");
    
        this.postsServiceImpl.savePosts(posts, false);
    }

    테스트 메서드를 실행 시키면 데이터가 정상적으로 Insert 된 것을 확인 할 수 있습니다.

    rs:PRIMARY> db.posts.find()
    { "_id" : ObjectId("62da3b0b11f3b1463dfc85d6"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }
    rs:PRIMARY> db.posts_transactions.find()
    { "_id" : ObjectId("62da3b0b11f3b1463dfc85d7"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }

    Rollback 케이스

    그럼 이번엔 강제로 예외를 발생시키면 Rollback이 되는지 확인해보겠습니다. 테스트 코드는 아래를 참고하시면 되겠습니다.

    @Test
    void mongodb_게시물_임의_예외_발생_롤백_케이스() {
        Posts posts = new Posts("fail_id", "mongodb Rollback!");
    
        this.postsServiceImpl.savePosts(posts, true);
    }

    만약 Rollback이 수행 되지 않았으면 member_id 필드에 fail_id라는 값이 존재해야 합니다.

    rs:PRIMARY> db.posts.find()
    { "_id" : ObjectId("62da3b0b11f3b1463dfc85d6"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }
    rs:PRIMARY> db.posts_transactions.find()
    { "_id" : ObjectId("62da3b0b11f3b1463dfc85d7"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }

    조회를 해보면 fail_id 값이 없는걸로 봐서 정상적으로 Rollback이 수행 되었음 알 수 있습니다.

    그 외

    MongoDB v4.2에서 테스트를 수행하려고 하면 아래와 같은 에러가 발생합니다.

    Write operation error on server host.docker.internal:27017. Write error: WriteError{code=263, message='Cannot create namespace board.posts in multi-document transaction.', details={}}.; nested exception is com.mongodb.MongoWriteException: Write operation error on server host.docker.internal:27017. Write error: WriteError{code=263, message='Cannot create namespace board.posts in multi-document transaction.', details={}}.

    Transactions에서 컬렉션 생성이 v4.4에서 부터 지원하기 때문에 v4.4 이하 버전인 v4.2에서는 위와 같은 에러가 발생합니다. 그러므로 v4.2에서는 사용하려는 컬렉션을 필히 생성해두어야 합니다.

    마치며

    본 작성글의 예제 Git은 GitHub - sungwookkim/mongodb-multi-transactions에서 확인하실 수 있습니다.

    반응형

    'DB' 카테고리의 다른 글

    PG(Postgresql) 대용량 Insert  (0) 2023.07.03
    docker-compose으로 Postgresql Replication 구성  (0) 2022.01.17

    댓글

Designed by Tistory.