이전 포스트에서도 작성을 했지만, 개인 프로젝트에서 불필요한 최적화 등을 이유로 많은 고민을 하고 있었다.
그러다보니 Coding Principle에 대해서 다시 되돌아보는 시간을 가지게 되었는데,
이전에 읽었던 내용 중에서 정리를 한 번 해보면 좋을 것 같은 내용이 있어 블로그에 적어본다!
이름하여 AHA Programming Principle.
개발자들이 따르는 일종의 규칙이자 clean code의 기반이 되는 DRY.
같은 코드를 반복해서 작성하지 말자는 원칙이다. *혹은 그저 조건에 따라 변경이 되는 구조로 바꿀 수도 있다.
반복되는 코드를 줄이기 위해 함수화하거나 프로퍼티로 변하게 되는데, 오토레이아웃을 예시로 적어보았다.
A. Sample Code - without Dryness
let firstInfo = UIView()
let secondInfo = UIView()
let thirdInfo = UIView()
let fourthInfo = UIView()
private func layoutUI() {
view.addSubview(firstInfo)
view.addSubview(secondInfo)
view.addSubview(thirdInfo)
view.addSubview(fourthInfo)
NSLayoutConstraint.activate(
firstInfo.topAnchor.constraint(equalsTo: view.topAnchor),
firstInfo.leadingAnchor.constraint(equalsTo: view.leadingAnchor, constant: 20),
firstInfo.trailingAnchor.constraint(equalsTo: view.trailingAnchor, constant: -20),
firstInfo.heightAnchor.constraint(equalsToConstant: 80),
...
])
}
위 예시를 본다면 4가지 뷰의 constraint를 일일히 적용해야 한다.
first ~ fourthInfo까지 제약을 주어야하는 만큼 코드 길이 증가, 겹치는 코드가 분명 발생한다.
이 때 나름의 Dry를 생각해본다면, 조건을 돌려서 변경을 할 수 있겠다.
B. Sample Code - with Dryness
let infoView = [UIView]()
private func layoutUI() {
infoViews = [firstInfo, secondInfo, thirdInfo, fourthInfo]
for infoView in infoViews {
view.addSubview(infoView)
infoView.translatesAutoresizingMaskIntoConstraints = false
* 겹치는 부분들을 여기서 잡아줄 수 있지 않을까!
NSLayoutConstraint.activate([
infoView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
infoView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
infoView.heightAnchor.constraint(equalToConstant: 80)
])
}
NSLayoutConstraint.activate([
firstInfo.topAnchor.constraint(equalTo: view.topAnchor, constant: 250),
secondInfo.topAnchor.constraint(equalTo: firstInfo.bottomAnchor, constant: 10),
thirdInfo.topAnchor.constraint(equalTo: secondInfo.bottomAnchor, constant: 10),
fourthInfo.topAnchor.constraint(equalTo: thirdInfo.bottomAnchor, constant: 10)
])
}
개인적으로 정말 간단하게 설명한다면 이런 형태의 코드로 Dryness를 설명할 수 있겠다.
물론 코드를 짜는 스타일과 선호도에 따라 위 예시가 이상할수도 있겠다!
중요 포인트는 DRY는 반복되는 코드를 최대한 줄이기 위함이라고 보면 된다.
그러면 Dry를 반박하는 원칙은 무엇이 있을까?
WET도 있지만, 완전 새로운 개념인 AHA도 DRY에 대한 문제점을 지적한다.
앞서 작성한대로 우리가 코드를 첫 번째 원칙을 따라 줄이는 이유는 개발자의 편의를 위한 일종의 약속으로 받아들여진다. 하지만 반복 코드를 줄이다보면 함수화 과정에서 너무 추상적이거나 엉켜있는 코드를 작성하는 경우도 있을 수 있다 생각한다.
AHA Principle은 이전에 작성한 코드를 분해하기 싫거나 힘든 상황 또는 문제를 마주하여 추상화만으로 문제를 해결하려는 자세에 대한 문제점을 보여준다.
🫠 그리고 억지스럽지만 예시 코드들은 Swift로 변환 및 단계별로 추상화되는 과정을 보다 쉽게 그리기 위해 작성했다. 🫠
실제 예시들이 있으시다면 공유 부탁드립니다!
0. 추상화 이전 모습
우리가 한 서비스의 개발자라 생각 해보자.
먼저 간단하게 직업, 이름(first name), 성(last name)과 닉네임을 담은 사용자 데이터가 있다.
// Mark: 사용자 정보
struct Name {
let occupation: String
let first: String
let last: String
}
struct Person {
let name: Name
let username: String
}
// Mark: 예시 객체
let john = Person(name: Name(occupation: "Doctor", first: "John", last: "Park"), username: "Doctor.PJ")
// Mark: 담당 프로젝트 일부 (Navigation, Profile, UserCard)
class NavigationController {
let navDisplayName = "\(john.name.first) \(john.name.last)"
}
class ProfileController {
let profileDisplayName = "\(john.name.first) \(john.name.last)"
}
class UserCardController {
let userDisplayName = "\(john.name.first) \(john.name.last)"
}
1. First Abstraction
잘 보았다면 navigation, profile, usercard에 사용자 이름이 출력되고 있다.
일단 잘(?) 굴러가고 있지만 너무 비효율적인만큼 나름의 추상화를 하기로 했다.
func getDisplayname(of user: Person) -> String {
return "\(user.name.first)"
}
class NavigationController {
let navDisplayName = getDisplayname(of: john)
}
class ProfileController {
let profileDisplayName = getDisplayname(of: john)
}
class UserCardController {
let userDisplayName = getDisplayname(of: john)
}
2. Second Abstraction (직업 추가)
코드를 바꾸고 하루 뒤, 기획팀으로부터 요청사항이 왔다.
기획: 앞으로 사용자들 정보 앞에 '직업'이 노출되도록 변경 작업 부탁드려요.
개발: 모든 페이지에서 보여지는 걸까요?
기획: Profile 페이지에서만요!
프로필 페이지에서만 직업란이 보여야 된다. 이미 어제 추상화한 코드가 존재하기에 해당 메서드를 수정하고자 한다. 참고로 작성자는 업계에서 개발자들 또한 이미 추상화되어 있는 코드, 다른 개발자들도 활용할 수 있다는 점 등등 필요성을 느끼지 못하여 유지하는 경우가 많다고 한다.
아무튼 요청에 따라 아래와 같이 변경하게 됐다고 하자.
// 직업 추가 여부를 더한다.
func getDisplayname(of user: Person, includeOccupation: Bool = false) -> String {
var displayName = "\(user.name.first) \(user.name.last)"
if includeOccupation {
displayName = "\(user.name.occupation) \(user.name.first)"
}
return displayName
}
class ProfileController {
let profileDisplayName = getDisplayname(of: john, includeOccupation: true)
}
3. third Abstraction (닉네임 추가)
어느날 다시 기획팀으로부터 요청사항이 왔다.
기획: UserCard에서는 사용자들 닉네임을 '[]' 안에 보이도록 작업 부탁 드려요!
개발: '[닉네임] 이름' < 이렇게 말씀하시는건가요?
기획: 네네!
이미 세 군데에 해당 메서드가 적용되어 있고 닉네임 출력만 하는 작업이다보니 기존 코드를 유지하기로 했다.
// 닉네임 추가 여부
func getDisplayname(of user: Person, includeOccupation: Bool = false, includeUsername: Bool = false) -> String {
var displayName = "\(user.name.first) \(user.name.last)"
if includeOccupation {
displayName = "\(user.name.occupation) \(user.name.first)"
}
if includeUsername {
displayName = "[\(user.username)] \(user.name) \(user.name.first)"
}
return displayName
}
class UserCardController {
let userDisplayName = getDisplayname(of: john, includeUsername: true)
}
4. fourth Abstraction (이니셜 추가)
다음날 또 기획팀으로부터 온 요청사항.
기획: navigation에서는 사용자 성함은 이니셜로 변경 부탁드려요!
개발: 혹시 어떤 이유 때문인지 알 수 있을까요?
기획: 최근에 외국에서도 이슈가 되서 그런지 외국 가입자가 늘었는데 이름이 너무 길어요.
다시 돌아가보면 이미 추상화된 코드가 위치해 있다.
이제 생각보다 코드가 길어지고 있지만 이미 추상화를 했고, 다른 페이지들에서도 해당 메서드를 호출하고 있다.
테스트 코드, 겹치는 프로젝트, 귀차니즘 등등 여러 이유들로 해당 코드를 유지하기 위해 아래와 같이 변경했다.
// 이니셜 추가 여부
func getDisplayname(of user: Person, includeOccupation: Bool = false, includeUsername: Bool = false, firstInitial: Bool = false) -> String {
var displayName = "\(user.name.first) \(user.name.last)"
if firstInitial {
var first = user.name.first
first = String(first.prefix(1)) + "."
displayName = "\(first) \(user.name.last)"
}
if includeOccupation {
displayName = "\(user.name.occupation) \(user.name.first)"
}
if includeUsername {
displayName = "[\(user.username)] \(user.name) \(user.name.first)"
}
return displayName
}
class NavigationController {
let navDisplayName = getDisplayname(of: john, firstInitial: true)
}
지금까지의 문제점이 보일까?
5. fifth Abstraction (직업 삭제)
작성자는 마지막으로 만일 Profile 페이지에서 그동안 보여지던 '직업'란을 삭제하게 되면 어떻게 되는지 보여준다.
// 직업 parameter 삭제
class ProfileController {
let profileDisplayName = getDisplayname(of: john)
}
ProfileController에서 그저 직업란만 삭제하면 된다!
..라고 생각할수도 있지만 이런 상황이 추상화에 있어 개발자가 마주하는 가장 큰 문제점이라고 지적한다.
이미 잘 굴러가는 코드라는 인식의 문제를 지적하는데, 코드 삭제 또는 변경하는 행위를 추가하는 행위보다 더 두려워 한다는 점이었다. 괜히 삭제하여 문제를 키우는 것보다 그대로 둔다는 점.
이렇게 될 경우, 메서드 내부의 '직업 삭제' 조건들은 어떻게 되는건가!!
막간 정리
횡설수설하며 나름 내용을 정리했는데, 다시 읽어보면서 머리에 들어온 내용이 잘 전달되고 있는지 고민이 된다.
아무튼 지금까지 엉망진창으로 바뀌어지는 코드를 보게 됐다.
최초 추상화한 메서드의 모습과 최종 메서드의 모습은 많이 변했다.
요청사항이 더해지면서 메서드는 코드 길이부터 내부 구조 또한 계속 복잡해졌다.
하지만 작성자는 실제로 개발자들은 '이미 추상화했기에' 혹은 '되어 있기에'를 바로 문제점을 발견하지 못하기도 한다고 말한다.
더불어 좋은 예시가 아니었지만, Boolean 값으로 위치별로 다르게 조건을 더할 수 있었다.
좋은 의도로 추상화했지만 본래 의도와 다르게 책임이 더해지면서 초기 의도와는 매우 달라지게 된 부분들과 함께
상황 혹은 개개인의 생각에 따라 어떻게 변할지 모르는 점을 지적하는데 많은 부분 공감을 하게 됐다.
예시 잘 들어주신 것 같은데요?
클래스 만들다 보니 같은 코드가 있어서 함수로 만들고
추가 개발하다보니 약간 다른 로직이 있어서 if문 넣는다고 파라미터에 bool 추가하고...
이러다보면 괴상망측 올인원 함수가 탄생해버리는거죠
적어주신 Avoid Hasty Abstration은 성급한(=무지성) 추상화를 하지 말라는 것 같아요
추상화가 올바르게 되지 않는다면 오히려 서로가 더 강하게 엮여서 유지보수 혹은 개선이 어려워질 수 있을 것 같습니다
예시로 들어주신 Navigation, Profile, UserCard Controller에서 displayName이 같은건 '우발적 중복'으로 볼 수 있을 것 같습니다
클린 아키텍쳐에서 나오는 내용인데 궁금하시면 아래 링크 참고 하시거나 검색해보시면 좋을 것 같아요
https://medium.com/@kukuku0517/clean-architecture-3부-34bd7f73da8
https://wedonttalknemore.tistory.com/13
https://twitter.com/veluxer62/status/1499538681254014977