Clean Code 3rd 스터디

leocodms·2023년 5월 2일
0

BackEnd

목록 보기
6/6

7장. 오류 처리

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야한다.
오류처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

오류 코드보다 예외를 사용하라

우리에겐 해당하지 않는 내용.

미확인(unchecked) 예외를 사용하라

  • Java의 예외

Checked Exception

  • 장점
    • 에러처리의 강제화 : 개발자가 오류를 무시하거나 적절하게 처리하지 못하는 것을 방지합니다.
    • 관심사의 분리 : 어플리케이션의 핵심 비즈니스 로직과 에러 핸들링 로직을 분리하여 코드를 모듈화 함.
    • 더 나은 문서화 : 코드를 더 자체 문서화할 수 있도록 함.
  • 단점
    • OCP 위반 : 하위 단계의 변경으로 인해 상위 단계의 모든 메서드 선언부를 수정해야 함.

    • 캡슐화 깨짐 : 모든 함수가 최하위 함수에서 던지는 예외를 알아야 함.

      public void a() throws FileNotFoundException { // 또 예외를 잡고
          b();
      }
       
      private void b() throws FileNotFoundException { // 또 예외를 잡고
          c();
      }
       
      private void c() throws FileNotFoundException {
          FileInputStream stream = new FileInputStream("fileName");
      }

→ 때로는 확인된 예외가 유용하기도 하지만, 일반적으로 의존성이라는 비용이 이익보다 크다.

예외에 의미를 제공하라

  • 오류 메시지에 정보를 담아 예외와 함께 던진다.
  • catch 블록에서 정보를 기록하도록 충분한 정보를 넘겨준다. → 오류를 정의할 때 가장 중요한 관심하는 오류를 잡아내는 방법이 되어야 한다.

흐름을 끊지 말자!

public UserLevel getUserLevel(Long id) {
    try {
        User user = userRepository.findById(id);
        return user.getLevel();
    } catch (UserNotFoundException e) {
        return UserLevel.BASIC;
    }
}

-> user를 id로 찾아와서 있으면 getLevel을 없으면 BASIC을 리턴해라
: try/catch로 되어있어서 코드를 이해하는 흐름을 방해

public UserLevel getUserLevelOrDefault(Long id) {
    User user = userRepository.findById(id);
    
    if (user == null) {
        return UserLevel.BASIC;
    } else {
        return user.getLevel();
    }
}

→ 예외를 없애고 흐름에 따라 읽히게 사용

특수 사례 객체 사용 가능.

null을 반환하지 마라

List<Employee> employees = getEmployees();

if (employee != null) {    
	for(Employee e : employees) {        
		totalPay += e.getPay();    
	}
}

직원 리스트를 가져와 총 급여를 구하는 로직

→ getEmployees가 null을 반환해 if 절이 생겼는데 굳이 null을 반환할 필요가 있을까?

List<Employee> employees = getEmployees();

for(Employee e : employees) {
    totalPay += e.getPay();
}

/////////////////

public List<Employee> getEmployees() {
	if (직원이 없다면)
		return Collections.emptyList();
}

null인 경우 Collections.emptyList()를 리턴해주게 변경 : 특수 사례 객체

→ NullPointeException이 발생할 가능성이 줄어들었다.

null을 전달하지 마라

  • 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피해라.
public double xProjection(Point p1, Point p2) {
    return (p2.x - p1.x) * 1.5
}

→ NullPointerException의 가능성 있다.

public double xProjection(Point p1, Point p2) {
    if (p1 == null || p2 == null) {
        throw InvalidArgumentException("~~");
    }
    return (p2.x - p1.x) * 1.5
}

→ 새로운 예외 유형으로 핸들링

: RuntimeException 발생 보다는 조금 나으나, 예외 처리를 해주어야 한다.

public double xProjection(Point p1, Point p2) {
    assert p1 != null : "p1 should not be null";
		assert p2 != null : "p2 should not be null";
    return (p2.x - p1.x) * 1.5
}

→ assert 문을 사용해 핸들링

: 코드 읽기는 편하나, null 전달 받으면 여전히 RuntimeException 발생

Kotlin의 Null 처리

NullPointerException이 발생할 수 있는 경우

  • throw NullPointerException 명시적 호출
  • ‼️ 연산자 사용
  • 자바 상호 작용
  • 초기화에 관한 데이터 불일치
    • 생성자에서 사용할 수 있는 초기화되지 않은 this가 전달되어 어딘가에 사용되는경우("leaking this")
    • superclass 생성자는 파생 클래스에서 구현이 초기화되지 않은 상태를 사용하는 open member를 호출

Kotlin에서의 null check

  1. 조건문으로 Null Check
val l = if (b != null) b.length else -1
val b: String? = "Kotlin"

if (b != null && b.length > 0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}
  1. Null Safe 연산자 ?.
val a = "Kotlin"
println(a?.length) // Unnecessary safe call

val b: String? = null
println(b?.length) 
  1. 엘비스 연산자 ?:
// if 조건문으로 처리한 경우
val l: Int = if (b != null) b.length else -1

// 동일한 코드를 ?: 연산자 사용 시
// null인 경우 default 값 할당
val l = b?.length ?: -1
  1. 강제 Not Null 처리 연산자 !!
// 이렇게 해 놓고 null을 넣으면 NPE가 발생
val l = b!!.length
  1. Safe Cast as?
// casting을 시도하고, casting이 불가능 하면 null을 반환
val aInt: Int? = a as? Int
  1. Collection의 Nullable Type
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
  1. let 함수
fun sendEmailTo(email: String) {
    println("Sending email to $email")
}

// not null인 경우 특정 구문을 수행
fun main(args: Array) {
    var email: String? = "yole@example.com"
    email?.let { sendEmailTo(it) }
    email = null
    email?.let { sendEmailTo(it) }
}
  1. lateinit

not null이지만 초기화 작업을 바로 하지 않아도 됨.

11장. 시스템

시스템 역시 깨끗해야 한다.
깨끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다.

깨끗한 시스템

  • Main 분리
  • Factory
  • 의존성 주입(Dependency Injection)
  • 모든 추상화 단계에서 의도는 명확히 표현해야 한다.
    • POJO를 작성
    • 구현 관심사를 분리

12장. 창발성

= 떠오름
하위 계층(구성 요소)에는 없는 특성이나 행동이 상위 계층(전체 구조)에서 자발적으로 돌연히 출현하는 현상

단순한 설계 규칙 1: 모든 테스트를 실행하라

단순한 설계 규칙 2~4: 리팩터링

  • 중복을 없애라
  • 개발자의 의도를 표현하라
  • 클래스와 메서드 수를 최소로 줄여라
profile
Backend Developer

0개의 댓글