DI, 의존성 주입
주니어 개발자가 정말 많이 접하는 단어이며, 개념이 추상적이어서 머리속에 개념이 확립되지 않는 것. - 나무

실제로 개발자로 취업하기 전부터 DI에 대해 많이 찾아보았고, 이해하려 노력하였다.

검색을 하다보면 DI의 장점들이 적혀있다.

  1. 모듈간의 결합도를 낮춘다
  2. 유지보수가 용이하다
  3. 테스트에 용이하다

등등 많은 장점들이 나오고 "이런 장점이 있기 때문에 사용하는구나" 정도로 이해하였다.

 

그러다가 한 영상을 찾게 되었고, 영상을 통해 느낀점을 적어보려 한다.

 

의존성 주입 (Dependency Injection)

객체에게 필요한 의존성을 주입하는 행위

-> 어떤 객체가 필요한 것들을 전달하는 행위

(화살표는 내가 쉽게 이해하기 위해 바꾼 문장이다.)

 

전달한다 = 누군가에게 받는다 = 나에게는 없는 것을 받는다

결국, 필요한 것들을 내가 직접 생성하는 것이 아닌, 다른 누군가에게 전달받는다

 

Example

class Car {
	var handle: Handle?
}
// 1. Car Class 내부에서 직접 생성하는 경우

class Car {
	var handle: Handle? = Handle()
}
// or
class Car {
	var handle: Handle?
    
    init(){
    	self.handle = Handle()
    }
}

// 2. Car Class 내부에서 직접 생성하지 않고 전달받는 경우

class CarFactory {
	var car: Car = Car()
    
	func setHandle() {
		self.car.handle = Handle()
    }
}

이런 식으로 의존성을 주입한다. (끝)

그래서 의존성 주입에 대한 정의는 알겠는데, 그래서 이걸 써서 얻는 장점인

  1. 모듈간의 결합도를 낮춘다
  2. 유지보수가 용이하다
  3. 테스트에 용이하다

이것들은 어떻게 느낄 수 있는가?

 

 

의존성 낮추기

protocol HandleType {
	func honk() // 경적을 울리다
}

final class AHandle: HandleType {
	func honk() {
    	print("뿌뿌")
    }
}

final class CHandle: HandleType {
	func honk() {
    	print("뛰뛰")
    }
}

final class BHandle: HandleType {
	func honk() {
    	print("빵빵")
    }
}

이처럼 핸들의 종류는 수도 없이 많을 것이며, 서로 다른 기능/책임을 갖고 있을 수 있다.

 

하나의 Car 객체는 Handle이 필요하다.

class Car {
	var handle: HandleType?
}

이때, HandleType Protocol 타입을 프로퍼티로 선언하여 내부 handle 프로퍼티를 선언하였다면

handle은 A, B, C Handle 모두 사용이 가능하다.

 

이때, CarFactory에서 car.handle = AHandle() 처럼 프로퍼티 주입을 사용할 수 있는데

이렇게 됐을 때, Car 클래스는 AHandle에 대한 의존성을 갖지 않게 된다.

-> 다시 말해, Car 클래스는 Handle 이라는 protocol에 대한 소스코드 의존성이 생겼을 뿐

-> AHandle Class 에 대한 소스코드 의존성이 생기진 않는다.

-> 또한, 소스코드 의존성은 빌드시간에 영향을 미치기 때문에 

 

-> 사실 이런 protocol을 사용하는 건 DI에 항상 연관되어서 나오는데

-> DI만을 따지자면 위에서 말한 "어떤 객체가 필요한 것들을 전달하는 행위" 뿐이다
-> DI와 protocol을 합쳐서 의존성(필요한 것)을 주입하고 소스 코드 의존성을 줄이는 것을 DI라고 하는 것으로 이해했다.

 

DI = 필요한 것을 주입하는 행위 + Procotol을 이용한 소스코드 의존성 감소

 

CarFactory, AHandle, BHandle, CHandle이 각각 다른 모듈에 있다고 가정해보자.

각각의 핸들을 교체할 때마다 해당 Handle 파일이 있는 모듈을 import해야 할 것이며

이는 빌드시간에 그대로 영향을 미친다.

 

이를 Protocol을 사용하여 사용하는 모듈에서 의존성 주입을 통해 주입하게 되면

이때 발생하는 개념이 "IoC (Inversion of Control)" 즉, 제어의 역전이다.

 

// 1. DI X

// 비즈니스 로직 수행 모듈 A

import B
...
...
let service = ServiceImpl()
let info = service.getInfo()


// 구현체 모듈 B
class ServiceImpl {
	func getInfo() -> Info {
    	...
    }
}


// 2. DI

// 비즈니스 로직 수행 모듈 A
// import B -> 사라짐
....
.....
init(service: Service) {
	self.service = service
}

....

let info = service.getInfo()


protocol Service {
	func getInfo() -> Info
}



// 구현체 모듈 B

import A // -> 생김

class ServiceImpl: Service {
	func getInfo() -> Info {
    	return info
    }
}

예제를 통해서 보면 1번 예제에서는 모듈 A에서 모듈 B를 import하여서 사용하였고

2번 예제에서는 DI를 통해 전달받기 때문에, 모듈 A에서 모듈 B를 import하지 않고

모듈 B에서 모듈 A를 import하였다.

 

즉, 상위 모듈이 하위 모듈(구현체)을 import 해야 하는 의존방향에서 역전이 된 것이다.

그렇다고 코드 호출 방향이 달라지는 것은 아니다.

여전히 상위 모듈(비즈니스 로직 수행)에서 하위 모듈(구현체)의 기능을 호출한다.

 

 

IoC = 방향이 같던 "코드 호출방향"과 "소스코드 의존성" 에서 "소스코드 의존성"의 방향을 반대로 뒤집어
하위모듈에 대한 상위모듈의 의존성을 역전시킨 것

 

장점

  • 확장과 재사용성
    • 새 기능 개발 용이
    • 기존 기능 수정 수월
    • 모듈별 독립적인 재사용 가능
  • 유지보수성
    • 영향 범위 파악이 쉬움
    • 빌드 시간 단축
  • 테스트 용이성
    • 테스트 대역으로 치환 용이

단점

  • 코드가 복잡해져 이해하기 어려워짐
    • 호출들이 protocol로 연결되어 있어서 호출을 따라가기가 힘들어짐
    • 디버깅이 어려워짐

 

 

 

 

 

 

 

 

 

JSON Decoing with Codable

이전에 작성한 포스팅에서 간단한 JSON을 Codable과 JSONDecoder를 이용해 파싱하였다.

https://wlgusdn700.tistory.com/50?category=913317

 

[Swift] Codable-CodingKey 을 이용해 JSON 파싱하기

Codable이란? A type that can convert itself into and out of an external representation. 자신을 변환하거나 외부 표현(JSON 같은 데이터를 주고 받는 형식)으로 변환할 수 있는 타입이다. 또한 Codable은 De..

wlgusdn700.tistory.com

 

하지만, 프로젝트를 진행하다보면 서버에서 내려준 JSON(Key:Value) 중에 굳이 없어도 되는 값들이 존재한다.

"""
{
	id: 10,
    name: "tree",
    nation: "대한민국"
    ....
}
"""

해당 JSON을 불러오고 여기서 id와 name만 사용한다고 가정하자.

필요없는 Key&Value는 선언하지 않고 Decodable을 채택/준수하면 된다.

 

struct Person: Decodable{
	let id: Int
    let name: String
  
//property 명과 key가 같기 때문에 CodingKeys를 작성하지 않아도 된다.  
//    enum CodingKeys: String, CodingKey{
//    
//    }
}

struct Person2: Decodable{
	let identifier: Int
    let nickname: String
    
    enum CodingKeys: String, CodingKey{
    	case identifier = "id"
        case nickname = "name"
    }
}

 


Nested Decodable

"""
{
	"id": 11,
    "name": "tree",
    "nation": "대한민국",
    "company": {
    	"name": "네카라쿠",
        "industry": "IT",
        ....
    }
    ....
}
"""

위의 JSON이 있다고 가정하자.

만약 내가 필요한 Key&Value가 name, companyIndustry 2가지가 필요하다고 가정하자.

기존의 방식을 사용하면 Person이라는 struct안에 Company라는 struct를 하나 더 두어서 구현하는 방식이 있다.

(물론 해당 방식도 틀리다고 생각하지는 않는다.)

 

struct Person: Decodable{

    let name: String
	let company: Company
    
    struct Company: Decodable{
    	let industry: String
    }
    
}

이렇게 구현 시, industry라는 property에 접근하기 위해서는 

person.company.industry 로 접근해야 한다. 사실 2-depth밖에 되지 않기 때문에, 불편해 보이지는 않는다.

더 심한 예를 들어보자.

 

"""
{
	"httpDomain": "www......",
    "response": {
    	...
        "person":{
        	"id": 29,
            "name": "tree",
            "company": {
                "industry": "IT",
                .....
            }
        }
        .....
    }
}
"""

depth를 한단계 더 추가하였다. (실제 프로젝트에서는 더 깊고, 배열 등의 자료구조까지 들어간다면 훨씬 복잡해진다.)

(나는 name과 industry만 필요한데, 굳이 Response, Person, Company라는 struct를 만들어야하네...? 코드가 너무 길어지는데..?)

 


사실 response.person.company.industry 를 짧게 줄이는 방법은 여러가지가 있을 것이다.

 

1. computed Property

읽기 전용 computedProperty를 선언한다.

struct Response: Decodable{
	....
    var industry: String{
	person.company.industry
    }
    ....
}

 

2. Subcript

배열인 경우에는 서브스크립트를 사용해서 가져올 수 있다.

 

3. init(from decoder: Decoder)

struct Response: Decodable{
.....
	let name: String
    let industry: String

    enum CodingKeys: String, CodingKey{
    	case name
        case response
        case person
        case company
        case industry
    }

    init(from decoder: Decoder) throws{
    //가장 큰 {}가 있는 영역 -> {}를 컨테이너라고 생각하는 것이 이해하기 쉬웠다.
        let values = try decoder.container(keyedBy: CodingKeys.self)
        //response가 차지하고 있는 {}가 있는 영역
        let response = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .response)
        //response 영역 내에 있는 name을 파싱
        self.name = try response.decode(String.self, forKey: .name)
        //response 영역 내에 있는 person 영역
        let person = try response.nestedContainer(keyedBy: CodingKeys.self, forKey: .person)
        //person 영역 내에 있는 company 영역
        let company = try person.nestedContainer(keyedBy: CodingKeys.self, forKey: .company)
        //company 영역 내에 있는 industry를 파싱
        self.industry = try company.decode(String.self, forKey: .industry)
        
        //물론 한번에 작성해도 된다.
        
        self.industry = try response.nestedContainer(keyedBy: CodingKeys.self, forKey: .person)
        						.nestedContainer(keyedBy: CodingKeys.self, forKey: .company)
                                .decode(String.self, forKey: .industry)
    }
.....
}

이렇게 작성하게 되면 여러개의 struct를 굳이 만들지 않고도 industry를 최상단 struct에서 사용할 수 있다.

 

또한, String이나 Int 그리고 모든 Key&Value에서 struct에 저장할 때! 변경/조작하고 싶다면 init(from:)에서 조작도 가능하므로

더 폭넓게 사용할 수 있다.

 

Codable이란?


A type that can convert itself into and out of an external representation.

자신을 변환하거나 외부 표현(JSON 같은 데이터를 주고 받는 형식)으로 변환할 수 있는 타입이다.

 

또한

Codable은 Decodable과 Encodable을 동시에 채택한 타입이다.

 

Decodable : 자신을 외부표현(external representation)에서 디코딩 할 수 있는 타입

Encodable : 자신을 외부표현(external representation)으로 인코딩 할 수 있는 타입

 

따라서, Codable은 자신을 외부표현으로 디코딩 & 인코딩이 가능한 타입 이라고 생각하시면 될 것 같다

 

Codable은 프로토콜

Codable은 프로토콜이므로 Class, Struct, Enum 에서 모두 사용할 수 있습니다. (이전 Protocol 포스팅에 있음)

그리고 타입에서 Codable을 채택했다는 것은 해당 타입을 serialize / deserialize 할 수 있다는 것

 

struct SomeType{
	var name: String
    var id: Int
}

이라는 구조체가 있다고 가정하자.

해당 타입에 Codable을 채택해보자

struct SomeType: Codable {
	var name: String
    var id: Int
}

이제 SomeType이라는 구조체는 외부표현(JSON형식)으로 변환이 가능한 타입입니다.

 

Encode

JSON으로 만드는 방법

//인코더 생성
let encoder = JSONEncoder()

//인스턴스 생성
let someInstance = SomeType(name: "First", id: 1)

//인코딩해서 jsonData에 저장 
// 1. encode메소드의 리턴타입은 Data
// 2. encode메소드는 throws -> try? 사용 -> try? 사용으로 jsonData는 옵셔널
let jsonData = try? encoder.encode(someInstance)

//Data 형식을 보기 좋게 String으로 변환
if let jsonData = jsonData, let jsonString = String(data: jsonData, encoding: .utf8){
	print(jsonString) // {"name" : "First", "id" : 1}
}

 

주로 주고받을 데이터 형식을 약속할 때, JSON을 사용한다. 하지만 어느 곳을 가도 {"name":"First","id":1} 처럼 가로로 쭉 늘어놓은 데이터로 약속을 하지 않을 것이다. 보통 봐온 JSON형식은 아래와 같다

{
	"name" : "First",
    "id" : 1
}

위처럼 JSONString을 이쁘게 만들 수 있다

//위와 같이 이쁘게 만들어준다
encoder.outputFormatting = .prettyPrinted

//key값을 기준으로 오름차순으로 정렬한다
encoder.outputFormatting = .sortedKeys

//그렇다면 나는 key값을 기준으로 오름차순으로 정렬한 이쁜 데이터를 받고싶어!
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

 

Decode

JSON을 내가 만든 타입으로 변환하기

주로 서버에 있는 데이터(JSON)을 받아서 내가 저장하고 싶은 타입의 모델에 저장하는 작업을 자주하게 된다.

이때 사용할 수 있는 것이 Codable이다.

 

//디코더 생성
let decoder = JSONDecoder()

//위에서 만든, Encode된 JSON을 Data로 변환
var data = jsonString.data(using: .utf8)

//Data 형식을 내가 만든 모델인 SomeType의 인스턴스로 변환
if let data = data, let someInstance = try? decoder.decode(SomeType.self, from: data){
	print(someInstance.name) // First
    print(someInstance.id) // 1
}

 

 

CodingKey

음... 쉽게 예를 들어보자

struct SomeType: Codable{
	var name: String
    var id: Int
}

위와 같은 타입에 Codable을 채택했다.

근데 넘어오는 데이터 형식이

{
	"name" : "First",
    "identifier" : 1
}

이렇게 넘어온다고 하자

 

내가 생성한 모델의 id와 넘어오는 JSON의 identifier의 "Key"가 다르다 -> 이렇게 되면 

No Value associated with key id 라고 나온다.

 

해결 방법은 2가지가 있다.

  1. JSON형식에 맞춰 모델의 프로퍼티 명을 작성하는 방법
    1. 오류는 없겠지만, 가독성 측면에서 좋지 않다
  2. CodingKey 사용!

CodingKey 사용

struct SomeType: Codable{
	var name: String
    var id: Int
    
    enum CodingKeys: String, CodingKey{
    	case name
        case id = "identifier" // identifier라는 키는 이제부터 id프로퍼티가 담당할거야!
    }
}

 

 

지금까지 Swift에서 모델에서 JSON으로 인코딩, JSON에서 모델로 디코딩 방법과

key값이 다를 때 사용하는 CodingKey에 대해 알아보았다.

 

 

 

 

 

 

 

 

상속

- 클래스는 메서드나 프로퍼티 등을 다른 클래스로부터 상속받을 수 있습니다.

- Super/Sub Class로 구분된다

- Swift의 Struct는 상속을 받을 수 없다

- Java에서의 상속의 개념과 같다

 

메서드 재정의

//func aa() 라는 메소드를 재정의
override func aa(){
	//...
}

 

프로퍼티 재정의

- 부모로부터 상속받은 인스턴스 프로퍼티나 타입 프로퍼티를 재정의할 수 있다

- 프로퍼티를 재정의하는 것은 프로퍼티 자체가 아니라, Getter/Setter/감시자 등을 재정의 하는 것을 의미함

- 부모 클래스에서 읽기전용이었어도 자식 클래스에서는 R/W 로 재정의 가능

  - B.U.T. R/W -> 읽기전용은 불가능

 

프로퍼티 감시자 재정의

- willSet / didSet 도 재정의 가능 -> 연산/저장 프로퍼티 가능

  - B.U.T. 상수 저장 / 읽기 연산 전용 프로퍼티는 재정의할 수 없다

    -> 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티는 값을 설정할 수 없으므로!

 

서브스크립트도 재정의가 가능하다

재정의 방지

- 부모 클래스를 상속받는 자식클래스에서 몇몇 특성을 재정의할 수 없도록 제한하기 위함

- final 키워드를 사용 (final var, final func, final class func ....)

 

클래스의 이니셜라이저 - 상속과 재정의

- 값 타입의 init 은 구분할 필요가 없지만 class에서는 지정/편의 init으로 구분된다

 

지정/편의 Init

지정 init

- 클래스의 주요 이니셜라이저

- 필요에 따라 부모클래스의 init을 호출할 수 있으며, class의 모든 프로퍼티를 초기화해야하는 임무를 맡음

- 클래스당 하나 이상 정의됨 -> 지정하지 않으면 기본 지정 init 사용

- if) 부모 클래스의 지정 init이 자식 클래스의 지정 init 역할이 가능하다면, 자식 클래스에서 지정 init 새성하지 않아도 됨

 

편의 init

- 초기화를 손쉽게 도와주는 역할

- 자신의 내부에서 지정 init을 호출함

- 지정 init의 매개변수가 많아, 외부에서 일일이 전달하기 어렵거나, 특정 목적에 사용하기 위해 사용

 

지정/편의 init 관계(클래스의 초기화 위임)

  1. 자식의 지정 init은 부모의 지정 init을 반드시 호출해야 함
  2. 편의 init은 자신을 정의한 클래스의 다른 init(편의/지정)을 반드시 호출
  3. 편의 init은 궁극적으로 지정 init을 반드시 호출

2단계 초기화

- 스위프트 컴파일러는 2단계 초기화를 오류없이 처리하기 위해 4가지 안전확인을 실행한다

  1. 자식의 지정 init이 부모의 init을 호출하기 전에 자신의 프로퍼티 모두 초기화 확인
  2. 자식의 지정 init은 상속받은 프로퍼티에 값을 할당하기 전에 반드시 부모의 init을 호출
  3. 편의 init은 자신의 프로퍼티를 포함하여, 어떤 프로퍼티라도 값을 할당하기 전에 다른 init을 호출
  4. 초기화 1단계를 마치기 전까지는 init은 인스턴스 메서드를 호출 불가
    1. 또, 인스턴스 프로퍼티의 값을 읽을 수도 없음
    2. self 프로퍼티를 자신의 인스턴스를 나타내는 값으로 활용 불가

- 클래스의 인스턴스는 초기화 1단계를 마치기전까지는 유효하지 않다

- 1단계를 거쳤을 때 비로소 유효한 인스턴스가 됨

 

  • 1단계
    • 클래스가 지정/편의 init을 호출
    • 클래스의 새로운 인스턴스를 위한 메모리가 할당 -> 아직 초기화되지 않은 상태
    • 지정 init은 클래스에 정의된 모든 저장 프로퍼티에 값이 있는지 확인
    • 지정 init은 부모의 init이 같은 동작을 행할 수 있도록 초기화를 양도
    • 부모는 상속 체인을 따라 최상위 클래스에 도달할 때까지 이 작업을 반복
    • 최상위 클래스에 도달했을 때, 모든 저장 프로퍼티에 값이 있다고 확인하면 인스턴스의 메모리가 초기화
  • 2단계
    • 최상위 클래스로부터 최하위 클래스까지 상속 체인을 따라 내려오면서 지정 init들이 인스턴스를 제각각 정의
      • 이 단계에서는 self를 통해 프로퍼티 값을 수정 가능
      • 인스턴스 메서드를 호출 가능
    • 마지막으로 각각의 편의 init을 통해 self. 를 통한 사용자 정의 작업을 진행 가능

 

 

+ Recent posts