본문 바로가기
Back-end/Spring Boot

[DDD] SpringBoot에서 Repository 추상화하기

by whatamigonnabe 2023. 8. 8.

DDD에 따라 Repository를 구현할 때 추상화해서 구현합니다. 언제든지 DB가 바뀔 가능성을 고려해서 더 유연하게 만들어야하기 때문입니다. 그런데 SpringBoot를 비롯한 프레임워크에서는 주로 기술을 사용하기 쉽도록 추상화된 인터페이스를 사용하고 여기엔 지켜야할 규칙이 있기 때문에 바로 적용하기 어렵습니다.

 

어려움

  • 도메인 엔티티와 기존 JpaRepository를 사용할 때 사용한 @Entity의 차이
    • @Entity 애노테이션을 비롯해서 여러 Jpa 기술에 종속적인 코드가 첨가된다.
    • 애그리거트 안에 여러 값객체를 가지고 있다.
  • ID 전략
    • JpaRepository를 사용하면 자동으로 ID를 부여하는데 이것 또한 기술 종속적이다.

 

위의 문제들을 해결하기 위한 방법을 알아보겠습니다.

 

도메인의 비즈니스 로직과 영속화 로직을 완전히 분리시킨다.

JpaRepository의 entity에는 @Entity @Column 등 여러 영속화에 대한 로직이 포함됐습니다. 그런데 이는 기술 종속적이기 때문에 분리시키는 작업이 필요합니다.

 

User Domain Entity

JPA 영속화를 위한 User Entity

그리고 레포지토리도 분리시킵니다. 

도메인 엔티티를 사용하는 추상화 레포지포티

JpaEntity를 사용하는 JpaRepository

추상화 레포지토리의 구현체

위처럼 엔티티와 레포지토리를 추상화시키고 분리시킴으로서 유연한 구조를 만들 수 있습니다.

 

식별자 전략

앞서 Jpa가 자동으로 식별자를 할당하는 것도 기술 종속적이라는 문제를 짚어봤습니다. 또한 부가적인 문제로 객체가 생성된 후 DB에 저장되기 전까지 식별자가 없는 상태로 있는 것을 객체를 불안정하게 만들기도합니다.

 

위의 문제를 해결하기 위해서는 식별자를 할당하는 기술을 분리해서 추상화하고, 객체가 생성되는 시점에 함께 할당하는 것 적절해보입니다.

 

식별자 생성 방법

식별자를 생성하는 방법은 크게 세 가지가 있습니다.

  • DB에서 생성하기
  • ID를 관리하는 DB로부터 ID를 얻어와 할당하기
  • DB 없이 유일 식별자를 생성하기

 

DB에서 생성하는 방법이 기존 Jpa의 @GenerateValue를 사용해서 provider의 기술을 사용하는 것입니다. 개발자 입장에서 간단하다는 장점이 있지만, 기술 종속적이고 객체 생성시점과 저장시점사이의 객체가 불완전합니다.

 

ID만을 관리하는 DB를 만들어서, 이곳에서 ID를 얻어와 할당할 수도 있습니다. 이 방법은 먼저 DB에 접속해야하기 때문에 속도가 느리지만, 엔티티를 저장할 때 기술종속성에서 벋어날 수 있고 구현하기 쉽습니다.

 

DB 없이 유일한 식별자를 만들 수 도 있습니다. 가장 대표적인 것이 UUID를 사용하는 것입니다. 중앙에서 관리하지 않아도되고 ID생성까지 시간이 빠르다는 장점이 있지만, UUID는 16바이트(일반적인 auto-increment의 두 배)로 공간상 단점이 있습니다. 또는 트위터에서 개발한 알고리즘인 snowflake를 사용하기도 합니다. 이는 8바이트로 이뤄져 있어서 사용되는 공간이 적고 DB에도 접근하지 않기 때문에 속도가 빠릅니다. 다만 단일 장애점이 생길 수 있고, snowflake가 시간 기반이기 때문에 여러 서버에서 ID를 생성할 시 충돌할 수도 있습니다.

 

저는 위의 방법 중 기술 종속적이지 않으면서 공간도 적게 사용하고 빠르게 ID를 생성하는 snowflake 방식을 선택했습니다.

 

 

Snowflake

snowflake는 timestamp 41 bit, Node Id 10bit, counter 12bit로 이뤄져있고, 만 앞 부호 1bit 까지 포함해서 64bit입니다. 

아래 페이지에서 자바로 구현한 코드를 인용했습니다.

https://www.callicoder.com/distributed-unique-id-sequence-number-generator/

import java.net.NetworkInterface;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Enumeration;

/**
 * Distributed Sequence Generator.
 * Inspired by Twitter snowflake: https://github.com/twitter/snowflake/tree/snowflake-2010
 *
 * This class should be used as a Singleton.
 * Make sure that you create and reuse a Single instance of SequenceGenerator per node in your distributed system cluster.
 */
public class SequenceGenerator {
    private static final int UNUSED_BITS = 1; // Sign bit, Unused (always set to 0)
    private static final int EPOCH_BITS = 41;
    private static final int NODE_ID_BITS = 10;
    private static final int SEQUENCE_BITS = 12;

    private static final int maxNodeId = (int)(Math.pow(2, NODE_ID_BITS) - 1);
    private static final int maxSequence = (int)(Math.pow(2, SEQUENCE_BITS) - 1);

    // Custom Epoch (January 1, 2015 Midnight UTC = 2015-01-01T00:00:00Z)
    private static final long CUSTOM_EPOCH = 1420070400000L;

    private final int nodeId;

    private volatile long lastTimestamp = -1L;
    private volatile long sequence = 0L;

    // Create SequenceGenerator with a nodeId
    public SequenceGenerator(int nodeId) {
        if(nodeId < 0 || nodeId > maxNodeId) {
            throw new IllegalArgumentException(String.format("NodeId must be between %d and %d", 0, maxNodeId));
        }
        this.nodeId = nodeId;
    }

    // Let SequenceGenerator generate a nodeId
    public SequenceGenerator() {
        this.nodeId = createNodeId();
    }

    public synchronized long nextId() {
        long currentTimestamp = timestamp();

        if(currentTimestamp < lastTimestamp) {
            throw new IllegalStateException("Invalid System Clock!");
        }

        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & maxSequence;
            if(sequence == 0) {
                // Sequence Exhausted, wait till next millisecond.
                currentTimestamp = waitNextMillis(currentTimestamp);
            }
        } else {
            // reset sequence to start with zero for the next millisecond
            sequence = 0;
        }

        lastTimestamp = currentTimestamp;

        long id = currentTimestamp << (NODE_ID_BITS + SEQUENCE_BITS);
        id |= (nodeId << SEQUENCE_BITS);
        id |= sequence;
        return id;
    }


    // Get current timestamp in milliseconds, adjust for the custom epoch.
    private static long timestamp() {
        return Instant.now().toEpochMilli() - CUSTOM_EPOCH;
    }

    // Block and wait till next millisecond
    private long waitNextMillis(long currentTimestamp) {
        while (currentTimestamp == lastTimestamp) {
            currentTimestamp = timestamp();
        }
        return currentTimestamp;
    }

    private int createNodeId() {
        int nodeId;
        try {
            StringBuilder sb = new StringBuilder();
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface networkInterface = networkInterfaces.nextElement();
                byte[] mac = networkInterface.getHardwareAddress();
                if (mac != null) {
                    for(int i = 0; i < mac.length; i++) {
                        sb.append(String.format("%02X", mac[i]));
                    }
                }
            }
            nodeId = sb.toString().hashCode();
        } catch (Exception ex) {
            nodeId = (new SecureRandom().nextInt());
        }
        nodeId = nodeId & maxNodeId;
        return nodeId;
    }
}

결론

영속화에 대한 코드를 분리하고 추상화함으로써 기술종속적이지 않고 변화에 유연한 구조를 만들어봤습니다. 

 

참조

https://wonit.tistory.com/653

도메인 주소 설계 철저 입문, 나루세 마사노부, 위키북스

https://www.callicoder.com/distributed-unique-id-sequence-number-generator/