클린 코드 7장 - 오류 처리

French Marigold·2023년 12월 4일
0

클린코드

목록 보기
7/13

  • 오류 처리 코드는 중요하지만 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다. 그러므로 우아하게 오류 코드를 처리하는 방법 또한 배워야 한다. 다음은 깔끔하게 오류 코드를 처리할 수 있는 고려 사항들이다.

오류 코드보다 예외를 사용하라 (130p) → 예외를 do try catch 구문으로 이해하면 될 듯

  • 오류를 처리하는 메소드를 이용해 오류 코드를 처리하는 방법
    • sendShutDown() 메소드 안에 1) 디바이스를 종료하는 알고리즘과 2) 오류를 처리하는 알고리즘이 뒤섞여 코드가 복잡하다.
final class DeviceController {
    func sendShutDown() {
        let handle = getHandle(dev1: DEV1)
        // DeviceHandel이 invalid한 상태가 아니라면 
        if handle != DeviceHandle.invalid {
            // 레코드 필드에 디바이스 상태를 저장한다.
            retrieveDeviceRecord(handle: handle)
            // 디바이스가 임시정지 상태가 아니라면 종료한다.
            if record.status != DEVICE_SUSPENDED {
                pauseDevice(handle: handle)
                clearDeviceWorkQueue(handle: handle)
                closeDevice(handle: handle)
            } else {
                logger.log(message: "Device suspended. Unable to shut down")
            }
        } else {
            logger.log(message: "Invalid handle for: \(DEV1)")
        }
    }
}
  • do try catch 구문을 통해 오류를 처리하는 방법 (오류 처리는 do try catch 구문을 사용하기) ⭐️⭐️
    • do try catch 구문을 사용하면 디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분류하여 훨씬 품질이 좋은 코드가 완성된다.
final class DeviceController {
    func sendShutDown() {
		// 오류를 처리하는 알고리즘 
        do {
            try tryToShutDown() // 오류가 없으면 디바이스를 종료 
        } catch let error as DeviceShutDownError {
			// 오류가 있으면 에러 메세지를 날림 
            logger.log(message: error.localizedDescription) 
        } catch {
			// 에러긴 한데 무슨 에러인지 모르겠을 때, print문을 날림
            print("Unexpected error: \(error).")
        }
    }

	// 디바이스를 종료하는 알고리즘
    private func tryToShutDown() throws {
        guard let handle = getHandle(devID: DEV1) else {
            throw DeviceShutDownError.invalidHandleError("Invalid handle for: \(DEV1)")
        }
        let record = retrieveDeviceRecord(handle: handle)
        pauseDevice(handle: handle)
        clearDeviceWorkQueue(handle: handle)
        closeDevice(handle: handle)
    }

    private func getHandle(devID: DeviceID) throws -> DeviceHandle? {
        throw DeviceShutDownError.invalidHandleError("Invalid handle for: \(devID)")
    }
}

Try-Catch-Finally 문부터 작성하라 (132p)

  • 먼저 강제로 do try catch를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다.
// Swift에서는 예외를 던지는 것이 일반적이지 않지만, 
// 여기서는 Java의 예외 처리 스타일을 유지하기 위해 Error 프로토콜을 이용한다.
// 또한, Swift의 XCTest 프레임워크는 예외를 기대하는 테스트를 직접 지원하지 않으므로, 
// 여기서는 오류가 발생하면 테스트가 실패하는 것으로 가정하겠음.
// Swift의 XCTest는 테스트가 예외를 던질 것으로 예상할 수 없으므로, 
// 대신 XCTAssertThrowsError를 사용하여 테스트가 예외를 던지는지 확인할 것임.

import XCTest

// 테스트 케이스를 작성 
final class SectionStoreTests: XCTestCase {
		var sectionStore: SectionStore!
    
    override func setUpWithError() throws {
        try super.setUpWithError()
        sectionStore = SectionStore()
    }
    
    override func tearDownWithError() throws {
        sectionStore = nil
        try super.tearDownWithError()
    }

    func testRetrieveSectionShouldThrowOnInvalidFileName() {
        XCTAssertThrowsError(try sectionStore.retrieveSection(sectionName: "invalid - file")) { error in
			 // 4. 에러가 StroageException에 속한 타입인지 검증      
			XCTAssertTrue(error is StorageException) 
        }
    }
}

// 구현 코드
class SectionStore {
		
		// 1. testRetrieveSectionShouldThrowOnInvalidFileName() 테스트 코드가 실행되면 
		// "invalid - file" 이라는 sectionName이 retrieveSection(sectionName:) 메소드에
		// 할당되어 해당 section의 이름이 존재하는지 검색
		// 2. invalid - file이 존재하면 파일을 종료하는 closeFile() 메소드를 실행,
		// 존재하지 않으면 retrievalError라는 에러를 내뱉음 
		// 3. 그 후, RecordedGrip 인스턴스를 생성
    func retrieveSection(sectionName: String) throws -> [RecordedGrip] {
        do {
            let stream = try FileHandle(forReadingFrom: URL(fileURLWithPath: sectionName))
            stream.closeFile()
        } catch {
            throw StorageException.retrievalError
        }
        return [RecordedGrip]()
    }
}

// 예외 및 모델
enum StorageException: Error {
    case retrievalError
}

struct RecordedGrip {}

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

  • Java에서는 확인된 예외와 미확인된 예외를 구분한다.
    • 확인된 예외: 컴파일러가 확인할 수 있는 예외로 일반적으로 애플리케이션의 비즈니스 로직에서 발생하며, 예상 가능하고 복구 가능한 상황에서 발생하는 예외이다. 이러한 예외는 try-catch 블록으로 처리하거나 throws 키워드를 사용하여 호출자에게 전달해야 한다.
    • 미확인된 예외: 컴파일러가 확인하지 않는 예외로, 주로 프로그래밍 에러, 예를 들어 null 참조나 배열 범위를 넘어서는 경우 등에 발생한다. 이러한 예외는 RuntimeException을 상속받는다. 이러한 예외는 보통 try-catch 로 처리하지 않고, 프로그램의 결함을 수정하여 예외가 발생하지 않도록 하는 것이 일반적이다.
  • Swift는 이러한 구분 없이 모든 예외를 미확인 예외로 취급함 ⭐️⭐️
  • Swift 내에서 예외를 처리하려면 do-try-catch 블록을 사용하면 된다.

예외에 의미를 제공하라 (135p)

  • throws를 통해 오류를 던질 때에는 전후 상황을 충분히 덧붙인다.
  • catch 블록에서 정확한 실패 유형을 언급한다.

호출자를 고려해 예외 클래스를 정의하라 (135p)

  • LocalPort 클래스처럼 ACMEPort를 감싸는 방법은 최선의 방법.
class ACMEPort {
    let portNumber: Int
    init(portNumber: Int) {
        self.portNumber = portNumber
    }
    
    func open() throws {
        // 여기에 포트를 여는 로직을 추가
        // 문제가 발생하면 적절한 에러를 던진다.
    }
}

enum PortError: Error {
    case deviceResponse
    case unlocked
    case gmx
}

enum PortDeviceFailure: Error {
    case failure(underlyingError: Error)
}

class LocalPort {
    private let innerPort: ACMEPort
    
    init(portNumber: Int) {
        innerPort = ACMEPort(portNumber: portNumber)
    }
    
    func open() throws {
        do {
            try innerPort.open()
        } catch PortError.deviceResponse {
            throw PortDeviceFailure.failure(underlyingError: error)
        } catch PortError.unlocked {
            throw PortDeviceFailure.failure(underlyingError: error)
        } catch PortError.gmx {
            throw PortDeviceFailure.failure(underlyingError: error)
        }
    }
}

정상 흐름을 정의하라 (135p)

  • catch 상황을 만들어 처리할 필요가 없다면 특수 사례 패턴을 사용해서 처리하면 코드가 훨씬 깔끔해진다.
  • 특수 사례 패턴이란 클래스가 특수한 사례를 처리하도록 하는 방식을 의미한다.
do {
	// 식비를 비용으로 청구했다면 청구한 식비를 총계에 더한다. 
    let expenses = try expenseReportDAO.getMeals(employeeID: employee.getID())
    m_total += expenses.getTotal()
} catch {
	// 식비를 비용으로 청구하지 않았다면 일일 기본 식비를 총계에 더한다. 
    m_total += getMealPerDiem()
}
struct MealExpenses {
    func getTotal() -> Int {
		// 여기에서 청구한 식비가 있다면 청구한 식비를 총계에 더하는 코드와
		// 청구한 식비가 없다면 일일 기본식비를 반환하는 코드를 만든다. 
    }
}

// 특수 사례 패턴을 이용하면 굳이 catch 상황을 만들어 처리할 필요가 없어지고 코드가 깔끔해짐
let expenses = MealExpenses()
m_total += expenses.getTotal()

결론 (142p)

  • 오류 처리를 프로그램 논리와 분리해 독자적으로 구분하면 튼튼하고 깨끗한 코드를 작성할 수 있다. ⭐️⭐️
profile
꽃말 == 반드시 오고야 말 행복

0개의 댓글