Skip to content

Commit 46a0838

Browse files
authored
Merge pull request #2 from jhkman/main
7. 오류 처리 update
2 parents d0901e2 + d5cb34c commit 46a0838

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed

7. 오류 처리/Readme.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# 오류처리
2+
깨끗한 코드와 오류 처리는 확실히 연관성이 있다. 오류 처리는 중요하지만 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워 진다면 깨끗한 코드라 부르기 어렵다.
3+
4+
## 1. 오류 코드보다 예외를 사용하라
5+
오류코드를 사용할시 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다.
6+
예외처리를 할시 코드도 더 깔끔해지고 논리가 오류처리 코드와 뒤섞이지 않게 된다.
7+
8+
### 1-1. 오류코드 sample
9+
```
10+
public class DeviceController {
11+
...
12+
public void sendShutDown() {
13+
DeviceHandle handle = getHandle(DEV1);
14+
// 디바이스 상태를 점검한다
15+
if (handle != DeviceHandle.INVALID) {
16+
// 레코드 필드에 디바이스 상태를 저장한다
17+
retrieveDeviceRecord(handle);
18+
// 디바이스가 일시정지 상태가 아니라면 종료한다.
19+
if (record.getStatus() != DEVICE_SUSPENDED) {
20+
pauseDevice(handle);
21+
clearDeviceWorkQueue(handle);
22+
closeDevice(handle);
23+
} else {
24+
logger.log("Device suspended. Unable to shut down");
25+
}
26+
} else {
27+
logger.log("Invalid handle for: " + DEV1.toString());
28+
}
29+
}
30+
...
31+
}
32+
33+
```
34+
35+
### 1-2. 예외처리 sample
36+
```
37+
public class DeviceController {
38+
...
39+
public void sendShutDown() {
40+
try {
41+
tryToShutDown();
42+
} catch (DeviceShutDownError e) {
43+
logger.log(e);
44+
}
45+
}
46+
47+
private void tryToShutDown() throws DeviceShutDownError {
48+
DeviceHandle handle = getHandle(DEV1);
49+
DeviceRecord record = retrieveDeviceRecord(handle);
50+
51+
pauseDevice(handle);
52+
clearDeviceWorkQueue(handle);
53+
closeDevice(handle);
54+
}
55+
56+
private DeviceHandle getHandle(DeviceID id) {
57+
...
58+
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
59+
...
60+
}
61+
...
62+
}
63+
64+
```
65+
66+
## 2. Try-Catch-Finally 문부터 작성하라
67+
try 블록에 무슨일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.
68+
그러므로 예외가 발생할 코드를 짤때는 try-catch-finally 문으로 시작하는 편이 낫다.
69+
그러면 try블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.
70+
먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다.
71+
그러면 자연스럽게 try블록의 트랜잭션 범위부터 구현하게 되므로 범위 내 트랜잭션 본질을 유지하기 쉬워진다.
72+
```
73+
//파일이 없으면 예외를 던지는지 알아보는 단위 테스트
74+
@Test(expected = StorageException.class)
75+
public void retrieveSectionShouldThrowOnInvalidFileName() {
76+
sectionStore.retrieveSection("invalid - file");
77+
}
78+
//try catch로 예외를 던지므로 테스트가 성공한다
79+
public List<RecordedGrip> retrieveSection(String sectionName) {
80+
try {
81+
FileInputStream stream = new FileInputStream(sectionName)
82+
} catch (Exception e) {
83+
throw new StorageException("retrieval error", e);
84+
}
85+
return new ArrayList<RecordedGrip>();
86+
}
87+
88+
```
89+
그런뒤 예외 유형을 좁혀 실제 예외를 찾아내면서 리펙토링한다.
90+
```
91+
public List<RecordedGrip> retrieveSection(String sectionName) {
92+
try {
93+
FileInputStream stream = new FileInputStream(sectionName);
94+
stream.close();
95+
} catch (FileNotFoundException e) {
96+
throw new StorageException("retrieval error", e);
97+
}
98+
return new ArrayList<RecordedGrip>();
99+
}
100+
101+
```
102+
103+
## 3. 미확인(unchecked) 예외를 사용하라
104+
확인된 예외는 OCP(Open Closed Principle, 개방폐쇄원칙 - '소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다'는 프로그래밍 원칙)를 위반한다.
105+
하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다.
106+
throw 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.
107+
108+
| | Checked Exception | UnChecked Exception |
109+
|---|:---:|:---:|
110+
| 확인 시점 | 컴파일 시점 | 런타임 시점 |
111+
| 처리 여부 | 반드시 처리 | 명시적으로 처리하지 않아도 됨 |
112+
| 트랜잭션 처리 | roll-back 하지 않음 | roll-back 함 |
113+
| 예시 | IOException, ClassNotFoundException | NullPointerException, ArithmeticException |
114+
아래 코드는 단순한 출력을 하는 메소드이다.
115+
```
116+
public void printA(bool flag) {
117+
if(flag)
118+
System.out.println("called");
119+
}
120+
121+
public void func(bool flag) {
122+
printA(flag);
123+
}
124+
125+
```
126+
문득 프린트를 안할 때 NotPrintException 을 던지기로 구현을 변경했을 때
127+
```
128+
public void printA(bool flag) throws NotPrintException {
129+
if(flag)
130+
System.out.println("called");
131+
else
132+
throw new NotPrintException();
133+
}
134+
135+
public void func(bool flag) throws NotPrintException {
136+
printA(flag);
137+
}
138+
```
139+
해당 함수 뿐만이 아니라 호출하는 함수도 수정을 해줘야 하기 때문에 OCP 를 위반하게 된다.
140+
141+
142+
143+
## 4. 예외에 의미를 제공하라
144+
예외에 정보를 충분히 담아서 던지면, 오류가 발생한 원인과 위치를 찾기 쉬워진다.
145+
146+
## 5. 호출자를 고려해 예외 클래스를 정의하라
147+
오류를 분류할 수 있는 방법은 많다.
148+
- 발생한 위치
149+
- 발생한 컴포넌트
150+
- 유형 : 네트워크 실패, 디바이스 실패, 프로그래밍 오류 등
151+
152+
하지만 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.
153+
외부 라이브러를 그대로 사용한 경우에는 외부 라이브러리가 던질 예외를 모두 잡아야 한다.
154+
155+
```
156+
ACMEPort port = new ACMEPort(12);
157+
158+
try {
159+
port.open();
160+
} catch (DeviceResponseException e) {
161+
reportPortError(e);
162+
logger.log("Device response exception", e);
163+
} catch (ATM1212UnlockedException e) {
164+
reportPortError(e);
165+
logger.log("Unlock exception", e);
166+
} catch (GMXError e) {
167+
reportPortError(e);
168+
logger.log("Device response exception");
169+
} finally {
170+
...
171+
}
172+
```
173+
대다수 상황에서 오류를 처리하는 방식은 오류 종류와 무관하게 비교적 일정하다
174+
1. log를 남긴다
175+
2. 계속 수행가능한지 확인한다
176+
위 경우에도 예외 유형과 무관하게 모두 동일했다. 이 경우 외부 라이브러리를 호출하는 API를 감싸면서 예외 유형을 하나만 던지게 수정해보자.
177+
178+
179+
```
180+
LocalPort port = new LocalPort(12);
181+
182+
try {
183+
port.open();
184+
} catch (PortDeviceFailure e) {
185+
reportError(e);
186+
logger.log(e.getMessage(), e);
187+
} finally {
188+
...
189+
}
190+
191+
public class LocalPort {
192+
private ACMEPort innerPort;
193+
194+
public LocalPort(int portNumber) {
195+
innerPort = new ACMEPort(portNumber);
196+
}
197+
198+
public void open() {
199+
try {
200+
innerPort.open();
201+
} catch (DeviceResponseException e) {
202+
throw new PortDeviceFailure(e);
203+
} catch (ATM1212UnlockedException e) {
204+
throw new PortDeviceFailure(e);
205+
} catch (GMXError e) {
206+
throw new PortDeviceFailure(e);
207+
}
208+
}
209+
}
210+
```
211+
외부 API를 감싸는 클래스는 매우 유용하다. 외부 API와 프로그램 사이에 의존성이 크게 줄어든다.
212+
213+
## 6. 정상 흐름을 정의하라
214+
catch문에서 예외를 처리하는 경우 코드가 지저분해지는 일이 발생할 수 있다.
215+
216+
```
217+
try {
218+
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
219+
//식비를 비용으로 청구시 총계에 더한다
220+
m_total += expenses.getTotal();
221+
} catch(MealExpensesNotFound e) {
222+
//그게 아니면 일일 기본 식비를 총계에 더한다
223+
m_total += getMealPerDiem();
224+
}
225+
```
226+
식비를 비용으로 청구했다면 그걸 더하고, 아니면 일일 기본 식비를 더하는 코드이다.
227+
만약 청구식비가 없으면 일일 기본 식비를 반환하도록 DAO를 수정하면 아래와 같이 간결하게 된다.
228+
```
229+
public class PerDiemMealExpenses implements MealExpenses {
230+
public int getTotal() {
231+
// 기본값으로 일일 기본 식비를 반환한다.
232+
}
233+
}
234+
```
235+
```
236+
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
237+
m_total += expenses.getTotal();
238+
```
239+
이와같은 경우를 특수사례패턴(Special Case Pattern) 이라 한다.
240+
클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다.
241+
242+
## 7. null을 반환하지마라
243+
null을 반환하면 호출자가 null을 확인해야 한다. null확인 코드로 가득한 화면을 계속봐야 한다.
244+
이건 호출자에게 문제를 떠넘기는 행위이다.
245+
null 대신 예외를 던지거나 특수 사례 객체(ex. Collections.emptyList())를 반환하라.
246+
```
247+
List<Employee> employees = getEmployees();
248+
249+
if (employees != null) {
250+
for(Employee e : employees) {
251+
totalPay += e.getPay();
252+
}
253+
}
254+
```
255+
위 예의 경우 null이 아닌 빈 리스트를 반환한다면 더 깨끗해진다.
256+
```
257+
List<Employee> employees = getEmployees();
258+
259+
for(Employee e : employees) {
260+
totalPay += e.getPay();
261+
}
262+
263+
public List<Employee> getEmployees() {
264+
if (..직원이 없다면..)
265+
return Collections.emptyList();
266+
}
267+
```
268+
269+
## 8. null을 전달하지 마라
270+
인수로 null을 전달하면 함수 내에서 예외를 던지거나 assert 문을 사용할 수는 있다.
271+
하지만 NullPointException 문제를 해결해 줄 수는 없다.
272+
애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.
273+
274+
```
275+
double xProjection(Point p1, Point p2) {
276+
if(p1 == null || p2 == null){
277+
throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection");
278+
}
279+
return (p2.x - p1.x) * 1.5;
280+
}
281+
```
282+

0 commit comments

Comments
 (0)