티스토리 뷰

DI

DI (Dependency injection) 와 테스트 코드에 대한 설명은 서로 연관성이 많기 때문에 함께 해야 할것 같다.

안드로이드 개발자라면 DI에 대해서는 많이 들어 보았을 것이다. 안드로이드 진영에서는 Dagger가 거의 DI의 국룰처럼 사용되고 있기 때문에 한번쯤은 사용해 보았을거라고 생각 한다.

사실 DI라는 것은 특정 라이브리리 없이도 적용이 가능하다. DI가 라이브러리에 의존적인 기능이 아니라 개념 적인것이라는걸 먼저 강조하고 싶다.

 

특히 처음 앱을 개발하는 개발자들이 범하는 오류 중 이런 개념적인 요소들을 특정 라이브러리(Dagger)에 의존적인 기능이라고만 생각하기도 하는데 DI를 왜 사용해야 하는지를 먼저 알고서 사용하기 바란다.

개인적으로 앱 개발을 할때 DI가 필요한 가장 중요한 이유중 하나가 단위 테스트 가능한 코드 작성을 위해서라고 말하고 싶다.

 

테스트가 가능한 코드를 작성하려면 클래스간 결합도가 낮아야 하며, 의존성을 가진 다른 클래스의 구현 여부와는 상관없이 테스트 하고자 하는 클래스의 기능에 대해서만 테스트를 할수 있어야 한다.

 

예를 들면 PostRepository를 테스트 한다고 할때, 이 클래스가 가지는 고유한 기능과 로직에 대해서만 테스트를 해야지 이 클래스가 의존하고 있는 LocalDataSource까지 테스트해서는 안된다.

 

아래 코드와 같이 만약 DI를 하지 않고 PostRepository내부에서 직접 RemoteDataSource를 생성해 사용한다면 이것은 PostRepository에 대한 테스트가 아닌 RemoteDataSource에 대한 테스트까지 함께 하게 됨으로 올바른 테스트라고 할수 없다.

class PostRepository {
  final _remoteDataSrouce = RemoteDataSource();

  Future<List<Post>> getPosts() async {
     final posts = await _remoteDataSrouce.getPosts();
     // do something
  }
}

RemoteDataSource와 상관없이 오직 PostRepository만 테스트 하려면 어떻게 해야 할까?

PostRepository내부에서 RemoteDataSource를 생성하는 것이 아니라 외부에서 생성된 RemoteDataSource를 주입받는 형태로 만들면 된다.

class PostRepository {
   final RemoteDataSource _remoteDataSource;

   PostRepository(RemoteDataSource remoteDataSource): _remoteDataSource = remoteDataSource;

   Future<List<Post>> getPosts() async {
       final posts = _remoteDataSource.getPosts();
   }
}

위 에서 처럼 RemoteDataSource를 생성자를 통해 외부에서 주입시켜 주도록 하였다.
이렇게 구성할 경우 ViewModel에서는 PostRepository를 아래와 같이 생성해 사용할 수 있다.

   class PostListViewModel {
     final PostRepository _postRepository;

     PostListViewModel() {
         final remoteDataSource = RemoteDataSource();
         _postRepository = PostRepository(remoteDataSource);
     }      

   }

위 코드를 보면 ViewModel이 직접 Repository를 생성하고 있는데, 이것역시 repository를 외부에서 주입되도록 만들어야 하지만, 여기서는 Repository부분만 수정하는 것으로 하겠다.

이제 PostRepository가 의존하고 있는 remoteDatasource를 외부(ViewModel) 에서 부터 주입받게 되었다.

 

DI 라이브러리 - GetIt

위에서 별도의 DI라이브러리 없이 코드내에서 직접 의존성 객체를 생성 후 주입하는 예시를 보았다. 하지만 실제 프로젝트에서는 이렇게 사용하지 않는다 보통은 DI라이브리를 사용하게 되는데 Flutter에서 사용하는 DI라이브러리를 하나를 소개 하고자 한다.

https://pub.dev/packages/get_it 

 

 

get_it | Dart Package

Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App"

pub.dev

 

GetIt사용

이전 예제에서 사용했던 PostRepository 예제 코드를 getIt을 이용해서 변경한 것이다.

class PostRepository {
  final RemoteDataSource _remoteDataSource;
  final LocalDataSource _localDataSource;

  PostRepository({RemoteDataSource? remoteDataSource, 
                  LocalDataSource? localDataSource}) 
       : _remoteDataSource = remoteDataSource ?? getIt.get<RemoteDataSource>(),
         _localDataSource = localDataSource ?? getIt.get<LocalDataSource>();
 
  ......
}

위 코드의 생성자에서 remoteDataSource나 localDataSource 가 외부에서 생성자에 전달되지 않은 경우 getIt에서 RemoteDataSource와 LocalDataSource를 가져다 사용하고 있다.

 

이렇게 하게 되면 PostRepository를 생성해서 사용하는 모든 곳에서는 별도의 추가 코딩 없이도 getIt에 설정된 객체를 주입해 사용할 수 있음으로 가독성에 좋아진다. 뿐만 아니라 테스트 코드에서 원하는 객체만 다른것으로 변경해서 주입할 수 있음으로 테스트코드도 원활하게 작성할 수 있을 것이다.

 

여기서 주의 점은 함수내부 코드 중간에 getIt.get<> 으로 객체를 꺼내 쓰지 않고 꼭 생성자에서 필요한 것을 주입받아 사용할 수 있도록 해야 한다는 것이다. 이렇게 하지 않을 경우 어디에서 주입이 발생하는지 알수 없고 객체의 의존성을 확인하기 힘들어 나중에 읽기 힘든 코드가 되기 때문이다 (물론 꼭 필요한 경우가 있다면 예외적으로 사용 할 수는 있지만 원칙적으로 무조건 생성자에서만 사용하도록 하자).

GetIt설정

위 예시 처럼 getIt를 사용하려면 전역설정으로 getIt이라는 변수를 생성하고,  앱 초기화 시점에 getIt의 설정을 수행할 필요가 있다.

 

아래 코드는 싱글턴으로 만든 RemoteDataSource와 LocalDataSource를 설정하는 코드이다.

/// /main.dart
void main() {
  setupGetIt();
  ...
 runApp(MyApp());
}

/// /config/di.dart
final GetIt getIt = GetIt.instance;
void setupGetIt() {
  getIt.registerSingleton<RemoteDataSource>(RemoteDataSource());
  getIt.registerSingleton<LocalDataSource>(LocalDataSource());
}

위 예시 외에도 GetIt을 설정하고 활용하는 다양한 방법이 있음으로 상세한 사용은 GetIt 사이트를 참고 하시기 바랍니다.

테스트 코드

그렇다면 이런 형태가 테스트코드와 무슨 관련이 있을까?
테스트 코드 하나를 작성해 보자

# repository_test.dart
void main() {
 test('PostRepostory getPosts 테스트', () async {
    final remoteDataSource = MockRemoteDataSource();
    final postRepository = PostRepository(remoteDatasource);
    final posts = await postRepository.getPosts();
    expect(posts.length, 10);
  });
 }

위에서 가장 중요한 부분은 MockRemoteDataSource를 생성해 사용한 부분이다.

Repository에 대한 테스트에만 집중해야 하기 때문에 RemoteDatasource를 주입하지 않고 MockRemoteDataSource를 주입해서 사용하였다.

 

아래 코드는 RemoteDataSource와 테스트용 MockRemoteDataSource의 예시이다. 

abstract class RemoteDataSource {
 Future<List<Post>> getPosts();
}

class RemoteDataSourceImpl implements RemoteDataSource {
    Future<List<Post>> getPosts() async {
        const path = '/api/posts';
        const params = <String, String>{};
        const uri = Uri.https("flutter_sample.com", path, params);
        fiinal res = await http.get(uri);
        if (res.statusCode == HttpStatus.ok) {
            final data = _bytesToJson(res.bodyBytes) as List;
            return data.map((el) => Post.fromMap(el as Map)).toList();
        } else {
            throw Exception("Error on server");
        }
    }
}

class MockRemoteDataSource implements RemoteDataSource {
 @override
 Future<List<Post>> getPosts() {
    final posts = [];
    .....
    return posts;
  }
}

실제 프로젝트에서는 위에서 처럼 MockRemoteDataSource를 직접 만들기 보다는 
mockito와 같은 라이브리를 사용해서 mock오브젝트를 만들어 사용한다. 

 

지금까지 DI와 테스트코드에 대해서 알아 보았다.

MVVM의 각 레이어별 테스트 코드 작성과 mockito에 대해서는 나중에 좀더 자세하게  포스팅 하도록 하겠다.

 

 

6부 - Exception 처리 에서  계속

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함