책 오브젝트를 기반으로 하는 코드스피츠 강의 오브젝트 - 4회차 를 정리한 내용입니다.
객체 지향 설계에 실패하는 이유: 디미터, 헐리우드 원칙 위반
객체는 자신 스스로 상태를 관리하기 때문에 묻게 되면 필연적으로 영향을 받는다.
SOLID 원칙 중 OCP, LSP 와 연관이 깊다.
OCP, LSP 원칙을 지킬려면 헐리우드 원칙을 사용할 수 밖에 없다.
리스코프 치환원칙
자신형을 부모형으로 안전하게 대체할 수 있다.
a, b, c 함수의 공통요소를 뽑아서 추상층으로 이동시킬 수 있다.
그러나 주의할 점은 성급한 추상화가 될 수 있다.
그 이유를 살펴보자!
c() 가 없는 Concreate3 이 추가되면서 우리는 fake c()를 추가하게 된다.
fake c() 를 추가하면서 리스코프 치환원칙에 위반이 된다.
fake c() 해결방법
추상층을 분리하므로 이 문제를 해결할 수 있다.
d의 문제
리스코프 치환원칙은 기존보다 스펙이 줄어드는 경우는 굉장히 쓸모가 있지만
늘어나는 경우는 고치는 방법은 쉽지 않다.
d() 가 생겨나면서 다운캐스팅을 하게된다.
d의 문제 해결하기 위해선 Generic 을 사용해야 된다.
Generic 으로 d의 문제를 해결해보자!
- Director 는 기획서(Paper)에 대한 사양서를 가지고 있다.
- 사양서는 ServerClient, Client 가 있다.
- Director 는 Paper 와 Programmer 를 연결한다.
- Programmer 는 FrontEnd, BackEnd 가 있다.
- Programmer는 Paper 를 알아야만 개발할 수 있기 때문에 Paper를 알고 있다.
LSP 위반하는 코딩!
전체소스는 여기
FrontEnd.makeProgram
Paper 는 language 와 library 에 정보를 가지고 있지 않기 때문에
정보를 받기 위해 Client 로 다운캐스팅을 하게 되지만 결국 LSP 와 OCP 를 위반한다.
더 위험한 것은
else 가 없으면 아무런 에러가 발생하지 않는다. (Context error 가 발생)
class FrontEnd: Programmer {
private var language: Language?
private var library: Library?
func makeProgram(paper: Paper) -> Program {
/*
Paper 는 language 와 library 에 정보를 가지고 있지 않기 때문에
정보를 받기 위해 Client 로 다운캐스팅을 하게 되지만 결국 LSP 와 OCP 를 어기게 된다.
*/
if paper is Client {
let pb = paper as! Client
self.language = pb.language
self.library = pb.library
}
// else 가 없으면 아무런 에러가 발생하지 않는다. (Context error 가 발생)
return makeFrontEndProgram()
}
private func makeFrontEndProgram() -> Program {
return Program()
}
}
Director.runProject
Director 는 FrontEnd에게 ServerClient 타입의 데이터를 전달하고 있다.
paper as! ServerClient
FrontEnd 에서는 Client 경우에 대해서만 처리하고 있다.
class FrontEnd: Programmer {
if paper is Client {
let pb = paper as! Client
self.language = pb.language
self.library = pb.library
}
}
이것은 위험한 Context error 를 발생시킨다.
FrontEnd 내부에서 다운 캐스팅을 하기 때문에
Test Case 를 100개를 작성해도 101번째 에러가 발생한다.
즉, 특정 상황에서만 발생하기 때문이다.
class Director {
func runProject(name: String) {
if !projects.containKey(key: name) {
fatalError("no project")
}
let paper = projects[name]
if paper is ServerClient {
let project = paper as! ServerClient
let frontEnd = FrontEnd()
let backEnd = BackEnd()
project.setFrontEndProgrammer(programmer: frontEnd)
project.setBackEndProgrammer(programmer: backEnd)
/*
paper as! ServerClient
frontEnd 에게 ServerClient Type 을 넘겨주고 있다.
이것은 Context error 를 발생 시킨다.
FrontEnd 내부에서 다운 캐스팅을 하기 때문에
Test Case 를 100개를 작성해도 101번째 에러가 발생한다.
즉, 특정 상황에서만 발생하기 때문이다.
class FrontEnd: Programmer {
if paper is Client {
let pb = paper as! Client
self.language = pb.language
self.library = pb.library
}
}
*/
let client = frontEnd.makeProgram(paper: project)
let server = backEnd.makeProgram(paper: project)
deploy(projectName: name, programs: [client, server])
} else if paper is Client {
let project = paper as! Client
let frontEnd = FrontEnd()
project.setProgrammer(programmer: frontEnd)
deploy(projectName: name, programs: [frontEnd.makeProgram(paper: project)])
}
}
}
FrontEnd 헐리우드 원칙 위반
이를 해결하기 위해서 다운캐스팅을 없애야 한다.
다운캐스팅을 없애기 위해선 헐리우드 원칙(묻지말고 말고 시켜라) 지켜야 한다.
Dry 원칙 위반
아래 코드는 FrontEnd, BackEnd 에서 둘다 중복 코드를 가지게 된다.
Programmer
이를 해결하기 위해선 protocol getProgram() 확장 메소드를 추가한다.
protocol Programmer {
// Programmer는 Director에게 Paper를 제공 받아 Program으로 모델링 하는 메소드가 필요하다.
func makeProgram(paper: Paper) -> Program
}
// 중복이 있었던 코드는 여기에서 일괄적으로 처리한다.
extension Programmer {
func getProgram(paper: Paper) -> Program {
paper.setData(programmer: self)
return makeProgram(paper: paper)
}
}
FrontEnd, BackEnd
makeProgram 안에서 추상화 메소드 getProgram 호출한다.
class FrontEnd: Programmer {
func makeProgram(paper: Paper) -> Program {
/*
Paper 는 language 와 library 에 정보를 가지고 있지 않기 때문에
정보를 받기 위해 Client 로 다운캐스팅을 하게 되지만 결국 LSP 와 OCP 를 어기게 된다.
*/
// if paper is Client {
// let pb = paper as! Client
// self.language = pb.language
// self.library = pb.library
// }
// else 가 없으면 아무런 에러가 발생하지 않는다. (Context error 가 발생)
// paper.setData(programmer: self)
// return makeFrontEndProgram()
return self.getProgram(paper: paper)
}
하지만 if 는 사라지지 않는다.
위에서 헐리우드 원칙, Dry원칙을 지키면서 if 를 구체층에서 없애는 데는 성공했지만
if 는 사라지지 않는다.
Client
class Client: Paper {
func setData(programmer: Programmer) {
if programmer is FrontEnd {
let frontend = programmer as! FrontEnd
frontend.setLibrary(library: library)
frontend.setLaunguage(language: language)
}
}
}
의존성을 역전으로 1:1 관계가 되어버린다.
ServerClient
class ServerClient: Paper {
func setData(programmer: Programmer) {
if programmer is FrontEnd {
let frontend = programmer as! FrontEnd
frontend.setLaunguage(language: frontendLanguage)
} else if programmer is BackEnd {
let backend = programmer as! BackEnd
backend.setLaunguage(language: backendLanguage)
backend.setServer(server: server)
}
}
}
serverClient 는 더욱 심각한 문제를 가지고 있다.
의존성을 역전으로 1:n 관계가 되어버린다.
모든 종류의 프로그래머에게 성립하도록 해야 하므로 책임이 더 많아졌다.
아래 그림은 if 의 제어가 어디로 흘러갔는지 표시하고 있다.
Generic 을 사용해보자!
Runtime time 의 instanceOf 를 제거하고 Compile time 에서 타입을 체크할 수 있도록 한다.
Generic을 사용하게되면 instanceOf 를 제거할 수 있고, 추상층 타입에서 구상층에 타입으로 사용할 수 있다.
즉, OCP, LSP 원칙을 지킬수 있게된다.
protocol Paper {
associatedtype T: Programmer
func setData(programmer: T)
}
class Client: Paper {
typealias T = FrontEnd
func setData(programmer: T) {
programmer.setLibrary(library: library)
programmer.setLaunguage(language: language)
}
}
ServerClient 는 1:N > Client 1:1
ServerClient 책임은 훨씬 무겁다.
Element 가 Collector 를 아는 편보다 Collector 가 Element 를 아는 것은 여러 원소를 알아야 하므로 불리하다고 설명한다 (???)
이말인 즉, Paper (ServerClient 이자 Element) 는 여러 Programmer (Colletor) 를 알아야 한다는 내용이다.
class ServerClient: Paper {
private var backendProgrammer: Programmer
private var frontendProgrammer: Programmer
}
아직도 이해가 안된단면 최초 UML 을 보자.
분명히 Programmer(Collector) 는 Paper(Element) 를 알고 있는 것이 유리하는 의미이다.
Paper는 다시 Marker Interface 가 된다.
protocol Paper {
// associatedtype T: Programmer
// func setData(programmer: T)
}
Paper에서 수행하면 setData 는 Programmer 로 옮겨가게 된다.
Programmer.setData 하는 이유는 헐리우드 원칙을 지키기 위함이다.
부모 자식간 통신할 경우에도 헐리우드 원칙을 지켜야 부모에 여파가 없다.
protocol HasProgrammer {
}
protocol Programmer: HasProgrammer {
associatedtype T: Paper
// Programmer는 Director에게 Paper를 제공 받아 Program으로 모델링 하는 메소드가 필요하다.
func setData(paper: T)
func makeProgram() -> Program
}
// 중복이 있었던 코드는 여기에서 일괄적으로 처리한다.
extension Programmer {
func getProgram(paper: T) -> Program {
// paper.setData(programmer: self)
/*
부모 자식간 통신할 경우에도 헐리우드 원칙을 지켜야
부모에 여파가 없다.
*/
setData(paper: paper)
return makeProgram()
}
}
Paper(Client) 는 어떻게 될까?
setData 의 책임은 사라지게 된다.
class Client: Paper {
var library: Library?
var language: Language?
var programmer: HasProgrammer?
func setProgrammer(programmer: HasProgrammer) {
self.programmer = programmer
}
// func setData(programmer: Programmer) {
// if programmer is FrontEnd {
// let frontend = programmer as! FrontEnd
// frontend.setLibrary(library: library)
// frontend.setLaunguage(language: language)
// }
// }
}
Paper(ServerClient) 는 어떻게 될까?
Paper(Client) 같이 setData에 대한 책임이 사라지지만
반면 다른 Paper(ServerClient) 는 (Paper) 1:N (Programmer) 관계이기 때문에 상당히 복잡하다.
클라이언트의 변화
FrontEnd, BackEnd 는 범용적인 project 인수를 받도록 설정되어 있다.
위에서 Generic 을 사용하여 특정 Paper 의 정보만 알도록 범위를 좁히는데 성공하였다.
FrontEnd<?>
BackEnd<?>
그러므로 Client 코드도 범용적인 FrontEnd, BackEnd 가 아닌 특정 범위의 Paper 만 알도록 하는 코드로 변경이 필요하다.
추상화 레벨의 가장 밑에 있는 곳(Director)까지 밀어내야만 모든 경우의 수가 Type 으로 해결된다.
FrontEnd, BackEnd 는 한 단계 더 추상화 계층으로 상승하게 된다.
// class FrontEnd: Programmer {
protocol FrontEnd: Programmer {
var language: Language? { get }
var library: Library? { get }
}
extension FrontEnd {
func makeProgram() -> Program {
return Program()
}
}
// class BackEnd: Programmer {
protocol BackEnd: Programmer {
var server: Server? { get }
var language: Language? { get }
}
extension BackEnd {
func makeProgram() -> Program {
return Program()
}
}
Director OCP 위반
Director 에서도 instanceof 분기를 사용해서 OCP 위반을 하고 있다.
if 경우의 수 만큼 Type을 생성하는 방법으로 해결해야 한다.
Director -> Paper 로 책임의 변화가 생긴다.
책임 역할 모델에서는 가장 적당한 객체(관련 정보를 가장 많이 알고있는 객체)가 그 역할을 가져간다.
Director에 있던 if instanceOf 분기를 상쇄시키기 위해선 계속 이야기하지만 그만큼 Type을 가져가야만 한다.
Director -> Paper 로 책임이 변경되면서 그 여파가 Paper 로 전달된다.
// class ServerClient: Paper {
protocol ServerClient: Paper {
var server: Server { get }
var backendLanguage: Language { get }
var frontendLanguage: Language { get }
var backendProgrammer: HasProgrammer? { get set }
var frontendProgrammer: HasProgrammer? { get set }
}
extension ServerClient {
mutating func setBackEndProgrammer(programmer: HasProgrammer) {
self.backendProgrammer = programmer
}
mutating func setFrontEndProgrammer(programmer: HasProgrammer) {
self.frontendProgrammer = programmer
}
}
// class Client: Paper {
protocol Client: Paper {
var library: Library? { get }
var language: Language? { get }
var programmer: HasProgrammer? { get set }
}
extension Client {
mutating func setProgrammer(programmer: HasProgrammer) {
self.programmer = programmer
}
}
마찬가지로 Client, ServerClient 는 구상층으로 부터 한 단계 추상층으로 상승하면서 if 가 사라지게 된다.
이제 Director -> Main 으로 if를 상쇄시키면서 생성하는 책임을 Main으로 밀어낼 수 있게 되었다.
Director
class Director {
func runProject(name: String) {
if let project = projects[name] {
deploy(projectName: name, programs: project.run())
} else {
fatalError("no project")
}
}
}
Main
let director = Director()
class FiananceClient: Client {
var library: Library?
var language: Language?
var programmer: HasProgrammer?
func run() -> [Program] {
class AFrontEnd: FrontEnd {
var language: Language?
var library: Library?
func setData(paper: FiananceClient) {
self.language = paper.language
self.library = paper.library
}
}
let frontEnd = AFrontEnd()
self.programmer = frontEnd
return [frontEnd.makeProgram()]
}
}
director.addProject(name: "투자", paper: FiananceClient())
director.runProject(name: "투자")
class FinancialLoanClient: ServerClient {
let server: Server = Server(name: "test")
let backendLanguage = Language(name: "java")
let frontendLanguage = Language(name: "kotlinJS")
var backendProgrammer: HasProgrammer?
var frontendProgrammer: HasProgrammer?
func run() -> [Program] {
class AFrontEnd: FrontEnd {
var language: Language?
var library: Library?
func setData(paper: FinancialLoanClient) {
self.language = paper.frontendLanguage
}
}
class ABackEnd: BackEnd {
var server: Server?
var language: Language?
var library: Library?
func setData(paper: FinancialLoanClient) {
self.language = paper.backendLanguage
self.server = paper.server
}
}
let frontend = AFrontEnd()
let backend = ABackEnd()
frontendProgrammer = frontend
backendProgrammer = backend
return [
frontend.getProgram(paper: self),
backend.getProgram(paper: self)
]
}
}
director.addProject(name: "여신", paper: FinancialLoanClient())
director.runProject(name: "여신")
마무리
전체소스는 여기에서 확인할 수 있다.
객체지향 설계 성공했다면 모든 if case 는 Main 까지 밀려야 성공이다.
그렇지 않다면 어디선가는 다운캐스팅을 하고 있을 것이다.
Main 까지 밀렸다면 다음에 할 것은 DI를 하는 것이다.
if case 를 Main 까지 밀어내야지만 DI가 작동한다.
if 를 없애기 위해 가장 안쪽 코드부터 main 까지 밀어내는 과정을 여러분들도 한번씩 해보면 정말 도움이 많이 되는 것 같습니다.
Java 코드를 Swift 로 변환하면서 abstract class 키워드를 사용할 수 없어서 많은 혼란이 있었습니다.
그러나 끝까지 예제를 완성하는데 성공하였습니다.
'객체지향설계' 카테고리의 다른 글
코드스피츠 1강 오브젝트 6회차 (1) | 2019.10.26 |
---|---|
코드스피츠 1강 오브젝트 5회차 (0) | 2019.10.25 |
코드스피츠 1강 오브젝트 3회차 (0) | 2019.10.22 |
코드스피츠 1강 오브젝트 2회차 (0) | 2019.10.21 |
코드스피츠 1강 오브젝트 1회차 (2) (0) | 2019.10.04 |