안녕하세요 개발자 홍성호 입니다.
엘레강트 오브젝트 책을 얼마전 부터 조금씩 보고 있는데 흥미로운 주제를 만나게 되었습니다.
https://book.naver.com/bookdb/book_detail.nhn?bid=17651286
(이책입니다...! 쬐끔밖에 읽지 않았지만 토론 주제가 많아서 완전 꿀잼입니다ㅋㅋ)
'1.3 챕터 생성자에 코드를 넣지 마세요' 인데요.
생성자 내부에서 행위를 하지말고 메소드를 통해서 직접 실행할 때까지 행위를 미루라는 내용이었습니다.
간략하게 얘기해서 헷갈릴 수 있을텐데요.
마침 어제 제가 작성했던 코드가 딱!! 생각나서 개선해보겠습니다.
(어제 작성했는데 바로 레거시가 되네요ㅋㅋㅋ)
예시
https://cozzin.tistory.com/70 에서 빌더 패턴을 스위프트로 구현해보면서 PrintWriter라는 객체를 만들어 봤었습니다.
PrintWriter는 init에서 FileManager를 통해서 도큐먼트의 url을 추출하고 있습니다.
final class PrintWriter {
private let filename: String
private let fileURL: URL
private var contents: String
init?(filename: String) {
// 이 부분에 행위가 내포되어 있음
guard let fileURL = FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(filename) else {
return nil
}
self.filename = filename
self.fileURL = fileURL
self.contents = ""
}
}
객체를 생성할 때 URL을 추출하는 동작이 무조건 수행되고 클라이언트 입장에서는 그것을 제어할 수 없습니다.
객체에 작업을 수행하도록 메세지를 전달했을 때 로직이 수행되도록 변경하는 것이 좋습니다.
생성자가 하는 일이 적어야 유지보수하기 좋은 코드가 만들어집니다.
개선해보기
그러면 실제로 동작을 수행하는 부분에 해당 코드를 옮겨보도록 하겠습니다.
실제 fileURL이 필요한 곳은 텍스트 파일을 저장하는 메소드입니다.
final class PrintWriter {
enum Exception: Error {
case invalidfileURL
}
private let filename: String
private var contents: String
init(filename: String) {
self.filename = filename
self.contents = ""
}
func close() throws {
guard let fileURL = FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(filename) else {
throw Exception.invalidfileURL
}
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
}
}
그러면 객체 생성이 실패할 일이 없기 때문에 생성시에 nil이 반환되는 경우가 사라집니다.
기존에 PrintWriter를 사용하는 쪽에서는 nil인 경우를 대비해서 Optional로 처리하고 있었습니다.
왜 객체가 nil이 되었는지 명시적으로 알 수 없다는 단점도 있었습니다.
단점으로 의심되는 것
한계가 있다면 메소드가 호출될 때마다 fileURL을 새롭게 만들고 있다는 점 입니다.
생성자에서 한번만 만들면 되던 fileURL 이었는데, 이제는 메소드가 호출될 때마다 매번 fileURL을 만들어야 합니다.
성능이 더 떨어지는건 아닐까요?
놀랍게도... 아닙니다!!!!
사실 무조건 아닌건 아니고 해결책이 있습니다.
책에서는 이것을 데코레이터로 개선할 수 있다고 소개합니다.
데코레이터로 캐싱를 하면 클라이언트가 원하는 대로 행위를 변경할 수 있다는 장점이 있습니다.
데코레이터 적용해보기
PrintWriter에도 제 나름대로 캐싱을 적용해보겠습니다.
fileURL을 반환하는 메소드를 따로 분리하고
데코레이터로 적용할 수 있도록 프로토콜로 만들어보겠습니다.
protocol PrintWritable {
init(filename: String)
func close() throws
func findFileURL() -> URL?
// ...생략...
}
final class PrintWriter: PrintWritable {
func close() throws {
guard let fileURL = self.findFileURL() else {
throw Exception.invalidfileURL
}
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
}
func findFileURL() -> URL? {
return FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(filename)
}
}
캐싱하는 객체는 이렇게 만들 수 있습니다.
final class CachedPrintWriter: PrintWritable {
private let writer: PrintWriter
private var cachedFileURL: URL?
init(filename: String) {
self.writer = PrintWriter(filename: filename)
}
func findFileURL() -> URL? {
if cachedFileURL == nil {
cachedFileURL = writer.findFileURL()
}
return cachedFileURL
}
// ...생략...
}
이제는 fileURL을 캐싱해야하는 때면 CachedPrintWriter를 사용하고
캐싱이 필요없는 경우에는 PrintWriter를 선택해서 사용할 수 있습니다.
여기서 클라이언트가 동작을 예측 가능할 수 있다는 장점이 드러납니다.
PrintWirter 개선 결과물은 아래 레포에 있습니다.
https://github.com/cozzin/DesignPattern/tree/main/DesignPattern/Builder/PrintWriter
결론
혹시 저처럼 생성자에서 너무 많은 행위를 하도록 코드를 작성하지는 않았나요?
저는 그렇게 작성한 경우가 많았는데, 앞으로 작업하면서 생성자를 가볍게 만들도록 노력해야겠습니다.
물론 모든 경우에 적용될 수는 없겠지만 글을 읽어주신 분들에게도 많은 도움이 되었으면 좋겠네요!!
혹시 잘못된 부분이나 논의할 내용이 있다면 댓글로 남겨주세요.
읽어주셔서 감사합니다 :)
🤩 피드백 반영!!
멘토님이 피드백을 주셔서 내용 추가합니다.
생성자에 내용을 비우라는 것은 응집도와도 관련이 있었습니다.
이번 글에서 응집도 차원으로 고려를 못해봤었데, 피드백 반영해서 조금 더 개선해보겠습니다.
아래 코드에서 다시 출발해보겠습니다.
final class PrintWriter: PrintWritable {
enum Exception: Error {
case invalidfileURL
}
let filename: String
private var contents: String
init(filename: String) {
self.filename = filename
self.contents = ""
}
func println(_ text: String) {
contents.append(text + "\n")
}
func close() throws {
guard let fileURL = self.findFileURL() else {
throw Exception.invalidfileURL
}
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
}
func findFileURL() -> URL? {
return FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(filename) // 여기서 한번만 사용됨
}
func result() -> String {
return contents
}
}
응집도는 내부변수가 얼마나 많은 함수에서 사용되고 있는가로 평가할 수 있습니다.
filename은 fileFileURL에서 단 한 번만 사용되고 있고 (응집도 낮음)
contents는 나머지 메소드에서 자주 사용되고 있습니다. (응집도 높음)
filename이 응집도 관점으로 보면 잘못된 위치에 있다는 것을 알 수 있습니다.
filename은 실제로 사용되는 메소드의 인자로 전달하는 것이 가장 적합합니다.
내용을 반영해보면 이런 모습이 됩니다.
PrintWritable의 모습도 변경했다고 가정하겠습니다.
final class PrintWriter: PrintWritable {
enum Exception: Error {
case invalidfileURL
}
// filename 프로퍼티는 삭제됨
private var contents: String
init() {
self.contents = ""
}
func println(_ text: String) {
contents.append(text + "\n")
}
func close(filename: String) throws { // filename을 인자로 전달 받음
guard let fileURL = self.findFileURL(filename: filename) else {
throw Exception.invalidfileURL
}
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
}
func findFileURL(filename: String) -> URL? {
return FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?
.appendingPathComponent(filename)
}
func result() -> String {
return contents
}
}
'스터디' 카테고리의 다른 글
[디자인패턴] 전광판을 예시로 풀어본 Bridge 패턴 (0) | 2021.05.24 |
---|---|
[디자인패턴] Builder 패턴 (1) | 2021.05.18 |
[디자인패턴] Template Method 패턴 (1) | 2021.05.05 |
[디자인패턴] Iterator 패턴을 Swift로 구현해보기 (0) | 2021.05.03 |