값의 존재 유무를 명시적으로 나타내는 방법, 값이 존재하는 경우는 실제 값을 가지고, 값이 없는 경우는 빈 상태를 나타내는 타입
classOption<T> { //Option 클래스는 제너릭 타입을 사용해서 값의 존재 유무를 나타내며privateT value;privateOption(T value) {this.value= value; }publicstatic <T> Option<T> of(T value) {returnnewOption<>(value); }publicTget() {if (value ==null) { // 값이 없는 경우 예외를 던지며thrownewIllegalStateException("No value present"); }return value; // 값이 있는 경우 실제값 반환 }}
타입 힌트는 기존의 코드베이스에 점진적으로 추가 가능하며, 타입 힌트를 이용해서 코드가 실행되기 전에 버그를 찾아되는 정적 타입 검사기 도입 하면 런타임 에러 해결 가능
타입 힌트(Type Hint)는 프로그래밍 언어에서 변수, 함수 매개변수, 함수 반환값 등에 대한 데이터 타입 정보를 제공하는 것을 말한다.
타입 힌트를 사용하면 코드의 가독성을 높이고 타입 관련 오류를 사전에 방지할 수 있다.
주로 정적 타입 언어에서 사용되며, 파이썬과 같은 동적 타입 언어에서도 타입 힌트를 지원
6. 입력값을 검사하자.
코드로 전달되는 입력값은 훼손 가능성이 높음. **사전 조건, 체크섬, 데이터 유효성 검사, 보안 관련 권장 기법, 보편적인 에러를 찾아주는 도구 **활용
원치 않는 입력값이 전달되면 최대한 이른 시점에 실행을 거부하도록 조치해두자.
최대한 많이 검사하자.
**입력 문자열**이 원하는 형식인지 확인하고, 문자열 앞 뒤의 공백도 처리해야 한다는 점
숫자는 적절한 범위 내에 있는지, 파라미터가 0보다 커야 한다면 반드시 파라미터도 확인
파라미터가 IP주소라면 적절한 IP주소인지도 확인
🍋 사전 조건, 사후 조건 이용
유효성 검사란 함수나 메소드의 매개변수에 대한 타입, 범위, 상태 등을 확인하는 과정
메소드에 전달되는 유효성 검사 필요
유효성을 검사하는 라이브러리나 프레임워크 사용
🍋 유효성 검사 방법
타입 힌트 사용
전처리 과정 추가
메소드 내부에서 인자의 유효성을 검사하는 전처리 과정을 추가할 수 있다.
예를 들어, 인자가 특정 범위 내에 있는지, 특정 조건을 만족하는지 등을 검사할 수 있다. 이때 검사에 실패하면 예외를 던지거나 기본값을 반환하는 등의 처리를 할 수 있다.
예외 처리
사전 조건, 사후 조건 설정
사전 조건은 함수가 실행되기 전에 검사해야 할 조건을 나타내며, 사후 조건은 함수가 실행된 후의 조건을 나타낸다.
사전 조건 설정 : b가 0이면 AssertionError 예외 발생
defdivide(a,b):assert b !=0,"b cannot be zero"return a / b
사후 조건 설정 : 합계가 음수가 아닌지 검사 후 사후 조건이 충족되지 않으면 AssertionError 예외 발생
defcalculate_sum(numbers): result =sum(numbers)assert result >=0,"sum should be non-negative"return result
유효성 검사 라이브러리 사용
문서화
🍋 유효성 검사 라이브러리, 프레임워크
1. cheackNotNull이라는 메소드2. @Size(min=0, max=100)
🍋 의도치 않은 데이터 변경이 일어나지 않았는지 체크섬을 이용해서 확인
1. 체크섬이란?
데이터의 특정 부분에서 생성된 해시 값 또는 체크섬 값을 사용하여 데이 터
데이터가 변경되면 체크섬 값도 변경되므로 이를 통해 데이터 변경 여부 확인 가능
예시
파일의 체크섬을 계산하여 저장하고 이후에 파일을 다시 읽어와서 체크섬을 다시 계산한 후 저장된 체크섬과 비교하는 방식으로 데이터 변경 여부를 확인
예제 python
import hashlibdefcalculate_checksum(data):# 데이터의 체크섬 값을 계산하는 함수 sha256 = hashlib.sha256() sha256.update(data.encode())return sha256.hexdigest()defsave_data_with_checksum(filename,data):# 데이터와 체크섬 값을 파일에 저장하는 함수 checksum =calculate_checksum(data)withopen(filename, 'w')as file: file.write(data +'\n') file.write(checksum)defverify_checksum(filename):# 저장된 데이터와 체크섬 값을 확인하는 함수withopen(filename, 'r')as file: lines = file.readlines() data = lines[0].strip() saved_checksum = lines[1].strip() calculated_checksum =calculate_checksum(data)if saved_checksum == calculated_checksum:print("Checksum verification successful. Data is intact.")else:print("Checksum verification failed. Data may be corrupted.")# 예시 데이터를 생성하고 저장data ="Hello, world!"filename ="data.txt"save_data_with_checksum(filename, data)# 데이터 무결성 검증verify_checksum(filename)
2. java 예제
importjava.security.MessageDigest;importjava.security.NoSuchAlgorithmException;publicclassDataIntegrityExample {// 입력된 데이터의 체크섬을 계산하는 메서드publicstaticStringcalculateChecksum(String data) throwsNoSuchAlgorithmException {// SHA-256 해시 알고리즘을 사용하는 MessageDigest 객체 생성MessageDigest md =MessageDigest.getInstance("SHA-256");// 데이터의 해시값 계산byte[] hash =md.digest(data.getBytes());StringBuilder hexString =newStringBuilder();// 해시값을 16진수 문자열로 변환하여 반환for (byte b : hash) {String hex =Integer.toHexString(0xff& b);if (hex.length() ==1) {hexString.append('0'); }hexString.append(hex); }returnhexString.toString(); }publicstaticvoidmain(String[] args) {try {// 원본 데이터 설정String originalData ="Hello, world!";// 원본 데이터의 체크섬 계산String checksum =calculateChecksum(originalData);System.out.println("Original Data: "+ originalData);System.out.println("Calculated Checksum: "+ checksum);// 데이터 변조 시뮬레이션String corruptedData ="Hacked, world!";// 변조된 데이터의 체크섬 계산String corruptedChecksum =calculateChecksum(corruptedData);System.out.println("Corrupted Data: "+ corruptedData);System.out.println("Corrupted Checksum: "+ corruptedChecksum);// 체크섬 값 비교를 통한 데이터 무결성 검증if (checksum.equals(corruptedChecksum)) {System.out.println("Checksum verification failed. Data may be corrupted."); } else {System.out.println("Checksum verification successful. Data is intact."); } } catch (NoSuchAlgorithmException e) {System.err.println("Error: "+e.getMessage()); } }}
🍋 검증된 라이브러리와 프레임워크를 이용해서 크로스 사이트 스크립팅 방지하자.
크로스 사이트 스크립팅 방지 (Cross-Site Scripting, XSS):
설명: 크로스 사이트 스크립팅은 악의적인 스크립트를 삽입하여 웹 애플리케이션의 사용자들에게 악성 행위를 유발할 수 있는 공격
예시: 입력 폼에 스크립트 코드를 삽입하여 다른 사용자의 정보를 획득하는 시도를 차단해야 한다. 대표적으로 HTML 이스케이프 함수를 사용하여 사용자 입력 데이터를 안전하게 처리가 필요
🍋 항상 입력값 검사해서 SQL 주입 공격 예방
SQL 주입 공격 방지:
설명: SQL 주입은 사용자 입력 데이터를 조작하여 데이터베이스 쿼리를 변조하는 공격
예제: 사용자 입력 데이터를 직접 쿼리에 삽입하지 않고, 프레임워크나 ORM 라이브러리의 매개변수화된 쿼리 기능을 사용하여 SQL 주입 공격을 예방할 수 있다.
🍋 strcpy 같은 명령 이용해서 메모리 조작 시 메모리 크기 파라미터 명시적 설정해서 버퍼 오버플로를 방지
버퍼 오버플로 방지
설명: 버퍼 오버플로는 할당된 메모리 공간을 초과하여 데이터를 쓰거나 읽는 공격으로 크래시를 유발하거나 실행 흐름을 조작할 수 있다.
예제: 문자열 복사 함수(strcpy)를 사용할 때 명시적으로 버퍼의 크기를 설정하거나, 메모리 관리 기능이 강화된 함수(strncpy, memcpy)를 사용하여 버퍼 오버플로를 예방할 수 있다.
🍋 보안 암호화 라이브러리나 프로토콜은 직접 작성말고 널리 사용되는 것 사용
설명: 보안 암호화 라이브러리나 프로토콜을 사용하여 데이터의 기밀성과 무결성을 보호하는 것이 중요
예제: 사용자 비밀번호 저장 시에는 안전한 해시 함수 (예: BCrypt)를 사용하여 암호화하고, 데이터 전송 시에는 HTTPS 프로토콜을 활용하여 데이터의 보안을 강화
7. 예외를 활용하자
특정한 리턴값으로 에러를 표현하는 것은 금물이다.
특정한 리턴값은 메소드 시그니처에 명확하게 드러나지 않을 수 있다. 2. 따라서 개발자는 리턴값이 에러를 의미한다는 점을 모를 수 있으며, 어떤 리턴값이 어떤 에러 상태를 의미하는지 기억하기도 어렵다.
예외는 이름도 있고 스택 추적 정보, 줄 번호, 메세지 등 nujll이나 -1만으로 표현하지 못하는 훨씬 더 많은 정보를 담고 있다.
8. 예외는 구체적으로 사용하자.
예외는 애플리케이션 로직을 제어하는 용도가 아니라, 실패를 처리할 때만 사용해야 한다. 또한 언어에 내장된 예외 타입이 문제를 구체적으로 표현할 수 있다면 임의로 예외는 정하지 않는다.
🍋 가능하면 언어에 내장된 예외를 사용하고 포괄적인 의미를 담는 예외는 만들지 않게 하자.
너무 포괄적으면 어떤 문제가 발생했는지 파악할 수 없어 예외 처리가 어려워 진다.
발생한 에러에 대해 정확한 정보를 알지 못하면 개발자는 애플리케이션 실행이 중단되게 할 수 밖에 없으며, 이는 심각한 문제
🍋 예외를 애플리케이션 로직으로 사용해서는 안된다.
우리는 코드가 예상치 못한 동작을 하지 않길 원할 뿐이지 코드 스스로 똑똑해지길 원하는 게 아니다.
예외를 이용해서 메소드를 분기하면 이해가 어려울 뿐 더러 디버깅도 어려워진다.
잘못된 예제
설명: 위의 코드는 OrderProcessingException을 상태 검증용으로 사용하여 애플리케이션 로직을 제어하려고 한다.
이로 인해 예외가 정상적인 흐름 제어에 사용되며, 코드의 가독성과 유지보수가 어려워질 수 있다.
애플리케이션 로직은 예외가 아닌 다른 방법으로 상태 검증 및 처리를 해야 한다. 아래 예제의 경우 if문 등을 사용하여 명시적인 상태 검증과 처리를 구현해야 한다.
publicclassIncorrectExceptionUsage {// 잘못된 예외 사용: 애플리케이션 로직으로 예외 활용publicstaticvoidprocessOrder(int orderStatus) throwsOrderProcessingException {try {if (orderStatus ==0) {thrownewOrderProcessingException("Invalid order status"); // 예외를 상태 검증에 사용 }// 주문 처리 로직// ... } catch (OrderProcessingException ex) {// 예외를 처리하는 코드logError(ex.getMessage()); } }publicstaticvoidmain(String[] args) {int orderStatus =0;try {processOrder(orderStatus); } catch (OrderProcessingException ex) {logError("Failed to process order: "+ex.getMessage()); } }privatestaticvoidlogError(String message) {System.err.println("Error: "+ message); }}
8. 예외는 일찍 던지고 최대한 늦게 처리하자.
🍋 일찍 던진다라는 의미
개발자가 관련 코드를 신속하게 찾을 수 있도록 에러가 발생한 지점으로부터 최대한 가까운 지점에서 예외를 던진다는 뜻으로 중간에 위치한 계층은 어설프게 에러를 처리하기 보다는 예외를 상위로 계층하는 게 맞다.
실제 에러가 발생한 지점에서 멀리 떨어진 곳으로 하면 어디서 문제가 발생했는지 알기 어려워진다.
진짜 문제는 다른곳에서 발생했다는 사실을 알아내야만 버그를 수정할 수 있기 때문이다.
🍋 예외를 늦게 잡는다라는 의미
예외를 처리할 적절한 위치에 도착할 때까지 계속 호출 스택을 통해 전파시킨다는 뜻이다.
예를 들어 애플리케이션이 데이터를 기록하려는데 남은 디스크 공간이 없는 경우 생각해보기
대처방안
실행을 중단하고 재시도
비동기적으로 재시도하는 방법
다른 디스크에 기록
사용자에게 경고 메세지를 보낼 수도 있다.
크래시가 발생하도록 놔두는 것도 하나의 방법
크래시란 컴퓨터 프로그램이나 시스템이 예기치 않게 중단되거나 동작하지 않는 현상을 의미 다시 말해, 프로그램이나 시스템이 오류로 인해 정상적으로 동작하지 않거나 멈추는 상태
적절한 조치는 애플리케이션에 따라 다르다. 데이터 베이스의 로그 선행 기입은 반드시 기록돼야 하는 반면 워드프로세서의 백그라운드 저장 기능은 조금 늦게 실행되도 무방하다.
🍋 최악의 경우
이 경우 예외는 로그에 기록되지도 않고 다시 던져지지도 않으며 다른 어떤 조치도 취해지지 않았다.
장애가 발생해도 아무도 알 수 없으며, 최악의 결과 초래
try{//..}catch(Exception e){//에러를 처리할 방법이 없으므로 무시}
느낌점
예외 처리를 어디서부터 어디까지 해야하는 것을 이 장에서 핵심을 집어주는 것 같다. 이 장은 너무 길어져서 여러 개로 나눠서 정리할 예정이며, 자바와 파이썬의 내장 예외처리에 대해 한 번 정리해야 겠다.