SoC(Separation of Concerns)는 소프트웨어 시스템을 격리된 책임 영역으로 나누는 원칙의 약어입니다. Martin Fowler에 따르면, 관심사 분리는 유지보수 가능한 코드의 핵심 요소입니다. SoC 원칙은 개발자가 다른 계층에 영향을 주지 않고 애플리케이션의 한 계층을 변경할 수 있게 하며, 이는 팀 모바일 개발에서 특히 중요합니다.
핵심 내용
SoC는 Separation of Concerns의 약자로, “책임 분리” 또는 “관심 영역 분리”를 의미합니다. 개발 맥락에서 concern이라는 용어는 사용자 인터페이스 표시, 클릭 처리, 데이터 검증, 네트워크 통신 또는 데이터베이스 작업 등 분리 가능한 모든 기능을 나타냅니다. SoC 원칙은 한 영역의 변경이 다른 영역에 영향을 미치지 않도록 이러한 영역을 중심으로 코드를 그룹화하도록 지시합니다.
약어 SoC는 기술 문헌, 아키텍처 논의 및 프레임워크 문서에서 널리 사용됩니다. 예를 들어, Android Architecture Components 문서에서는 ViewModel과 View를 분리하는 동기로 SoC가 반복적으로 언급됩니다. iOS 커뮤니티에서는 Massive View Controller 문제를 논의할 때 이 용어가 사용되는데, 이는 SoC 부재의 직접적인 결과입니다.
SoC는 일회성 작업이 아니라 지속적인 과정임을 이해하는 것이 중요합니다. 애플리케이션이 성장함에 따라 새로운 책임 영역이 나타나고 아키텍처를 재검토해야 합니다. 좋은 코드베이스는 각 concern이 분리되고 관리 가능한 안정적인 상태에 도달하기 전까지 여러 번의 분리 반복을 거칩니다.
Separation of Concerns와 그 약어 SoC는 동일한 원칙을 나타냅니다. 차이점은 사용 컨텍스트뿐입니다. 전체 이름은 공식 문서, 교육 자료 및 새로운 개발자에게 개념을 처음 설명할 때 사용됩니다. SoC는 간결함이 중요한 기술 논의, 코드 리뷰 및 문서에서 편리합니다.
전문 환경에서 두 용어는 상호 교환 가능합니다. 개발자가 “여기서 SoC가 위반되었습니다” 또는 “이것은 Separation of Concerns를 위반합니다”라고 말할 수 있으며 의미는 변하지 않습니다. 그러나 채용 공고 및 아키텍처 요구 사항에서는 전체 이름이 더 자주 사용되는 반면, 채팅 및 코드 리뷰에서는 약어가 사용됩니다. 업계에 원활하게 진입하려면 두 변형을 모두 아는 것이 필요합니다.
용어상의 혼동이 있습니다. SoC 약어는 하드웨어 컨텍스트에서 System-on-a-Chip(칩 위의 시스템)을 위해서도 사용됩니다. 모바일 개발에서 컨텍스트는 항상 환경에서 명확합니다. 논의가 코드 아키텍처에 관한 것이라면 Separation of Concerns를 의미합니다. 이 기사에서 SoC는 항상 책임 분리 원칙을 나타냅니다.
3계층 아키텍처는 모바일 애플리케이션에서 SoC를 구현하는 가장 일반적인 방법입니다. 코드를 Presentation(UI), Domain(비즈니스 로직), Data(데이터 소스)로 나눕니다. 각 계층에는 엄격히 정의된 클래스 유형이 포함되며 인터페이스를 통해 이웃과 격리됩니다. 이 접근 방식은 iOS, Android 및 Flutter 프로젝트에 동일하게 효과적입니다.
View와 ViewModel은 프레젠테이션 계층을 구성합니다. View는 인터페이스 렌더링과 사용자 이벤트 전달을 담당합니다. ViewModel은 화면 상태를 보유하고 Domain 계층의 데이터를 표시 준비된 형식으로 변환합니다. ViewModel은 Activity, Fragment 또는 UIViewController에 대한 참조를 가지지 않으며, 이는 UI와 로직 간의 SoC를 보장합니다.
예를 들어, Android Jetpack에서 ViewModel은 화면 회전 후에도 유지되며 UI는 다시 생성됩니다. SoC가 없으면 Activity에 상태를 저장해야 하며, 생명 주기 관리와 데이터가 혼합됩니다. ViewModel은 이 문제를 격리된 방식으로 해결하여 책임 분리 원칙의 깔끔한 구현을 보여줍니다.
Use Cases에는 플랫폼 독립적인 비즈니스 규칙이 포함됩니다. 이 계층은 Android SDK, iOS UIKit 또는 Flutter framework를 임포트하지 않습니다. Use Case는 Repository에서 데이터를 받아 비즈니스 로직을 적용하고 결과를 반환합니다. SoC 덕분에 단일 Use Case를 여러 화면과 플랫폼에서 재사용할 수 있습니다.
전형적인 예는 등록 양식을 위한 ValidateAndSaveUseCase입니다. 이메일과 비밀번호의 유효성을 검사하고, UserRepository를 호출하여 저장하며, ValidationResult를 반환합니다. UI도 데이터베이스도 검증 규칙을 알지 못하며, 한 곳에 집중되어 있어 변경이 용이합니다.
Repository는 데이터 소스를 애플리케이션의 나머지 부분으로부터 추상화합니다. ViewModel은 데이터가 REST API, GraphQL, 로컬 데이터베이스 또는 캐시 중 어디에서 오는지 알지 못합니다. Repository는 사용할 소스를 결정하고 이 로직을 인터페이스 뒤에 숨깁니다. 이것이 데이터 검색과 데이터 소비 사이의 SoC입니다.
DataSource는 더 깊은 분리를 제공합니다. RemoteDataSource는 HTTP 요청만 담당하고, LocalDataSource는 Room, CoreData 또는 SharedPreferences 작업을 담당합니다. Repository는 캐싱 전략을 적용하여 이들을 결합합니다. 각 DataSource는 독립적으로 교체할 수 있으며, 이는 서버 또는 데이터베이스 간 마이그레이션 시 중요합니다.
이러한 다중 계층 DataSource 시스템은 인프라 수준에서 SoC를 구현합니다. 네트워크 통신, 로컬 저장소 및 캐싱은 각각 고유한 로직과 생명 주기를 가진 별도의 concerns입니다. HTTP 클라이언트를 교체할 때 RemoteDataSource만 변경되고 Repository 및 상위 계층은 영향을 받지 않아 책임 분리의 실용적 가치를 확인할 수 있습니다.
MVP(Model-View-Presenter)는 모바일 개발에서 명시적으로 SoC를 구현한 최초의 패턴 중 하나입니다. Presenter는 로직을 포함하고 인터페이스를 통해 View를 제어합니다. View는 수동적이며 Presenter가 지시하는 내용만 표시합니다. 분리는 테스트를 단순화합니다. Presenter는 에뮬레이터 없이 테스트되며 View는 너무 단순해서 고장날 것이 없습니다.
MVVM은 반응형 바인딩을 추가했습니다. View는 Observable 또는 StateFlow를 통해 ViewModel의 변경 사항을 구독합니다. ViewModel은 View에 대한 참조를 보유하지 않아 메모리 누수 위험을 제거하고 concerns를 더욱 분리합니다. Android에서는 Jetpack ViewModel과 LiveData 덕분에 MVVM이 표준이 되었고, iOS에서는 Combine과 RxSwift 덕분입니다.
Robert Martin의 Clean Architecture는 SoC를 링으로 급진적으로 분리합니다. 외부 링(프레임워크와 드라이버)은 내부(엔티티)에 의존하지만 그 반대는 아닙니다. 실제로 모바일 프로젝트가 4개의 링을 모두 구현하는 경우는 드물며, Presentation 주변의 Domain 및 Data 계층으로 충분합니다. 그러나 “내부로의 의존성” 원칙은 프레임워크를 변경할 때 상당한 이점을 제공합니다.
// View — 표시만, 로직 없음
final class LoginViewController: UIViewController {
let viewModel: LoginViewModel
func loginTapped() {
viewModel.login(emailField.text, passwordField.text)
}
}
// ViewModel — 화면 로직 포함, UIKit을 모름
final class LoginViewModel {
private let loginUseCase: LoginUseCase
func login(email: String?, password: String?) {
loginUseCase.execute(email, password)
}
}
// Use Case — 비즈니스 로직, 플랫폼 독립적
final class LoginUseCase {
private let repo: AuthRepository
func execute(email: String?, password: String?) {
guard let e = email, let p = password else { return }
repo.authenticate(e, p)
}
}
예제는 SoC의 세 가지 수준을 보여줍니다. LoginViewController는 이벤트만 전달하고, LoginViewModel은 상태를 관리하며, LoginUseCase는 비즈니스 규칙을 포함합니다. 각 클래스는 독립적으로 테스트되며 UI 프레임워크를 변경해도 Use Case에 영향을 미치지 않습니다.
Massive View Controller는 iOS에서 가장 흔한 SoC 위반입니다. UI를 관리하고, 네트워크 요청을 처리하고, JSON을 파싱하고, 데이터를 저장하는 클래스는 모든 수준에서 원칙을 위반합니다. 해결책은 각 책임을 별도의 컴포넌트(NetworkingService, JSONParser, CoreDataStack)로 추출하고 ViewController에는 View 관리만 남기는 것입니다.
Android에서 유사한 문제는 God Activity 또는 God Fragment입니다. 데이터를 로드하고, 양식을 검증하고, 대화 상자를 표시하고, UI를 업데이트하는 하나의 액티비티입니다. 이는 ViewModel과 Repository를 도입하여 상태 관리와 데이터 처리를 위임함으로써 해결됩니다. ViewModel은 화면 회전 시 데이터 손실도 방지합니다.
세 번째 위반은 플랫폼 코드와 비즈니스 코드의 혼합입니다. 예를 들어, SwiftUI View 또는 Android Composable에 직접 HTTP 요청을 배치하는 것입니다. 이는 코드를 이식 불가능하게 만들고 테스트를 어렵게 만듭니다. 올바른 접근 방식은 요청을 Repository로 이동하고 Use Case를 통해 호출하며 View는 결과만 구독하는 것입니다. 시스템의 각 요소는 자신의 작업을 해결하고 경계를 넘지 않습니다.
자주 묻는 질문
아닙니다. SoC는 시스템을 책임 영역으로 나누는 더 일반적인 원칙입니다. SOLID는 객체 지향 설계를 위한 다섯 가지 특정 규칙의 집합입니다. SOLID의 첫 번째 원칙(Single Responsibility)은 단일 클래스 수준에서의 SoC의 특수한 경우입니다.
단일 변경 이유 규칙(Single Responsibility)을 사용하세요. 클래스가 UI, 데이터 형식 및 비즈니스 규칙의 변경으로 인해 변경된다면 SoC가 위반된 것입니다. ArchTest(Android) 및 StrictConcurrency(iOS)와 같은 도구가 이러한 위반을 자동으로 감지하는 데 도움이 됩니다.
이론적으로 추가 계층은 간접 호출을 추가하지만, 실제로 모바일 애플리케이션 성능에 미치는 영향은 무시할 수 있습니다. 컴파일러는 많은 호출을 인라인화하고 JIT 및 AOT 최적화가 오버헤드를 제거합니다. 코드 유지보수성은 추상화로 인해 손실되는 것보다 훨씬 더 많은 이점을 제공합니다.
UI에서 네트워크 요청을 추출하여 Repository에 넣는 것부터 시작하세요. 그런 다음 비즈니스 로직을 Use Cases로 이동하세요. 의존성 주입을 사용하여 계층을 연결하세요. 변경을 반복적으로 수행하고 새 코드를 테스트로 커버하세요. 이렇게 하면 리팩토링이 기존 기능을 깨뜨리지 않음을 보장합니다.
프로토타입에서는 속도를 위해 SoC를 위반할 수 있습니다. 그러나 프로토타입이 프로덕션 개발로 전환되면 리팩토링 비용이 빠른 시작의 이점을 초과할 수 있습니다. 최적의 방법은 프로토타입에서도 최소한의 분리(UI와 데이터)를 유지하여 출시 시 모든 것을 처음부터 다시 작성하지 않도록 하는 것입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.