Flutter에서 앱 구조를 설계할 때, 특히 REST API나 소켓 통신을 다루게 되면
Service와 Repository를 어떻게 나누는 게 맞을까?
이 질문 하나만 명확히 정리돼도 실무 구조 설계가 훨씬 쉬워진다.
| 계층 | 역할 | 설명 |
|---|---|---|
| Model | 데이터 구조 | fromJson, toJson 포함 |
| Repository | API 통신 담당 | API 요청만 수행 (getUser(), postComment() 등) |
| Service | 비즈니스 로직 조합 | 여러 Repository를 묶거나 상태 흐름 관리 |
| ViewModel | 상태 관리 전담 | Service를 호출하고 상태 업데이트 |
| View | UI 구성 | ViewModel의 상태만 사용 |
[View] → [ViewModel] → [Service] → [Repository] → [서버 API]
[View] → [ViewModel] → [SocketService] → [Socket Server]
↑ 계속 연결 유지
lib/
├── models/
│ └── user.dart
├── repositories/
│ └── user_repository.dart # API 요청만 담당
├── services/
│ ├── auth_service.dart # 로그인/회원가입 로직
│ └── socket_service.dart # 소켓 연결, 메시지 송수신
├── viewmodels/
│ └── auth_view_model.dart # 상태 + 로딩 처리
└── main.dart
Repository: API 요청 전담
class UserRepository {
final Dio dio;
Future<Map<String, dynamic>> fetchUser(String id) async {
final res = await dio.get('/users/$id');
return res.data;
}
}
class AuthService {
final UserRepository userRepository;
AuthService(this.userRepository);
Future<User> login(String email, String pw) async {
final data = await userRepository.fetchUser(email);
return User.fromJson(data);
}
}
class AuthViewModel extends StateNotifier<AsyncValue<User?>> {
final AuthService authService;
AuthViewModel(this.authService) : super(const AsyncValue.data(null));
Future<void> login(String email, String pw) async {
state = const AsyncLoading();
try {
final user = await authService.login(email, pw);
state = AsyncData(user);
} catch (e, st) {
state = AsyncError(e, st);
}
}
}
✅ services/socket_service.dart로 통일
왜? 소켓은 요청-응답이 아닌 "지속적 연결 + 이벤트 통신"이기 때문
import 'package:socket_io_client/socket_io_client.dart' as IO;
class SocketService {
late IO.Socket socket;
void connect() {
socket = IO.io('https://server.com', {
'transports': ['websocket'],
});
socket.connect();
socket.on('message', (data) {
print('받은 메시지: $data');
});
}
void sendMessage(String msg) {
socket.emit('message', msg);
}
void disconnect() {
socket.disconnect();
}
}
✅ 가능하다!
특히 작은 앱, MVP, 단순 로직일 경우 Service에서 API 호출도 직접 해도 무방하다.
class AuthService {
Future<User> login(String email, String pw) async {
final res = await Dio().post('/login', data: {
'email': email,
'password': pw,
});
return User.fromJson(res.data);
}
}
| 상황 | 추천 구조 |
|---|---|
| MVP, 개인 앱 | ViewModel → Service 구조로 간단하게 |
| 유지보수, 협업 | ViewModel → Service → Repository 구조로 확장 |
| 실시간 소켓 통신 | SocketService 따로 관리 (service 계층에 둠) |
| 통신만 필요할 경우 | Repository 단독 사용도 가능 |