글 작성자: 개발섭

클린코드 with java 17기를 들었던 경험 및 거기서 피드백 받았던 내용을 공유를 하려합니다.

저는 10월 30일부터 12월 31일까지 클린코드 강의를 코드리뷰, 강의를 끝까지 완료를 했습니다. 실제 강의 일자는 6주 과정이였지만, 업무 + 과제 까지 다해내게기엔 굉장히 빡센 과정이였지만... 배운점은 있었고, 다 아는 내용일지언정, 공유는 드리는 것이 좋아보여 전달을 드립니다.

 

객체지향 생활 체조란?

겍체지향 생활체조를 지켜 보는것이 왜 중요한가? (저도 못지키지만...)
생활체조 원칙은 좋은 품질의 소프트웨어를 만들기 위한 응집도(cohension), 느슨한 결합(loose coupling), 무중복(zero duplication), 캡슐화(encapsulation), 테스트 가능성(testability), 가독성(readability), 초점(focus)을 실현하기 위한 훈련 지침이다.

지침은 다음과 같은데

 

  1. 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  2. else 예약어(keyword)를 쓰지 않는다.
  3. 모든 원시값과 문자열을 포장(wrap)한다.
  4. 한 줄에 점을 하나만 찍는다.
  5. 줄여쓰지 않는다(축약 금지).
  6. 모든 엔티티(entity)를 작게 유지한다.
  7. 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  8. 제일 클래스(first-class) 콜렉션을 쓴다.
  9. 게터(getter)/세터(setter)/프로퍼티(property)를 쓰지 않는다.

 

해당 내용중에서 적용하기 쉽고, 중요한 원칙은 아래에 설명과 함께 같이 적어봅니다.
중요한건 묶음 단위로 처리가 가능한점이며, 해당하는 내용들은 100% 적용하기 힘든 점도 있다. 한점만 찍는다던가. getter setter를 무조건 쓰지말고 코딩하는것은 원칙적으로 고수하는 방법은 어렵습니다.
완전히 지키는 건 어렵더라도, 해당하는 것들을 지키면 좋았던 것들에 대해서 이야기 해보려 합니다.

당장 지키기 좋은 클린 코드 전략들

  1. Early Return / else 사용하지 않기. → indent를 줄여내려가자 (1,2)

제일 쉬운 방법입니다. 일단 기본적으로 Else를 사용하지 않는 것입니다. if를 통해 조건을 분기 처리하며, 해당하는 조건을 만족한 경우 분기 처리 이후 return을 통해 다른 분기를 삭제하는 방향으로 처리합니다.
해당하는 전략을 Early Return 이라 부르며, 해당하는 방식으로 처리하는 경우 Else가 없어지는 형태로 구현하기 쉬워집니다.
장점: 체조 1장,2장에 동시에 처리가 가능한 부분으로써 if절이 줄면 줄수록 함수로 빼면 뺄수록 indent가 줄어들어 로직 이해해 도움을 줄 수 있습니다.
단점: 함수로 모두 다 빼게되면 모든 indent가 사라지는 장점은 있으나, 코드의 가독성이 떨어집니다. 간단한 조건도 모두 함수로 만들게되면 각 함수에 대한 조건들을 10라인 밑에서 찾는 경우가 있으니, 뭐 적절하게 섞어서 사용하는걸 추천합니다.

  1. 상수화 처리하기. (Magic Number를 없에자.)

최근 제가 많이 하는 것으로 조건중 숫자가 있는 경우 그 숫자를 상수 취급하여, 해당하는 숫자를 java로는 static final를 통해 상수처리를 하게됩니다.
하드코딩으로 만들어진 숫자인 Magic Number는 코드를 작성한 당사자는 알수 있겠으나, 그외의 개발자에게는 그 숫자가 무엇을 뜻하는지에 대해서는 알 수 없게됩니다. 해당 코드에 대한 로직의 이해도가 떨어지게되며, 백로그를 찾으러 다녀야하는 문제 역시 발생합니다.
아래의 예제를 보면서 확인해봅시다. 상수화는 magic Number를 없에는데 큰 도움을 줍니다. 이런 코드의 이런 숫자가 뭘 뜻하는지... 알 수있을까? 그리고 만약에 바꿔야한다면? 바꾸더라도 다른곳에서는 그 값이 안 바뀌었다면?

elapsedTime >= 10000 혹은 threadTitle.length <= 100 당장은 이해하기 쉽긴하겠지만... 상수로 되있으면 코드를 바꿀일은 없다. 이렇게 처리해도됨. ELAPSED__MAX_TIME = 10_000일때 20_000으로만 바꿔도 실제 코드는 변경되지 않기때문에 훨씬 수정이 용이하다.

 

3. 리턴하는 함수를 클래스로 만들자.
코드 리뷰에서 종종 이야기들었던 내용중 하나입니다. 아래의 일급 컬렉션 적용이 잘되있을수록 유리한 접근입니다. 일종의 함수 Return 타입을 클래스나 Wrapper 타입으로 전달받는 것입니다.
클래스안의 상태값이 없어지게됩니다. 일급 컬렉션과 일급 클래스와 동일한 이야기이지만,
클래스안에 인스턴스 변수가 많아지면 많아질 수록 일종의 상태값이 계속 생기게됩니다. 그럼 자동으로 getter를 사용해야할일이 늘며, 비즈니스 로직이 밖으로 노출될 가능성이 높아진다.

public class LottoGame {
    private List<Lotto> lottos = new ArrayList<>();

    public void buyLotto(int price) {  //로또사기 함수를 만들때 우리는 이렇게 할수도 있다. 
        int gameCount = getGameCount(price);
        while(countLotto() < gameCount) {
            Lotto lotto = new Lotto(makingLottoNumbers());
            lottos.add(lotto);
        }
    }
}

//로직을 사용하기위해서는..
lottoGame.getLottos().stream().filter() 처럼 장황한 코드를 작성할 가능성이 높아진다.

주로 저는 애매한 경우 적용하긴했는데, 이러는 것보다는... 아래처럼 적용하면

public class LottoGame {
    //private List<Lotto> lottos = new ArrayList<>(); -> 이 부분이 삭제된다. 

    public List<Lotto> buyLotto(int price) { 
        int gameCount = getGameCount(price);
        while(countLotto() < gameCount) {
            Lotto lotto = new Lotto(makingLottoNumbers());
            lottos.add(lotto);
        }
        return lottos;
    }
}

// 해당 코드 사용시
List<Lotto> lottos = lottoGame.buyLotto() 
lottos.stream().filter()
비슷해보이지만 사실 큰 차이를 만드는 점은 다음과 같다.

 

일급컬렉션에서도 이야기하겠지만. 이러면 Lotto라는 객체를 클래스화한다음에 장황한 코드들을 뒤로 숨길 수 있다.
당첨된 로또만 가져오기 → stream.filter()를 아예 메소드화 시켜서 빼버리면 장황한 코드가 함수하나로 표현이 가능하다.

추가적으로 일급 컬렉션에 대한 내용은 묶어서 좀 더 풍성하게 구현해보겠습니다.