본문 바로가기

DDD/도메인 주도 설계 철저 입문

레포지토리 구현 방법과 테스트 방법 - 「도메인 주도 설계 철저 입문」 5장 (2)

본 포스트 시리즈는 「도메인 주도 설계 철저 입문」책을 요약한 내용입니다.

 

 

이전 발행 글 보기

더보기

 

 

 


 

05장 데이터와 관계된 처리를 분리하자 - 리포지토리 (2)

 

(2) 리포지토리의 구현과 테스트

 

 

리포지토리의 구현

리포지토리의 인터페이스를 정의한 후에는 리포지토리를 구현해야 한다. 리포지토리를 구현한 코드는 다음과 같다. 

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserRepository implements IUserRepository {
    private String connectionString = "jdbc:your_database_connection_string"; // 연결 문자열 설정

    @Override
    public void save(User user) {
        String sql = """
            MERGE INTO users
            USING (
                SELECT ? AS id, ? AS name
            ) AS data
            ON users.id = data.id
            WHEN MATCHED THEN
                UPDATE SET name = data.name
            WHEN NOT MATCHED THEN
                INSERT (id, name)
                VALUES (data.id, data.name);
        """;

        try (Connection connection = DriverManager.getConnection(connectionString);
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, user.getId().getValue());
            statement.setString(2, user.getName().getValue());
            statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace(); // 예외 처리
        }
    }

    @Override
    public User find(UserName userName) {
        String sql = "SELECT * FROM users WHERE name = ?";

        try (Connection connection = DriverManager.getConnection(connectionString);
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, userName.getValue());

            try (ResultSet resultSet = statement.executeQuery()) {
                if (resultSet.next()) {
                    String id = resultSet.getString("id");
                    String name = resultSet.getString("name");

                    return new User(new UserId(id), new UserName(name));
                } else {
                    return null;
                }
            }
        } catch (SQLException e) {
            e.printStackTrace(); // 예외 처리
            return null;
        }
    }
}

 

 

테스트용 리포지토리

테스트를 위해서 테스트용 데이터베이스를 설치하고, 새로 개발된 테이블을 생성하고, 데이터를 채워넣는 일은 매우 번거롭다. 따라서 메모리를 데이터베이스로 삼는 딕셔너리 기반 리포지토리를 구현하는 것이 좋다.

import java.util.HashMap;
import java.util.Map;

public class InMemoryUserRepository implements IUserRepository {
    // 테스트 케이스 확인을 위해 외부에서 접근할 수 있게 public으로 둡니다.
    public Map<UserId, User> store = new HashMap<>();

    @Override
    public User find(UserName userName) {
        // 사용자 이름으로 대상을 찾습니다.
        for (User user : store.values()) {
            if (userName.equals(user.getName())) {
                // 찾은 사용자의 깊은 복사본을 반환합니다.
                return cloneUser(user);
            }
        }
        return null; // 사용자를 찾지 못하면 null을 반환합니다.
    }

    @Override
    public void save(User user) {
        // 저장할 때 깊은 복사를 수행합니다.
        store.put(user.getId(), cloneUser(user));
    }

    // 사용자 객체의 깊은 복사를 담당하는 메서드
    private User cloneUser(User user) {
        return new User(user.getId(), user.getName());
    }
}

 

 

위와 같은 인메모리 리포지토리를 가지고 테스트하는 코드는 다음과 같다. 

public class EntryPoint {
    public static void main(String[] args) {
        InMemoryUserRepository userRepository = new InMemoryUserRepository();
        Program program = new Program(userRepository);
        program.createUser("john");

        // 리포지토리에서 데이터를 꺼내 확인한다
        User head = userRepository.store.values().iterator().next();
        assert head.getName().getValue().equals("john") : "사용자 이름이 john이 아닙니다.";
    }
}

 

 

인메모리 리포지토리 테스트 의의

그런데 인메모리 리포지토리는 `IUserRepository` 인터페이스를 구현한 예시일 뿐이다. 인터페이스는 createUser()과 같은 동작이 어떻게 수행되어야 하는지에 대해서는 정의하고 있지 않다. 따라서 `InMemoryUserRepository`에 대한 테스트가 성공적이라고 해서 `IUserRepository`를 구현한 다른 구현체 (예: `DatabaseUserRepository`) 즉, 실제 데이터베이스와 소통하는 리포지토리까지 알맞게 동작한다는 것을 보장할 수는 없을 것이다.

 

그럼에도 불구하고 인메모리 리포지토리를 테스트하는 의의는 IUserRepository의 비즈니스 로직이 특정 구현체에서라도 잘 작동한다는 것이 확인되므로, 비즈니스 로직에 대한 신뢰도가 생긴다고 볼 수 있다. 즉, createUser()이 실제로 유저를 생성하고 있다는 것 정도는 확인할 수 있는 것이다.

 

 

ORM을 사용한 리포지토리

SQL을 직접 사용하는 대신 ORM을 사용하여 리포지토리 인터페이스를 정의하면 다음과 같다. 

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    // 사용자명이 중복되는지 확인하는 메서드
    boolean existsByName(String name);

    // 사용자명으로 유저를 찾는 메서드
    Optional<UserEntity> findByName(String name);
}

 

 

이 경우에 Spring Data JPA는 findBy~와 같은 구현체들을 미리 만들어놓았기 때문에 개발자가 직접 구현하지 않아도 된다. 

 

 

 


 

요약

 

1. 리포지토리의 인터페이스를 정의한 후에는 인터페이스를 구현해야 한다.

2. JPA와 같은 ORM 기술을 사용하면 기본 구현체가 있어 개발자가 직접 구현하지 않아도 되는 부분이 많다.

3. 테스트를 위한 데이터베이스 환경을 구축하는 대신 인메모리 리포지토리를 구현하는 것이 좋다.