본문 바로가기

개발공부/Java

패스트캠퍼스 환급챌린지 25일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기

반응형

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

Ch10. 상태 관리와 예외 처리

 

 

 

자바에서 "상태"를 다루는 지혜: 예측 가능한 소프트웨어를 위한 예외 처리와 정합성 

"상태 관리"와 이를 통해 "프로그램의 정합성(Consistency)"을 확보하는 방법에 대해 이야기해보려 합니다. 특히, 예상치 못한 상황을 우아하게 처리하는 예외 처리(Exception Handling) 기법에 깊이 초점을 맞추어, 예측 가능하고 안정적인 소프트웨어를 만드는 방법을 알아보겠습니다.


1. 프로그램의 "상태"란 무엇인가? 그리고 왜 중요한가? 

정보 기술 및 컴퓨터 과학에서, 시스템이 상태를 가졌다(stateful)는 것은 이전 이벤트나 사용자 상호작용을 기억하도록 설계되었다는 의미입니다. 이렇게 기억된 정보를 시스템의 상태(State)라고 부릅니다. 쉽게 말해, 우리 프로그램이 어떤 시점에 '어떤 모습'으로 존재하는지를 정의하는 것이 바로 '상태'입니다.

우리가 만든 쇼핑몰 애플리케이션만 보더라도 다양한 상태가 존재했습니다.

  • 판매 개시 상태: ShoppingContext의 init() 메소드가 호출되면, 주문을 받을 준비가 완료된 상태가 됩니다. 이전에 orderMap이 초기화되지 않아 NullPointerException이 발생할 수 있는 잠재적 문제가 있었습니다.
  • 할인 적용 상태: isDiscountTime 변수가 true로 설정되면 특정 상품에 할인이 적용되는 상태가 됩니다.
  • 주문 상태: 고객과 상품, 할인 여부에 따라 주문 내용이 달라지는 복합적인 상태입니다.
  • 프로그램 실행 상태: 프로그램이 실행 중인지, 종료되었는지와 같은 전반적인 생명 주기 상태입니다.

이처럼 프로그램은 단일한 상태로 존재하지 않습니다. 시스템(System) -> 객체/모듈(Object/Module) -> 함수(Function) -> 문(Statement) -> 식(Expression) -> 토큰(Token)으로 이어지는 유기체적인 구조 속에서, 수많은 함수와 다양한 범위의 변수들이 존재하며, 함수 수행 전후로 변수들이 변경되면서 시스템 전체의 상태도 변화합니다. 결국, 모든 변수들이 곧 상태를 지니고 있다고 할 수 있습니다.이러한 상태 변화는 복잡성을 야기하며, 우리가 예상치 못한 시나리오를 만들어내기도 합니다. 예를 들어, "매장 판매 개시 이전에 주문하는 상황"과 같이, 특정 상태에서만 허용되어야 하는 작업이 잘못된 시점에 발생할 수 있습니다. 이러한 예측 불가능성을 제어하고 프로그램이 항상 올바른 상태를 유지하도록 보장하는 것이 바로 정합성(Consistency) 관리입니다.


2. 프로그램 정합성: 예측 가능한 동작을 위한 약속 

데이터베이스 시스템에서 정합성(Consistency) 또는 정확성(Correctness)은 주어진 데이터베이스 트랜잭션이 영향을 받는 데이터를 허용된 방식으로만 변경해야 한다는 요구 사항을 의미합니다. 컴퓨터 과학에서 일관성 모델(Consistency Model)은 프로그래머와 시스템 간의 계약을 명시하는데, 프로그래머가 메모리 작업 규칙을 따르면 메모리가 일관성을 유지하고 읽기, 쓰기, 업데이트 결과가 예측 가능함을 시스템이 보장하는 것입니다.

간단히 말해, 우리 프로그램이 '오작동'하지 않고 우리가 의도한 대로 정확하게 작동하는 상태를 유지하는 것을 정합성이라고 합니다. 이는 데이터가 올바른 상태를 유지하고, 시스템의 동작이 예측 가능하며, 오류가 발생해도 데이터가 손상되지 않도록 하는 것을 목표로 합니다.

분산 시스템에서는 결과적 일관성(Eventual Consistency)과 같은 완화된 일관성 모델을 사용하기도 합니다. 이는 궁극적으로 모든 데이터가 일관성을 가질 것을 보장하지만, 일시적으로는 불일치할 수 있음을 허용하는 모델입니다. 이는 트랜잭션의 ACID(Atomicity, Consistency, Isolation, Durability) 속성과 대비되기도 합니다.

우리의 쇼핑몰 시스템에서도 "판매 개시 전에 주문 불가"와 같은 비즈니스 규칙은 바로 이 '정합성'을 지키기 위한 중요한 제약 조건이 됩니다.

3. 자바의 예외 처리(Exception Handling): 예상치 못한 상황에 대한 우아한 대응 

프로그램 실행 중 발생하는 예외적 조건 또는 비정상적인 상황에 대응하는 과정을 예외 처리(Exception Handling)라고 합니다. 자바에서는 throw 문을 사용하여 예외를 발생시키고 , try-catch-finally 블록을 사용하여 예외를 처리합니다. 예외는 Throwable 클래스의 서브클래스 인스턴스이며, 예외가 발생하면 제어는 catch 블록으로 전달됩니다.

3.1. NullPointerException과 같은 런타임 예외

개발자들이 가장 자주 만나고 골치 아파하는 예외 중 하나는 바로 NullPointerException입니다. 이는 애플리케이션이 객체가 필요한 경우에 null을 사용하려고 할 때 발생합니다ShoppingContext에서도 init() 메소드 호출 이전에 orderMap이 초기화되지 않은 상태에서 order() 메소드가 호출되면, NullPointerException이 발생합니다. 이는 "판매 개시 전에 주문 불가"라는 비즈니스 규칙을 어겼을 때 발생하는 프로그램의 부적절한 상태 변화를 보여주는 명확한 예시입니다.

// ShoppingContext.java (수정 전 - NullPointerException 발생 가능)
public class ShoppingContext {
    private Map<String, Product> productCatalog;
    private Map<Integer, Order> orderMap; // 초기화되지 않은 상태

    // ... 다른 메소드들 ...

    public Order order(Customer customer, Product product) {
        // init() 이 호출되지 않았다면 orderMap은 null -> NullPointerException 발생!
        orderMap.put(1, new Order(customer, product)); // [cite: 186, 2298]
        // ...
        return null; // 예시를 위한 임시 반환
    }

    public void init() {
        this.productCatalog = new HashMap<>();
        this.orderMap = new HashMap<>(); // 여기서 초기화됨
        System.out.println("금일 판매를 시작합니다."); // [cite: 2367]
    }
}

// ShoppingScenario.java (NullPointerException 발생 시나리오)
public class ShoppingScenario {
    public static void main(String[] args) {
        ShoppingContext shopping = new ShoppingContext();
        // 1. 판매 개시 전 주문 시도 (문제 발생!)
        shopping.order(new Customer("홍길동", null, null), null); // [cite: 176]
        // ... 이후 init() 호출 ...
        // shopping.init();
    }
}

이처럼 예측하지 못한 NullPointerException은 개발자가 의도한 비즈니스 규칙 위반에 대한 명확한 메시지를 주지 않고, 프로그램이 왜 비정상적으로 종료되었는지 이해하기 어렵게 만듭니다.

3.2. throw 문을 활용한 사용자 정의 예외

이러한 문제를 해결하기 위해 자바의 throw을 활용하여 사용자 정의 예외를 발생시킬 수 있습니다. 특정 비즈니스 규칙이 위반되었을 때, 그 상황에 맞는 명확한 예외를 던지는 것이 중요합니다. 쇼핑몰 시나리오에서는 "판매 개시 이전에 주문할 수 없다"는 규칙을 위한 ShoppingIsNotReadyException을 만들 수 있습니다.

// ShoppingIsNotReadyException.java (사용자 정의 예외 클래스)
// RuntimeException을 상속받아 Unchecked Exception으로 만듦
public class ShoppingIsNotReadyException extends RuntimeException { // [cite: 225]
    public ShoppingIsNotReadyException(String message) { // [cite: 226]
        super(message); // 부모 클래스(RuntimeException)의 생성자 호출
    }
}

// ShoppingContext.java (수정 후 - 사용자 정의 예외 발생)
public class ShoppingContext {
    private Map<String, Product> productCatalog;
    private Map<Integer, Order> orderMap;
    private int orderKey = 0; // 주문 키 관리

    public ShoppingContext() {
        // 생성자에서 orderMap을 초기화하지 않아 NullPointerException을 유도했었음
        // 이제는 init() 호출 여부로 상태를 관리하도록 변경
    }

    public Order order(Customer customer, Product product) {
        // NullPointerException 방지 대신 비즈니스 규칙 위반 검사
        if (orderMap == null) { // 판매 개시 상태가 아닌 경우 [cite: 217]
            throw new ShoppingIsNotReadyException("init() 메소드 호출 이전에는 주문할 수 없습니다."); // [cite: 218]
        }
        // ... 주문 로직 ...
        Order newOrder = new Order(customer, product); // 예시를 위해 단순화
        orderMap.put(orderKey++, newOrder); // [cite: 219]
        System.out.println(newOrder.toString()); // [cite: 220]
        return newOrder;
    }

    public void init() {
        this.productCatalog = new HashMap<>();
        this.orderMap = new HashMap<>(); // 여기서 orderMap 초기화
        System.out.println("금일 판매를 시작합니다.");
    }

    // ... 다른 메소드들 ...
}

// ShoppingScenario.java (수정 후 - 사용자 정의 예외 발생 확인)
public class ShoppingScenario {
    public static void main(String[] args) {
        ShoppingContext shopping = new ShoppingContext();
        try {
            // E. 판매 개시 전에 주문 불가
            shopping.order(new Customer("고객1", null, null), null); // [cite: 176]
        } catch (ShoppingIsNotReadyException e) {
            System.out.println("예외 발생: " + e.getMessage()); // [cite: 206]
            System.out.println("오류를 올바르게 처리했습니다.");
        }

        shopping.init(); // [cite: 178]

        // 4. 주문(정상 가격)
        // 이제 orderMap이 초기화되었으므로 정상 주문 가능
        // shopping.order(customers.get(0), catalog.get("book1")); // [cite: 180] (이전 코드 라인)
        // 실제 작동을 위해선 ProductFactory와 Product/Customer 객체 생성이 필요
        Map<String, Product> catalog = shopping.prepareCatalog(); // 카탈로그 준비
        Customer customer = new Customer("정상고객", "010-1234-5678", "test@test.com");
        shopping.order(customer, catalog.get("book1"));
    }
}

이제 init() 메소드 호출 전에 order()를 호출하면 ShoppingIsNotReadyException이 발생하며, "init() 메소드 호출 이전에는 주문할 수 없습니다."라는 명확한 메시지를 받게 됩니다. 이는 프로그램의 정합성을 관리하는 데 매우 유용합니다.

3.3. try-catch-finally 블록으로 예외 처리

예외가 발생했을 때 프로그램이 비정상적으로 종료되는 것을 막고, 특정 작업을 수행하도록 하려면 try-catch-finally 블록을 사용합니다. try 블록 안에서 예외가 발생할 수 있는 코드를 작성하고, catch 블록에서 해당 예외를 처리하며, finally 블록은 예외 발생 여부와 관계없이 항상 실행되어야 하는 코드를 포함합니다.

// try-catch-finally 블록 예시
public class TryCatchFinallyExample {
    public static void main(String[] args) {
        ShoppingContext shopping = new ShoppingContext();

        try {
            // 상품 준비 전에 주문 시도
            shopping.order(new Customer("테스트", null, null), null);
        } catch (ShoppingIsNotReadyException e) { // [cite: 262]
            System.out.println("예외를 잡았습니다: " + e.getMessage());
            // 여기에서 사용자에게 적절한 메시지를 보여주거나, 로깅 등의 오류 처리 로직을 넣을 수 있습니다.
        } finally { // [cite: 2289]
            System.out.println("예외 처리 블록이 종료되었습니다. (finally 블록 실행)"); // [cite: 2291]
        }

        System.out.println("프로그램이 계속 실행됩니다.");
    }
}

이 그림은 예외가 발생했을 때 (Method where error occurred) 적절한 예외 핸들러(Method with an exception handler)를 찾을 때까지 메소드 호출 스택을 따라 예외가 전달되는 과정(Throws exception -> Forwards exception -> Catches some other exception)을 보여줍니다.

3.4. Checked Exception vs. Unchecked Exception

자바의 예외는 크게 두 가지로 나뉩니다.

  • Checked Exception (검사 예외): Exception 클래스를 상속받는 예외들입니다. IOException , SQLException 등이 여기에 해당하며, 이들은 반드시try-catch로 처리하거나 throws 키워드를 사용하여 메소드 시그니처에 명시해야 합니다. 이는 컴파일러가 "반드시 대비하도록 유도하는 장치" 역할을 합니다.
     
  • Unchecked Exception (비검사 예외) / RuntimeException: RuntimeException 클래스를 상속받는 예외들입니다. NullPointerException , UnsupportedOperationException 등이 여기에 해당하며, 이들은 명시적으로 처리하지 않아도 컴파일 오류가 발생하지 않습니다. 주로 프로그램의 논리적 오류나 프로그래머의 실수로 인해 발생하는 예외에 사용됩니다.

일반적으로 비즈니스 로직의 유효성 검사 등 예측 가능한 오류 상황에는 사용자 정의 RuntimeException을 사용하고, 외부 시스템과의 연동(파일 I/O, 데이터베이스 통신 등)과 같이 복구 가능성이 있는 예외 상황에는 Checked Exception을 사용하는 것이 권장됩니다.

3.5. assert 문과 테스트 프레임워크 (JUnit)

assert 문은 개발 단계에서 특정 조건이 true임을 '단언'할 때 사용됩니다. 조건식이 false일 경우 AssertionError를 발생시킵니다. javac -Dassert MyProgram.java와 같이 컴파일 시에 활성화할 수 있습니다. 그러나 assert 문은 주로 개발 및 디버깅 목적으로 사용되며, 실제 운영 환경에서는 비활성화될 수 있어 프로덕션 코드의 오류 처리에 적합하지 않습니다. 더 나은 대안은  JUnit과 같은 테스트 자동화 프레임워크를 활용하는 것입니다. JUnit은 자바용 단위 테스트 프레임워크로, xUnit 계열 중 하나이며, Kent Beck, Erich Gamma 등이 개발에 참여했습니다.

JUnit을 사용하면 코드의 각 단위(메소드, 클래스)가 예상대로 작동하는지 자동으로 검증할 수 있으며, 이는 개발 과정에서 오류를 조기에 발견하고 소프트웨어의 품질을 높이는 데 크게 기여합니다. VS Code의 Test Runner for Java 확장 프로그램도 JUnit 테스트 케이스를 실행하고 디버깅하는 데 유용합니다.

오늘의실습

오늘의 느낀점

정합성이란 말을 데이터베이스에서만 들었어. 새롭게 알게 된 게 정말 놀라웠다. 단순히 데이터 일관성을 넘어, '판매 개시 전에 주문 불가' 같은 비즈니스 규칙이 프로그램 정합성을 지키는 거라니, 시야가 확 트였다. 예외 처리는 그냥 오류 막는 방어적인 코딩이라고 생각했었는데, 근데 NullPointerException처럼 애매한 오류 대신 ShoppingIsNotReadyException처럼 명확한 예외를 던지니까, 프로그램 상태를 더 잘 제어하고 정확한 피드백을 줄 수 있다는 걸 알게 되었따. 이건 버그 잡는 걸 넘어, 비즈니스 로직을 튼튼하게 만드는 핵심 도구라는 것을 다시 한번 깨닫는 기회였다.

솔직히 테스트 코드 짜는 건 늘 어렵고 귀찮다고 느꼈는데 assert 문이 아니라 JUnit 같은 전문 테스트 프레임워크로 코드 건전성을 확보하는 게, 결국 프로그램 정합성을 보장하는 가장 확실한 길이라는 걸 다시 깨닫게 되었다. 쉽지 않겠지만, 이번 기회에 JUnit 포함해서 테스트 기법을 다시 한번 제대로 공부하고 꼭 써봐야겠다고 다짐했다! 

 

 

https://fastcampus.info/4n8ztzq

 

(~6/20) 50일의 기적 AI 환급반💫 | 패스트캠퍼스

초간단 미션! 하루 20분 공부하고 수강료 전액 환급에 AI 스킬 장착까지!

fastcampus.co.kr

 

 

반응형