소프트웨어 악취 - 명령 추상화

ds_chanin

·

2022. 1. 6. 02:05


소프트웨어 악취를 제거하는 리펙토링을 읽고 내용을 재정리하였다.

명령 추상화로 인한 악취

연산 프로세스를 클래스로 표현하는 경우 발생한다. 연산 클래스라고 부르기도 한다.

문제는 클래스 내부에 연산 프로세스를 나타내는 메서드가 단 하나만 존재할 때 발생한다. 보통 다른 메서드의 내부 도우미 메서드로 사용된다. 다시 말해 클래스 내부에서 메서드로 존재해야 하는 연산이 클래스 자체로 표현된 것이다.

특정 개념의 추상화를 표현한 클래스는 데이터와 함께 관련된 메서드를 캡슐화 해야 하는데 그렇지 않을 때 발생한다. 이로 인해 추상화는 설계의 복잡성이 늘어나고 동일 데이터에 대해 동작하는 메서드가 여러 곳에 분리되어 응집성이 줄어들게 된다.

때로는 연산을 객체로 격상시키는 편이 나을 때도 있다.

원인

보통 객체지향에서 말하는 메세지를 전달하는 행위가 아닌 절차적인 사고가 원인이라고 한다.

악취로 인한 영향

이해 가능성

단일 메서드를 포함하는 클래스가 많아지는 클래스 폭발로 이어져 설계가 복잡해지고, 설계의 이해 가능성을 떨어뜨릴 수 있다.

재사용 가능성

일련의 기능을 다른 맥락에서 재사용하고 싶다면 새로운 맥락에 맞추는 클래스와 함께 명령 추상화된 일련의 클래스들도 함께 가져와서 집합적으로 사용해야한다.

테스트 가능성

단일 연산 여러개로 여러 추상화를 집합적으로 테스트해야한다.

고려사항

첫번째로 연산 프로세스를 관련된 데이터와 함께 추상화하여 응집력을 높인다. 객체가 연산 클래스를 내부에서 불러 사용하는게 아닌 연산 클래스에 구현되어있던 프로세스를 프로세스에 필요한 데이터와 함께 둠으로서 하나의 객체로서 역할을 할 수 있게 한다.

// AS-IS
class Account() {
	var balance: Long = 0
		protected set

	fun apply(amount: Long) {
		this.balacne += amount
	}
}

class Trade(
	val tradeType: String,
	val tradeAmount: Long,
) 

class TradeInvoker {

	fun (account: Account, trade: Trade) {
		val tradeAmount = if(trade.tradeType == 'CHARGE') {
			trade.tradeAmount
		} else if(trade.tradeType == 'USE') {
			trade.tradeAmount * -1
		}
		
		account.apply(tradeAmount)
	}

}

//-----------------------------------------------------------

// TO-BE
class Account() {
	var balance: Long = 0
		protected set

	fun apply(amount: Long) {
		this.balacne += amount
	}
}

class Trade(
	val tradeType: String,
	val tradeAmount: Long,
) {
	
	fun apply(account: Account) {
		val tradeAmount = if(tradeType == 'CHARGE') {
			this.tradeAmount
		} else if(trade.tradeType == 'USE') {
			this.tradeAmount * -1
		}

		account.apply(tradeAmount)
	}
}

두번째로 연산 프로세스 자체를 객체로 격상하여 시스템의 유연성을 높일 수 있다.

이러한 목적으로 다음과 같은 디자인 패턴을 고려할 수 있다.

  • 상태 패턴
  • 명령 패턴
  • 전략 패턴

디자인 패턴에 대한 설명은 굳이 하지 않겠다.