diff --git "a/item73/item73_\354\266\224\354\203\201\355\231\224 \354\210\230\354\244\200\354\227\220 \353\247\236\353\212\224 \354\230\210\354\231\270\353\245\274 \353\215\230\354\240\270\353\235\274.md" "b/item73/item73_\354\266\224\354\203\201\355\231\224 \354\210\230\354\244\200\354\227\220 \353\247\236\353\212\224 \354\230\210\354\231\270\353\245\274 \353\215\230\354\240\270\353\235\274.md" new file mode 100644 index 0000000..d6fe467 --- /dev/null +++ "b/item73/item73_\354\266\224\354\203\201\355\231\224 \354\210\230\354\244\200\354\227\220 \353\247\236\353\212\224 \354\230\210\354\231\270\353\245\274 \353\215\230\354\240\270\353\235\274.md" @@ -0,0 +1,55 @@ +### item73 추상화 수준에 맞는 예외를 던져라 + +- 수행하려는 일과 관련이 없어보이는 예외가 튀어나올 때가 있다. 메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해버릴 때 종종 일어나는 일이다. +- 저수준 예외란 소프트웨어의 구체적인 구현 단계나 하위 계층(DB, 네트워크, 파일 시스템 등)에서 발생하는 예외이다. 쉽게 말해, 비즈니스 로직(무엇을 할지)과 상관없는 기술적인 세부 사항(어떻게 할지)에서 터진 에러들이다. + + 대표적으로 SQLException, IOException,SocketTimeoutException 등이 있다. + +- 저수준 예외가 문제가 되는 더 큰 이유는 내부 구현 방식을 드러내어 윗 레벨 API를 오염시킨다. 다음 릴리스에서 구현 방식을 바꾸면 다른 예외가 튀어나와 기존 클라이언트 프로그램을 깨지게 할 수도 있는 것이다. +- 이 문제를 피하려면 상위 계층에서 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다. 이를 `예외 번역`이라고 한다. + + 예를 들어, 회원 가입을 하는데 이미 ID가 있어서 DB에서 에러가 난다고 했을 때, + +```java +public void join(Member member) { + try { + repository.save(member); + } catch (SQLException e) { + // 1. 저수준 예외(SQLException)를 잡음 -> 서버가 SQLException에 대해서 알게 되어 캡슐화가 깨질 수 있음 + // 2. 상황에 맞는 고수준 예외(DuplicateMemberException)로 '번역'해서 던짐 + throw new DuplicateMemberException("이미 존재하는 회원입니다.", e); + } +} +``` + +- 다만 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 `예외 연쇄`를 사용하는 게 좋다. 예외 연쇄란 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다. + 예를 들어, 회원 가입을 하는 데 오류가 발생했다고 가정을 했을 때, + +```java +public void install() throws InstallException { + try { + startSetup(); + } catch (IOException e) { + // 새로운 예외(InstallException)를 만들면서, 원본 예외(e)를 같이 넣어줍니다. + throw new InstallException("설치 중 오류가 발생했습니다.", e); + } +} +``` + +왜 이렇게 하는 것이냐? -> 그냥 예외가 발생하면 관리자는 어디서 문제가 발생했는지 파악하는데 시간이 걸린다. 그래서 SQL의 문법에 오류가 있는 것인지, ID가 중복된 것인지, 서버에 문제가 있는 것인지 정확히 파악하지 못 한다. 이때, 위처럼 오류의 원본을 함께 실어주면 IOException 즉, 디스크 용량이 부족해서 오류가 생겼다는 것을 단번에 알 수 있게 해준다. + +- 고수준 예외의 생성자는 (예외 연쇄용으로 설계된) 상위 클래스의 생성자에 이 '원인'을 건네주어, 최종적으로 Throwable 생성자까지 건제니게 한다. + +```java +class HighLevelException extends Exception { + HighLevelException(Thowable cause) { + super(cause); + } +} +``` + +- 대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있다. 예외 연쇄는 문제 원인을 프로그램에서 접근할 수 있게 해주며, 원인과 고수준 예외의 스택 추적 정보를 잘 통합해준다. + + `물론 이 역시 남발하는 것은 좋은 방법이 절대 아니다! 주의하도록 하자.` + +- 차선책도 알아보자. 아래 계층에서의 예외를 피할 수 없다면, 상위 계층에서 그 예외를 조용히 처리하여 문제를 API 호출자에까지 전파하지 않는 방법이 있다. 이 경우 발생한 예외는 java.util.logging같은 적절한 로깅 기능을 활용하여 기록해두면 좋다. 그렇게 해두면 클라이언트 코드와 사용자에게 문제를 전파하지 않으면서도 프로그래머가 로그를 분석해 추가 조치를 취할 수 있게 해준다. diff --git "a/item80/item80_\354\212\244\353\240\210\353\223\234\353\263\264\353\213\244\353\212\224 \354\213\244\355\226\211\354\236\220, \355\203\234\354\212\244\355\201\254, \354\212\244\355\212\270\353\246\274\354\235\204 \354\225\240\354\232\251\355\225\230\353\235\274.md" "b/item80/item80_\354\212\244\353\240\210\353\223\234\353\263\264\353\213\244\353\212\224 \354\213\244\355\226\211\354\236\220, \355\203\234\354\212\244\355\201\254, \354\212\244\355\212\270\353\246\274\354\235\204 \354\225\240\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..550cfb2 --- /dev/null +++ "b/item80/item80_\354\212\244\353\240\210\353\223\234\353\263\264\353\213\244\353\212\224 \354\213\244\355\226\211\354\236\220, \355\203\234\354\212\244\355\201\254, \354\212\244\355\212\270\353\246\274\354\235\204 \354\225\240\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,31 @@ +### item80 스레드보다는 실행자, 태스크, 스트림을 애용하라 + +- 자바5에서 `java.util.concurrent` 패키지가 등장했다. 이 패키지는 실행자 프레임워크라고 하는 인터페에스 기반의 유연한 태스크 실행 기능을 담고 있다. 그래서 매우 간단한 코드로 작업 큐같은 기존의 어려웠던 코드를 쉽게 생성할 수 있게 되었다. +- 실행자 프레임워크란 복잡한 스레드 생성과 관리를 개발자가 직접 하지 않고, 프레임워크가 알아서 처리해주는 시스템이다. + +```java +//실행자 서비스 생성 +ExecutorService exec = Executors.newSingleThreadExcutor(); +//실행자에게 태스크 넘기기 +exec.execute(runnable); +//실행자 종료(이 작업 실패시 VM이 종료되지 않음) +exec.shutdown(); +``` + +- 실행자 서비스의 기능 + 1. 특정 태스크가 완료되기를 기다린다. + 2. 태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다. + 3. 실행자 서비스가 종료하기를 기다린다. + 4. 완료된 태스크들의 결과를 차례로 받는다. + 5. 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다. +- 큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩토리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다. +- 가변 스레드 풀 실행자: `Executors.newCachedThreadPool();` +- 이 실행자는 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다. 하지만, 무거운 프로덕션 서버에는 좋지 못하다. + + 이 실행자는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임 돼 실행된다. 가용한 스레드가 없다면 새로 하나를 생성한다. 서버가 아주 무겁다면 CPU 사용률이 100%로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더욱 악화시킨다. + +- 고정 스레드 풀 실행자: `newFixedThreadPool();` +- 위와 같은 문제가 나타나면 스레드 개수를 고정하거나 완전히 통제할 수 있는 `ThreadPoolExecutor(수동모드)`를 직접 사용하는 편이 좋다. 다만 일반적으로는 수동모드의 사용을 권장하지 않는다. +- 자바7이 되면서 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원하도록 확장됐다. +- `ForkJoinTask`의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다. 이렇게 하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다. 물론 포크-조인에 적합한 형태의 작업이어야 한다. +- 더 많은 실행자 프레임워크의 기능은 `자바 병렬 프로그래밍`을 참고하길 바란다. diff --git "a/item84/item84_\355\224\204\353\241\234\352\267\270\353\236\250\354\235\230 \353\217\231\354\236\221\354\235\204 \354\212\244\353\240\210\353\223\234 \354\212\244\354\274\200\354\244\204\353\237\254\354\227\220 \352\270\260\353\214\200\354\247\200 \353\247\220\353\235\274.md" "b/item84/item84_\355\224\204\353\241\234\352\267\270\353\236\250\354\235\230 \353\217\231\354\236\221\354\235\204 \354\212\244\353\240\210\353\223\234 \354\212\244\354\274\200\354\244\204\353\237\254\354\227\220 \352\270\260\353\214\200\354\247\200 \353\247\220\353\235\274.md" new file mode 100644 index 0000000..9ce24f2 --- /dev/null +++ "b/item84/item84_\355\224\204\353\241\234\352\267\270\353\236\250\354\235\230 \353\217\231\354\236\221\354\235\204 \354\212\244\353\240\210\353\223\234 \354\212\244\354\274\200\354\244\204\353\237\254\354\227\220 \352\270\260\353\214\200\354\247\200 \353\247\220\353\235\274.md" @@ -0,0 +1,36 @@ +### item84 프로그램의 동작을 스레드 스케줄러에 기대지 말라 + +- 잘 작성된 프로그램이라면 스레드 스케줄링 정책에 좌지우지 돼서는 안 된다. 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램은 다른 플랫폼에 이식하기 어렵다. +- 좋은 프로그램을 작성하는 가장 좋은 방법은 실행 가능한 `스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 하는 것이다. 또 실행 준비가 된 스레드들은 맡은 작업을 완료할 때까지 계속 실행되도록 만들자.` (대기 중인 스레드는 실행 가능하지 않다.) +- 실행 가능한 스레드 수를 적게 유지하는 방법은 각 스레드가 무언가 유용한 작업을 완료한 후에는 다음 일거리가 생길 때까지 대기하도록 하는 것이다. `스레드가 당장 처리해야 할 작업이 없다면 실행돼서는 안 된다.` +- 스레드는 절대 바쁜 대기(busy waiting) 상태가 되면 안 된다. + -> 공유 객체의 상태가 바뀔 때까지 쉬지 않고 검사해서는 안 된다는 뜻이다. +- 예시 + +```java +public class SlowCountDownLatch { + private int count; + + public SlowCountDownLatch(int count) { + if (count < 0) + throw new IllegalArgumentException(count + " < 0"); + this.count = count; + } + + public void await() { + while(true) { // 여기서 바쁜 대기 상태가 발생 + synchronized(this) { + if (count == 0) + return; + } + } + } + + public sychronized void countDown() { + if (count != 0) + count--; + } +} +``` + +- 하나 이상의 스레드가 필요도 없이 실행 가능한 상태인 시스템은 흔하게 볼 수 있다. 이런 시스템은 성능과 이식성이 떨어질 수 있다. diff --git "a/item90/item90_\354\247\201\353\240\254\355\231\224\353\220\234 \354\235\270\354\212\244\355\204\264\354\212\244 \353\214\200\354\213\240 \354\247\201\353\240\254\355\231\224 \355\224\204\353\241\235\354\213\234 \354\202\254\354\232\251\354\235\204 \352\262\200\355\206\240\355\225\230\353\235\274.md" "b/item90/item90_\354\247\201\353\240\254\355\231\224\353\220\234 \354\235\270\354\212\244\355\204\264\354\212\244 \353\214\200\354\213\240 \354\247\201\353\240\254\355\231\224 \355\224\204\353\241\235\354\213\234 \354\202\254\354\232\251\354\235\204 \352\262\200\355\206\240\355\225\230\353\235\274.md" new file mode 100644 index 0000000..83453f3 --- /dev/null +++ "b/item90/item90_\354\247\201\353\240\254\355\231\224\353\220\234 \354\235\270\354\212\244\355\204\264\354\212\244 \353\214\200\354\213\240 \354\247\201\353\240\254\355\231\224 \355\224\204\353\241\235\354\213\234 \354\202\254\354\232\251\354\235\204 \352\262\200\355\206\240\355\225\230\353\235\274.md" @@ -0,0 +1,61 @@ +### item90 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라 + +- 직렬화: 데이터를 전송하기 위해 바이트 형태로 변환하는 기술 +- 생성자가 아닌 직렬화를 통해 생성된 객체는 버그와 보안 문제가 일어날 가능성이 커진다. 이 위험을 줄이기 위해 있는 방법이 바로 `직렬화 프록시 패턴`이다. +- 직렬화 프록시 패턴은 + +1. 바깥 클래스의 핵심 데이터만 들고 있는 중첩 클래스를 설계해 private static으로 선언한다. 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시다.(중첩 클래스의 생성자는 단 하나여야 하고, 바깥 클래스를 매개변수로 받아여 한다.) + +```java +public class Period implements Serializable { //바깥 클래스 + private final Date start; + private final Date end; + + public Period(Date start, Date end) { + if (start.compareTo(end) > 0) throw new IllegalArgumentException(); + this.start = start; + this.end = end; + } + + private static class SerializationProxy implements Serializable { //중첩 클래스(직렬화 프록시) + private final Date start; + private final Date end; + + SerializationProxy(Period p) { //논리적 상태 정밀 + this.start = p.start; + this.end = p.end; + } + } +} +``` + +2. 바깥 클래스에 writeReplace 메서드를 추가한다. + +```java +private Object writeReplace() { //자바 직렬화 시스템이 이 메서드를 발견하면, Period 대신 Proxy를 저장한다. + return new SerializationProxy(this); +} +``` + +3. 보안상의 이유로 readObeject 메서드를 바깥 클래스에 추가한다. + +```java +//공격자가 Period를 직접 역직렬화 하려는 것을 막아준다. +private void readObeject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvaildObjectException("프록시가 필요합니다.") +} +``` + +4. 역직렬화 시 진짜 객체인 Period로 바꾸기 위한 readResolve 메서드를 SerializationProxy 클래스에 추가하면 끝이다. + +```java +private Object readResolve() { + return new Period(start, end); +} +``` + +- 직렬화 프록시 패턴의 한계 + +1. 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다. +2. 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다. 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하려 하면 ClassCastException이 발생할 것이다. 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어진 것이 아니기 때문이다. +3. 이 패턴이 강력함과 안전성을 주는 만큼 성능의 저하가 있을 수 있다.