예전에 서비스 내에서 HTTP을 사용해 HR 시스템에서 정보를 가져오는 로직에 문제가 발생한 적이 있었다.
원인은 사용하는 HttpClient객체가 static으로 선언되어 있어 멀티 쓰레드 환경에서 통신이 꼬여버린 것이다.
해결방안으로 HttpClientBuilder를 사용해 PoolingHttpClientconnectionManager, requestConfig 객체를 주입받은 CloseableHttpClient 객체를 싱글톤으로 등록 후 호출할때마다 재사용하는 방식을 사용했다.
그런데 다른 프로젝트의 SM업무를 맡던 도중 HTTP 통신할때마다 DefaultHttpClient 객체를 생성하고 있었다. 또한 인스턴스를 통신할때마다 생성하는 부분은 있지만 close시키는 부분이 없어서 DefaultHttpClient가 어떤녀석인지, 그리고 CloseableHttpClient와 무슨 차이점이 있는지 궁금해졌다.
2. CloseableHttpClient와 DefaultHttpClient
이 두 클래스 모두 HttpClient 인터페이스의 구현클래스이다. 하지만 이녀석들의 차이에 대해 상세하게 정리된 내용을 찾지못해 실제 테스트를 통해 알아보기로 했다.
예외 메시지는 "연결이 여전히 할당되어 있습니다. 다른 연결을 할당하기 전에 연결을 해제해야 합니다." 라는 뜻이다.
12번 라인에서 수행된 연결이 아직 끊기지 않은 상태에서 13번 라인의 execute 코드가 실행되어 그런 것같다. 그렇다면 과연 언제 끊기는 걸까? 생명주기가 궁금해졌지만 이는 더 파고들어야 알수있을 것 같다. 일단 여기서 확인된 점은 DefaultHttpClient 클래스는 생명주기가 끝나기 전까지 한번의 HTTP 요청을 수행할 수 있다는 것이다.
스터디로 File 업로드, 다운로드 로직을 작성하던 도중, 특정 부분에 빨간줄이 등장했다. 자연스럽게 마우스 가져가보니 Exception!. 별생각 없이 add thrwos declaration 클릭. 상황종료.
이처럼 예외 처리에 큰 의미나 생각을 부여하지 않았다.
그러던 어느날, 고객사측에서 에러 문의가 들어와 로그를 확인해보니 딱 봐도 DB 예외로 보이는 ERDBException이 발생하고 있었다.(커스텀 익셉션인 것!) 그래서 DB 예외라는 필터를 머릿속에 장착한 뒤 로직을 확인해보니... 이게웬걸.. 모든 예외에 대한 처리를 ERDBException으로 던지고있었다.
그리고 깨달았다. 예외처리를 제대로 하지 않으면 유지보수 시 혼란을 초래할 수 있다는 것을...
2. 예외? 에러?
예외에 대한 기본 개념을 정립하기 위해 구글링을 하니 "예외와 에러의 차이" 가 눈에 들어왔다. 사실 이 두 개념은 비슷하다고 생각했으나, 막상 확인해보니 예외 공부에 아주 중요한 키포인트가 됐던 내용이었다.
정리하면 에러는 시스템 레벨에서 발생하는 아주 심각한 수준의 문제이다. 예를들면 서버의 과부하가 걸려 발생하는 OutOfMemory 같은 것이다. 이러한 에러는 프로그래머가 미리 예측하지 못하며 로직으로 처리할 수 없다.
이에반해 예외는 프로그래머가 작성한 로직으로 인해 발생하는 문제이다. 미리 예측하여 처리할 수 있기 때문에 올바른 처리방법을 통해 핸들링하는 것이 중요하다.
3. 예외 코드 예제
예외 코드에 대한 예제 코드를 txt 파일을 조회하는 것으로 작성해보도록 하겠다.
위 코드에 빨간줄이 아주 많다; 빨간줄이 나타나는 이유는 예외처리를 하지 않아서인데, 이처럼 예외처리를 꼭 해줘야하는 것들이 있다. 이러한 예외들을 CheckedException이라고 한다. 말 그대로 컴파일 시점에서 체크해주는 예외이다.
런타임 시점에 발생하는 예외들도 있는데 이는 UnCheckedException이라고도 하고 RuntimException이라고 한다. 이러한 예외는 컴파일 시점에 체크되지 않아 위 소스처럼 에러가 뜨지 않는다.
4. try, catch, finally
예외를 처리하기 위해서 사용하는 기본적인 문법이 있다. 바로 try, catch, finally이다.
try는 예외 발생 가능성이 있는 로직이 포함된 블럭, catch는 예외가 발생했을 때 처리되는 로직이 포함된 블럭, finally는 예외가 발생하던 안하던 최종적으로 처리되는 로직이 포함된 블럭이다.
간단한 예제를 통해 실습해보자.
현재 File 객체의 매개변수 값으로 텍스트파일의 경로를 넣어놨는데, 이 경로는 실제로 존재하지 않는 파일의 경로이다. 이로인해 try 문 안의 fis = new FileInputStream 부분에서 FileNotFoundException (FileNotFoundException 예외는 Exception을 상속하고 있음) 이 발생하게 되는데, 이때 catch 블록으로 이동하여 예외 처리 후, 최종적으로 finally문에서 스트림을 닫게 된다.
그럼 이런 의문이 들 수 있다.
Q : catch의 매개변수에 Exception을 넣으면 결론적으로 try 문에서 발생한 모든 예외들은 저 하나의 catch문에서 처리되는게 아닌가? 그럼 다양한 예외에 대한 핸들링하지 못하잖아?
맞다. 저렇게 무작정 Exception으로만 넣게되면, 모든 예외를 한곳에서 핸드링하는 격이기 때문에 상황에 맞게 예외를 분기해서 처리해야 한다. 한곳에서 처리하는 것이 효율적인 예외도 있겠지만, 예를 들어 로직에 SQLException과 FileNotFoundException 예외가 발생할 수 있는 상황이라면 각각의 예외에 대한 세부 로그를 남기는 등의 예외처리를 하면 좋지 않을까 싶다. (참고로 모든 예외를 Exception으로 통일한 프로젝트도 있었다.)
5. throw, throws
예외란 프로그래머가 작성한 로직에 의해 발생된다. 그렇다면 프로그래머가 예외를 발생시킬수도 있을까? 있다. 추가적으로 예외에 대한 책임을 전가할수도 있다. 이게 바로 throw와 throws이다.
- throw는 예외를 강제로 발생시킨 후, 상위 블럭이나 catch문으로 예외를 던진다.
위 예제는 main 메서드에서 myException메서드를 호출하고, 여기서 throw를 통해 Exception을 강제로 발생시키고 있다.
때문에 catch 블럭으로 처리가 위임되고, 여기서 예외를 처리하고 있다. 이는 콘솔 로그를 통해 알수있다.
- throws는 예외가 발생하면 상위메서드로 예외를 던진다.
일반적으로 throws를 사용하면 try, catch 구문이 생성되지 않는 것을 확인할 수 있는데, 이 이유는 throws구문에 의해 예외에 대한 처리를 호출부로 위임하기 때문이다.
throw를 통해 예외를 발생시키고 throws는 이 예외를 밖으로 던져버리고 있다.
추가적으로 이 두가지를 합친 방식도 있다.
throw + throws는 예외처리를 catch문에서도 하고, 호출부로 예외를 던진다.
22줄에 throw e를 통해 예외를 발생시키면 throws에 의해 상위 메서드로 예외를 던지게 된다.
서버에서 API 통신이나 HTTP 통신에 대한 응답 값으로 Json 형식의 문자열 데이터가 오는 경우가 있다. 이때 데이터의 특정 key에 해당하는 값에 접근하기 위해 String 클래스에서 제공하는 메서드를 사용할 수도 있으나, 데이터가 복잡해지고, Node 가 많아질 수록 데이터 조작 및 접근에 한계를 느끼게 된다.
이를 해소할 수 있는 방안으로 문자열 데이터를 JsonObject로 변환하는 방식이 있다. 이를 사용해보자.
3번 줄의 configuration status 구문은 이 설정파일이 로드될 때 발생하는 로그에 대한 레벨을 설정하는 부분이다.
말이 어렵지 실행화면을 보면 이해가 갈것이다.
log4j2.xml 의 내부 설정을 로드하면서 발생하는 DEBUG 이상의 로그들을 출력중이다.
status를 info로 설정하면 아~~~무 로그도 출력되지 않는다.
그 이유는 log4j2.xml 설정파일을 로드할 때의 정보는 내부적으로 DEBUG레벨로 찍고 있기 때문이다.
INFO 레벨은 DEBUG레벨보다 상위의 레벨이기 때문에 로그가 찍히지 않게 된다.
그렇다면 log4j2.xml 파일을 로드할 때 문제가 생기도록 코드를 바꾼 후 status를 info로 설정한다면?
내부적으로 error 로그가 발생할 것이고, 이는 info 레벨보다 높기 때문에 로그가 찍힐 것이다.
테스트로 5번줄의 Appenders 태그 명을 Appenderss로 바꾼 후 실행시켰다.
제일 아랫줄의 ERROR test.LogTest 부분이 log4j2.xml 을 로드하면서 생긴에러이다.
* configuration 부분을 설명하기 위해 먼저 MainClass와 LogTest를 구현하였다. 현재 MainClass와 LogTest 를 생성만 한 상태에서는 위처럼 테스트가 불가능하다. 이런 설정이구나 라고만 이해하고 넘어가자.
Appenders 태그 안에 실질적인 로그 설정 코드를 삽입한다.
현재 콘솔에 로그를 출력시키기 위해 <Console> 태그 관련 코드를 삽입하고 로그로 찍히는 패턴을 PatternLayout 태그를 통해 설정한다.
%d는 로그 시간에 관한 설정을 나타내는데 괄호 안의 형태로 포멧시킬 수 있다.
%p는 로그 레벨, %c는 로그가 발생한 클래스 경로, %m은 로그 메시지, %n은 개행이다.
%5p는 로그 레벨이 출력되는 기본 문자열 길이를 5로 설정한다는 의미이다.
12번 줄의 loggers 는 설정한 로그 코드를 적용하는 부분이다.
root 태그를 사용하면 현재 시스템에서 발생하는 모든 로그를 찍어낼 수 있고, level을 debug로 설정하여 debug 이상의 로그만 출력되도록 한다. Appender를 이용해서 앞서 설정한 console을 적용시키면, 결과적으로 기동하는 시스템내에서 발생하는 모든 로그 중 debug레벨 이상은 모두 찍히게 된다.