스터디 그룹원 분들과 이야기를 하는 도중에 나온 주제에 대한 고찰을 진행해보고자 한다.
문제 상황
우선 문제 상황은 아래와 같다.
추상화를 해놓고 다시 구체화? 이게 뭔가 이상하다.
프로토콜(혹은 상속으로)로 특정 객체가 프로토콜을 채택하게 하는 목적이 뭔가?
- 달리 해석하면, 추상화를 왜 하는 걸까?
- 아래의 원인이 가장 클 것 같다.
- 코드의 간결성
- 타입 안정성
- 확장성
- 아래의 원인이 가장 클 것 같다.
- 위와 같은 기대효과 때문에 우리는 객체 간 의존성이 생기는 경우에도 인터페이스를 통해→ 이를 달성하기 위해서 추상화를 통해 런타임 의존성을 가지도록 한다.
- “
개방 폐쇠 원칙
을 준수하도록 의존성 전이를 최소화” 하려고 노력한다.
그런데..!
막상 다른 곳에서 타입을 통한 분기처리 등, 여러가지 상황에서 우리는 타입캐스팅을 통해 해당 객체가 어떤 구체 타입을 가지는 지 확인을 하려고 한다.
이에 대한 궁금증이 생겼다.
(사실 이전까지 아무생각없이 코드를 짜던 나에게 이와 같은 고민은 신선했다..!)
Intro
오늘은 아래 2가지에 대해 알아보고자 한다.
- 추상화 → (타입 캐스팅을 통해) 다시 구체화
- 이게 맞나?
- 권장되는 형식이 있을까?
> 저의 생각이 많이 들어가있을 수도 있습니다.
> 또한 글의 흐름이 의식의 흐름대로 진행될 수도 있으며,,,
> 언제든 틀린 부분이나 궁금한 점은 환영입니다.
구글링을 통한 답변을 얻기 쉽지 않아, chatGPT 선생님의 답변도 가져와보았다.
프로토콜을 사용하여 객체를 추상화하는 것은 좋은 접근 방식입니다.
다른 곳에서 해당 객체를 구체적인 타입으로 다시 캐스팅하는 것은 몇 가지 trade-off가 있습니다
오 장점도 있고 단점도 있나보다..!
- 장점
- 구체적인 타입의 특정 기능을 사용할 수 있다.
- 타입에 따라 다른 로직을 적용할 수 있다.
- 단점
- 추상화의 이점을 상실
- 코드가 더 복잡해지고 유지보수가 어려워질 수 있다…
사실 단점에서 2번째 항목이 가장 큰 문제가 아닐까 싶다.
오늘 그룹회의 과정에서도 고수분에게 질문을 드렸다.
“왜 이런 사항들에 대해 생각을 해보게 되었나요?”
: “(남이 보기에) 불편하니까 생각해보게 된 것 같아요”
오... 인정입니다..!
복잡해지고 유지보수가 어려워 진다는 것은 확장성에도 문제가 생길 수 있다.
그럼 어떻게 해결할 수 있지?
검색해본 결과 해결방법은 아래와 같이 존재한다.
- 다형성 활용
- 전략 패턴 사용
- 방문자 패턴 활용
- 프로토콜 확장 사용
💠 전략(Strategy) 패턴 - 완벽 마스터하기
Strategy Pattern 전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다. 여기서 '전략'이란 일종의 알고리즘이 될 수
inpa.tistory.com
protocol Shape {
func commonBehavior()
}
extension Shape where Self: Rectangle {
func rectangleSpecificBehavior() { /* Rectangle 특화 동작 */ }
}
extension Shape where Self: Photo {
func photoSpecificBehavior() { /* Photo 특화 동작 */ }
}
(각각 뭔지 알아보면 포스팅을 못할 거 같다.)
어떻게 적용해볼 수 있을까?
우선 나의 코드를 보면서 예시 상황을 봐보자.
- 리팩토링 전이라 손을 볼 곳이 많지만 양해바랍니다 :)
Photo 클래스는 Shape 프로토콜을 채택한다.
Rectangle 클래스는 Shape 프로토콜을 채택하는 ShapeColorable 프로토콜을 채택한다. (backgroundColor 프로퍼티 추가)
protocol Shape: AnyObject {
var id: String { get }
var size: Size { get set }
var position: Point { get set }
var alpha: AlphaNumber { get set }
}
protocol ShapeColorable: Shape {
var backgroundColor: RGBColor { get set }
}
class Rectangle: ShapeColorable {
let id: String
var size: Size
var position: Point
var alpha: AlphaNumber
var backgroundColor: RGBColor
init(id: String, size: Size, position: Point, backgroundColor: RGBColor, alpha: AlphaNumber) {
self.id = id
self.size = size
self.position = position
self.backgroundColor = backgroundColor
self.alpha = alpha
}
}
class Photo: Shape {
static let fixedSize = CGSize(width: 150, height: 120)
let id: String
let imageData: Data // 바이너리 이미지 데이터로 관리
var size: Size
var position: Point
var alpha: AlphaNumber
init?(id: String, imageData: Data, size: Size, position: Point, alpha: AlphaNumber) {
self.id = id
self.imageData = imageData
self.size = size
self.position = position
self.alpha = alpha
}
func getUIImage() -> UIImage? {
guard let image = UIImage(data: imageData) else { return nil }
return image.resized(to: Photo.fixedSize)
}
}
ViewController에서 Rectangle, Photo를 새롭게 만들어주는 각각의 버튼 클릭시 createShapeView라는 메서드를 실행한다.
func createShapeView(from shape: Shape) -> UIView {
let frame = CGRect(x: shape.position.x, y: shape.position.y, width: shape.size.width, height: shape.size.height)
if let rectangle = shape as? Rectangle {
let view = UIView(frame: frame)
view.backgroundColor = UIColor(
red: CGFloat(rectangle.backgroundColor.red) / RGBConstants.value,
green: CGFloat(rectangle.backgroundColor.green) / RGBConstants.value,
blue: CGFloat(rectangle.backgroundColor.blue) / RGBConstants.value,
alpha: CGFloat(shape.alpha.wrappedValue) / AlphaConstants.value)
return view
} else if let photo = shape as? Photo {
let imageView = UIImageView(frame: frame)
imageView.image = photo.getUIImage()
imageView.alpha = CGFloat(shape.alpha.wrappedValue) / AlphaConstants.value
imageView.contentMode = .scaleAspectFit
return imageView
}
return UIView(frame: frame)
}
이는 사실 다형성을 이용하면 타입 캐스팅을 하는 문제가 해결이 된다.
protocol Shape: AnyObject {
var id: String { get }
var size: Size { get set }
var position: Point { get set }
var alpha: AlphaNumber { get set }
// 새로 추가된 메서드
func createView() -> UIView
}
protocol ShapeColorable: Shape {
var backgroundColor: RGBColor { get set }
}
class Rectangle: ShapeColorable {
let id: String
var size: Size
var position: Point
var alpha: AlphaNumber
var backgroundColor: RGBColor
init(id: String, size: Size, position: Point, backgroundColor: RGBColor, alpha: AlphaNumber) {
self.id = id
self.size = size
self.position = position
self.backgroundColor = backgroundColor
self.alpha = alpha
}
func createView() -> UIView {
let frame = CGRect(x: position.x, y: position.y, width: size.width, height: size.height)
let view = UIView(frame: frame)
view.backgroundColor = UIColor(
red: CGFloat(backgroundColor.red) / RGBConstants.value,
green: CGFloat(backgroundColor.green) / RGBConstants.value,
blue: CGFloat(backgroundColor.blue) / RGBConstants.value,
alpha: CGFloat(alpha.wrappedValue) / AlphaConstants.value)
return view
}
}
class Photo: Shape {
static let fixedSize = CGSize(width: 150, height: 120)
let id: String
let imageData: Data
var size: Size
var position: Point
var alpha: AlphaNumber
init?(id: String, imageData: Data, size: Size, position: Point, alpha: AlphaNumber) {
self.id = id
self.imageData = imageData
self.size = size
self.position = position
self.alpha = alpha
}
func getUIImage() -> UIImage? {
guard let image = UIImage(data: imageData) else { return nil }
return image.resized(to: Photo.fixedSize)
}
func createView() -> UIView {
let frame = CGRect(x: position.x, y: position.y, width: size.width, height: size.height)
let imageView = UIImageView(frame: frame)
imageView.image = getUIImage()
imageView.alpha = CGFloat(alpha.wrappedValue) / AlphaConstants.value
imageView.contentMode = .scaleAspectFit
return imageView
}
}
이와 같이 프로토콜에 메서드를 명시해준 뒤, 이를 채택하는 클래스에서 다형성을 지키게 하면..!
// 매우 간단해진, 타입 캐스팅이 필요없는 createShapeView 완성
func createShapeView(from shape: Shape) -> UIView {
return shape.createView()
}
이렇게 변경하는 것의 효과는 아래와 같다.
createShapeView
함수는 구체적인 Shape 타입을 알 필요가 없다.- 새로운 Shape 타입을 추가할 때
createShapeView
함수를 수정할 필요가 없다. - 책임 분리 → ViewController가 아닌 각 Shape을 채택하는 구체 타입에서 자신에게 맞는 View를 생성할 책임을 갖는다.
- 사실 위 Shape 프로토콜에서 where Self 를 이용해서도 해결할 수 있다.
- 이것도 타입 캐스팅아닌가?
- where: 컴파일 시간에 타입을 체크
- 컴파일 시간에 타입 안전성을 보장하는 정적인 방법
- as?: 런타임에 타입을 체크
- 런타임에 유연한 타입 체크를 제공하는 동적인 방법
- 컴파일 시간 안전성: 런타임 에러의 가능성을 줄인다.
- 성능: 런타임 타입 체크의 오버헤드를 제거할 수 있다.
- 코드 구조: 특정 타입에 대한 특화된 동작을 (명확하게) 정의할 수 있다.
protocol Shape: AnyObject { .... } protocol ShapeColorable: Shape { .... } class Rectangle: ShapeColorable { .... } class Photo: Shape { .... } extension Shape { func createView() -> UIView { let frame = CGRect(x: position.x, y: position.y, width: size.width, height: size.height) let view = UIView(frame: frame) view.alpha = CGFloat(alpha.wrappedValue) / AlphaConstants.value return view } } extension Shape where Self: ShapeColorable { func createView() -> UIView { let view = UIView(frame: CGRect(x: position.x, y: position.y, width: size.width, height: size.height)) view.alpha = CGFloat(alpha.wrappedValue) / AlphaConstants.value view.backgroundColor = UIColor( red: CGFloat(backgroundColor.red) / RGBConstants.value, green: CGFloat(backgroundColor.green) / RGBConstants.value, blue: CGFloat(backgroundColor.blue) / RGBConstants.value, alpha: 1.0 ) return view } } extension Shape where Self == Photo { func createView() -> UIView { let frame = CGRect(x: position.x, y: position.y, width: size.width, height: size.height) let imageView = UIImageView(frame: frame) imageView.image = getUIImage() imageView.alpha = CGFloat(alpha.wrappedValue) / AlphaConstants.value imageView.contentMode = .scaleAspectFit return imageView } }
- where: 컴파일 시간에 타입을 체크
이와 같이 추상화를 통한 설계를 유지하는 것이 코드 가독성, 책임 분리 등 여러가지 장점을 유발 할 수 있다.
하지만, 코드를 작성하다보면 어쩔 수 없이 타입캐스팅을 통해 추상화된 것을 구체 타입으로 변환하는 과정이 필요할 수도 있다.
이를 방지하는 것이 설계의 묘미아닐까..?
다양한 해결방법에 대해 하나하나 적용해보지 못해 아쉽지만..
당장에 내 코드를 다형성을 통해 캐스팅을 하지 않도록 수정할 수 있는 것만으로도 기쁘다..!
'iOS' 카테고리의 다른 글
MVVM, Binding (0) | 2024.10.30 |
---|---|
Clean Architecture 적용해보기 (2) | 2024.09.29 |
의존성, 의존성 주입?(2) (2) | 2024.09.09 |
의존성, 의존성 주입(1) ? (2) | 2024.09.09 |
테스트 코드? (3) | 2024.09.01 |