추상화가 항상 좋지는 않구나! AHA!

7과11사이·2024년 3월 15일
2

이전 포스트에서도 작성을 했지만, 개인 프로젝트에서 불필요한 최적화 등을 이유로 많은 고민을 하고 있었다.
그러다보니 Coding Principle에 대해서 다시 되돌아보는 시간을 가지게 되었는데,
이전에 읽었던 내용 중에서 정리를 한 번 해보면 좋을 것 같은 내용이 있어 블로그에 적어본다!

이름하여 AHA Programming Principle.


DRY (Don't repeat yourself)

개발자들이 따르는 일종의 규칙이자 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! (Avoid Hasty Abstraction)

앞서 작성한대로 우리가 코드를 첫 번째 원칙을 따라 줄이는 이유는 개발자의 편의를 위한 일종의 약속으로 받아들여진다. 하지만 반복 코드를 줄이다보면 함수화 과정에서 너무 추상적이거나 엉켜있는 코드를 작성하는 경우도 있을 수 있다 생각한다.

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)
}
  • 추상화한 하나의 메서드 덕분에 모든 파일에 변경사항을 바로 적용할 수 있도록 있게 됐다.
    필요하다면 displayName(of:) 메서드 내부만 변경하면 출력되는 사용자 데이터를 바꿀 수 있으니 더 편리다.
    바꾼 코드에 만족하며 다른 일을 하기 시작했다.

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)
}
  • 직업 추가 여부를 묻는 parameter가 추가됐다.
    프로필 페이지에서만 직업이 노출되기에 profileController에서만 불리언 값을 '참'으로 변경하여 끝냈다. 쉽게 만들었고 기존 추상화 코드도 크게 변경하지 않았기에 만족스러운 수정사항이었다!

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)
}
  • 전과 동일한 형식으로 Boolean 값으로 추가 여부를 확인, 필요한 위치에서 변경했다.
    오늘도 필요한 결과를 만들어 냈다! 집 가야지

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 값으로 위치별로 다르게 조건을 더할 수 있었다.
좋은 의도로 추상화했지만 본래 의도와 다르게 책임이 더해지면서 초기 의도와는 매우 달라지게 된 부분들과 함께
상황 혹은 개개인의 생각에 따라 어떻게 변할지 모르는 점을 지적하는데 많은 부분 공감을 하게 됐다.


선언하는 프로퍼티 수를 줄이기 위해, 파라미터로 값을 모두 전달하면 읽기 쉽다고 생각했기에 스스로 불필요한 추상화를 많이 했다고 생각한다. 개인 프로젝트에서도 이런 코드를 작성하지 않도록 최대한 고민하다보니 고개를 많이 끄덕이지 않았나 싶다!

참고

1개의 댓글

comment-user-thumbnail
2024년 3월 15일

예시 잘 들어주신 것 같은데요?

클래스 만들다 보니 같은 코드가 있어서 함수로 만들고
추가 개발하다보니 약간 다른 로직이 있어서 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

답글 달기