클린 코드와 클린 아키텍처
아래 내용은 내 나름대로 클린 코드와 클린 아키텍처를 이해하고 정리한 것이다.
결론부터 말하자면
클린 코드와 클린 아키텍쳐는 코드를 레고블럭처럼 작성하는 것이라고 생각한다.
마치 2 X 3 레고블럭과 같은 기본적인 블럭들로 성을 만들듯이 코드를 작성하는 것이다.
그리고 만들어진 성에서 특정 부분을 교체해야 할때는 전체를 다시 만들지 않고 교체할 부분만 교체하는 것이다.
또한 가장 중요한 부분이라고 생각되는게 존재하는데,
다른 누가 봐도 코드가 쉽게 이해되면서 쉽게 기능을 추가하고 수정할 수 있지만 사이드 이펙트는 최소로 해야 한다는 것이다.
함수는 한가지의 일만 해야한다.
함수를 살펴보다 보면 함수가 하는것이 A 를 하거나 B를 하고 C를 한다 라고 적혀있는 경우가 있다.
만약 내가 이 함수를 다시 수정해야 한다면 나는 할수 있다. 하지만 시간이 조금만 지나거나, 다른 사람이 작업해야 한다면
이 함수가 무엇을 하는지 파악하기가 어려울 것이다.
차라리 A를 하는 함수와 B를 하는 함수, C를 하는 함수로 나누는 것이 더 좋다.
하지만 이 부분은 나에게도 딜레마 이기도 한데, 다양한 기능을 수행하는 조건문이나 반복문은 완전히 제거할 수 없다 생각하기 때문이다.
함수의 이름은 함수가 하는 일을 잘 설명해야 한다.
위에서 말한 함수가 한가지 일만 해야한다는 것과 연계된 내용이다.
함수가 한가지 일만 한다는 가정에서 함수의 이름을 보면 함수가 하는 일을 파악할 수 있어야 한다.
따라서 다른 사람이 코드를 수정해야 할때 구현 부분을 보지않고 함수의 이름만 보고도 함수가 하는 일을 파악할 수 있게 되고 작업 시간을 단축시킬 수 있다.
함수는 인자를 최소화 해야한다.
함수의 인자가 많아지면 많아질수록 함수가 하는 일을 파악하기가 어려워진다.
인스턴스는 함수의 인자로 전달되어야 한다.
함수 내부에서 인스턴스를 생성하는 것은 좋지 않다. 인스턴스를 생성한다는 것은 그 함수의 의존성을 높이는 것이기 때문이다.
의존성이 높은 함수는 테스트하기가 어렵다.
사이드 이펙트를 최소화 하면서 기능을 추가해야 한다.
위에서 말한 함수의 역할과 연계되는 내용이다. 어떤 함수가 분기를 통해 다양한 기능을 수행한다면, 기능을 추가할때 그 함수를 수정해야 한다.
하지만 의존성을 최소화 하면서 기능을 추가한다면, 기능을 추가할때 그 함수를 수정하지 않아도 된다. 이때 사용하는 것이 DI(Dependency Injection)이다.
DI(Dependency Injection)
어려 개념이 종합적으로 사용된다고 생각한다.
보통 함수들은 종속성을 구현체에 의존하고 있다. 하지만 DI는 종속성을 구현체가 아닌 추상화된 인터페이스에 의존하게 만든다.
이렇게 하면 구현체를 교체할때, 인터페이스만 교체하면 되기 때문에 기존 코드를 수정할 필요가 없다.
만약 어떤 게임에서 캐릭터가 공격을 할때, 캐릭터는 공격을 하지만 어떤 공격을 할지는 캐릭터가 결정하지 않는다. 케릭터는 Weapon.attack이라는 메서드를 호출할 뿐이다.
이때 Weapon 인터페이스를 구현한 Sword, Gun, Bow 등의 구현체를 교체하면 캐릭터가 사용하는 공격이 바뀌는 것이다.
만약 이런 DI를 사용하지 않고 캐릭터가 공격을 할때마다 공격을 하는 코드를 작성한다면,
class Character {
string weapon;
attack() {
if (this.weapon === "sword") {
// sword attack
} else if (this.weapon === "gun") {
// gun attack
} else if (this.weapon === "bow") {
// bow attack
}
}
}
이런 코드가 되는데, DI를 사용하면
abstract class Weapon {
abstract attack();
}
class Sword extends Weapon {
attack() {
// sword attack
}
}
class Gun extends Weapon {
attack() {
// gun attack
}
}
class Character {
weapon: Weapon;
attack() {
this.weapon.attack();
}
}
이런식으로 작성되면서 Character는 Weapon 인터페이스에만 의존하게 된다. 따라서 어떤 공격을 할지는 Character가 결정하지 않고, Weapon 인터페이스를 구현한 구현체가 결정하게 된다. 그러므로 Weapon 인터페이스를 구현한 구현체를 교체하면 캐릭터가 사용하는 공격이 바뀌게 되고 Weapon을 추가한다면 Character 클래스를 수정하지 않고 Weapon 인터페이스를 구현한 구현체만 추가하면 된다.
테스트의 중요성
과연 내가 작성한 코드가 100% 신뢰성이 있을까? 전에는 제 잘난척에 내 코드는 100% 완벽한 코드라고 생각했었다.
하지만 막상 알고 보면 그 코드는
- 어딘가 엉성함
- 엣지케이스를 고려하지 않음
이런 성격이 강하게 띄었다.
그래서 테스트를 작성하고 테스트를 통과하는 코드를 작성하면서 코드의 신뢰성을 높이는 것이 중요하다는 것을 알게 되었다.
하지만 통과하기 위한 테스트를 작성하는 것이 아니라, 작업 결과물이 테스트 결과를 통과하게끔 하는것이 중요하다.
번외로 클린 아키텍쳐 책에서 봤는데, 테스트를 했을때의 작업 시간, 작업 결과물 과 테스트를 하지 않았을때 작업 시간, 작업 결과물이 차이가 존재했다.
여기까지는 당연하지만, 놀라운것은 더 빠른쪽이 테스트를 한 쪽이었다는 것이다. 따라서 테스트 하기 좋은 코드를 작성하면 작업 시간을 단축시킬 수 있다는 것이다.
하지만 테스트 코드는 다음 룰을 지켜야 한다.
- 하나의 테스트에는 하나의 assert문만 존재해야 한다.
- 테스트는 외부 데이터에 의존하지 않는 코드를 작성해야 한다.
- 테스트는 독립적으로 실행할수 있어야 한다.
외부 데이터에 의존하지 않는 코드를 작성해야 한다.
무슨 말인고 하니, 외부 데이터에 직접적으로 의존하지 않는 코드를 작성해야 한다는 것이다. 외부 데이터에 의존하지 않고 비즈니스 로직을 다루는 코드를 작성하면 테스트가 쉬워진다.
그 이유가 외부 데이터에 의존하지 않는 코드와 외부 데이터를 뿌려주는 코드로 나뉘게 되면서 함수의 목적이 좀더 분명해지는 효과를 얻을 수 있기 때문이다.
테스트를 한다면 테스트를 위한 데이터를 집어넣고 결과를 확인하면 그만이다.
또한 외부 데이터가 어떻게 구성되었는지, 뭐가 변경이 일어나는지에 대해서는 신경쓰지 않아도 된다.
우발적 종복
코드를 작성하다 보면 거의 비슷한 코드를 작성하는 경우가 있다. 이런 중복을 보면 우리는 이를 제거하려고 한다.
하지만 이 중복을 꼭 제거해야 할까?
만약 중복된것중 하나가 다른 이유로 변경이 필요하다면 이건 중복이라 보면 안된다.
댓글남기기