9장 - 설계의 건전성을 해치는 악마

9.1 데드 코드

if (level > 99) {
	level = 99;
}

// 생략

if (level === 100) {
	addSpecialAblity();
}

이렇게 절대로 실행되지 않는 조건 내부에 있는 코드를 데드 코드 혹은 도달 불가능한 코드라고 함

이 악마는 겉보기에는 큰 문제가 나지 않을 것 같더라도 여러 가지 폐해를 가져오기 쉬움

코드의 가독성을 떨어트림 - 해당 코드를 읽는 사람이 데드 코드 주변을 읽을 때마다 어떤 조건에서 실행되는지 생각하게 만들고 어떤 의도로 만들었고 왜 그냥 두었는지 혼란스럽게 만듦

언젠가 버그가 될 가능성이 있음 - 지금까지는 실행되지 않던 데드 코드가 사양 변경에 의해 도달 가능한 코드로 바뀔 수 있음

그러므로 데드 코드는 발견 즉시 제거하는 것이 좋고, 깃허브 등으로 변경이력을 관리하면 코드를 제거했을 때 발생할 수 있는 문제를 걱정하지 않아도 됨


9.2 YAGNI

실제 개발을 할 때 미래를 예측하고 미리 만들어 두는 코드가 있음
하지만 이렇게 미리 만들어둔 로직은 실제로 거의 사용되지 않고 버그의 원인이 되기도 함

YAGNI 소프트웨어 원칙

'You Aren't Gonna Need it.'의 약자로 '지금 필요 없으면 하지말라!'는 소프트웨어 원칙

그럼 YAGNI 원칙을 지키지 않으면 어떤 문제가 발생할까?

소프트웨어에 대한 요구는 매일 변하므로 사양이 확정되지 않고 명확하게 언어화 되지 않은 요구를 미리 예측하고 구현해도 이러한 예측은 대부분 맞지 않음

예측에 들어맞지 않은 로직은 데드 코드가 되고, 이런 코드들은 대부분 복잡함
그렇기에 데드코드와 마찬가지로 가독성을 낮추고 읽는 사람에게 혼란성을 야기할 가능성이 큼

그리고 코드에 변경이 존재하여 데드 코드가 실행되면 버그가 발생할 가능성이 굉장히 높음
왜냐하면 이렇게 만들어진 로직은 사양에는 없기 때문

예측해서 코드를 만드는 것도 결국엔 시간 낭비기 때문에 지금 필요한 기능을 최대한 간단한 형태로 만드는 것이 가독성과 유지 보수성을 높이는 것

"불필요한 작업을 하지 않을수록 중요한 작업에 집중할 수 있음"


9.3 매직 넘버

로직 내부에 직접 작성이 되고 의미를 알기 힘든 숫자, 구현자 본인만 의미를 아는 숫자를 매직 넘버라고 함

class Comic {
	boolean isOk() {
    	return 60 <= value;
    }
    
    void tryConsume() {
    	int tmp = value - 60;
        value = tmp;
    }
}

이렇게 60이라는 숫자가 자주 등장 하는데 어떤 설명도 없으면 읽는 사람은 저 상수가 어떤 의미를 내포하는지 알 수 없음

이렇게 매직 넘버는 동일한 값이 여러 위치에 등장하여 중복 코드를 만들어낼 뿐만 아니라 60에서 50으로 변경될시에 매직 넘버를 쓴 부분을 하나하나 확인하며 수정해야 함
수정을 하다 실수라도 발생한다면, 곧바로 버그가 됨

매직 넘버를 사용하지 않으려면, 상수를 활용하면 되고 final (JS에선 const)를 활용하여 변수로 정의하면 훨씬 알기 쉬워짐

// 요로케이르케이러케 (특정 유튜버 모사 아님)
const TRIAL_READING_POINT= 60 

이렇게 상수로, 변수로 만들어야하는지에 대한 이야기는 생략하겠음


9.4 문자열 자료형에 대한 집착

다음과 같은 String 자료형이 존재한다고 생각해보자

// 레이블, 표시 색, 최대 문자 수
String title = "타이틀,255,250,240,64";

읽어 들인 CSV 파일에서 데이터를 추출할 때 split 메서드를 활용하여 추출하지만
그런 용도가 아닌데 의미 다른 여러 개의 값을 하나의 String 변수에 무리하게 넣으면 의미를 알기 어려움

이로 인해 가독성이 크게 저하되기 때문에 앞 장 (5.5.1절)에서 나온 것처럼 기본 자료형에 대한 집착은 클래스뿐만이 아니라 변수마저 추가하지 않으려는 경향 때문에 이런 코드가 생겨남

의미가 다른 값은 각각 다른 변수에 저장하는 것이 좋음


9.5 전역 변수

자바 언어 사양에서는 전역 변수가 없지만 코드 9.5처럼 변수를 public static으로 선언하면 모든 곳에서 접근할 수 있음

자명하듯이 여러 로직에서 전역 변수를 참조하고 값을 변경하면, 어디에서 어떤 시점에서 값을 변경했는지 파악하기 굉장히 힘들고 전역 변수를 참조하는 로직을 변경해야 할 때 해당 변수를 참조하는 다른 로직에서도 버그가 발생함

동기화가 필요한 경우에도 문제가 발생,
제대로 설계하지 않으면 락을 얻기 위해 대기하는 시간이 길어져 퍼포먼스를 크게 떨어뜨려, 동기화에 문제가 생기면 데드락 상태에 빠질 수 있음

데드락(Dead lock)

데드락은 CS에서 다수의 프로세스나 스레드가 서로 상호 작용하며 발생할 수 있는 상태 중 하나로, 다음 조건이 동시에 충족될 때 발생


1. 상호 배제 : 자원은 한 번에 하나의 프로세스만 사용 가능
2. 점유 대기 : 프로세스가 이미 어떤 자원을 보유하고 있으면서, 다른 프로세스가 보유한 자원을 대기하고 있는 상태
3. 비선점 : 프로세스가 스스로 자원을 내려놓기 전 자원을 강제로 빼앗을 수 없는 상태
4. 순환 대기 : 프로세스 간의 자원 요청 순환 그래프가 형성 되어야함

이런 조건들이 모두 충족될 때, 프로세스들은 서로가 가진 자원을 기다리며 무한 대기 상태에 빠지게 되고 이것을 데드락이라 함

설계가 제대로 이루어지지 않은 시스템에서는 거대 데이터 클래스가 아주 쉽게 만들어지고
전역 변수를 직접적으로 사용하지 않더라도, 전역 변수와 같은 개념을 알게 모르게 사용하게 되는 것

전역변수로 만들기 보다는 최대한 한정된 클래스에서만 접근할 수 있는 형태로 설계하는 것이 좋음


9.6 null 문제

모든 null에 관해 예외처리를 하기위해 예외처리를 하다보면 수 많은 null 체크가 만들어지고
결국 null 체크 코드가 너무 많아져서 가독성이 떨어질 것이고 실수로 null체크를 안 하는 곳이 생기면 곧바로 버그가 될 것임

애초에 null이란?
초기화 하지 않은 메모리 영역에서 값을 읽으면 문제가 되기 때문에 이를 피하기 위해 null이 발명된 것
null은 메모리 접근과 관련된 문제를 방지하기 위한 최소한의 구조로서,

null 자체가 '잘못된 처리'를 의미

방어구를 장비하지 않은 상태, 상품명이 설정되지 않거나 배송지가 입력되지 않은 상태를 null로 표현하는 코드가 많고 '무언가 갖고 있지 않은 상태'와 '무언가 설정되지 않은 상태'는 그 자체로 의미가 있는 훌륭한 상태인데
null은 이러한 상태조차 존재하지 않음을 뜻함 (공허 그 자체..)

그렇게 때문에 null을 이런 형태로 사용한다면 큰 솔실을 불러일으킬 수 있음

일반적으로 변수에 값이 없음을 나타낼 땐 대신 undefined를 사용하는 것이 더 바람직
만약 변수를 초기화하고 값이 없음을 나타내려면 아무런 값도 할당하지 않은 것이 자연스러운 방법

9.6.1 null을 리턴/전달하지 않기

null 체크를 하지 않으려면 애초에 null을 다루지 않게 만들어야함

구체적으로는 다음을 만족해야한다는 것

  • null을 리턴하지 않는 설계 : null을 리턴하지 않게 작성
  • null을 전달하지 않는 설계 : null을 변수에 할당하지 않는 설계

null 안전이라는 기능도 존재함 (null이 들어가면 아예 허용하지 않고 컴파일 오류)


9.7 예외를 catch하고서 무시하는 코드

try {
	뭔가();
} catch (Excepction e) {
	// 아무것도 하지 않음
}

위 코드는 try-catch로 예외를 catch해놓고도 별다른 처리를 하고 있지 않음
이렇게 예외를 무시하는 코드는 굉장히 사악한 로직

9.7.1 원인 분석을 어렵게 만듦

이러한 코드의 문제는 에러가 나도, 잘못된 상태에 빠져도 외부에서는 아무런 문제가 없는 것처럼 보이게 만듦
잘못된 데이터를 이용 하다가도 데이터가 만들어질 가능성도 존재함

예외를 무시하면, 잘못된 상태를 바로 파악할 수 없고 이후 서비스 사용자에 의해 보고될 가능성이 큼

하지만 이러한 문제가 보고됐다 하더라도 예외를 catch하고도 무시했기 때문에 개발자 본인도 어디에서 문제가 됐는지 알 수 없는 상황이 발생함

결국 DB 레코드, 로그, 관련 코드를 하나하나 확인한 후에야 원인을 알 수 있을 거임

9.7.2 문제가 발생했다면 소리치기

이러한 상황을 피하기 위해선 잘못된 상태에 대해 관용을 베풀면 안 됨

잘못된 상태에서 계속 처리를 하는 것은 도화선에 불이 붙은 폭탄을 들고 돌아다닌 것과 마찬가지이므로 도화선에 불이 붙은 것을 확인 했다면 바로 불을 끄는 행위를 해야만 함

그러므로 예외를 확인 했다면 곧바로 통지, 기록하는 것이 좋음

3.2.1절의 가드가 있는 생성자도 마찬가지로 잘못된 상태를 막아주는 설계이기 때문에, 문제가 발생하는 즉시 소리쳐서 잘못된 상태를 막는 것이 좋은 구조라고 할 수 있음


9.9 기술 중심 패키징

패키지를 구분할 때 폴더를 적절하게 나누지 않으면 악마가 옴 (그만 좀와!)

만약 다음과 같은 온라인 쇼핑몰의 폴더 구조가 있다고 하면

  • 디자인 패턴별로 구분한 폴더

디자인 패턴에는 이전에 다루었던 값 객체 이외에도 유스케이스를 표현한 유스케이스 패턴, 고유성을 책무로 하는 엔티티 패턴 등이 있음

이 폴더 구성은 디자인 패턴에 따라서 분류한 것

주문처는 주문과 관련이 있을 것이지만 사실 재고에 사용 되고 있다고 가정시에
이름만으로는 이를 구분하기 어려우므로
어느 순간부터 갑자기 발주 금액이 주문 유스케이스에서 사용될 가능성이 있음 ==> 이는 버그가 발생

이렇게 원래 용도와 다른 로직들이 섞이면 로직이 복잡하고 혼란스러워질 것

현재 파일들을 보면 크게 재고, 주문, 결제 용도로 나눌 수 있지만
이를 디자인 패턴에 따라 구분했기 때문에 어떤 것이 어떤 종류와 관련됐는지 구분하기 힘듦

이처럼 구조에 따라 폴더와 패키지를 나누는 것을 기술 중심 패키징이라고 함

레일스나 장고, 스프링 같은 여러 웹 프레임워크는 MVC 아키텍처를 사용하는데
이와 같은 형태를 기술 중심 패키징이라고 할 수 있음

프레임워크에 표준적인 구조가 기술 중심 패키징이다보니, 이에 따라서 폴더 구조를 기술 중심 패키징에 맞게 구성하기 쉬움

위 파일들 중에서 장바구니 엔티티, 안전 재고량 등 비즈니스 개념을 나타내는 클래스를 비지니스 클래스라고 부름

이러한 비지니스 클래스들을 기술 중심 패키징에 따라 폴더로 나눠 구분하면, 관련성을 알기 매우 힘들어지고
파일 단위로 묶여 응집도가 낮아짐

이렇게 되면 재고 유스케이스에서만 사용되는 안전 재고량 클래스를 package private으로 만들 수 있고 주문과 결제 등 관계없는 유스케이스에서 참조할 위험을 방지할 수 있음

또한, 관련된 개념끼리 모여있으므로

결제와 관련된 사양이 달라졌을 때 결제 폴더 내부의 파일만 읽으면 됨
관련 파일을 이리저리 찾아다니지 않아도 됨


9.10 샘플 코드 복사해서 붙여넣기

공식 사이트에는 언어 사양과 라이브러리가 문서가 있고 거기엔 샘플 코드가 존재함
물론 기술 커뮤니티 사이트, 개발자 개인 블로그에서도 설명과 함께 샘플 코드를 제공할 것

하지만 이때, 샘플 코드를 그대로 복사하고 붙여넣기 하면 설계 측면에서 좋지 않은 구조가 됨

샘플 코드는 어디까지난 설명하기 위해 작성된 것이므로 유지보수성과 변경 용이성까지 생각해서 작성된 코드가 아님

'샘플 코드 보고 따라해야징 룰루랄랑 저 공식문서 보고했음!' 라고 하는 순간 그냥 이상한 로직이라 바아로 악마가 옴 (또..!)

그러니까 샘플코드는 참고만 하고, 클래스 구조를 잘 설계해서 써야함


9.11 은 탄환

새로운 기술과 방법을 익히면 곧바로 쓰고 싶어짐
얘를 쓰면 문제가 바로 해결될 것처럼 매력적이기 때문

하지만 단순하지 않고 대부분 매우 복잡하게 얽혀있으므로 상황을 고려하지 않고
'내가 알고 있는 편리한 기술'을 활용하면 문제가 해결되기는 커녕 오히려 더 심각해질 수 있음

소프트웨어 설계에 GoF의 디자인 패턴이라는 저명한 책이 있음
이 책보고 괜히 새로 배운 디자인 패턴을 적용했다가 유지보수를 하는 사람만 기능 확장을 하며 고생만 할 수 있음

은 탄환 (비장의 무기, 묘책)은 소프트웨어 개발엔 존재하지 않음

이 책은 사양 변경이 있을 때 조금이라도 쉽게 하기 위한 설계를 설명
그렇기 때문에 사양 변경이 없으면 쓸데없이 설계 비용만이 커짐


중요한 것은!!!

어떤 문제가 있을 때, 어떤 방법이 해당 문제에 효과적인지 비용이 더 들지는 않는지 평가하고 판단하는 자세

문제와 목적을 머릿속에 새겨 두고, 적절한 기술을 선택할 수 있도록 노력하면 됨

설계엔 Best란 없고, 항상 Better를 목표로 할 뿐...

profile
기록, 꺼내 쓸 수 있는 즐거움

0개의 댓글