클린 아키텍처의 구조
어제는 클린 아키텍처가 3가지 레이어로 나뉜다고 정리했다.
(Presentation, Domain, Data Layer)
여기에는 Entity, Use Case, Interface Adapters, Frameworks and Drivers와 같은 계층들이 아래와 같이 분류된다.
Presentation Layer
- UI와 같은 화면표시 및 사용자 입력 처리 계층
- ViewModel, Widget - UI 계층
- 컨트롤러 - UI, UseCase 간의 연결다리 역할. 사용자의 요청을 받아 UseCase를 실행하고 그 결과를 뷰모델에 전달.
Domain Layer
- 시스템의 비즈니스 로직을 담고 있는 핵심 계층
- Entity : 데이터 모델 클래스 (ex. User 등)
- UseCase : Entity로 해야할 일 (ex. 회원가입, 정보수정 등의 서비스)
서비스 계층에서는 데이터소스 계층에서 데이터를 가져와서 조작, 저장함으로써 무엇을 할것인지를 결정한다.
필요에 따라서는 데이터를 가공하여 로직 수행.
ex. 주문정보를 받아 검증 (재고 확인) 후 결제
ex. 주문정보를 데이터소스 계층에 전달
ex. 주문 완료 이벤트 (이메일 발송, 포인트 적립 등)
Data Layer
- 데이터소스 작업 처리 계층 (ex. DB, API 등)
- DTO(DataTransferObject) : 데이터 담는 형식
- Repository : 데이터 처리방식을 결정하여 Entity에게 전달
- DataSource : 네트워크 통신이나 내부 DB 연동 등으로 데이터를 가공 (CRUD)
데이터 가공이란, 데이터를 어떻게 가져올지를 보통 이야기한다.(액세스 로직 캡슐화)
ex. 주문정보 DB 에 저장
ex. 주문 ID 생성
ex. 저장된 주문정보 서비스 계층으로 반환
클린 아키텍처의 장점
- 유지보수가 용이하여 각 계층을 수정해도 서로 영향을 받지 않는다.
(MVVM 아키텍처로 작업하면서 모든 파일을 수정해야 했던 악몽을.. 기억하자..ㅎ) - 테스트 용이
- 단방향 의존성을 가지기 때문에 가짜 데이터로 테스트가 가능하다 (Mocking)
- 외부 의존성으로부터 독립적인 로직을 구성했기에 로직만을 테스트할 수 있다.
만약 의존하고 있는 부분이 있다면 아래와 같이 인터페이스를 두어 의존성을 단방향으로 수정한다.
계층별 사용법
DTO 사용법
DTO : DataTransferObject의 약자로, 데이터소스에서 엔티티에 의존하지 않게 하려고 구현.
데이터 운반체. 데이터를 주고받을 때 사용하는 공통 형식의 개념이다.
특정 계층에서 필요한 데이터만 담고 있다.
데이터 형식간 변환이 가능하다. (날짜형식, 숫자 형식 등을 계층에 맞게 변환하여 전달 가능)
- 데이터 캡슐화 : 사용자의 모든 정보가 담긴 DB에서 DTO를 통해 지금 필요한 정보(이메일, 자기소개 등)만 담아 전달.
도메인객체에는 DB에 저장된 모든 정보가 담겨있다고 치면,
DTO에는 화면에 표시할 정보만 담고있다고 보면 된다고 한다.
단순히 데이터만 담아야 하며 로직이 들어가면 안되며, 특정 계층에 종속되지 않도록 정의해야 한다.
- 서비스 계층에서는 도메인 객체를 이용해 비즈니스 로직을 처리한다.
- 컨트롤러에서는 DTO를 사용하여 클라이언트에게 응답한다.
Repository 사용법
1. 데이터 레이어에서의 레파지토리
- 역할 : 데이터 액세스 로직 캡슐화하는 구현체 (Impl 접미사를 붙여서 구현체임을 표시한다)
DB 등의 구체적인 데이터 소스에 접근하여 데이터를 가져오고 저장. - 구현 : SQL 쿼리, HTTP요청, 파일 I/O → 구체적인 데이터 액세스 기술로 구현됨
- 의존성 : 특정 데이터 소스에 의존 → DB, API 등에 직접 접근함
- 장점 : 데이터 액세스 로직 캡슐화로 코드 재사용성 향상
데이터 소스 변경에 대한 영향 최소화
class ProductRepositoryImpl implements ProductRepository {
final Database _database;
ProductRepositoryImpl(this._database);
@override
Future<List<Product>> getAllProducts() async {
final results = await _database.query('SELECT * FROM products');
return results.map((row) => Product.fromMap(row)).toList();
}
// ... 다른 데이터베이스 관련 메서드들 ...
}
2. 도메인 레이어에서의 레파지토리
- 역할 : 도메인 모델에서 필요로 하는 추상화된 데이터 액세스 인터페이스를 정의.
도메인 레이어는 이 인터페이스를 통해 데이터에 접근하므로 구체적인 데이터 소스에 대한 정보 알필요 없음 - 구현 : 데이터 레파지토리에 의해 구현됨.
(도메인 레이어 속 레파지토리 : 인터페이스 / 데이터 레이어 속 레파지토리 : 구현체) - 의존성 : 데이터 접근에 대한 추상화된 인터페이스에 의존함.
- 장점 : 도메인 레이어를 데이터 레이어로부터 분리함으로써 도메인 로직이 데이터 액세스 로직에 영향받지 않도록 함
abstract class ProductRepository {
Future<List<Product>> getAllProducts();
// ... 다른 데이터 접근 메서드들 ...
}
Repository 명칭에 대한 이해
레파지토리라는 말을 들었을땐 저장소 라는 느낌이 들어서 여기에 데이터를 저장하는 것인가? 하고 갸우뚱했었다.
그러나 이 역할에 대해 이해하려면 데이터를 컬렉션처럼 나누어 다루는 개념으로 생각해야 한다.
예를 들어 getAllProducts()라는 메서드는 객체 컬렉션에서 모든 상품을 가져오는 것처럼 보이지만
실제로 데이터가 어디에 어떻게 저장되어있는지 모르기 때문에
이런 도메인 레이어 속에 있는 레파지토리는 단순히 데이터 접근 인터페이스를 보여준다고 보면 된다.
추상화에 대한 개념 이해
개념을 요약하면 복잡한 시스템의 세부사항을 숨기고 핵심적인 부분만 드러내는 것을 말한다.
클린 아키텍처에서는 상위계층 (ex. 도메인 계층) 은 하위 계층(ex. 데이터 계층)의 구체적인 구현에 의존하지 않고
추상화된 인터페이스에 의존한다.
- 장점
- 유연성 증가 : 하위 계층의 구현이 변경되어도 상위계층은 영향을 받지 않는다.
- 테스트 용이성 향상 : 실제 DB연결 없이 로직 테스트 가능
- 복잡성 감소 : 지금 당장 집중해야 하는 핵심 로직
- 사용처
- 유스케이스 : 외부시스템(이메일 서버, 결제 시스템 등)과의 상호작용을 추상화 → 도메인 로직을 외부시스템 변경으로부터 보호
- 엔티티 : 도메인 모델을 표현하는 객체. 구체적인 데이터에 의존하지 않으면서 도메인 개념을 추상화함.
- 활용팁
- 인터페이스 적극 활용 : 구현세부사항을 숨기고 계층간 결합도를 낮춤
- 의존성 주입 : 구현체는 외부에서 주입받음으로써 유연성, 테스트 용이성 향상
- 추상 클래스 : 인터페이스와 유사하게 추상 클래스를 사용하여 공통기능 추상화 → 하위 클래스에서 구체적 구현 제공가능
// 추상화된 레파지토리 인터페이스
abstract class UserRepository {
Future<User> getUserById(String id);
Future<void> saveUser(User user);
}
// 데이터베이스를 사용하는 레파지토리 구현체
class DatabaseUserRepository implements UserRepository {
final Database _database;
DatabaseUserRepository(this._database);
@override
Future<User> getUserById(String id) async {
// ... 데이터베이스에서 사용자 정보를 가져오는 로직 ...
}
@override
Future<void> saveUser(User user) async {
// ... 데이터베이스에 사용자 정보를 저장하는 로직 ...
}
}
// 유스케이스
class GetUserUseCase {
final UserRepository _userRepository;
GetUserUseCase(this._userRepository);
Future<User> execute(String id) async {
return _userRepository.getUserById(id);
}
}
구현체란?
개념에 대해 살펴보면, 어떤 기능을 실제로 동작하게 만드는 구체적인 코드를 의미한다.
인터페이스는 설계도와 같은 역할을 하며, 어떤 기능을 제공할지는 정의하지만 실제로 어떻게 구연할지는 정의하지 않는다.
구현체는 인터페이스에 정의된 기능을 실제로 동작하게 만드는 구체적인 코드이다.
도메인 레이어 속 레파지토리 인터페이스에서는 사용자 정보를 가져오고 저장하는 기능을 정의하며
실제로 DB 연결을 통해 쿼리하는 코드는 데이터레이어 속에 있는 레파지토리 구현체에서 작성된다.
'내일배움캠프 (Flutter 5기) > Flutter' 카테고리의 다른 글
디버그용 / 릴리즈용 DB 분기하기 + get 명령어 사용법 (0) | 2025.02.18 |
---|---|
DataSource / Repository / Implement 에 대한 차이점 정리 (0) | 2025.01.24 |
Firebase API Key 보안설정 (.env) (0) | 2025.01.23 |
데이터 모델이란? (0) | 2025.01.22 |
클린 아키텍처 / MVVM 아키텍처 (0) | 2025.01.20 |