null
값 사용을 피함
null object pattern
(null
대신에 빈 배열을 사용하는 패턴)이나 option type
등을 통해 null pointer exception
을 방지
메소드의 시작 지점에서 NotNull annotation
기능 활용
불변 변수를 사용
불변 변수를 사용하면 병렬 프로그래밍은 더 간단해지며, 변수 값이 바뀌지 않을 것임을 인지하는 컴파일러나 런타임은 더 효율적으로 동작할 수 있음.
타입 힌트와 정적 타입 검사를 사용
변수가 보관할 수 있는 값을 제한한다. 변수는 String
타입보다는 Enum
으로 선언할 수 있음. 변수를 선언할 때는 가장 구체적인 타입을 사용할 것.
입력값을 검사
사전 조건, 체크섬(checksum) 및 데이터 유효성 검사, 보안 관련 권장 기법, 보편적인 에러를 찾아주는 도구 등을 활용. 원치 않는 입력값이 전달되면 최대한 이른 시점에 실행을 거부하도록 조치하기.
외부로부터의 입력은 위험함. 악의적인 사용자가 입력값에 코드나 SQL을 주입할 수도 있고, 애플리케이션의 제어권을 가져가려고 버퍼 용량을 초과하는 값을 전달할 수도 있음.
strcpy
strncpy
같은 명령(문자를 복사하는 함수)을 이용해서 메모리를 조작할 때는 메모리 크기 파라미터를 명시적으로 설정해서 버퍼 오버플로를 방지해야 함. 보안 및 암호화 라이브러리나 프로토콜은 직접 작성하지 말고 널리 사용되는 것을 활용할 것.
예외를 활용
null, 0, -1 같은 특정한 리턴값으로 에러를 표현하는 것은 금물. 모든 최신 개발 언어는 예외나 표준 예외 처리 패턴을 제공하니 이런 것을 사용할 것.
예외는 구체적으로 활용
가능하면 언어에 내장된 예외를 사용하고 포괄적인 의미를 담는 예외는 만들지 않게 할 것. 예외는 애플리케이션 로직을 제어하는 용도가 아니라 실패를 처리할 때만 사용해야 함.
언어에서 내장된 예외 타입의 예: FileNotFoundException
, AssertionError
, NullPointerException
예외는 일찍 던지고 최대한 나중에 처리
'일찍 던지고 늦게 잡는' 원칙을 따를 것. throw early 라는 말은 개발자가 관련 코드를 신속하게 찾을 수 있도록 에러가 발생한 지점으로부터 최대한 가까운 지점에서 예외를 던진다는 뜻. 실제 에러가 발생한 지점으로부터 너무 먼 위치에서 예외를 던지면 어디서 문제가 생겼는지 찾기가 어려워짐.
catch exception late 라는 말은 예외를 처리할 적절한 위치에 도착할 때까지 계속 호출 스택을 통해 전파시킨다는 뜻.
어설픈 에러 처리 중 최악인 경우는 처리할 수 없는 예외를 catch 블록 내에서 무시하고 그냥 '삼켜버리는' 것.
호출하는 코드가 예외를 던지면 예외를 완전히 처리하거나 혹은 상위 스택으로 전파할 것.
재시도는 현명하게
실패한 요청을 무턱대로 재시도하지 말 것.
실패할 수밖에 없다면 빨리 실패하고 실패 상황을 전파하는 방안도 생각해야 함. 쉽게 디버깅할 수 있도록 에러와 관련된 정보는 반드시 확인이 가능해야 함.
시스템에 멱등성 idempotent 을 부여
멱등성: 동일한 작업을 여러 번 실행해도 항상 같은 결과가 출력됨
리소스를 해제
더 이상 필요하지 않은 메모리, 데이터 구조, 네트워크 소켓, 파일 핸들 등을 모두 해제할 것.
로그 레벨의 사용
TRACE
: 특정 패키지나 클래스에서만 켜지며 최대한 상세한 내용을 출력하는 레벨DEBUG
: 프로덕션 상황에서 문제가 발생했을 때 적합한 레벨INFO
: 애플리케이션 상태에 대해 알아두면 좋을 만한 정보를 위한 레벨WARN
: 잠재적으로 문제가 될 만한 상황에 대한 메시지를 출력하기 위한 레벨INFO
레벨로 옮겨야 함ERROR
: 데이터베이스 기록 작업이 시패하면 대체로 해당 레벨에 기록FATAL
: 가장 위험한 수준의 메세지를 출력하기 위한 레벨로그는 원자적으로 작성
데이터와 결합했을 때만 정보가 유용하다면 한 메시지에 모든 정보를 원자적으로 atomically 저장할 것
로그는 신속하게 기록
민감한 데이터는 로그에 기록하지 말 것
지표는 숫자로 표현한 로그와도 같아서 애플리케이션의 동작을 측정한다. 쿼리 실행 시간은 얼마나 걸리는지, 큐에 저장된 값은 몇 개인지, 디스크에 얼마나 많은 데이터가 기록되고 있는지 등 애플리케이션의 동작의 측정하면 문제를 인지하는 데 도움이 되며 디버깅에도 유용하다.
COUNTER
: 특정 이벤트가 발생한 횟수를 측정
GAUGE
: 특정 시점을 기준으로 측정하므로 값이 올라가거나 내려갈 수 있다. 큐나 스택, 맵의 크기 같은 통곗값을 제시
HISTOGRAM
: 규모에 따라 이벤트를 특정 범위로 구분. 각 범위에는 해당 범위 내의 이벤트 값이 발생하면 값이 증가하는 카운터가 있다. 주로 요청 처리에 걸린 시간이나 데이터 페이로드 크기 같은 지표를 측정
모든 것을 측정할 것
측정은 비용이 적게 드는 작업이므로 가급적 많은 지표를 수집해야 함. 다음과 같은 데이터 구조, 작업, 동작은 모두 측정할 것.
- 리소스 풀
- 캐시
- 데이터 구조
- CPU 집약적 작업 : 데이터 직렬화 작업은 상당히 큰 비용이 드는 작업임으로 특히 더 주의하자. 단순히 데이터 구조를 JSON으로 변환하는 작업이 코드에서 가장 비용이 많이 드는 작업인 경우도 많다.
- IO 집약적 작업
- 데이터 크기
- 예외와 에러
- 원격 요청 및 응답
지나치게 창의적인 설정은 금물
원하는 동작을 할 수 있는 가장 간단한 방법을 채택하자. 단일한 표준 형식을 채택한 정적 설정 static configuration 파일이 가장 이상적이다
동적 설정은 그 복잡성 때문에 대부분의 경우에는 권장하지 않는다. 중간에 여러 가지 설정이 바뀔 수 있다는 가능성을 철저히 생각해야 하기 때문이다. 또한 언제 누가 설정을 바꿨는지 여부는 운영 이슈를 디버깅하는 데 매우 중요한 반면, 실제로 그 정보를 추적하기는 어렵다. 게다가 다른 분산 시스템에 대한 의존성도 더해진다. 그다지 마음에 들지 않겠지만 대부분의 경우에는 새로운 설정을 적용하기 위해 프로세스를 재시작하는 것이 운영 면이나 아키텍처 면에서 가장 좋은 방법이다.
모든 설정을 로그에 기록하고 검증할 것
애플리케이션이 어떤 설정값을 사용하는지 알 수 있도록 시작 시점에 즉시 (안전한) 설정값을 로그에 기록.
설정값을 로드할 때는 항상 검증해야 함. 검증은 가능한 한 이른 시점(설정을 로드한 직후)에 한 번만 하면 됨.
기본값을 제공할 것
관련된 설정을 그룹화할 것
설정도 코드처럼 테스트할 것
설정 파일은 깔끔하게 유지할 것
배포된 설정은 변경하지 말 것
O 이것만은 지키자 | X 이것만은 피하자 |
---|---|
런타임 에러보다는 컴파일 타임에 이러를 검출하자 | 예외를 이용해 애플리케이션 로직을 결정해서는 안 된다 |
가능하면 불변 변수를 사용하자 | 리턴 코드로 예외를 처리해서는 안 된다 |
입력과 출력을 검증하자 | 처리할 수 없는 예외는 잡지 말자 |
OWASP가 발표하는 10대 웹 어플리케이션 취약점은 숙지해두자 | 로그를 여러 줄로 나누지 말자 |
버그 확인 도구는 물론 타입 또는 타입 힌트도 사용하자 | 보안 정보나 민감한 데이터를 로그에 기록하지 말자 |
예외 발생 시에는 리소스(특히 소켓, 파일 포인터, 메모리 등)를 해제하자 | 머신의 설정을 직접 수정하지 말자 |
코드에 지표를 추가하자 | 비밀번호나 보안 정보를 설정 파일에 기록하지 말자 |
애플리케이션에 설정을 추가해두자 | 커스텀 설정 형식은 채택하지 말자 |
모든 설정을 검증하고 로그에 기록하자 | 불필요한 동적 설정은 사용하지 말자 |