개발독서/코드품질

[개발독서] 내 코드가 그렇게 이상한가요 (9장 설계의 건정성)

보리시스템 2024. 5. 3.
<더 생각해보기>
- [전체] 11가지 경우와 관련해 실제로 겪은 문제 상황이 있었는지?
- [전역 변수] '영향 범위를 최소화하도록 설계했지만 중복 코드가 많아진 경우' VS '전역 변수 등의 사용으로 영향 범위가 넓은 경우'?
- [메타 프로그래밍] 실제로 메타 프로그래밍이 사용 된 경험? 사용한 이유? 장단점?
- [패키지 구조] '기술 중심(계층형) VS 비즈니스 중심(도메인형)', 기술 중심 패키징이 더 효율적인 경우는?

 


 

9장 설계의 건전성을 해치는 여러 악마
9.1 데드 코드
9.2 YAGNI 원칙
9.3 매직 넘버
9.4 문자열 자료형에 대한 집착
9.5 전역 변수
9.6 null 문제
9.7 예외를 catch하고서 무시하는 코드
9.8 설계 질서를 파괴하는 메타 프로그래밍
9.9 기술 중심 패키징
9.10 샘플 코드 복사해서 붙여넣기
9.11 은 탄환

 


 

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

9.1 데드 코드

실행되지 않는 불필요한 코드는 발견 즉시 삭제하자.
단 코드 변경 이력 관리는 필수!

 

의미
절대로 실행되지 않는 조건 내부에 있는 코드


=> 불필요한 코드
=> 데드 코드(dead code) = 도달 불가능한 코드(unreachable code)

발생문제
1. 코드 가독성 저해

=> 실행되지 않는 코드를 왜 남겨두었는지, 다른 의도가 있는 건지 등 다른 개발자의 혼란 발생

2. 버그가 될 가능성
=> 사양 변경에 의해 코드가 실행돼 버그가 될 수 있음

해결방법
발견 즉시 삭제

=> 단 깃허브 등 변경 이력 관리를 통해 코드 제거에 따른 발생 문제를 방지할 수 있음

 

  • 예시 코드
// <나쁜 코드> addPoint()는 실행되지 않는 데드 코드임
if (level > 99) {
  level = 99;
}

...

if (level == 1) {
  initPoint();
}
else if {
  addPoint();
}

 

<더 보면 좋을 내용>
런타임 데드 코드 분석 도구 Scavenger - 당신의 코드는 생각보다 많이 죽어있다.

*유튜브 영상링크 https://www.youtube.com/watch?v=qE7HY7Y-5vs

 

 


 

9.2 YAGNI 원칙

당장에 필요한 기능 구현에만 집중하자
미리 만든 기능은 결국 데드 코드, 시간 낭비가 된다!

 

의미
"You Aren't Gonna Need it." (지금 필요 없는 기능은 만들지 말라)

=> 개발 시 미래를 예측하고 기능을 미리 만들어 두는 경우가 있음

발생문제
1. 소프트웨어에 대한 요구는 매번 바뀌기 때문에 예측이 틀릴 때가 많음

=> 데드 코드 발생
=> 이렇게 만들어지는 로직은 일반적으로 복잡하므로 가독성 저해, 혼란 발생에 따라 버그 발생 가능성이 높아짐

2. 결국 시간 낭비가 됨
=> 지금 필요한 기능을 최대한 간단한 형태로 만드는 것이 가독성/유지보수성을 높임

해결방법
YAGNI

 


 

9.3 매직 넘버

의미를 알기 힘든 숫자는 상수를 활용하자

 

의미
로직 내부에 직접 작성되어 있어 의미를 알기 힘든 숫자

발생문제
1. 숫자의 의도를 알기 힘듦

=> 구현자 본인만 그 의도를 알 수 있음

2. 동일한 값이 여러 위치에 등장해 중복 코드를 만들어 냄
=> 사양 변경으로 숫자를 바꿔야 할 때 실수하면 버그가 됨

해결방법
상수를 활용!

=> 단 서비스를 바로 동작시키기 위한 목적으로 매직 넘버를 사용하는 경우, 커밋 시에 반드시 상수로 변경하기

 

  • 예시 코드
// <나쁜 코드> 설명이 없다면 100이라는 숫자의 의도를 알기 힘듦
class MagicNumber {
	boolean isOk() {
    	return 100 <=value;
    }
	...
}

// <좋은 코드> 100이라는 숫자를 TRIAL_READING_POINT라는 상수로 정의함
class ReadingPoint {
    private static final int MIN_POINT = 0; // 최소 포인트
    private static final int TRIAL_READING_POINT = 100; // 트라이얼용 포인트
    final int value; // 포인트 값
    
    ...
    
    boolean canTryRead() {
    	return TRIAL_READING_POINT <= value;
    }
    
    ...
}

 


 

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

의미가 다른 값은 각각 다른 변수에 저장하자

의미
의미가 다른 여러 값을 하나의 String 변수에 무리하게 넣는 경우

발생문제
1. 의미를 알기 어려움


2. split 메서드 등의 활용이 필요해 로직이 복잡해지고 가독성이 떨어짐
=> 읽어 들인 CSV 파일에서 데이터를 추출할 때 split 메서드를 사용해야 하는 경우가 있겠음

해결방법
의미가 다른 값은 각각 다른 변수에 저장

 

  • 예시 코드
// <나쁜 코드> 하나의 String 변수에 여러 값을 저장함(제목 문자열, 가격)
String title = "보리시스템,10000";

// <좋은 코드> 의미가 다른 값은 각각 다른 변수에 저장함
String title = "보리시스템";
int price = 10000;

 


 

9.5 전역 변수

전역 변수처럼 영향 범위가 넓은 구조는 최대한 지양하자

의미
모든 곳에서 접근할 수 있는 변수

=> 여러 곳에서 호출할 수 있는 구조이고, 호출되기 쉬운 구조임

발생문제
1. 어디에서 어떤 시점에 값을 변경했는지 등 파악하기 어려움


2. 전역 변수를 참조하고 있는 로직 변경 시 해당 변수를 참조하는 다른 로직에서 버그 발생 여부를 검토해야 함

3. 동기화가 필요한 경우에도 문제가 발생하고 이에 따라 데드락 상태에 빠질 수 있음
=> 동기화는 제대로 설계하지 않으면 락을 얻기 위해 대기하는 시간이 길어져 성능을 크게 떨어뜨림

4. 거대 데이터 클래스도 전역 변수와 같은 성질을 띠기 때문에 2, 3번의 문제가 발생함

해결방법
영향 범위가 최소화되도록 설계하기

=> 관계없는 로직에서는 접근할 수 없게 설계하기
=> 최대한 한정된 클래스에서만 접근할 수 있는 형태로 설계하기

 

  • 예시 코드
// <나쁜 코드> 자바 언어에는 전역 변수가 없지만, public static으로 변수를 선언하면 모든 곳에서 접근할 수 있음
public ClassName {
    public static int GlobalVariableName;
}

 


 

9.6 null 문제

애초에 null을 다루지 않게 하자 (null은 리턴도 전달도 X)

의미
배송지가 미입력 된 상태 등을 null로 표현하는 경우

=> null은 애초에 초기화하지 않은 메모리 영역에서 값을 읽을 때 발생하는 문제를 피하기 위해 만들어진 것
=> 다시 말해, 메모리 접근 관련 문제를 방지하기 위한 최소한의 구조로서 null 자체가 '잘못된 처리'를 의미함

발생문제
예외가 발생하지 않도록 모든 곳에서 null 체크를 해야 함

=> 가독성이 떨어지고, 실수로 null을 체크하지 않은 곳에는 버그가 발생 

해결방법
1. null을 리턴/전달하지 않는 설계로 만들기


2. null 안전 자료형 사용하기
=> null 안전 자료형? null을 아예 저장할 수 없게 만드는 자료형
=> 일부 프로그래밍 언어(코틀린 등)의 경우 모든 자료형에 null 안전 자료형을 사용해 null을 할당하는 코드는 컴파일조차 되지 않음

 

  • 예시 코드 (캐릭터의 방어력과 방어구 장비의 방어력 등을 모두 더해 그 합계를 리턴하는 메서드)
// <나쁜 코드> 방어구 장비를 장착하지 않은 상태를 null로 표현하는 경우 null 체크 코드가 여기저기 필요함
class Member {
  private Equipment head;
  private Equipment body;
  private Equipment arm;
  
  ...
  
  // 모든 방어구 장비 해제
  void takeOffAllEquipments() {
    head = null;
    body = null;
  }
  
  ...
  
  // 방어구의 방어력과 캐릭터의 방어력을 합산해 리턴 => null 체크 코드가 필요
  int totalDefence() {
    int total = defence;
    
    if (head != null) {
      total += head.defence;
    }
    if (body != null) {
      total += head.defence;
    }
    
    return total;
  }
  
  // 다른 위치에서도 null 예외가 발생함 => null 체크 코드가 필요
  void showBodyEquipment() {
    if (body != null) {
      showParam(body.name);
      showParam(body.defence);
    }
  
  ...
  }
}

// <좋은 코드> null 사용 안하기
class Equipment {
  static final Equipment EMPTY = new Equipment("장비없음",0,0);
  
  final String name;
  final int defence;
  ...
  
  Equipment(final String name, final int defence){
    if (name.isEmpty()) {
      throw new IllegalArgumentException("잘못된 이름입니다.");
    }
    
    this.name = name;
    this.defence = defence;
  }
}

class Member {
  ...
  
  // 모든 방어구 장비 해제
  void takeOffAllEquipments() {
    head = Equipment.EMPTY;
    body = Equipment.EMPTY;
  }
  
  ...
  }

 


 

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

try-catch 통해 catch한 예외는 바로 통지, 기록하도록 처리하기

의미
try-catch로 예외를 catch해놓고 별다른 처리를 하지 않아 예외를 무시하는 코드

발생문제
오류가 나도 어느 시점에 어떤 코드에서 문제가 발생했는지 찾기 어려움

=> DB 레코드, 로그, 관련 코드를 하나하나 확인해야 원인 파악이 가능함

해결방법
예외를 catch하면 바로 통지, 기록하기 (필요 시 바로 복구하기)

 

  • 예시 코드
// <나쁜 코드> 예외를 catch하지만 예외에 대한 처리가 없음
try {
  reservations.add(product);
}
catch (Exception e) {
}

// <좋은 코드> catch문에서 예외 처리하기
try {
  reservations.add(product);
}
catch (Exception e) {
  reportError(e); // 오류 보고, 로그 기록
  requestNotifyError("예약할 수 없는 상품입니다."); // 상위 레이어에 오류 관련 통지
}

 


 

9.8 설계 질서를 파괴하는 메타 프로그래밍

메타 프로그래밍은 시스템 분석 용도나 아주 작은 범위에서만 활용하기

의미
메타 프로그래밍을 용법/의도를 제대로 이해하지 못한 채로 사용한 경우

=> 메타 프로그래밍? 프로그램 실행 중 해당 프로그램 구조 자체를 제어하는 프로그래밍

발생문제
1. 리플렉션으로 인한 클래스 구조와 값 변경 문제

=> 리플렉션 API? 자바에서 메타 프로그래밍을 활용해 클래스 구조를 읽고 쓸 때 사용하는 것
=> 일반적인 프로그래밍에서는 접근할 수 없는 부분까지 접근할 수 있음
=> final로 지정한 변수 값도 바꿀 수 있고, private으로 외부 접근이 불가한 변수에도 접근할 수 있음

2. 자료형의 장점을 살리지 못하는 하드 코딩
=> 예시 코드 2번에서 상세 내용 확인

해결방법
메타 프로그래밍은 시스템 분석 용도로 한정하거나 아주 작은 범위에서만 활용해 리스크를 최소화하기

 

  • 예시 코드 (1. 리플렉션으로 인한 클래스 구조와 값 변경 문제)
// <나쁜 코드> 잘못된 값이 들어올 수 없도록 구조를 만들었으나, 리플렉션으로 값을 변경함
class Level {
  // 상수 MIN, MAX와 가드를 활용해서 레벨을 1~99 내에서만 지정할 수 있게 함
  private static final int MIN = 1;
  private static final int MAX = 99;
  // 인스턴스 변수 value에 final 수식자를 붙여 이후에 변경하지 못하도록 함
  final int value; 
  
  private Level(final int value) {
    if (value < MIN || MAX < value) {
      throw new IllegalArgumentException();
    }
    this.value = value;
  }
  
  // 초기 레벨 리턴
  static Level initialize(){
    return new Level(MIN);
  }
  
  // 레벨을 1씩 증가하도록 함
  Level increase(){
    if (value < MAX) return new Level(value + 1);
    return this;
  }
  
  ...
}

// 리플렉션을 사용해 값을 변경함
Level level = Level.initialize();
System.out.println("Level : " + level.value); // 출력값 Level: 1

Field field = Level.class.getDeclaredField("value");
field.setAccessible(true);
field.setInt(level, 999);

System.out.println("Level : " + level.value); // 출력값 Level: 999

 

  • 예시 코드 (2. 자료형의 장점을 살리지 못하는 하드 코딩)
// <나쁜 코드> 메타 정보로 인스턴스를 생성한 경우 문자열로만 인식해 클래스, 메서드 등 이름 변경 시 그 대상에서 제외됨
package customer;

// User 클래스
class User {
...
}

// 메타 정보로 인스턴스를 생성하는 메서드
static Object generateInstance(String packageName, String className)
throws Exception {
  String fillName = packageName + "." + className;
  Class klass = Class.forName(fillName);
  Constructor constructor = klass.getDeclaredConstructor();
  return constructor.newInstance();
}

// 메타 정보로 User 클래스 인스턴스 생성
User user = (User)generateInstance("customer", "User");

// => User 클래스 이름을 Employer로 변경한다고 하면?
// IntelliJ IDEA와 같은 IDE에는 클래스, 메서드 등의 이름을 한번에 변경해주는 기능이 있는데 
// 메타 정보로 User 클래스 인스턴스를 생성한 경우 "User"는 User 자료형으로 인식 못해 이름 변경 안됨
// 변경된 결과는 Employer user = (Employer)generateInstance("customer", "User");

 


 

9.9 기술 중심 패키징

비즈니스 개념을 기준으로 폴더를 구분하자 (계층형보다는 도메인형 패키지 구조로)

의미
구조에 따라 폴더와 패키지를 나누는 것

=> 레일스, 장고, 스프링 등 여러 웹 프레임워크에서 채택하는 MVC 아키텍처가 그 예

발생문제
비즈니스 개념을 나타내는 클래스인 비즈니스 클래스를 기술 중심 패키징에 따라 폴더를 구분하면 그 관련성을 알기 매우 힘들어짐
=> 파일 단위로 묶여 응집도가 낮아짐

해결방법
재고, 주문, 결제 등 비즈니스 개념을 기준으로 폴더를 구분하기

=> 이렇게 구성하면 재고 유스케이스에서만 사용되는 '안전 재고량 클래스'를 package private으로 만들 수 있음
=> 또한 주문, 결제 등 관계없는 유스케이스에서 참조할 위험을 방지할 수 있음
=> 사양이 달라지는 경우에도 해당 폴더 내부의 파일만 읽으면 됨

 


 

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

샘플 코드는 설명용 코드일 뿐! 참고만 하자

의미
공식 문서, 기술 커뮤니티 사이트, 블로그 등에서의 샘플 코드는 언어의 사양과 라이브러리의 기능을 설명하기 위해 작성된 것임

발생문제
그대로 샘플 코드를 사용하는 경우 설계 측면에서 좋지 않은 구조가 되기 쉬움

해결방법
샘플 코드는 참고만 하고, 클래스 구조를 잘 설계해 사용하기

 


 

9.11 은 탄환

은 탄환(Silver bullet, 문제 해결을 위한 비장의 무기/묘책)은 없다
설계에 Best라는 것은 없다. 항상 Better를 목표로 할 뿐이다

의미
현실에서 발생하는 여러 문제는 특정 기술 하나로 해결할 수 있을 정도로 단순하지 않음

=> 실험적인 목적으로 개발한 프로토타입, 사양 변경을 할 필요가 없는 소프트웨어 등에 대해서도 이 책의 설계를 적용하면?

발생문제
상황을 고려하지 않고 자신이 알고 있는 편리한 기술을 활용하면 문제가 오히려 심각해질 수도 있음

=> 쓸데 없이 설계 비용만 커질 뿐...

해결방법
문제 발생 시 어떤 방법이 효과적인지, 비용이 더 들지는 않는지 평가하고 판단하는 자세 가지기