최근 우테코 프리코스를 진행하고 있는데, 얼마 전에 1주차를 마치게 되었다. 일단 깃을 활용한 작업들에 있어서 한 번 크게 깨달은 게 있었고(커밋 단위 및 주기에 관한 고민), 처음 숫자 야구 게임
을 구현하고 테스트를 통과한 뒤에 리팩토링을 진행하게 되었는데, 엉망진창인 코드들을 고치면서 또 한 번 깨달음을 얻게 되었다.
1주차 미션을 간단하게 회고해보면서, 리팩토링 전후의 나의 코드를 뜯어보고 어떠한 개선점들이 있었는지 살펴보려고 한다.
나는 1주차 미션을 진행하기 전에 해당 문서를 먼저 둘러보았다. 대부분은 이해가 되는 부분이었지만 일급 콜렉션
이나 getter/setter 없이 구현
등의 규칙들은 솔직히 어떤 식으로 지켜야 할 지 감이 오지 않았다. 그래서 일단 나머지라도 지키면서 구현하기로 했고, 추가로 통상적으로 쓰이는 MVC 패턴에 대해서도 공부해서 적용하기로 했다.
일단 MVC 패턴을 어느정도 학습한 결과 도달한 결론은, 어플리케이션에서 각 클래스의 역할 분담이 확실해야 한다는 것이었다. 크게 Model(계산 및 로직)
, View(입력 및 출력)
, Controller(Model과 View를 합침)
로 나뉜다는 패턴을 이해하고 나서 그대로 구현하였고, Controller의 역할을 지켜주기 위해 Model과 View는 서로에 대한 참조가 일절 생기지 않도록 노력했다.
이런 식으로 아키텍처도 적용하고 테스트까지 통과하는데 이틀이 안 결려서, 솔직히 처음에는 약간 우쭐한 상태에 빠져 있었다. 그래도 시간이 많이 남았으니 앞선 기수에서는 다들 어떻게 코드를 짰을까 궁금해서 곧이어 이전 레포를 뒤져보게 되었는데 여기서 큰 충격에 빠지게 되었다.
나름 잘 짰다고 생각한 몇몇 사람들의 코드를 확인해보았는데, 좀 큰일이라는 생각이 들었다.
일단 첫 번째로, MVC 패턴을 제대로 적용했다고 생각했었는데 첫 시도였던지라 역할 분담이 별로 좋지 못했다. 예를 들어 야구 게임 결과를 출력하는 상황에서, Model 계층에서 게임 결과를 모두 계산한 다음 View 계층으로 문자열만을 넘겨야 되는데, View에서 모든 일을 다 하고 있었다든가 하는 문제가 꽤 있었다.
그리고 두 번째로, getter / setter의 남용이 너무 심했다. 솔직히 이번 프리코스 전까지 나는 올바른 객체지향에 대해 별로 생각해본 적이 없었다. 그렇기에 getter / setter를 사용하면서 생기는 정말 수많은 문제들에 대해서 알지 못했다. (이에 대해서는 뒤에서 자세히 알아보려고 한다.)
그 밖에도 객체지향적 관점에서, 유지보수적 관점에서, 협업의 관점에서 문제가 되는 사항들이 넘쳐났다. 나는 아직 우물 안 개구리였구나..
라는 좌절감도 있었지만, 그것보다도 제출까지 4일밖에 안 남았는데 고쳐야 될 코드가 산더미라는 사실 때문에 머리가 참 어질어질했다.
이후로는 거의 학교에서나 집에서나 하루종일 앉아서 리팩토링을 했다. 우선 다행인 점은, 내가 참고했던 그 클린한 코드들의 작성자들도 처음부터 좋은 코드를 작성한 것은 아니었고, 대부분이 첫 구현 이후에 리팩토링을 진행했었다. 최종적으로는 나도 개선된 모습을 보여야겠다
라는 생각을 가지면서 먼저 리팩토링 목록을 문서로 작성하였다.
제일 먼저 리팩토링한 것은 indent depth를 줄이는 것이었다. indent depth라는 것은? 들여쓰기의 깊이를 의미한다. 우테코 클린코드 가이드에서는 indent depth를 최대 2로 제한하고 있다. 예를 들어 다음과 같이 for문 안에 if문을 넣게 되면 주어진 indent depth를 모두 다 쓰게 된다.
for(int i = 0; i < 5; i++) {
if (condition) {
// 로직
}
}
그렇다면 indent depth를 2로 제한하면 무슨 이점이 있을까? 일단 가장 눈에 보이는 장점은 가독성이다. 만약 반복문 안에 반복문, 그리고 if문이 무더기로 들어가 있다면 코드를 읽기가 굉장히 어렵겠지만, 이를 줄인다면 가독성이 훨씬 좋아진다. 그리고 보통 indent depth가 지나치게 많다면 로직을 잘못 짠 경우가 많기 때문에 이를 줄이면서 코드를 교정해나갈 수 있다.
내 코드 중에는 indent depth가 4까지 도달한 코드들도 많았다. indent depth를 최대 2로 줄이기 위해서, 나는 기존에 복잡하게 짰던 로직들을 더 효율적으로 처리하기 위해 노력했다. 그리고 최대한 줄이고 줄였지만 아직 2가 되지 못한 경우, 따로 메소드를 분리하였다.
처음에는 잘 몰랐지만, 숫자 야구 게임
을 만들다 보니 게임 내의 여러 에러나 시스템 메시지들, 그리고 입력값 검증 조건 등을 상수로 만들어야 겠다는 생각이 들었다.
숫자 야구 게임
자체는 실제로 서비스하는 어플리케이션들에 비하면 굉장히 작은 규모이지만, 추후 확장을 생각했을 때 이러한 부분들을 상수 처리하지 않으면 텍스트 하나를 변경해야 할 때 엄청나게 많은 변경점들이 생길 것이기 때문이었다.
상수 처리에는 static 상수와 Enum 두 가지의 방법이 있었는데, 그 중에서 Enum이 필드 및 메소드를 추가할 수 있었고 활용도가 높다고 생각되어 선택하게 되었다.
// 시스템 메시지 관련 Enum
public enum SystemMessage {
TYPE_NUMBER("숫자를 입력해주세요 : "),
GAME_END("3개의 숫자를 모두 맞히셨습니다! 게임 종료"),
GAME_RESTART("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."),
EXIT_FLAG("1"),
RESTART_FLAG("2");
private final String message;
SystemMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
// 입력값 범위 검증 관련 Enum
public enum RangeCondition {
START(1),
END(9);
private final int number;
RangeCondition(int number) {
this.number = number;
}
// 범위 검증 메소드
public static boolean isNotInRange(int number) {
return number < START.number || number > END.number;
}
}
이런 식으로 설계하여 전용 메소드를 통해 텍스트를 불러 오도록 했으며, 검증 조건에 관련된 Enum에 경우, 검증할 수 있는 메소드를 Enum 안에 구현하여 검증 기능에 대한 응집력을 높였다.
솔직히 전까지는 getter와 setter를 쓰면 안 된다라는 얘기를 들어본 적도 없었다. 전에 스프링 강의를 들을 때 setter가 엔티티의 필드 값을 수정할 수 있어 사용에 유의해야 한다라는 말은 들었지만, "그래서 setter를 안 쓰면 private 필드의 값을 어떻게 바꿀 건데?"
라는 의문이 계속 남아있었다. 한동안 우테코 커뮤니티와 여러 블로그들을 찾아본 결과 그 답을 어느 정도 유추할 수 있었다.
일단 getter와 setter를 쓰지 말자
라는 얘기는 필드의 값을 바꾸지 말자는 얘기가 아닌, 객체의 조작에 관한 로직을 객체 안에서 처리하도록 하라는 얘기였다. setter같은 경우에는 대놓고 필드를 수정하는 메소드인데, 여러 클래스에서 setter를 남용하게 된다면 나중에 도대체 이 객체가 어디에서 수정된 건지 알기가 매우 힘들어진다.
getter의 경우도 마찬가지다. number = getNumber() + 1
, setNumber(number)
이런 식으로 간다면 결국엔 getter 메소드 역시 외부에서 필드를 수정하도록 이용되는 셈이고, 만약 getter로 반환된 타입이 List와 같은 컬렉션일 경우, add 등을 통해 곧바로 수정할 수 있다(!)
아무튼 이러한 이유로, getter / setter를 사용하는 대신 비즈니스 로직에 대한 처리를 객체 내부에서 처리하는 것이 좋다. 즉, 객체 내부의 값을 외부에서 가져가서 처리하는 것이 아닌, 객체 자신의 일은 자신이 처리하는 것이다. 찾아보니 이런 식의 아키텍처를 도메인 주도 개발(DDD)이라고 부르는 것 같았다.
해당 원칙에 따라서 리팩토링을 진행하려고 하니 초기 설계부터 갈아엎을 곳이 넘쳐났고, 코드 하나하나를 짤 때마다 신중하게 생각할 수밖에 없었다. 솔직히 필요할 때마다 getter와 setter를 호출해 쓰고 싶어서 정말 미칠 지경이었다.
결국에는 setter는 모두 제거했지만 getter는 모두 제거하지 못했다. 아무래도 초기부터 잘못됐던 설계를 어느 정도 지키면서 리팩토링을 하려다 보니 한계가 있었던 것 같다. 다음 미션 때는 getter와 setter 모두 단 한 개도 없는 코드를 만들기 위해 노력해보려고 한다...!
이번에 클린 코드를 적용해보면서, 기존에 내가 얼마나 자바 코드를 편하게 짜왔는지 알게 되었다. 객체 지향 언어를 사용하고 있었지만, 객체 지향의 장점을 거의 활용하지 못한 코드였던 것 같다.
아직은 연습하고 적용해야 할 것들이 정말 많다. 일급 컬렉션이나 원시값 포장에 대해서는 아직도 감이 잘 안 잡히는 것 같고, getter / setter를 제거하면서 비즈니스 로직을 객체 내부로 모두 몰아넣다 보니 발생하는 도메인 계층의 비대화와 같은 문제도 어떤 식으로 해결해야 할 지 잘 모르겠다.
지금으로선 아직 머리가 복잡하긴 한데, 아무래도 미션을 거듭해나가면서 계속 자료를 찾아보고 고민을 하다보면 저절로 감이 잡히고 해결될 문제일 거라 생각한다. 제발 그랬으면 좋겠다.
☆눈부신 성장을 기대해주세요☆