[개체지향] 예외
예외
try{
}catch(<예외 클래스 1> 변수명){
}finally{
//예외 발생 여부와 상관없이 항상 실행되는 코드
// 생략가능
}
catch와 예외 클래스
- C#의 예외와 마찬가지로 특정 예외를 캐치할 수 있음
- IOException 입출력과 관련된 예외 클래스들의 부모 클래스
- EOFException,FileNotFoundFException, FileSystemFException 등
- ArithmeticException : 산술과 관련된 예외(0으로 나누기)
- IndexOutOfBoundsException : 배열이나 문자열의 색인 범위가 넘어갈 때 등등
- IOException 입출력과 관련된 예외 클래스들의 부모 클래스
- 특정하기 어렵거나 모든 예외를 잡고 싶다면 Exception 클래스 사용
다중 catch블록 작성시 주의할 점
- 부모 예외 클래스가 자식보다 먼저 나오면 안 됨
try{
}catch(Exception e){
}catch(FileNotFoundException e){
}finally{
...
}
// 잘못된 예
try{
}catch(FileNotFoundException e){
}catch(Exception e){
}finally{
...
}
// 올바른 예
- FileNotFoundException가 발생해도 첫 번째 catch블록이 실행됨
C# 예외 처리 예
static void PrintLines(string path){
if(!File.Exists(path)){
return;
}
string[] lines;
try
{
lines = File.ReadAllLines(path);
}
catch(FileNotFoundException e)
{
Console.Error.WriteLine($"file not found: {e.Message}");
return;
}
foreach (var line in lines)
{
//출력
}
}
java 예시
public void printLines(String relativePath){
Path path = Paths.get(getClassPath(), relativePath);
List<String> lines;
try{
lines = File.readAllLines(path);
System.out.format("%d lines read%s", lines.size(), NEW_LINE);
}catch(IOException e){
System.out.println(e.getMessage());
e.printStackTrace();
return;
}
for(String line : lines){
System.out.println(line);
}
}
printStackTrace getMessage
public void printStackTrace()
- 현재 발생한 예외의 호출 스택을 보여줌
- C#의 StackTrace 프로퍼티와 동일
public String getMessage()
- 예외가 왜 발생했는지를 설명
- 메시지가 업을 수도 있음 이때는 null 반환
finally 사용 예
파일에 두번 쓰는 예 C#
static void SriteByte(string path, byte b)
{
FileStream fs = null;
try
{
fs = File.Open(path, FileMode.Open);
fs.WriteByte(b);
}
catch (IOException e)
{
Console.Error.WriteLine($"{e.Message}");
return;
}
}
string path;
WriteByte(path,67);
WriteByte(path,67)
- 두번째 쓰려고하면 예외 발생 파일을 닫지 않았기 때문
static void SriteByte(string path, byte b)
{
FileStream fs = null;
try
{
fs = File.Open(path, FileMode.Open);
fs.WriteByte(b);
fs.Close();
}
catch (IOException e)
{
Console.Error.WriteLine($"{e.Message}");
fs.Close();
return;
}
}
- 이렇게 닫는 코드를 추가해도 문제가 발생할 수 있었음
- IOException 말고 다른 예외가 발생할 경우
static void SriteByte(string path, byte b)
{
FileStream fs = null;
try
{
fs = File.Open(path, FileMode.Open);
fs.WriteByte(b);
}
catch (IOException e)
{
Console.Error.WriteLine($"{e.Message}");
return;
}
finally
{
if(fs != null)
{
fs.Close();
}
}
}
자바에서
public void writeByte(String relativePath, byte b){
Path path = Paths.get(getClassPath(), relativePath);
FileOutputStream out = null;
try{
out = new FileOutputStream(new File(path.toString()), true);
out.write(b);
out.close();
}catch (IOException e){
e.printStackTrace();
return;
}
}
- 이 java코드가 정상적으로 도는 것 처럼 보일 수 있음 그러나 이렇게 하지 말것
GC가 파일을 대신 닫아준다 그러나
- 가비지 수집기가 참조되지 않는 FileOutputStream개체를 해제
- 그때 GC가 호출하는 finalize()가 clase()메서드를 호출
- 그러나 그 시점이 언제인지 알 수 없음
- 또한 GC가 실행되기전에 OS 리소스의 한계에 다다를 수도 있음 이럴때도 예외 발생
java9부터 finalize() 사용을 피하라함
- 향후 해당 메서드를 삭제할 예정
- 따라서 finalize()에 의존해 clase()를 호출하는 습관을 버려야 함
- 훌륭한 프로그래머는 이런 마법같은것(언젠가 GC가 내가 닫지 않은 파일을 닫아준다)에 의존하지 않음
- finalize()에 대해 좀 더 궁금하다면 java 문서를 확인할 것
close()를 호출하는 방법
- 직접 호출
- try with resources
- java 7부터 사용가능
- 이 과목에서 다루지 않음
- 궁금하면 직접 java 문서를 확인할것
올바른 예 java
public void writeByte(String relativePath, byte b){
Path path = Paths.get(getClassPath(), relativePath);
FileOutputStream out = null;
try{
out = new FileOutputStream(new File(path.toString()), true);
out.write(b);
}catch (IOException e){
e.printStackTrace();
return;
} finally {
if(out != null){
try{
out.close();
}catch(Exception e){
}
// C#에서는 생략가능
}
}
}
정리 예외가 발생했을 때 진행 순서
- try 블록의 실행이 중단됨
- catch 블록중에 발생한 예외를 처리할 수 있는 블록이 있는지 찾음 위에서부터 하나식 평가
- 예외를 처리할수 있는 catch 블록이 없다면
- finally 블록을 실행후 한단계 높으느 try 블록으로 전달
- 예외를 처리할 수 있는 catch 블록이 있다면
- 해당 catch 블록 안의 코드들이 실행
- finally블록을 실행
- try 블록 이후의 코드들이 실행됨
catch블록에서 다시 예외 던지기
- catch and rethrow
- 단 이때 호출 스택이 유지되어야 함
- 잘못사용하면 호출 스택이 날라감
- 언어마다 방식이 다름
C#
static void SriteByte(string path, byte b)
{
FileStream fs = null;
try
{
fs = File.Open(path, FileMode.Open);
fs.WriteByte(b);
}
catch (ArgumentNullException e)
{
throw;
}
catch (Exception e)
{
Console.Error.WriteLine($"{e.Message}");
return;
}
finally
{
if(fs != null)
{
fs.Close();
}
}
}
C#에 throw e 하면 다날라감
java
public void writeByte(String relativePath, byte b){
Path path = Paths.get(getClassPath(), relativePath);
FileOutputStream out = null;
try{
out = new FileOutputStream(new File(path.toString()), true);
out.write(b);
}catch (NullPointerException e){
System.err.println("found null pointer!");
throw e;
}catch (IOException e){
e.printStackTrace();
return;
} finally {
if(out != null){
try{
out.close();
}catch(Exception e){
}
// C#에서는 생략가능
}
}
}
- rethrow는 좋은 습관이 아님 그러나 반드시 해야한다면 호출 스택을 유지하면서 던질 것.
나만의(custom) 예외 만들기
public final class UserNotFoundException extends RuntimeException{
public UserNotFoundException(){
super();
}
public UserNotFoundException(String message){
super(message);
}
public UserNotFoundException(String message, Throw cause){
super(message, cause);
}
}
RuntimeException의 생성자
RuntimeException()
RuntimeException(String message)
RuntimeException(String message, Throwable cause)
RuntimeException(Throwable cause)
- 자식 클래스들의 생성자에서 반드시 super()를 호출해야 함
- 호출 안 할 경우 message, cause가 저장되지 않음
- message는 getMessage()로 확인 가능
- cause는 getCause()로 확인 가능
사용예
public static void main(String[] args){
db.findUser("pope");
...
}
public User findUser(String username){
for(User user : users){
if(user.getUsername().equals(username)){
return user;
}
}
// NOTE : 좋은 코드는 아님. 그저 예외를 설명하기 위한 예
throw new UserNotFoundException(username);
}
C# 예
public sealed class UserNotFoundException : Exception
{
public UserNotFoundException(string message) : base(message)
{
}
}
- C#은 Exception을 상속
- java는 RuntimeException을 상속
- java도 Exception을 상속 받을 수있음
Exception을 상속받은 예외
public final class UserNotFoundException extends Exception{
public UserNotFoundException(){
super();
}
public UserNotFoundException(String message){
super(message);
}
public UserNotFoundException(String message, Throw cause){
super(message, cause);
}
}
java의 예외는 크게 두 분류로 나뉨
- 다른 언어의 예외와 다른 점
- 역사적인 이유도 있고 정신적인 이유도 있음
- 그래서 배경 스토리를 먼저 좀 알아야함
JVM 환경에서 도는 프로그램에서 발생한 예외를 전형 처리 안 하면 어떻게 되는지 알아보자
- main 메서드에서 까지 아무런 처리를 안 해주면 jvm이 오류 메시지를 보여주고 프로그램을 종료시킴
- JVM에서 프로그램을 종료시켜서 OS나 기계에는 아무 영향없음
가상머신없는는 옛 언어의 작동
- 언젯적 Os/기계를 쓰느냐에 따라 다름
아주 옜날 하드웨어/OS
- 한 번에 프로그램 하나만 실행할 수 있음
- 따라서 하드웨어에 있는 메모리를 모든 프로그램이 공유함
- CPU에서 0으로 나누려 함
- CPU의 ALU는 0으로 나누는 연산을 처리할 수 없음
- 하드웨어가 멈춤
- 기계를 재부팅해야 함.
웹서버같이 지속적인 조작 업이 알아서 실행돼야 하는 프로그램이라면?
근래의 하드웨어/os
- 여러 프로그램이 동시에 실행 됨
- 따라서 각 프로그램마다 별도의 메모리 공간(가상 메모리)을 제공
- jvm이 보장해주던 안전성을 os가 책임져주는 꼴
- cpu에서 0으로 나누려 함
- cpu의 alu는 0으로 나누는 연산을 처리할 수 없음
- cpu등에서 문제가 있다는 인터럽트나 시그널등을 보내줌(이걸 프로그램에서 제대로 처리 안 해주면 프로그램이 크래시 날 수 있음)
- os가 그 상황을 캐치한 뒤 프로그램을 종료하고 가상 메모리를 해제(그 프로그램의 외부 리소스에 영향이 미치지 않음)
예외처리를 제대로 하지 못하는 이유
이런 주장을 하는 사람이 있음
-
함수에서 오류코드(INT, 열거형,null 등)을 반환해서 오류 상황을 알려주는 건 절대 금지
- 함수에서 반환하는 것은 무조건 올바른 값
- 문제가 잇으면 무조건 예외를 던져야한다. 그리고 호출자는 그 예외를 제대로 처리해줘야한다.
문제는 수십 년이 지나도록 이걸 제대로 한 사람이 드물었음
함수가 더이상 블랙박스가 아니게 됨
- 어느 함수에서 어떤 예외를 던지는지 한눈에 안 보임
- 알려면 모든 함수의 속을 다 보며 확인해야 함
- 여러단계에 함수 호출 시 확인해야 하는 함수 수는 수천 개
- 캡슐화의 반대되는 행위
- 함수 위 주석에 표기하기도 했지만 사람들이 잘 안읽음
- 인간의 이해 없이 만든 방법론은 실패하기 마련
java는 이에 좀 더 대비되어 있었음
- java가 예외를 크게 두 분류로 나눴기 때문
- checked 예외 : java에만 있는 예외
- IOException
- SQLException
- TimeoutException
- unchecked 예외
- ArithmeticException
- BufferOverflowException
- ClassCastException
unchecked 예외
- C# 예외와 동일 : 어디서 어떤 예외가 나는지 한눈에 안 보임
- RunctimeException을 상속
- 컴파일러가 따로 검사를 안 해줘서 unchecked 예외
checked 예외
- 컴파일러가 예외 처리를 제대로 하는지 확인 해줌
- 어느 메서드가 어떤 예외를 던지는지 명확히 알 수 있음
- 예외가 발생하는 코드에서 이 둘 중 하나를 안 하면 컴파일 오류
- 발생한 예외를 그 메서드 안에서 처리
- 처리를 안 할 경우 그 사실을 메서드 시그내처 옆에 표기
public User findUser(String username) throws UserNotFoundException{
for(User user : users){
if(user.getUsername().equals(username)){
return user;
}
}
// NOTE : 좋은 코드는 아님. 그저 예외를 설명하기 위한 예
throw new UserNotFoundException(username);
}
// throws UserNotFoundException 빼먹으면 컴파일 오류
public static void main(String[] args){
User user = null;
try{
user = db.findUser("pope");
} catch (UserNotFoundException e){
e.printStackTrace();
}
//무조건 예외처리를 해야함
}
<메서드 선언> throws <예외 클래스 이름>
public User findUser(String username) throws UserNotFoundException{}
- 다음과 같은 메서드의 시그내처 옆에 반드시 추가해야함
- checked 예외를 던지는 메서드
- 다른 메서드에서 발생한 checked 예외를 처리하지 않는 메서드
- 추가 안 하면 컴파일 오류
// 위처럼 main에서 예외처리를 하기 싫은 경우
public static void main(String[] args) throws UserNotFoundException{
user = db.findUser("pope");
}
사실 RuntimeException도 Exception을 상속 받음
- 과거 java 커뮤니티는 checked 예외를 선호
- 지금은 약간 실패한 느낌
checked 예외를 넣었던 이유
API 제작자가 이건 클라이언트가 반드시 처리해야 할 예외라고 알려주는 용도
처리하라는 의미
- 프로그램 종료? : JVM까지 예외를 올린다.
- 처리 안하고 계속 올릴시에 throw가 굉장히 많아짐
- 이게 의도였다면 unchecked 예외를 사용 하면 되기에 틀린 추측
- 예외를 무시하고 진행
- 출력만 하고 try 블록 다음으로 진행
- 그러면 왜 예외를 던지는가
- 어떻게든 프로그램을 정상상태로 회복할것.
exception safe programming
- 과연 회복이 쉬운가? : 매우 힘듬
- 클래스 몇 개만 있는 간단한 구조에서는 가능함
- 복잡해질수록 어려워 짐
- 여전히 필요한 곳이 일부 있음
- 모든곳에서 이럴 수 없다는 것
근래의 예외 처리 트렌드
- 그냥 unchecked 예외를 쓰자고 함
- 다시 다른 언어와 똑같아짐
- 심지어 checked예외를 unchecked 예외로 감싸서 다시 던지자 라고 의견을 제시하는 사람들도 증가함
- 호출 트리에서 봤던 문제는 고치지 못함 : 누가 어떤 예외를 던지는지 모름.
- 예외로부터 안전한 최선의 방법은 재부팅
- 예외로부터 회복하지 않는다
- 디버깅에 필요한 정보를 최대한 남기고 프로그램 종료
- 뒤에 좀 더 자세히 배움
예외를 얼마나 세분화해서 처리해야하는가(exception granularity)
예전에 좀 더 선호하던 방향
예외를 종류별로 catch한다
- 예외 형(type)에 따라 처리를 달리 해줘야 했기 때문 : 그리고 궁극적으론 회복
- 주로 checked 예외를 사용했음
- 따라서 문제 하나하나마다 예외 클래스를 만들곤 했음
요즘에 좀 더 선호하는 방향
모든 예외는 한번에 처리하자
Exception으로 한방에 잡자 (많이 동의)
- 주로 main()함수에서 한 번만(덜 동의)
- 따라서 다시 예외를 던지는 일도 훨씬 적어짐
- 여전히 던질 때는 세세한 예외 형을 던짐(커스텀 예외 포함)
- catch를 Exception으로 한방에 할 뿐(많이 동의)
- 조금더 극단적인 주장도 있음
- 던지는 것도 무조건 RunTimeException()으로 던짐
이런 글이 많이 보이니 조심할것
대부분의 프로그램은 모든 예외로부터 회복해야한다~
안 그러고 프로그램을 종료시켜도 되는 유일한 경우는 프로그램을 시작할 때 뿐이다.
- 기본적으로 나쁜 조건
지켜보는 사람 없이 작동해야한다면?
- 대표적인 예 : 웹서버
- 근래의 OS는 하드웨어 재부팅이 필요 없음
- 각 프로그램마다 별도의 가상 메모리를 제공
- 프로그램 크래시가 기계 크래시로 이어지지 않음
- 프로그램 재실행은 다른 프로그램이 담당
- 실행 중인 프로세스가 사라지면 다시실행하도록 설정 가능
- 재시작 시 가상 메모리를 배정받으니 프로그램이니 깨끗한 상태로 시작
- 이걸 무시하고 예외로부터 안전한 프로그래밍을 시도하다 실패했다면?
- 프로그램이 이상한 상태에 빠져 더 큰 문제가 생길 수도 있음
- 이런 상태의 프로그램을 흔히 좀비 프로그램이라고 함
- 보자 자세한 내용은 C++~에서
상황에 맞는 예외처리 방법을 선택해야 한다
제어 흐름용으로 예외를 사용하지 말 것
제어흐름 용으로 예외를 사용
- goto와 개념이 같음
- goto가 아니라 호출 스택 어디로도 점프가능
- goto보다 더 기가 막힌 hack
절대 다음에 실행할 코드를 결정하는 용도로 쓰지 말 것
정말 나쁜 예 재귀함수 한방에 빠져나오기
void search(TreeNode node, Object data) throws ResultException{
if(node.data.equals(data)){
throw new ResultException(node);
}else {
search(node.leftChild, data);
search(node.rightCild, data);
}
}
자바 자체 라이브러리에도 이런 부분이 있음
public static int parseInt(...) throws ...
- 문자열로부터 정수를 읽는 데 실패하면 무조건 예외 발생
- 정수가 아닌 경우에 프로그램의 로직을 달리하고 싶다면
- 예: 사용자에게 다시 정수를 입력해달라고 요청
- 어쩔 수 없이 이건 예외를 받아서 판단하는 수 밖에 없음
C# 은 int.TryParse()로 해결
- 프로그래머의 의도에 따라 원하는 함수를 선택
- 반드시 int라고 가정하면, Parse()
- int가 아닐 경우 로직을 달리하고 싶다면, TryParse()
- java에서도 정 원한다면 직접 구현해서 사용가능
- 본인의 메서드 안에서 try-catch를 사용해야함
- 궁금하면 검색
예외적인 상황에만 예외를 사용해야 하는 경우
- 할 수 있다고 다 해야 하는 건 아니다.
Why not use exceptions as regular flow of control?
- 예외는 오류상황이라는 전제하에 모든 툴이 개발되어 있음
- 그걸 자기 혼자 이상하게 사용하면 툴의 혜택을 못 받음
컴파일 경고를 안 고치는 것과 똑같은 이야기
- 컴파일 할 때마다 별 의미 없는 경고가 94개 나옴
- 정말 실수한 걸 컴파일러가 찾아서 95번째 경고를 보여주면 프로그래머가 알아 챌 수 있을까
올바른 오류 처리 방법
오류상황, 예외 상황
- 프로그램은 기본적으로 happy path를 따름
- 근데 그 happy path가 아닌 경우가 생기기도함
- error condition exceptional condition 이라함
- 근데 예외 상황이 프로그래밍 언어의 Exception을 의미하는 경우도 있음
- 오류상황이라는 용어를 사용해서 혼란을 줄일 계획
- 오류상황에 빠진 프로그램은 어떻게 진행해야 하는가
- 그걸 결정하는게 오류 처리방법
- throw try catch등으로 예외를 던지고 처리하는 것도 그중 일부
오류상황은 예측 가능한 상황을 의미
- 프로그램 실행 중에 기본적으로 일어나지 않는 일
- 하지만 여전히 일어날 수 있는 일
- 따라서 이런 상황을 처리하는 코드는 프로그램 기능의 일부
- 그러나 프로그래머가 미리 예측치 못했다면?
- 처리 코드가 있을 수 업음
- 버그
- 버그 발견후 제대로 처리하는 코드를 추가 -> 다시 빌드
4가지 오류 상황 처리법
- The Object Oriented Thought Process 에 기초
- 무시하고 넘어간다 무시
- 문제를 일으킬수 있는 상황이 있는지 검사하고, 그렇다면 프로그램을 종료한다 종료
- 문제를 일으킬 수 있는 상황이 있는지 검사하고, 그렇다면 실수를 고친 뒤 계속 프로그램을 실행되게 한다. 수정
- 문제가 발생하면 예외를 던진다 예외
- 1,2,3은 java 탄생전부터 많이 사용하던 방법
- 한동안 4만하면 프로그램 품질이 높아진다고 약 팔던 사람들이 잇었음
- 1,2,3은 개체지향적으로 사용할 수 없다며 배척하려는 움직임도
훌륭한 프로그래머는 다르다 했다
- 훌륭한 프로그래머는 문제 해결에 가장 적합한 방법을 사용
- 4가지 방법마다 다 적합한 상황이 있음
- 따라서 이 방법들을 모두 다 사용
- 이런 점을 잘 염두에 두고 이번 강의를 볼 것
처음부터 문제없는 코드가 최고
프로그램은 여러 시스템으로 구성되어 있다.
- 각 공간의 제작자는 그 공간을 완전히 통제
- 자기 공간에 대해 매우 잘 앎
- 그러나 다른 제작자의 공간은 통제 못함
- 다른 공간에 대해 잘 알지 못하기 쉬움
가장 훌륭한 마음가짐
내 가족 믿기
- 내 시스템 안에 들어온 데이터는 언제나 유효
- 예외 상황을 고려할 필요가 없음
- 예외 상황이 발생하면 그건 버그
- 깊은 함수 호출이 있어도 복잡하지 않음
이웃 의심
- 남으로부터 받아오는 데이터는
- 언제든 유효하지 않을 수 있음
- 경계에서 반드시 검증해야함
- 잘못된 데이터는 경계에서 곧바로 거부
- 남에게 문제가 있다고 알려줌
남에게 문제를 알려주는 방법
- boolean, null 반환
- 오류코드(int, enum) 반환
- 예외 던짐
통제가능한 코드냐 아니냐가 중요
- 경계에서 검증만 잘하면 꽤 많은 오류 처리 코드를 줄일 수 있음
1. 무시한다
다음 중 한가지 일이 발생
- 곧바로 크래시
- 일단 작동하지만 언젠가 크래시
- 안정적이지 못한 상태로 계속 동작
2. 미리 검사 후 프로그램을 종료한다.
- 어떤 문제가 있엇는지 사용자에게 보여주고 정상 종료
- 주로 팝업 창이나 로그(log) 파일
- 종료방법
- C# : Application.Exit() 호출
- java :System.exit(int) 호출
- 사용자는 그런 문제가 있었군이라 생각하고 다시 프로그램을 실행
- 크래시에 비해 나은점
- 제대로 시스템 상태를 정리하고 프로그램을 종료할 가능성이 높음
- 따라서 프로그램 종료 후 시스템이 좀 더 안정적이 ㄹ가능성이 높음
- 그보다 훨씬 더 큰 장점
- 작업하던 내용을 날리지 않고 저장해 줄 수 있음
3. 미리 검사 후 문제를 고친다.
- 예 분모가 0일경우 1로 바꿈
- UX 고려 시 이보다 나은 해법
- 사용자에게 올바른 값을 입력하라고 다시 요청
- 이게 방금 전에 말한 입력값 검증 방법 중 하나
- 이건 개체지향적이 아니라며 무조건 배척하려는 사람들도 있음
- 특히 모든 오류 처리를 예외로 하자는 사람들
- 이 덕에 오류코드파 vs 예외파 논쟁이 한동안 계속됨
- UX를 포기하면서까지 OO정신을 계승할 이유가 없음
- 예외는 다른 방법에 비해 성능이 가장 느림
- 결론: 설계와 필요한 성능 수준에 따라 OO에서도 충분히 좋은 방법
- 단점도 있음
- 문제가 처음 발생한 곳을 파악하기가 쉽지 않음
- 문제가 발생했다는 사실조차 모른 채 오랜시간이 흐를 수 있음
- 예: 파일 읽는 함수에서 파일을 못 찾았는데 빈 문자열을 반환하면?
- 빈 파일을 읽는 것과 다르지 않으므로 그 순간에는 올바른 방법일 수 있음
- 나중에 그 파일에 문자열을 덧붙이려고 하면 그건 불가능한 연산
- 프로그래머는 있던 파일이 사라졌다 생각할 수도 있음
4 예외를 던진다
- 예외는 다음 두 가지를 지원
- 문제가 발생했다는 사실을 알려줄 수 있음
- 그 예외를 처리할 수 있음
- 대부분의 OO언어는 예외를 지원
예외는 OO의 일부가 아니다.
- 비슷한 시기에 나온게 고작
- 극단적인 진영은 OO에서 무조건 예외처리만 해야한다고 함.
반대증거 : 안드로이드 앱은 Java 기반이다.
예외 외에는 해결책이 없는 경우 : 생성자
- 문제를 수정한다 는 불가능
- 이미 개체가 생성됨
- 생성자에서 null을 반환할 수 있음
- 문제가 발생했다는 사실을 알려주려면 예외가 유일한 방법
- 언어의 제약으로 인해 생성자 안에서 문제가 생기면 되돌릴 방법이 없음
크래시의 진정한 문제점은 작업물을 잃어버리는 것.
크래시의 문제점 해결 : 자동 세이브
- 문서 편집기가 5분마다 자동으로 세이브해 줌
- 심지어 온라인 문서 편집기는 언제나 저장됨
- history 무한 되돌리기 기능까지 존재
- 뻑나도 문제X
크래시와 메모리 덤프
- 디버깅을 잘하는 프로그래머들은 오히려 크래시를 선호
- 문제를 일찍 발견
- 메모리 덤프 등을 통해 실제 크래시가 발생한 이유를 곧바로 조사 가능
- 메모리 덤프 예
- 외부 프로그램이 내 프로그램 실행
- 내 프로그램에서 크래시 발생
- 메모리 덤프 생성
- 개발사에게 인터넷으로 전송
- 이 메모리 덤프를 디버거에서 열면 실제 모든 스택 값을 다 볼 수 있음
- 게임이나 윈도우 OS등의 프로그램이 이걸 잘 활용 중
- 예외를 던지면 이 정도 정보 밖에 없음
- 호출 스택 정보(문자열)
- 메시지(null인 경우가 빈번)
- 예외 형
프로그램 종료가 매력적인 이유
- 잘못된 예외처리 때문에 이상한 상태에 빠지는 일을 방지
- 물론 크래시보다 좋음
- 최근 5분 동안의 작업물도 다 저장해 줄 수 있으니
4가지 처리법의 순위
- 수정
- 종료
- 예외 : 남에게 폭탄 돌리는 꼴
- 무시 : 책임감과 많이 멈
이미 예측한 상황 + 고치기 쉬운경우
- 고치려면 일단 고쳐도 안전하며 인간이 하기에 너무 어렵지 않아야 함
- 고치고 계속 프로그램 진행(수정, 예외)
- 내 코드 안에서는 수정이 더 나은 방법이라 생각
- 예외는 두 공간 사이의 경계에서만 던지는 게 더 명백
- 다른 시스템에서 발생한 예외 역시 경계에서 처리하고 계속 진행
이미 예측한 상황 + 고치기 어려운 경우
- 최종 사용자에게 메시지를 보여주고 싶다면
- 예 : 프로그램에서 팝업 창을 띄워 메시지 보여주기
- 중간 단계 어디에서도 catch안하고 최상위 함수까지 예외를 전달
- 최상위 함수에서 팝업창을 띄워 메시지를 보여줌
- GUI앱이 아니라면 main 함수에서 로그 파일에 기록
- 최종 사용자에게 메시지를 보여주기 실핟면
- 예 : 처음부터 GUI가 없는 프로그램
- 최소한 로그는 남기는 게 좋음(이메일로 로그를 보내달라 요청할 수 있음)
- 크게 두 방법
- 문제 발생지점에서 로그를 남기고 곧바로 프로그램 종료
- 예외를 던져 main에서 catch후 로그를 남기고 종료
예외를 main에서 로그를 남기고 종료
- 앞에서 봤듯이 중간에 누가 catch하는 문제
- 내 의도와는 달리 클라이언트가 잘못 catch
- 로그 외에 자세한 디버깅 정보도 없다는 문제
- 메모리 덤프나 스택정보가 없음
- 차라리 문제지점에서 일부로 크래시를 내서 메모리 덤프를 하는게 나을지도
- 로그를 제대로 안 적는 문제
- 문서화도 잘 안 하는 프로그래머 로그를 제대로 남길까
- 메모리 덤프는 프로그래머가 게을러도 모든 정보가 있음
예측 못한 상황
- 처리 코드가 있을 리 없음
- 따라서 수정하는 방법을 논할 가치도 없음
- 앞서 본 이미예측 + 고치기 어려운 경우의 방법과 비슷
- 예측 못했으니 로그를 남겼을리 없음
- 프로그래밍 언어 따라 둘 중하나
- 혹시라도 발생할 수 있는 모든 예외를 main()에서 catch한 뒤 로그를 남김
- 단 자세한 디버깅 정보 없음
- 크래시가 나서 곧바로 메모리 덤프가 생김
- 충분히 자세한 디버깅 정보 있음
- 혹시라도 발생할 수 있는 모든 예외를 main()에서 catch한 뒤 로그를 남김
jvm에서도 메모리 덤프는 가능하지만
- 제한적인 경우에만 가능하면 설정도 안 쉬움
- 자바 진영에 예외를 최고라 생각한 사람이 많아서?
마지막 교훈
실행중에 문제를 고치려 한다고 프로그램의 안정성이 높아지는 건 아니다. 좀비가 되면 그게 더 큰 문제