ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [코드 컴플리트] 방어적 프로그래밍
    개발론 2024. 9. 26. 19:31

     
    방어적 프로그래밍이란 무엇일까?
     
    방어적 프로그래밍은 프로그램이 예상치 못한 상황에서도 안정적으로 동작하도록 하는 기법이다.
    오류를 쉽게 찾고, 수정하기 쉽게 만들며, 프로덕션 코드에 미치는 영향을 최소화하는 것이 목적이다.
     
    a 를 넣으면 b 를 리턴하는 f1 이라는 함수가 있다고 해보자.
     

     
    여기서 에러값인 a' 를 입력하면 어떻게 동작해야 할까?
     
    책에서 말하는 내용은 다음과 같다.
     

    소프트웨어에서 쓰레기를 넣으면 쓰레기가 나온다고 말해서는 안된다. 좋은 프로그램은 쓰레기를 입력받았다고 하더라도 절대로 쓰레기를 내뱉지 않는다.

     
    따라서 
     

     
    우측처럼 잘못된 입력이 들어왔다고 시스템이 고장나거나 쓰레기값을 뱉는 것이 아닌,
    그에 맞는 error 를 뱉도록 해서 외부로부터 잘못된 값을 받더라도
    정상적으로 처리를 해줘야 한다.
     
    이렇게 구현하려면 어떻게 해야 할까
     

    1. 외부로부터 들어오는 모든 데이터의 값을 검사하라

    파일이나 사용자 네트위크 등 외부적인 인터페이스에서 들어오는 값들이
    허용되는 범위 안에 있는지 꼭 검사를 해야 한다.
    문자열이나 숫자는 그에 맞는 길이나 특수문자 등을 조심하고,
    보안 응용프로그램을 작성하고 있다면
    버퍼 오버플로를 시도하거나,  SQL 명령문을 주입하는 등의 검증도 필요하다고 한다.
     

    2. 루틴의 모든 입력 매개변수 값을 검사하라. 

    위와 같이 외부에서 온 것이 아닌 다른 루틴 ( 코드컴플리트에서 말하는 루틴은 자바의 메소드라 생각해도 된다.)
    에서 온 값도 결국은 외부의 값이다. 이또한 검증해야 한다.
     

    3. 잘못된 입력을 어떻게 처리할 것인지 결정하라.

    책에 작성되어 있는 내용은 
    코드를 작성하기 전부터 의사 코드 작성, 테스트 케이스 작성 등을 통해
    버그가 만들어지는 것을 예방하는 것이 좋다고 한다.
     
    어떻게 처리할 것인지 결정하라 해놓고 애초에 만들지 않는게 좋다고 작성되어 있어서
    황당하지만 8.3에 이에 대한 설명이 들어있다 안내해두었으니 
    일단은 이정도로 넘어가자.
     

    어썰션

     
    그리고 방어적 프로그래밍에서 중요한 개념 중 하나가 어썰션이다!
     
    https://docs.oracle.com/javase/8/docs/technotes/guides/language/assert.html

    Programming With Assertions

    Why not provide a compiler flag to completely eliminate assertions from object files? It is a firm requirement that it be possible to enable assertions in the field, for enhanced serviceability. It would have been possible to also permit developers to elim

    docs.oracle.com

     
    어썰션은 프로그램에서 "이 조건은 반드시 참이어야 해!"라고 주장하는 것이라고 한다.
    마치 "이 답은 무조건 맞아야 해!"라고 말하는 것이다.
     

    int age = getUserInput();
    assert age > 0 : "나이는 양수여야 합니다. 현재 값: " + age;

     
    기본적으로 java 에서는 assertion 이 꺼져있기에
    이것을 실행 시에 -ea 라는 옵션을 줘서 켜준다면
     

    Exception in thread "main" java.lang.AssertionError: 나이는 양수여야 합니다. 현재 값: -1

     
    처럼 에러를 뱉는다고 한다.

    이를 사용하면 절대 발생해서는 안되는
    조건을 확인할 수 있고
    개발단계에서 버그를 빠르게 발견할 수 있다.
    프로덕션코드에선 비활성화한다.

    예외 처리

    예외는 무시해서는 안 되는 오류를 프로그램의 다른 부분에 알리는 데 사용된다.

    예외 사용시 주의사항

    • 정말로 예외적인 상황에만 예외를 발생시킨다.
    • 책임을 회피하기 위해 예외를 사용하지 않는다.
    • 가능한 로컬에서 오류를 처리한다.
    • 생성자와 소멸자에서는 예외를 던지지 않도록 한다.
    • 예외는 적절한 추상화 수준에서 던진다.
    • 예외는 신중하게 사용해야 하며, 과도한 사용은 코드 의 복잡성을 증가시킬 수 있다.

    정말로 예외란

    ```java
    public class Employee {
        public void Initialize(String name, String address, String phoneNumber) 
        throws EmployeeDataNotAvailableException {
            if (name == null || name.equals("")) {
                throw new EmployeeDataNotAvailableException("Name not available");
            }
            if (address == null || address.equals("")) {
                throw new EmployeeDataNotAvailableException("Address not available");
            }
            if (phoneNumber == null || phoneNumber.equals("")) {
                throw new EmployeeDataNotAvailableException("Phone number not available");
            }
            // ... 초기화 코드 계속 ...
        }
    }
    
    ```


    위의 코드는 모든 것을 예외로 처리하고 있는데
    이런 코드보다는

    ```java
    public class Employee {
        public boolean Initialize(String name, String address, String phoneNumber) {
            if (name == null || name.equals("")) {
                return false;
            }
            if (address == null || address.equals("")) {
                return false;
            }
            if (phoneNumber == null || phoneNumber.equals("")) {
                return false;
            }
            // ... 초기화 코드 계속 ...
            return true;
        }
    }
    
    ```


    이렇게 boolean 처리하고

    ```java
    public class Employee {
        public void Save() throws EmployeeDataSaveException {
            try {
                // 데이터베이스에 직원 정보 저장
            }
            catch (SQLException e) {
                throw new EmployeeDataSaveException("Unable to save employee data", e);
            }
        }
    }
    
    ```


    이와 같이 진짜 오류일 때 예외를 던진다.

    적절한 추상화란

    ```java
    public class DatabaseService {
        
        public void saveUser(User user) throws SQLException {
            try {
                connectToDatabase();
                insertUserData(user);
            } catch (SQLException e) {
                // 저수준 예외를 그대로 상위 레벨로 전파
                throw e;
            }
        }
    
        private void connectToDatabase() throws SQLException {
            // 데이터베이스 연결 로직
            throw new SQLException("Database connection failed");
        }
    
        private void insertUserData(User user) throws SQLException {
            // 사용자 데이터 삽입 로직
            throw new SQLException("Failed to insert user data");
        }
    }
    
    ```


    위 코드를 보면

    saveuser 메소드가 데이터베이스 관련 저수준 예외인
    SQLException 을 직접 던지고 있다.
    이는 메소 드의 추상화 수준과 맞지 않다.
    또한
    API 사용자에게 데이터베이스 관련 예외를 처리하도록 강제하여 구현 세부사항을 불필요하게 노출시키고 있다.
    SQLException'은 사용자 저장 실패의 구체적인 이유를 명확히 전달하지 못하기에 의미있는 컨텍스트를 전달하기 어렵다.
    API 사용자는 데이터베이스 관련 예외를 처리해야 하므로, 비즈니스 로직과 데이터 접근 로직이 불필요하게 섞일 수 있다.

    따라서

    저 catch 안에는
    ` throw new UserSaveException("Failed to save user", e); ` 를 넣어주는 것이 적절하다.

    그렇다면 예상하지 못한 에러는 어떻게 처리해야할까?

    ```java
    import java.util.logging.Logger;
    import java.util.logging.Level;
    
    public class GlobalExceptionHandler implements Thread.UncaughtExceptionHandler {
    
        private static final Logger LOGGER = Logger.getLogger(GlobalExceptionHandler.class.getName());
    
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            LOGGER.log(Level.SEVERE, "Uncaught exception in thread '" + t.getName() + "'", e);
            
            // 개발자에게 이메일 알림 보내기
            sendEmailAlert(e);
            
            // Slack 알림 보내기 
            sendSlackAlert(e);
        }
    
        private void sendEmailAlert(Throwable e) {
            // 이메일 전송 로직 구현
            // 예: JavaMail API 사용
        }
    
        private void sendSlackAlert(Throwable e) {
            // Slack 알림 전송 로직 구현
            // 예: Slack API 사용
        }
    
        public static void main(String[] args) {
            // 글로벌 예외 핸들러 설정
            Thread.setDefaultUncaughtExceptionHandler(new GlobalExceptionHandler());
    
            // 예외 발생 테스트
            throw new RuntimeException("테스트 예외 발생");
        }
    }
    
    ```


    이런 예외를 사용해서 예상하지 못한 에러가 났을 때
    바로 개발자에게 알려야한다.
    이 때 사용하는 슬랙이나 이메일 외의 방법은
    오류 모니터링 툴인 센트리나 데이터독이 있겠다.

    책의 내용 외에 나로서 생각나는 방법은
    에러메세지를 의미있게 담아주는 것이다!

    여기까지 방어적 프로그래밍에 대한 정리였다!

Designed by Tistory.