Altiora Petamus

in-memory DB 구현 본문

Java/Spring Framework

in-memory DB 구현

Haril Song 2021. 6. 30. 14:17

어떤 데이터베이스를 사용할지 정해지지 않은 상황에서 당장 Rest api 를 만들기 시작해야한다면 어떤 방법을 사용할 수 있을까요?

간단한 CRUD는 in-memory DB 를 구현하여 작성해놓으면 추후 DB가 정해졌을 경우 바로 교체하여 사용할 수 있습니다. 사실 테스트 용도로 자주 사용되는 H2 데이터베이스를 사용해도되지만, 직접 구현하며 흐름을 알아보도록 하겠습니다.

우선 lombok, Spring Web 정로만 선택하여 gradle 프로젝트를 생성해주겠습니다.

 

db package 아래에 MemoryDbRepository interface 를 만들어줍니다.

public interface MemoryDbRepositoryIfs<T> {

    // Create
    T save(T entity);

    // Read
    Optional<T> findById(Long index);

    // Delete
    void deleteById(Long index);

    List<T> findAll();

}

인터페이스에는 간단한 create, read, delete 메서드를 정의해주고, Generic 으로 작성하여 다양한 Entity 들을 사용할 수 있도록 합니다. update 가 없는 이유는 save 를 처리하는 과정에서 update가 이루어질 것이기 때문에 뒤에 따로 작성하도록 하겠습니다.

이를 구현할 Abstract Class 를 만들어주겠습니다.

public abstract class MemoryDbRepository<T> implements MemoryDbRepositoryIfs<T> {

    private final List<T> db = new ArrayList<>();

    @Override
    public T save(T entity) {
        return null;
    }

    @Override
    public Optional<T> findById(Long index) {
        return Optional.empty();
    }

    @Override
    public void deleteById(Long index) {

    }

    @Override
    public List<T> findAll() {
        return db;
    }
}

ArrayList 자료구조를 사용해서 DB처럼 동작할 수 있도록 구현합니다.

이제 모든 Entity 들의 공통적으로 가지고 있어야하는 index 를 멤버변수로 갖고 있는 MemoryDbEntity 를 작성해줍니다. 이 클래스는 상속을 활용하여 제네릭의 와일드카드를 제한해주는 용도로 사용합니다.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemoryDbEntity {

    private Long index;

}

prime key 를 흉내내는 index 를 MemoryDbEntity 에 만들어줍니다. 모든 Entity 들은 이 MemoryDbEntity 를 상속하게될 것 입니다.

public abstract class MemoryDbRepository<T extends MemoryDbEntity> implements
    MemoryDbRepositoryIfs<T> {

    private final List<T> db = new ArrayList<>();
    private Long index = 0L;

    @Override
    public T save(T entity) {
        var optionalEntity = db.stream()
            .filter(i -> i.getIndex().equals(entity.getIndex())).findFirst();

        // data 가 이미 존재하는 경우 = Update
        if (optionalEntity.isPresent()) {
            Long preIndex = optionalEntity.get().getIndex();
            entity.setIndex(preIndex);
            deleteById(preIndex);
        } else {
            index++;
            entity.setIndex(this.index);
        }
        db.add(entity);
        return entity;
    }

    @Override
    public Optional<T> findById(Long index) {
        return db.stream()
            .filter(i -> i.getIndex().equals(index)).findFirst();
    }

    @Override
    public void deleteById(Long index) {
        var optionalEntity = db.stream()
            .filter(i -> i.getIndex().equals(index)).findFirst();
        optionalEntity.ifPresent(db::remove);
    }

    @Override
    public List<T> findAll() {
        return db;
    }
}

<T extends MemoryDbEntity> 에 의해서 MemoryDbEntity 를 반드시 상속받아야만 이 추상클래스를 상속하는게 가능해집니다. 이로 인해서 람다식 안의 getIndex() 호출이 가능해집니다. iMemoryDbEntity 를 상속하고 있다는 보증이 있기 때문입니다.

이미 entity 가 db 내부에 존재하는 경우에는 update 를 위한 코드를 만들어줍니다. index 를 증가시키지 않고 다시 설정하여 값만 변경해주는 방식으로 구현했습니다.

이제 간단한 entity 를 생성하여 기능이 잘 동작하는지 테스트해보겠습니다.

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Book extends MemoryDbEntity {

    private String author;
    private int price;

}

작가와 가격정보 정도만 들고 있는 Book Class 를 만들고 MemoryDbEntity 를 상속합니다.

이제 BookRepository 를 작성해줍니다.

package com.example.memorydb.repository;

import com.example.memorydb.db.MemoryDbRepository;
import com.example.memorydb.domain.Book;
import org.springframework.stereotype.Repository;

@Repository
public class BookRepository extends MemoryDbRepository<Book> {

}

마치 JPA 를 사용하듯이 작성할 수 있습니다. Book class 가 MemoryDbEntity 를 상속했기 때문에 제네릭의 와일드카드로 사용이 가능한 것을 볼 수 있습니다.

Test 작성

이제 테스트를 작성하여 기능이 제대로 동작하는지 확인해보겠습니다.

하지만 CRUD 테스트의 경우, 각 테스트가 먼저 실행된 테스트의 영향을 받아서 잘못된 결과를 출력하게 될 수 있기 때문에 rollback 과정이 필요합니다. rollback 을 구현할 수 있도록 메서드를 하나 만들어주고 테스트 코드를 작성해줍니다.

package com.example.memorydb.db;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public abstract class MemoryDbRepository<T extends MemoryDbEntity> implements
    MemoryDbRepositoryIfs<T> {

    private final List<T> db = new ArrayList<>();
    private Long index = 0L;

    ...
    // rollback
    public void clear() {
        this.index = 0L;
        db.clear();
    }
}
package com.example.memorydb.repository;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.example.memorydb.domain.Book;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class BookRepositoryTest {

    @Autowired
    private BookRepository bookRepository;

    @AfterEach
    void rollback() {
        bookRepository.clear();
    }


    @DisplayName("1. Create")
    @Test
    void test_1() throws Exception {

        Book book = createBook();
        bookRepository.save(book);
        assertEquals(1, bookRepository.findAll().size());

    }

    @DisplayName("2. Read")
    @Test
    void test_2() throws Exception {
        Book book = createBook();
        bookRepository.save(book);
        Optional<Book> findBook = bookRepository.findById(book.getIndex());

        assertTrue(findBook.isPresent());
        assertEquals(1, book.getIndex());
        assertEquals(1, bookRepository.findAll().size());

    }

    @DisplayName("3. Update")
    @Test
    void test_3() throws Exception {
        Book book = createBook();
        Book expected = bookRepository.save(book);
        expected.setPrice(30000);

        Book updateBook = bookRepository.save(expected);

        assertEquals(1, bookRepository.findAll().size());
        assertEquals(30000, updateBook.getPrice());

    }

    @DisplayName("4. Delete")
    @Test
    void test_4() throws Exception {
        Book book = createBook();
        Book saveBook = bookRepository.save(book);

        bookRepository.deleteById(saveBook.getIndex());

        assertTrue(bookRepository.findAll().isEmpty());
        assertEquals(0, bookRepository.findAll().size());
    }


    private Book createBook() {
        Book book = new Book();
        book.setAuthor("haril");
        book.setPrice(17000);
        return book;
    }

}

 

테스트가 잘 작동하는 것을 확인할 수 있습니다. 이제 이 메모리 db 로 백엔드를 만들다가 서비스에 사용할 DB 가 정해졌을 때 바로 전환하여 사용하면 됩니다.

최종 프로젝트 구조

 

'Java > Spring Framework' 카테고리의 다른 글

다양한 HTTP Mapping 2  (0) 2021.07.31
다양한 HTTP Mapping 1  (0) 2021.07.28
Spring Security 사용하기 1  (0) 2021.06.26
Spring Security란?  (0) 2021.06.19
[Spring] AOP 활용하기  (0) 2021.05.20