- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 명령어
- SWIFT
- scheduledTimer
- struct
- ReLU
- r
- Request
- Linux
- Observable
- Python
- 연산자
- Optional
- sigmoid
- ios
- cocoapods
- decode
- deeplearning
- 오블완
- swiftUI
- SQL
- substr
- tapply
- barplot
- HTTP
- rxswift
- 시각화
- MVC
- rest api
- 티스토리챌린지
- 딥러닝
iOS 개발 기록 블로그
Java, Spring 프레임워크 기초와 IoC, DI의 개념과 iOS 관련 예시로 이해하기 본문
Spring
Spring은 엔터프라이즈 수준의 애플리케이션을 구축할 수 있는 강력하고 유연한 프레임워크로, 특히 REST API를 구축하는 데에 많이 사용됩니다.
또한, iOS와의 연동이 잘 이루어질 수 있도록 HTTP/JSON 기반의 API 설계를 지원하므로 iOS 앱과 함께 사용하는 데에도 매우 유용합니다.
소개
Spring Framework
Spring Framework는 Java 기반의 오픈소스 애플리케이션 프레임워크입니다.
주로 웹 애플리케이션과 RESTful API 서버를 구축하는 데에 많이 사용됩니다.
Spring은 매우 유연하고 강력하며, IoC (Inversion of Control)와 DI (Dependency Injection)를 기반으로 구성 요소들을 관리합니다.
IoC (Inversion of Control)
IoC는 프로그램의 흐름 제어를 개발자가 아닌 프레임워크가 맡게 하는 설계 원칙을 의미합니다.
즉, 전통적인 방식에서는 개발자가 객체를 생성하고 관리하지만, IoC에서는 객체의 생성과 생명주기 관리, 의존성 주입 등을 프레임워크가 대신 수행합니다.
일반적으로, 객체가 서로 의존하는 방식은 아래와 같습니다.
- 전통적인 방식 (일반적인 제어 흐름)
- 개발자가 객체를 생성하고, 그 객체를 직접 사용하는 다른 객체에 전달하는 방식
- 예를 들어, 객체 A는 객체 B를 생성하여 사용하고, 객체 B는 객체 C를 생성하여 사용하는 방식
class A {
private B b;
public A() {
this.b = new B(); // 객체 A가 객체 B를 생성
}
}
class B {
private C c;
public B() {
this.c = new C(); // 객체 B가 객체 C를 생성
}
}
- IoC (제어의 역전)
- 객체 A가 객체 B를 생성하지 않고, 객체 B가 필요할 때 객체 B를 외부에서 주입
- 즉, 객체의 생성과 연결을 외부에서 제어하게 되므로 객체 A는 객체 B가 필요한지, 어떤 방식으로 생성될지 몰라도 됩니다.
- 이 역할을 하는 것이 IoC 컨테이너입니다.
class A {
private B b;
// 의존성 주입 (Dependency Injection)
public A(B b) {
this.b = b; // 객체 B는 외부에서 주입받음
}
}
IoC의 이점
- 유연성
- 객체의 생성과 관리를 프레임워크가 맡기 때문에, 객체 간의 결합도를 줄여 유연한 설계를 할 수 있습니다.
- 테스트 용이성
- 외부에서 의존성 주입을 받기 때문에, 실제 객체 대신 테스트용 객체(목 객체, 더미 객체 등)를 쉽게 주입할 수 있어 테스트가 용이합니다.
- 재사용성
- 객체를 직접 생성하지 않으므로, 특정 객체를 여러 곳에서 재사용하거나 교체하기가 용이합니다.
이것만 봐선 잘 이해가 가지 않아서 iOS의 예제를 같이 보겠습니다.
iOS 예제
전통적인 방식
class ApiClient {
func fetchData() {
print("Fetching data from server...")
// 네트워크 요청 코드
}
}
class ViewController: UIViewController {
var apiClient: ApiClient?
override func viewDidLoad() {
super.viewDidLoad()
// 직접 ApiClient를 생성하여 사용
apiClient = ApiClient()
apiClient?.fetchData()
}
}
IoC 적용
protocol NetworkClient {
func fetchData()
}
class ApiClient: NetworkClient {
func fetchData() {
print("Fetching data from server...")
// 네트워크 요청 코드
}
}
class MockApiClient: NetworkClient {
func fetchData() {
print("Fetching mock data...")
// 모의 데이터 응답
}
}
class ViewController: UIViewController {
var networkClient: NetworkClient
// 의존성 주입을 통해 외부에서 NetworkClient를 주입받음
init(networkClient: NetworkClient) {
self.networkClient = networkClient
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
networkClient.fetchData()
}
}
의존성 주입을 외부에서 처리
// 실제 ApiClient를 주입
let apiClient = ApiClient()
let viewController = ViewController(networkClient: apiClient)
// 테스트 시 MockApiClient를 주입
let mockApiClient = MockApiClient()
let testViewController = ViewController(networkClient: mockApiClient)
결론적으로, IoC와 DI를 통해 얻을 수 있는 이점은 아래와 같습니다.
- 객체 간의 결합도를 낮추고
- 코드를 보다 유연하고 재사용 가능한 방식으로 설계 가능
DI (Dependency Injection)
DI는 IoC의 구체적인 구현 방식 중 하나입니다.
객체가 필요한 의존성을 외부에서 주입해주는 방식입니다.
IoC는 제어권을 프레임워크에 위임하는 것이지만, DI는 객체가 다른 객체의 의존성을 외부에서 주입받도록 해주는 방법입니다.
DI (의존성 주입)의 종류
1. 생성자 주입 (Constructor Injection)
생성자 (Initializer) 를 통해 주입받는 방식
// NetworkClient 프로토콜 정의
protocol NetworkClient {
func fetchData()
}
// 실제 네트워크 요청을 처리하는 ApiClient 클래스
class ApiClient: NetworkClient {
func fetchData() {
print("Fetching data from server...")
}
}
// 의존성을 생성자에서 주입받는 ViewController
class ViewController: UIViewController {
var networkClient: NetworkClient
// 생성자에서 의존성 주입
init(networkClient: NetworkClient) {
self.networkClient = networkClient
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
networkClient.fetchData()
}
}
`ViewController`가 직접 객체를 생성하지 않고, 외부로부터 생성자를 통해 `NetworkClient`라는 객체를 주입받는 형태입니다.
이를 통해, `ViewController`는 `NetworkClient`가 무엇인지 알 필요도 없고, 외부에서 주입받기 때문에 결합도도 낮아집니다.
2. 세터 주입 (Setter Injection)
의존성 객체를 세터 메서드를 통해 주입받는 방식입니다.
class ViewController: UIViewController {
var networkClient: NetworkClient?
// 세터 메서드를 사용하여 의존성 주입
func setNetworkClient(networkClient: NetworkClient) {
self.networkClient = networkClient
}
override func viewDidLoad() {
super.viewDidLoad()
// 세터를 통해 의존성 주입 후 사용
networkClient?.fetchData()
}
}
// 사용하는 부분
let vc = ViewController() // ViewController 인스턴스 생성
let networkClient = ApiClient() // ApiClient 인스턴스 생성 (NetworkClient 프로토콜을 준수)
vc.setNetworkClient(networkClient: networkClient) // 세터 메서드를 통해 의존성 주입
let navigationController = UINavigationController(rootViewController: vc)
navigationController.pushViewController(vc, animated: true)
여기서는 ViewController 클래스가 세터 메서드를 통해 networkClient 객체를 주입받습니다.
객체 생성 후 나중에 의존성을 주입할 수 있기 때문에, 초기화 과정에서 의존성을 주입받기 어려운 경우에 유용합니다.
3. 필드 주입 (Field Injection)
필드 주입은 필드에 @Autowired 등의 어노테이션을 사용해 의존성을 주입하는 방식입니다.
Spring에서는 흔히 사용되는 방식입니다.
하지만 Swift에서는 기본적으로 이런 방식이 지원되지 않으므로, 주로 @IBOutlet을 사용할 때 비슷한 개념을 볼 수 있습니다.
class ViewController: UIViewController {
@IBOutlet var networkClient: NetworkClient?
override func viewDidLoad() {
super.viewDidLoad()
networkClient?.fetchData() // IBOutlet을 통해 연결된 객체 사용
}
}
위 방식은 자동으로 연결된 객체를 사용할 수 있게 해주기 때문에, 간편하지만 결합도가 다소 높을 수 있습니다.
그리고 Swift에서는 스토리보드를 사용할때 쓰이기 때문에 사용할 일이 없을 것입니다.
기본적으로 생성사 주입 방식을 사용하면 되고 생성 과정에서 주입하기 어려운 상황일 때만 세터 주입을 하는 것이 좋습니다.
DI 예시
DI를 사용하지 않은 전통적인 방식
class ApiClient {
func fetchData() {
print("Fetching data from the server...")
}
}
class ViewController: UIViewController {
var apiClient: ApiClient
override func viewDidLoad() {
super.viewDidLoad()
// ViewController가 ApiClient를 직접 생성
apiClient = ApiClient()
apiClient.fetchData()
}
}
이 방식에서는 ViewController가 ApiClient를 직접 생성하고 사용합니다.
ViewController가 ApiClient에 대한 의존성을 완전히 알고 있기 때문에, ApiClient가 바뀌면 ViewController도 함께 수정해야 합니다.
DI를 사용한 방식
이제 DI를 사용해서 의존성을 외부에서 주입받도록 변경해봅시다.
class ViewController: UIViewController {
var apiClient: ApiClient
// 의존성 주입
init(apiClient: ApiClient) {
self.apiClient = apiClient
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// 외부에서 주입받은 apiClient 사용
apiClient.fetchData()
}
}
이제 ViewController는 ApiClient 객체를 직접 생성하지 않고, 외부에서 주입받습니다.
이를 통해 ViewController는 ApiClient에 대해 알 필요가 없으며, ApiClient가 다른 종류의 객체로 교체되더라도 ViewController는 그대로 사용할 수 있습니다.
예를 들어, 테스트 시에는 MockApiClient를 주입하여 테스트할 수 있습니다.
외부에서 의존성 주입하기
// 실제 ApiClient 객체를 생성하여 주입
let apiClient = ApiClient()
let viewController = ViewController(apiClient: apiClient)
// 테스트 시에는 MockApiClient를 주입할 수 있음
let mockApiClient = MockApiClient()
let testViewController = ViewController(apiClient: mockApiClient)
이제 ViewController는 ApiClient가 무엇인지 모릅니다.
단지 NetworkClient 프로토콜을 준수하는 객체만 필요로 합니다.
실제로는 ApiClient나 MockApiClient가 주입되어 사용되지만, ViewController는 이 두 객체에 대해 알 필요가 없습니다.
'Back-end' 카테고리의 다른 글
REST API를 RxSwift+Moya 환경과 접목하여 알아보자 (0) | 2024.11.19 |
---|---|
REST API 이해하기 (iOS URLSession 사용 예시 포함) (0) | 2024.11.18 |
HTTP/1.1은 무엇일까? (0) | 2024.11.17 |
HTTP의 정의, 기본 작동 원리, 요청/응답 구조, HTTP/HTTPS (1) | 2024.11.17 |
오블완 챌린지 기념 iOS 개발자 서버 개발 찍먹 (5) | 2024.11.15 |