이 글에서 가장 하고 싶은 말은 "클린 코드", "TDD", "DDD" 등은 그저 도구라는 것입니다. 저 모든 것들은 단순히 "이렇게 하면 좋다더라~"를 한 단어로 묶어 개념화한 도구입니다. 따라서 도입하는 것이 생산성을 높이면 도입하면 되는 것이고, 떨어지면? 도입하지 않아도 됩니다.
주석을 달았더니 팀이 코드를 더 쉽게 이해하고 생산성이 증가하는데? -> 그럼 주석을 다는 게 클린 코드입니다. 주석을 다는 행위로 인해 코드의 가독성이 증가했기 때문입니다.
소프트웨어를 개발하는 사람들은 생각도 soft하게 할 필요가 있지 않나 생각합니다. 어떻게 커스터마이징하느냐에 따라서 우리 팀만의 클린코드 컨벤션이 존재할 수 있다고 생각합니다.
한 줄로 정리하면, XX를 하면 클린 코드? XX를 안 하면 클린 코드? 이런 게 아니라, 생산성을 증가시키면 그게 뭐가 됐든 클린 코드라 생각합니다.
강의 내용에 대해서 말하자면 평소에 생각하던 부분들과 일치하는 부분이 많아 그리 어렵지 않게 들을 수 있었습니다. 그런데 가끔 "아, 이렇게도 생각할 수 있네?", "이런 게 기준이 될 수 있겠네?" 하는 부분들이 있어 생각했던 것보다 이미 많은 것을 얻어간 느낌입니다.
사실 강의 자체의 퀄리티가 좋습니다.
이번 주는 강의를 듣고 미션을 수행하는 것이 조금 버거웠습니다. 강의의 양 자체가 적지 않기도 했고, 더 우선적으로 수행해야 할 과제들이 있어 더욱 그러지 않았나 생각됩니다.
이러한 말을 한 이유는 클린코드, SOLID , DDD , TDD.... 등등의 것들은 전부 생산성을 위한 것이거든요 아~ 이렇게하면 생산성이 늘어나던데? 좋던데?와 같은 말인것이죠 , 또 그들은 일종의 "상품"으로서 작용되는 부분도 있습니다. TDD라는 용어가 만들어지기 전까지는 Test코드를 작성을 안했을까요?? 그렇진 않을 것 입니다.
그저 테스트와 관련한 여러가지 산재돼있는 개념을 TDD라는 명칭으로 묶어 만든 일종의 "상품"이라고 생각합니다. 또 이런것들은 전부 생산성을 늘리는 Best Practice중 하나다 라고 볼 수 있습니다.
따라서 우리는 이것을 일종의 원칙이나 법처럼 적용시켜 개발할 이유는 없다고 생각합니다. 당장 내일 새로운 기능을 배포를 해야하는 상황에서 "클린코드를 위해서 리팩토링을 10시간 하겠습니다" 이런 말을 한다면 아마 팀장님한테 혼나겠죠
다른 예시로 "클린코드를 지향하니까 주석은 안답니다"같은 말씀을 하시는 분들이 계십니다. 근데 주석을 안단다고 진짜 코드가 클린해지나요? 조금 의문인 부분도 있거든요
강의에서도 비슷한 말을 합니다. "무조건"이 아니라는 것이죠 개인적으로는 "이런식으로 하면 좋던데??" 정도로 받아들이는 것이 가장 좋지 않을까 생각합니다.
객체를 만들 때 1개의 관심사로 책임이 정의돼있는지 확인하는 것은 굉장히 중요합니다. 저는 개인적으로는 "객체"라는 표현보다는 "타입"이라는 표현을 더 좋아합니다.
클래스를 만든다는 것과 타입을 만든다는 것은 동일한 것인데 어감에서 오는 차이가 크다고 생각하거든요 타입에 대해 말하는 것이 훨씬 더 명확하게 의도가 전달된다 생각합니다. 이 타입이 가져야 할 역할은 무엇이지? , 이 타입으로 나는 무엇을 하고싶지? , 이 타입이 해야만 하는 일은 뭐지? 와 같이 말이죠
int라는 타입을 생각해 봤을 때 마땅히 숫자형 데이터가 해야할 무언가가 떠오르지 않나요? int 객체라고 하면 그런 부분에서 직관성이 떨어진다 생각합니다.
"setter를 자제하자" 이건 사실 당연한데요 setter로 열려있는 경우에는 누구나 해당 값을 임의로 변경할 수 있기 때문에 최대한 지양하는 것이 좋습니다.
특히 dto같은 경우에는 setter가 더욱 필요 없는데 사용자가 준 값을 내가 임의로 바꿀일이 많지는 않거든요 하지만 재밌는점은 "getter도 사용하지말자"입니다.
사실 getter의 경우에는 그냥 lombok으로 달아놓고 자유롭게 쓰거든요 setter의 경우만 고민을 합니다. 근데 getter도 사용하지말자? 이건 좀 재밌게 느껴졌습니다.
예시를 보면 바로 알 수 있는 부분인데요 1번코드와 2번코드 무엇이 더 직관적인가요? 당연히 2번코드지 않나 생각합니다.
또 관련된 설명을 하면서 이것이 person이라는 객체를 존중하지 않는 폭력적인 코드라는 설명하시는데 이 부분이 가장 흥미있고 재밌었습니다.
쉽게 말하면 객체는 캡슐화돼있는 것인데 getter를 남발하면 캡슐화를 왜 하냐? getter를 쓸수도 있지만 안쓸수도 있지 않을까?? 라는 것 이죠
참 중요한 걸 얻고 가는데요
"캡슐화돼있는 데이터를 바깥에서 알고있다고 생각하지 말아라" "우리는 알고있지만 바깥에있는 객체도 알고있을거라 생각하지마" 이것이 캡슐화의 전부인 것 같습니다.
"주니어를 위한 면접질문" 같은 곳에 가면 꼭 등장하는 SOLID입니다.
매우 간단한 코드로도 SOLID를 표현 가능하다 생각하는데요
public class ChefService {
private final Chef chef; // Interface Chef라는 타입이 해야할 역할을 정의한 Specification
public ChefService(Chef chef) { // subTyping 이용 , Chef타입의 서브타입을 주입 가능
this.chef = chef; // 나는 Korean,American 선택해서 넣을 수 있음
} // 후에 다른 Chef가 추가되어도 생성할때 넣는 인자만 변경하면 됨 OCP , DIP
public void makeFood(){
chef.cook(); // korean,american의 코드가 변경되도 ChefService가 신경 쓸 부분은 아님
// cook이라는 행위를 Chef에 정의해 둠으로써 통일화된 추상화를 제공함
// Korean,American의 cook메서드의 내부 구현이 바뀌어도 신경 안써도 됨
}
}
이 코드로 SOLID의 모든 것을 표현 가능합니다.
DIP = 의존성 역전 원칙 -> 추상화에 의존해라 Chef라는 추상화 정도가 높은 Interface 타입을 받음으로써 ChefService는 추상화에 의존하고있습니다.
그로인해 각각의 Chef를 하나의 모듈처럼 사용가능합니다. Korean을 넣었다가...American넣었다가... 아니면 다른 Chef을 만들어 주입해도 문제가없습니다. 따라서 OCP 원칙을 지키는 길이기도 합니다.
OCP = 개방 폐쇠원칙 -> 확장은 열려 있고 수정은 닫혀 있다. Chef 타입 1000개 만들어서 사용해도 생성자 주입할때 들어가는 코드만 변경해주면 문제없음.
public interface Chef {
void cook(); // 요리하세요
}
public class AmericanChef implements Chef{
@Override
public void cook() {
// 양식요리
}
}
public class KoreanChef implements Chef{
@Override
public void cook() {
// 한식요리
}
}
LSP = 리스코프 치환 원칙 -> Super Type이 정의한 근본적인 역할을 따라야 한다. SuperType인 Chef에 cook이라는 기능이 명시돼 있습니다. 마땅히 요리하는 행위를 생각하겠죠??
근런데 갑자기 KoreanChef의 cook() 메서드를 음식을 먹는 기능으로 변경한다고 해보겠습니다. 이러면 위에서 정의내린 Chef타입의 근본적인 기능이 변경되는 것 입니다. 하지만 우리의 코드는 SuperType이 정의한 "요리" 행위를 잘 하는것으로 보이니 LSP를 지키는 것으로 보입니다.
ISP , SRP = 인터페이스 분리 원칙 , 단일 책임 원칙 -> 하나의 책임만 가져라 Chef는 요리하는 행위만 신경쓰면 됩니다. 그것이 Chef이라는 타입이 해야할 책임입니다.
그런데 갑자기 Chef에게 drive() 기능을 주면 어떤가요? 요리사라는 타입이 해야할 일인가요?? 이경우 SRP가 깨진다고 볼 수 있습니다. ISP도 일맥상통한다 생각합니다.
SOLID 개념은 각각의 것들이 유기적으로 결합되어 있어 SOLID중에 3가지만 만족한다, 4가지만 만족한다, 이런 건 없다고 생각합니다. 하나가 안지켜지면 전체적으로 안지켜지고 하나만 잘 지켜도 전체적으로 완성이되는 개념이라 봅니다.
마지막으로 간단하게 코드를 변경해 보았는데요 개인적으로는, [직관적인 코드 = 읽기 쉬운 코드 = 좋은 코드 = 클린코드] 라는 생각이 있습니다. 그래서 사람에게 말하는듯이 이름을 작성하는 것을 좋아합니다.
특히 boolean 타입의 경우에는 isXXX~ 식으로 많이 작성하는 것 같습니다.
public boolean validateOrder(Order order) {
if (order.getItems().size() == 0) {
log.info("주문 항목이 없습니다.");
return false;
} else {
if (order.getTotalPrice() > 0) {
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
} else {
return true;
}
} else if (!(order.getTotalPrice() > 0)) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
}
return true;
}
읽기쉽게 , 내가 생각하는 clean code로 변경
public boolean validateOrder(Order order) {
if (order.isEmpty()) {
log.info("주문 항목이 없습니다.");
return false;
}
if (order.isTotalPriceNegative()) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (order.isCustomerInfoMissing()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
같은 세계에서는 추상화의 정도가 같아야 한다. 라는 의미입니다. 와 이건 정말 생각하지 못했던 부분이라 놀랐는데요
이런 피드백을 한번쯤은 들어보셨을 겁니다.
"메서드 여러개로 분리해라"
이유는 간단한데요 거대한 기능을 하나의 메서드로만 관리하면 너무 많은 책임을 갖게 됩니다. 기능이 고도화 될 수록 메서드는 더욱 거대해지고 이는 직관적으로 표현하기가 어려짐을 의미합니다.
따라서... 책임의 분리 -> 직관적인 표현 + 유지보수의 편의성을 위해 메서드를 분리 하는 것인데요
하지만 "그럼 정말 모든 메서드를 기능별로 하나하나씩 쪼개서 최대한 작은 단위로 만들어야 하나?" 라는 질문을 한다면 저는 아니요 라는 답변을 할 것입니다.
여러 행위를 한다 해도 그것이 하나의 주제와 문맥으로 설명 가능하다면 하나로 만들어도 된다 생각합니다.
왜냐하면 메서드가 많아지는 것도 직관적인 것과는 거리가 멀어지기 때문입니다. 메서드를 분리한다는 것은 결국 코드의 양이 늘어난다는 것이며 누군가가 내 코드를 다룰 때 찾아봐야 하는 부분 또한 늘어납니다. 따라서 마냥 쪼개는 것이 좋다고는 생각 안합니다.
그리고 이런 고민은 커밋을 할때도 똑같이 발생 하는데요 커밋도 한번에 많은 양을 올리지 마라 , 쪼개서 하나의 단위씩 올려야 좋다. 라고 말하곤 합니다. 그럼 정말 하나의 코드 변경 = 1커밋 이여야 할까? , 만약 우리가 리펙토링을 통해서 이름을 변경했다고 했을 때 A이름변경 , B이름변경 , C이름변경 ......... 이렇게 이름변경에 관한 커밋을 수십개씩 쪼개놓았다면 어떨까요?
코드를 리뷰하려고 봤는데 커밋이 수십개씩 쌓여있으면 리뷰하기가 겁나는 경험이 있으실 겁니다. 설령 그것이 큰 내용이 없는 코드라 할지라도 너무 잘게 쪼개져있으면 집중력있게 리뷰하는 것은 어려워 집니다. 중요한 변경사항이 아니라면 차라리 묶어서 표현하는게 좋지 않을까요? User 관련 객체 이름변경.. 등으로
이렇게 되면 머리가 아파집니다. 무작정 쪼갤수도 , 무작정 합칠수도 없다면 무엇을 기준으로 쪼개고 합쳐야 하나?? 라는 것이죠 이 추상화 레벨에 대한 것은 판단에 대한 하나의 기준이 될 수 있다 생각합니다.
-> 지금 얘가 가지고 있는 추상화 레벨의 정도가 주변의 것들과는 조금 다른거 같은데?? -> 위에는 전부 강하게 추상화하여 구체화를 숨기고 명시적으로 어떤 기능을 할 것임만을 말했는데 갑자기 구체화가 등장하네?? -> 아 이거는 메서드 분리 해야겠다
와 같은 흐름을 통하는 것이죠 , 이 부분에 대해서는 기존에 생각했던 것이 아니라 참 인상깊은 부분이였습니다.