You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
코드에서 통상적으로 발견되는 문제는 상위 수준의 모듈이 하위 수준의 모듈에 의존성을 가지게 되는 경우이다
구체화된 객체에 대해 의존하면서 의존성이 잘못 주입되어 변경으로 인한 사이드 이펙트가 커지는 상황에 직면하게 된다
따라서, 모듈 간의 의존 관계를 끊고, 변경이 다른 코드에 영향을 최소화 시킬 수 있는 방법을 제시한다
코드는 어느정도는 의존 관계는 생기기 마련이다
하지만 정리하지 않은 코드는 경직성을 띄고, 커다란 사이드 이펙트를 가져온다
최악의 경우는 서로를 양방향으로 의존하는 경우이며
ex. 서로가 상대의 클래스 인스턴스를 프로퍼티로 가지는 경우, 한 쪽의 방향은 반드시 weak 참조 필요
더 나아가 세 클래스의 의존 사이클이 발생하는 경우도 발생하게 된다
이럴 경우 어느 한 부분도 분리하여 사용할 수 없고 하나의 불필요한 chunk 단위로 모두를 사용해야한다
따라서 이를 방지하기 위해 이론적으로는 의존 관계를 정리하여 단방향으로 흐를 수 있도록 개선해야 한다
하지만, 경우에 따라 클래스C가 클래스A, 클래스B와 함께 동작이 이루어져야 하는 동작이 필요하다면
직접적인 참조로 인한 의존성을 가지는 것이 아닌, DIP를 준수하여 이를 해소할 수 있다
의존관계를 잘 정리하여 유연성이 극대화된 시스템은 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템을 의미한다
의존한다는 것은 의존하는 대상이 변경될때 영향을 받으므로,
'추상'은 '구체'보다 변화가 적기때문에 '추상'에 의존함으로서 변경에 유연한 코드를 유지하도록 한다
ex. UITableView는 추상적이고, 이를 상속 및 준수하여 만든 개발자의 코드는 구체적이다
UITableView는 직접적으로 개발자의 CustomCell을 직접적으로 의존(참조)하지 않으며
개발자의 코드는 UITableView, UITableViewCell처럼 추상적인 class에 의존한다
따라서, 추상적인 부분에서 구체적인 부분에 동작을 위임하게 되고 - delegate, dataSource
UITableView 입장에선 delegate, dataSource, UITableViewCell만으로 동작을 하게 된다
직접적인 의존이 아닌 인터페이스에 의해서 의존을 하게 된다
ex. UIViewController와 CustomViewController
UIViewController는 뷰의 라이프사이클에 대한 정책을 결정하는 추상적인 클래스다
CustomViewController는 개발자가 커스텀으로 만든 구체적인 구현이다
CustomViewController는 UIViewController에 의존하지만
UIViewController는 CustomViewController에 의존하지 않고, 존재 자체도 모른다
DIP는 소프트웨어 모듈을 분리하는원칙으로
상위 계층(추상)이 하위 계층(구현)에 대한 의존관계를 역전시켜 상위 계층이 하위 계층의 구현으로 부터 독립되게 한다
상위 계층과 하위 계층 모두 추상에 의존하여 하며, 구현끼리 서로 의존하게 해서는 안된다
추상은 구현에 의존해서는 안되며, 구현이 추상에 의존해야 한다
두 모듈 간의 의존관계를 단반향으로 만들어주며, 추상화된 부분의 코드는 재활용성이 증가하게 된다
protocol, 상위 class는 추상적인 개념
상위 class에서 추상적인 로직은 하위 class에 의존하지 않고 존재를 모르는 상태로 작성되어야 한다
하위 class는 구체화를 위해서 메서드 override를 통해서 구현
Hollywood 원칙
"영화(추상)에 출연하고 싶으면 배우(구체)의 연락처를 주고 가시고 먼저 연락하지 마세요. 필요하면 저희가 연락드리겠습니다"
UITableView에서도 동일한 형태를 보임
개발자가 구현한 쪽(구현)에 delegate와 dataSource를 UITableView(추상)에다 등록해두면
필요할 때 콜백으로 함수가 불리는 동작을 직접 구현해서 사용
레이어의 구분
무엇이 추상이고 무엇이 구현인지 알아야 한다
추상적인 부분(정책)은 자주 변경되지 않는 부분, 해당 로직의 뼈대
구체적인 부분(세부)은 자주 변경 및 추가 되는 부분
의존성 이행은 스파게티를 만들게 된다
ex. A를 사용하는데 B를 써야하고 이에 따라 C도 딸려와서 사용해야 하는 경우
적합한 위치에서 DIP를 이용해서 의존성 이행을 막아줘야한다
인터페이스를 제공하는 쪽은 상위 레이어(추상화, 정책, 클라이언트)
인터페이스: 가장 변화가 적어야함
상위레이어: 변화가 비교적 적고, 재활용성이 높음
하위레이어: 자주 변경
인터페이스의 소유
한 클래스(서버)가 다른 클래스를 직접적으로 참조한다면 반대쪽 클래스(클라이언트)가 인터페이스를 제공하여 의존 관계를 역전시켜야 한다
UITableViewController(서버), UITableView(클라이언트): 구체화된 내용을 누가 제공을 하는가에 따라 달라짐
인터페이스의 변경은 클라이언트의 요구에 의해서만 발생
변경을 유발한 쪽에서 인터페이스를 가지고 있는 것이 유리
추상화에 의존하라
어떤 변수도 구체 클래스에 대한 참조를 가지지 말라
어떤 클래스도 구체 클래스에서 파생하지 말라
어떤 메소드도 기반 클래스에 구현된 메서드를 오버라이드 하지 말라
클래스의 휘발성에 따른 적용
비-휘발적인 클래스에 의존하는 건 큰 해가되지 않기에 강하게 위의 의존 법칙을 따른 필요가 없다
로직이 자주 변경되지 않는 추상화된 클래스는 비-휘발적인 클래스에 가깝다
ex. String, Data 등의 표준 라이브러리에서 제공하는 클래스의 경우
버튼과 램프 예제
버튼이 램프에 직접 의존
classSwitchButton{privatevarlamp:Lampprivatevaron:Bool=false{
didSet {print("스위치 버튼이 \(on ?"켜졌습니다":"꺼졌습니다")")}}publicinit(lamp:Lamp){self.lamp = lamp
}publicfunc isOn()->Bool{return on
}publicfunc toggleSwitch(){ifisOn(){
on =false
lamp.toggleLight()}else{
on =true
lamp.toggleLight()}}}classLamp{privatevarisLighted:Bool=false{
didSet {print("램프 불이 \(isLighted ?"켜졌습니다":"꺼졌습니다")")}}publicfunc toggleLight(){self.isLighted.toggle()}}letlamp=Lamp()letswitchButtom=SwitchButton(lamp: lamp)
switchButtom.toggleSwitch()
switchButtom.toggleSwitch()
// Prints "스위치 버튼이 켜졌습니다"
// Prints "램프 불이 켜졌습니다"
// Prints "스위치 버튼이 꺼졌습니다"
// Prints "램프 불이 꺼졌습니다"
스위치 버튼에서 Lamp 대신 SwitchButtonInterface를 참조하도록 DIP 적용
protocolSwitchButtonInterface{func toggleLight()}classSwitchButton{privatevarlamp:SwitchButtonInterface=Lamp()privatevaron:Bool=false{
didSet {print("스위치 버튼이 \(on ?"켜졌습니다":"꺼졌습니다")")}}publicfunc isOn()->Bool{return on
}publicfunc toggleSwitch(){ifisOn(){
on =false
lamp.toggleLight()}else{
on =true
lamp.toggleLight()}}}classLamp:SwitchButtonInterface{privatevarisLighted:Bool=false{
didSet {print("램프 불이 \(isLighted ?"켜졌습니다":"꺼졌습니다")")}}publicfunc toggleLight(){self.isLighted.toggle()}}letswitchButton=SwitchButton()
switchButton.toggleSwitch()
switchButton.toggleSwitch()
여전히 SwitchButton이 concrete class인 Lamp를 직접 생성하고 있다
의존성을 뒤집어 Interface를 참조하도록 하였지만, 아직 class dependency가 남아 있다
간단한 Factory Pattern을 적용하면 아래와 같이 Lamp와의 class dependency를 제거할 수 있다
Factory 패턴을 통한 분리
protocolSwitchButtonInterface{func toggleLight()}classSwitchButton{privatevarlamp:SwitchButtonInterface=Factory.getObject()privatevaron:Bool=false{
didSet {print("스위치 버튼이 \(on ?"켜졌습니다":"꺼졌습니다")")}}publicfunc isOn()->Bool{return on
}publicfunc toggleSwitch(){ifisOn(){
on =false
lamp.toggleLight()}else{
on =true
lamp.toggleLight()}}}classLamp:SwitchButtonInterface{privatevarisLighted:Bool=false{
didSet {print("램프 불이 \(isLighted ?"켜졌습니다":"꺼졌습니다")")}}publicfunc toggleLight(){self.isLighted.toggle()}}classFactory{staticfunc getObject()->SwitchButtonInterface{returnLamp()}}letswitchButton=SwitchButton()
switchButton.toggleSwitch()
switchButton.toggleSwitch()
Factory Pattern을 적용하여 SwitchButton의 Lamp class dependeny를 제거 하였다
Factory는 SwitchButton이 알지 못하게 SwitchButtonInterface를 구현한 어떤 concrete class를 반환한다
Factory Pattern을 적용한 SwitchButton을 보면 이제 Lamp 대신 Factory를 강하게 Dependency 하고 있다
이제 SwitchButton은 Factory와 한 몸이 되었다
다른 프로젝트에서 재사용 하려면 결국 Factory를 수정해서 적용해야한다
따라서 SwitchButton은 아직도 component가 되지 못한다
또한 decoupling을 이런식으로 적용하게 되면 프로젝트 내의 모든 클래스들이 각각 자신만의 factory를 갖게 될지도 모른다
DIP 이 후 남아있는 클래스 의존성 해결 + 의존성 주입을 통한 독립성 확보
classSwitchButton{privatevardevice:Switchableprivatevaron:Bool=false{
didSet {print("스위치 버튼이 \(on ?"켜졌습니다":"꺼졌습니다")")}}publicinit(device:Switchable){self.device = device
}publicfunc isOn()->Bool{return on
}publicfunc toggleSwitch(){ifisOn(){
on =false
device.toggleState()}else{
on =true
device.toggleState()}}}classLamp:Switchable{privatevarisActivated:Bool=false{
didSet {print("램프 불이 \(isActivated ?"켜졌습니다":"꺼졌습니다")")}}publicfunc toggleState(){self.isActivated.toggle()}}classFan:Switchable{privatevarisActivated:Bool=false{
didSet {print("환풍기가 \(isActivated ?"켜졌습니다":"꺼졌습니다")")}}publicfunc toggleState(){self.isActivated.toggle()}}letlamp=Lamp()letfan=Fan()letlampSwitchButton=SwitchButton(device: lamp)letfanSwitchButton=SwitchButton(device: fan)
lampSwitchButton.toggleSwitch()
lampSwitchButton.toggleSwitch()
Constructor Inecjtion을 적용한 SwitchButton이다
DI는 크게 Constructor Injection, Interface Injection, Method Injection으로 사용되지만,
좀 더 명확한 Constructor Injection을 선호한다
이제 SwitchButton은 Factory도 Lamp도 의존하지 않은 독립적인 존재가 되었다
어떤한 concrete에 대한 dependency가 없으니 외부로부터의 변경사항에 대한 영향도가 매우 적어졌다
따라서 SwitchButton은 어디에서도 수정 없이 곧바로 재사용 가능한 Component가 되었다
용광로 예제
용광로의 조절기를 제어하는 소프트웨어는 어떤 식으로 설계해야할까?
이 소프트웨어는 IO 채널에서 현재 온도를 읽어 다른 IO 채널에 명령어를 전송하는 방식으로 용광로를 제어하게 된다
swift 코드에서 in/out을 구현하기 제한적이기에 c언어 코드로 작성한다면 다음처럼 구현된다
// swift에서 변수 / 상수 선언과 동일#defineTHERMOMETER 0X8x
#defineFURNACE 0x87
#defineENGAGE 1
#defineDISENGAGE 0
// 기준이 되는 Temp 기반으로 지속적으로 순회하며 // input으로 받은 정보를 토대로, 다시 용광로 output에 전달 voidRegulate(doubleminTemp, doublemaxTemp){
for (;;) {
while(in(THERMOMETER) >minTemp) wait(1);
out(FURNACE, ENGAGE);
while(in(THERMOMETER) <maxTemp) wait(1);
out(FURNACE, DISENGAGE);
}
}
알고리즘의 상위 수준 목적은 분명하지만 구체적인 사항으로 너무 어지럽다
그렇기 때문에 이 코드는 다른 하드웨어를 제어하는 데에는 사용할 수 없다
물론 이 코드가 짧기 때문에 큰 손실이라고 볼 수는 없겠지만 그렇다고 하더라도 재사용의 여지를 남겨두지 않는 것은 아쉬운 일이다
좀 더 개선하여 아래와 같이 이 소프트웨어를 설계했다고 생각해보자
이렇게 하면 조절 함수가(Regulate) 2개의 파라미터를 받게되는데, 그 파라미터는 둘 다 인터페이스이다. Thermometer 인터페이스는 온도를 읽어들이고, Heater 인터페이스는 켜지거나 꺼진다
위와 같이 설계하면 상위 수준의 조절 정책이 온도 조절기나 용광로의 구체적인 사항에 의존하지 않도록 의존성을 역전시키게 되었다
알고리즘은 재사용 가능하게 된다
동적 다형성과 정적 다형성
추상 클래스나 인터페이스를 사용한 동적 다형성을 활용하여 의존성을 역전시켰고, regulate 함수를 일반적인 것으로 만들었다
하지만 다른 방법도 있는데, 스위프트의 템플릿이 제공하는 '제네릭'를 사용하여 다형성의 정적 형태를 구성할 수 있다 관련 포스팅을 보면 성능상의 이점과 최적화를 가능하게 한다
정의
코드에서 통상적으로 발견되는 문제는 상위 수준의 모듈이 하위 수준의 모듈에 의존성을 가지게 되는 경우이다
구체화된 객체에 대해 의존하면서 의존성이 잘못 주입되어 변경으로 인한 사이드 이펙트가 커지는 상황에 직면하게 된다
따라서, 모듈 간의 의존 관계를 끊고, 변경이 다른 코드에 영향을 최소화 시킬 수 있는 방법을 제시한다
코드는 어느정도는 의존 관계는 생기기 마련이다
하지만 정리하지 않은 코드는 경직성을 띄고, 커다란 사이드 이펙트를 가져온다
최악의 경우는 서로를 양방향으로 의존하는 경우이며
ex. 서로가 상대의 클래스 인스턴스를 프로퍼티로 가지는 경우, 한 쪽의 방향은 반드시 weak 참조 필요
더 나아가 세 클래스의 의존 사이클이 발생하는 경우도 발생하게 된다
이럴 경우 어느 한 부분도 분리하여 사용할 수 없고 하나의 불필요한 chunk 단위로 모두를 사용해야한다
따라서 이를 방지하기 위해 이론적으로는 의존 관계를 정리하여 단방향으로 흐를 수 있도록 개선해야 한다
하지만, 경우에 따라 클래스C가 클래스A, 클래스B와 함께 동작이 이루어져야 하는 동작이 필요하다면
직접적인 참조로 인한 의존성을 가지는 것이 아닌, DIP를 준수하여 이를 해소할 수 있다
의존관계를 잘 정리하여 유연성이 극대화된 시스템은 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템을 의미한다
의존한다는 것은 의존하는 대상이 변경될때 영향을 받으므로,
'추상'은 '구체'보다 변화가 적기때문에 '추상'에 의존함으로서 변경에 유연한 코드를 유지하도록 한다
ex. UITableView는 추상적이고, 이를 상속 및 준수하여 만든 개발자의 코드는 구체적이다
UITableView는 직접적으로 개발자의 CustomCell을 직접적으로 의존(참조)하지 않으며
개발자의 코드는 UITableView, UITableViewCell처럼 추상적인 class에 의존한다
따라서, 추상적인 부분에서 구체적인 부분에 동작을 위임하게 되고 - delegate, dataSource
UITableView 입장에선 delegate, dataSource, UITableViewCell만으로 동작을 하게 된다
직접적인 의존이 아닌 인터페이스에 의해서 의존을 하게 된다
ex. UIViewController와 CustomViewController
UIViewController는 뷰의 라이프사이클에 대한 정책을 결정하는 추상적인 클래스다
CustomViewController는 개발자가 커스텀으로 만든 구체적인 구현이다
CustomViewController는 UIViewController에 의존하지만
UIViewController는 CustomViewController에 의존하지 않고, 존재 자체도 모른다
DIP는 소프트웨어 모듈을 분리하는원칙으로
상위 계층(추상)이 하위 계층(구현)에 대한 의존관계를 역전시켜 상위 계층이 하위 계층의 구현으로 부터 독립되게 한다
상위 계층과 하위 계층 모두 추상에 의존하여 하며, 구현끼리 서로 의존하게 해서는 안된다
추상은 구현에 의존해서는 안되며, 구현이 추상에 의존해야 한다
두 모듈 간의 의존관계를 단반향으로 만들어주며, 추상화된 부분의 코드는 재활용성이 증가하게 된다
protocol, 상위 class는 추상적인 개념
상위 class에서 추상적인 로직은 하위 class에 의존하지 않고 존재를 모르는 상태로 작성되어야 한다
하위 class는 구체화를 위해서 메서드 override를 통해서 구현
Hollywood 원칙
"영화(추상)에 출연하고 싶으면 배우(구체)의 연락처를 주고 가시고 먼저 연락하지 마세요. 필요하면 저희가 연락드리겠습니다"
UITableView에서도 동일한 형태를 보임
개발자가 구현한 쪽(구현)에 delegate와 dataSource를 UITableView(추상)에다 등록해두면
필요할 때 콜백으로 함수가 불리는 동작을 직접 구현해서 사용
레이어의 구분
무엇이 추상이고 무엇이 구현인지 알아야 한다
의존성 이행은 스파게티를 만들게 된다
ex. A를 사용하는데 B를 써야하고 이에 따라 C도 딸려와서 사용해야 하는 경우
적합한 위치에서 DIP를 이용해서 의존성 이행을 막아줘야한다
인터페이스를 제공하는 쪽은 상위 레이어(추상화, 정책, 클라이언트)
인터페이스의 소유
한 클래스(서버)가 다른 클래스를 직접적으로 참조한다면 반대쪽 클래스(클라이언트)가 인터페이스를 제공하여 의존 관계를 역전시켜야 한다
UITableViewController(서버), UITableView(클라이언트): 구체화된 내용을 누가 제공을 하는가에 따라 달라짐
인터페이스의 변경은 클라이언트의 요구에 의해서만 발생
변경을 유발한 쪽에서 인터페이스를 가지고 있는 것이 유리
추상화에 의존하라
어떤 변수도 구체 클래스에 대한 참조를 가지지 말라
어떤 클래스도 구체 클래스에서 파생하지 말라
어떤 메소드도 기반 클래스에 구현된 메서드를 오버라이드 하지 말라
클래스의 휘발성에 따른 적용
비-휘발적인 클래스에 의존하는 건 큰 해가되지 않기에 강하게 위의 의존 법칙을 따른 필요가 없다
로직이 자주 변경되지 않는 추상화된 클래스는 비-휘발적인 클래스에 가깝다
ex. String, Data 등의 표준 라이브러리에서 제공하는 클래스의 경우
버튼과 램프 예제
버튼이 램프에 직접 의존
스위치 버튼에서 Lamp 대신 SwitchButtonInterface를 참조하도록 DIP 적용
여전히 SwitchButton이 concrete class인 Lamp를 직접 생성하고 있다
의존성을 뒤집어 Interface를 참조하도록 하였지만, 아직 class dependency가 남아 있다
간단한 Factory Pattern을 적용하면 아래와 같이 Lamp와의 class dependency를 제거할 수 있다
Factory 패턴을 통한 분리
Factory Pattern을 적용하여 SwitchButton의 Lamp class dependeny를 제거 하였다
Factory는 SwitchButton이 알지 못하게 SwitchButtonInterface를 구현한 어떤 concrete class를 반환한다
Factory Pattern을 적용한 SwitchButton을 보면 이제 Lamp 대신 Factory를 강하게 Dependency 하고 있다
이제 SwitchButton은 Factory와 한 몸이 되었다
다른 프로젝트에서 재사용 하려면 결국 Factory를 수정해서 적용해야한다
따라서 SwitchButton은 아직도 component가 되지 못한다
또한 decoupling을 이런식으로 적용하게 되면 프로젝트 내의 모든 클래스들이 각각 자신만의 factory를 갖게 될지도 모른다
DIP 이 후 남아있는 클래스 의존성 해결 + 의존성 주입을 통한 독립성 확보
Constructor Inecjtion을 적용한 SwitchButton이다
DI는 크게 Constructor Injection, Interface Injection, Method Injection으로 사용되지만,
좀 더 명확한 Constructor Injection을 선호한다
이제 SwitchButton은 Factory도 Lamp도 의존하지 않은 독립적인 존재가 되었다
어떤한 concrete에 대한 dependency가 없으니 외부로부터의 변경사항에 대한 영향도가 매우 적어졌다
따라서 SwitchButton은 어디에서도 수정 없이 곧바로 재사용 가능한 Component가 되었다
용광로 예제
용광로의 조절기를 제어하는 소프트웨어는 어떤 식으로 설계해야할까?
이 소프트웨어는 IO 채널에서 현재 온도를 읽어 다른 IO 채널에 명령어를 전송하는 방식으로 용광로를 제어하게 된다
swift 코드에서 in/out을 구현하기 제한적이기에 c언어 코드로 작성한다면 다음처럼 구현된다
알고리즘의 상위 수준 목적은 분명하지만 구체적인 사항으로 너무 어지럽다
그렇기 때문에 이 코드는 다른 하드웨어를 제어하는 데에는 사용할 수 없다
물론 이 코드가 짧기 때문에 큰 손실이라고 볼 수는 없겠지만 그렇다고 하더라도 재사용의 여지를 남겨두지 않는 것은 아쉬운 일이다
좀 더 개선하여 아래와 같이 이 소프트웨어를 설계했다고 생각해보자
이렇게 하면 조절 함수가(Regulate) 2개의 파라미터를 받게되는데, 그 파라미터는 둘 다 인터페이스이다. Thermometer 인터페이스는 온도를 읽어들이고, Heater 인터페이스는 켜지거나 꺼진다
위와 같이 설계하면 상위 수준의 조절 정책이 온도 조절기나 용광로의 구체적인 사항에 의존하지 않도록 의존성을 역전시키게 되었다
알고리즘은 재사용 가능하게 된다
동적 다형성과 정적 다형성
추상 클래스나 인터페이스를 사용한 동적 다형성을 활용하여 의존성을 역전시켰고, regulate 함수를 일반적인 것으로 만들었다
하지만 다른 방법도 있는데, 스위프트의 템플릿이 제공하는 '제네릭'를 사용하여 다형성의 정적 형태를 구성할 수 있다
관련 포스팅을 보면 성능상의 이점과 최적화를 가능하게 한다
정적 다형성은 소스 코드의 의존성을 깔끔하게 끊어주지만, 동적 다형성과 비교하여 다음의 문제는 해결해주지 못한다
런타임에서 Heater와 Thermometer의 타입이 변경될 수 없다
새로운 종류의 Heater와 Thermometer를 사용할 때 재-컴파일과 재-배포를 필요로 한다
그러므로 속도가 매우 절실하게 필요한 것이 아니라면, 동적 다형성이 더 나은 선택일 수 있다
결론
전통적은 절차 지향 프로그래밍은 정책이 구체적인 것에 의존하는 의존성 구조를 만든다
구체적인 사항의 변경이 정책의 변경을 불러 일으키므로 좋지 않다
객체 지향 프로그래밍은 이러한 의존성 구조를 역전시켜,
구체적인 사항과 정책이 모두 추상화에 의존하고, 클라이언트가 서비스 인터페이스를 소유하도록 한다
의존성 역전이 잘 되어 있으면 객체 지향 설계를 잘 했다고 말할 수 있다
프로그램의 의존성이 역전되어 있다면 객체 지향 설계를 한 것이며, 그렇지 않다면 절차 지향 설계를 한 것이다
재사용 가능한 프레임워크를 만들기 위해서 DIP를 응용할 줄 알아야 한다
또한 변경에 탄력적인 코드를 작성하기 위한 전략이 된다
추상화와 구체적인 사항이 서로 분리되어 있으므로 유지보수하기 쉬워진다
The text was updated successfully, but these errors were encountered: