반응형

1. 개요

 이전 포스팅에서 테스트 코드를 통해 두 클래스를 비교한 결과 다음과 같은 정보를 얻을 수 있었다.

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 메서드 존재 여부 X O
HTTP 통신 횟수 1 ConnectionPool 설정에 따라 다름
ConnectionPool X O

오늘 알아볼 것은

첫째, 생성한 DefaultHttpClient에서 execute를 2번 이상 실행했을 때 즉, HTTP 통신을 2번 이상 요청했을 때 예외가 발생한 원인과 생명주기.

둘째, CloseableHttpClient의 ConnectionPool 설정 정보를 jar 파일을 통해 알아보겠다.


2. DefaultHttpClient

jar파일과 예외 로그를 기반으로 코드를 찾아가보니 다음과 같은 부분에서 예외가 발생함을 확인하였다.

execute 메서드를 따라가다.

conn이라는 값이 null이 아닐 때 위 에러가 발생한다. conn은 ManagedClientConnectionImpl 형의 멤버필드였으며, 어디선가 주입이 된것같은데... 결론은 찾을 수가 없었다. 멍청한자식.

 

서치를 통해 얻은 정보를 정리한 결과 예외 발생 원인은 다음과 같았다.

DefaultHttpClient 객체를 생성하면 내부적으로 basicClientConnectionManager 인스턴스가 주입된다. 이 인스턴스는  HTTP 통신에 대한 커넥션 정보를 저장하고 있다. 단, 하나의 최초 연결한 하나의 커넥션 정보만 저장한다.

두 개의 커넥션을 연결하려 했기때문에 예외가 발생했으며, 실제 에러 로그를 확인해보니 basicClientConnectionManager 클래스의 메서드 안에서 발생함을 확인할 수 있었다.

아래 로그의 Asserts.java:34가 위 코드 사진의 첫번째 빨간 블럭부분이었다. (기존 커넥션 정보가 있기때문에 발생)

 

정리하면 DefaultHttpClient 클래스는 하나의 HTTP 통신만을 처리할 수 있도록 내부적으로 구현되어져 있다.

만약 이 인스턴스를 사용해 두번의 통신을 처리하고싶다면 두개의 인스턴스를 생성해야한다.


3. CloseableHttpClient

생성자를 찾아가보니 다음과 같은 코드가 있었다. httpClientBuilder.create().build(). 요녀석을 파헤쳐보자

createDefault()

create().build() 메서드 확인 결과, PoolingHttpClientConnectionManager를 생성한 후 connectionManager로 사용하는 것이 보인다.

create().build()

 

생성된 ConnectionPool의 Default maxTotal, maxConPerRoute는 다음과 같이 2, 20임을 확인할 수 있다.

CPool

변수
maxTotal 최대 커넥션 개수
maxConPerRoute 라우트당 최대 커넥션 개수(ip:port 별 최대 커넥션 개수)

 

커스텀을 하지 않고 사용할 경우 최대 커넥션 개수가 2개이기때문에 실제 서비스를 운영하기엔 문제가 있다.

PollingHttpClientConnectionManager 은 커스텀이 가능하다. 때문에 상황에 맞게 커스텀하여 CloseableHttpClient를 구현한다면 많은 HTTP 통신 요청을 필요로 하는 서비스에 적절하게 사용될 수 있다.


4. 마치며

DefaultHttpClient와 CloseableHttpClient에 대한 아~주 미세한 차이에 대해서도 몰랐었지만, 이번 스터디를 통해 차이점은 물론이며, 실제 서비스에 왜 저 클래스를 사용했는지도 이해하게 되었다.

DefaultHttpClient의 생명주기, 언제 connection이 끊어지는지에 대해서는 확인하지 못해 뭔가 깨림직한 기분이지만, 오늘하루도 잘 보냈음에 위안을 삼는다!

반응형
반응형

1. 개요

 예전에 서비스 내에서 HTTP을 사용해 HR 시스템에서 정보를 가져오는 로직에 문제가 발생한 적이 있었다.

 원인은 사용하는 HttpClient객체가 static으로 선언되어 있어 멀티 쓰레드 환경에서 통신이 꼬여버린 것이다.

 해결방안으로 HttpClientBuilder를 사용해 PoolingHttpClientconnectionManager, requestConfig 객체를 주입받은   CloseableHttpClient 객체를 싱글톤으로 등록 후 호출할때마다 재사용하는 방식을 사용했다.

 

 그런데 다른 프로젝트의 SM업무를 맡던 도중 HTTP 통신할때마다 DefaultHttpClient 객체를 생성하고 있었다. 또한 인스턴스를 통신할때마다 생성하는 부분은 있지만 close시키는 부분이 없어서 DefaultHttpClient가 어떤녀석인지, 그리고 CloseableHttpClient와 무슨 차이점이 있는지 궁금해졌다.


2. CloseableHttpClient와 DefaultHttpClient

  이 두 클래스 모두 HttpClient 인터페이스의 구현클래스이다. 하지만 이녀석들의 차이에 대해 상세하게 정리된 내용을 찾지못해 실제 테스트를 통해 알아보기로 했다.


2.1. DefaultHttpClient, CloseableHttpClient 테스트코드 1.

 각각의 클래스에 DefaultHttpClient, CloseableHttpClient 인스턴스 생성 후 HTTP GET 통신을 하는 코드를 작성하였다.


2.2. CloseableHttpClient

DefaultHttpClient를 사용하여 HTTP GET 통신을 하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UseHttpClient {
 
    private static final String URL = "http://www.naver.com";
    
    public static void main(String[] args) {
        HttpClient httpClient = null;
        try {
            httpClient = new DefaultHttpClient(); // httpClient 4.3버전 이후 deprecated 처리.
            
            HttpGet httpGet = new HttpGet(URL);
            
            HttpResponse response = httpClient.execute(httpGet);
            
            System.out.println(":: DefaultHttpClient Response ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
 
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
cs

17번째 라인부터는 응답 데이터를 읽어오는 부분인데 네이버 홈페이지 코드가 방대하게 나와서 굳이 출력하진 않았다.

중요한건 DefaultHttpClient 인스턴스를 사용해 HTTP 통신을 한번 했다는 점이다.


2.2. CloseableHttpClient

 CloseableHttpClient를 사용하여 HTTP GET 통신을 하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class UseCloseableHttpClient {
 
    private static final String URL = "http://www.naver.com";
    
    public static void main(String[] args) {
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(URL);
            
            CloseableHttpResponse response = httpClient.execute(httpGet);
            
            System.out.println(":: CloseableHttpResponse ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(httpClient != null) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}
cs

마찬가지로 16번째 라인부터는 응답 데이터를 읽어오는 부분이다.

여기서도 중요한건 CloseableHttpClient 인스턴스를 사용해 한번 통신했다는 점이다.


2.3. 첫번째 테스트로 인해 알게된 차이점

 첫번째 테스트로 인해 알게된 이 두 클래스의 차이점을 정리해보았다. 생성방식이 달랐고, CloseableHttpClient 클래스는 close 메서드가 있었다. 왜 있지?? 아직까지는 큰 차이를 느끼진 못해 다음 테스트를 진행하였다.

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 여부 X O

3.1. DefaultHttpClient, CloseableHttpClient 테스트코드 2.

 close라는 부분이 눈에 밟혀 각각의 인스턴스에서 HTTP 통신 메서드인 execute를 두번씩 호출해보았다.


3.2. DefaultHttpClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UseHttpClient {
 
    private static final String URL = "http://www.naver.com";
    
    public static void main(String[] args) {
        HttpClient httpClient = null;
        try {
            httpClient = new DefaultHttpClient(); // httpClient 4.3버전 이후 deprecated 처리.
            
            HttpGet httpGet = new HttpGet(URL);
            
            HttpResponse response = httpClient.execute(httpGet);
            HttpResponse response2 = httpClient.execute(httpGet); // 추가한 코드
            
            System.out.println(":: DefaultHttpClient Response ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            System.out.println(":: response 2 result code : "+response2.getStatusLine().getStatusCode()); // 추가한 코드
 
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
cs

13번 라인과 17번 라인에 호출 및 응답 코드를 출력하는 코드를 추가하였다.

실행 결과 다음과 같은 Exception이 발생하였다.

13번 라인에서 Exception 발생

예외 메시지는 "연결이 여전히 할당되어 있습니다. 다른 연결을 할당하기 전에 연결을 해제해야 합니다." 라는 뜻이다.

12번 라인에서 수행된 연결이 아직 끊기지 않은 상태에서 13번 라인의 execute 코드가 실행되어 그런 것같다. 그렇다면 과연 언제 끊기는 걸까? 생명주기가 궁금해졌지만 이는 더 파고들어야 알수있을 것 같다. 일단 여기서 확인된 점은 DefaultHttpClient 클래스는 생명주기가 끝나기 전까지 한번의 HTTP 요청을 수행할 수 있다는 것이다.


3.3. CloseableHttpClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class UseCloseableHttpClient {
 
    private static final String URL = "http://www.naver.com";
    public static void main(String[] args) {
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(URL);
            
            CloseableHttpResponse response = httpClient.execute(httpGet);
            CloseableHttpResponse response2 = httpClient.execute(httpGet); // 추가한 코드
            
            System.out.println(":: CloseableHttpResponse ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            System.out.println(":: response 2 result code : "+response2.getStatusLine().getStatusCode()); // 추가한 코드
            
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(httpClient != null) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}
cs

마찬가지로 11, 15번 라인에 통신 및 응답코드를 출력하는 코드를 추가하였다.

실행 결과, 예외가 발생하지 않으며, 두 요청 모두 response Code 200을 응답받았다.

그렇다면 이 CloseableHttpClient의 생명주기는 어떻게 될지가 궁금해졌다. 마침 close 메서드도 있으니 추가 테스트를 진행해보았다. 그런데 예상치 못한 예외 코드가 출력되었다.


3.4. CloseableHttpClient close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class UseCloseableHttpClient {
 
    private static final String URL = "http://www.naver.com";
    public static void main(String[] args) {
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(URL);
            
            CloseableHttpResponse response = httpClient.execute(httpGet);
            httpClient.close(); // 추가한 코드
            CloseableHttpResponse response2 = httpClient.execute(httpGet);
            
            System.out.println(":: CloseableHttpResponse ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            System.out.println(":: response 2 result code : "+response2.getStatusLine().getStatusCode());
            
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(httpClient != null) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}
cs

 

11번 라인에서 Exception 발생

커넥션 풀이 종료됐다는 예외였다!

아. 생성한 CloseableHttpClient 인스턴스는 기본적으로 커넥션 풀을 지원한다는 것을 알게되었고, close 메서드는 이 인스턴스에 할당된 커넥션 풀을 close 시키는 것이었다.

3.2 테스트에서 2번을 연속으로 호출했을 때 예외가 발생하지 않았던 이유도 요청을 커넥션 풀을 통해 요청이 처리되기 때문이었다.


3.4. 두번째 테스트로 인해 알게된 차이점

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 여부 X O
HTTP 통신 횟수 1 ConnectionPool 설정에 따라 다름
ConnectionPool X O

 

이제 DefaultHttpClient에서 발생했던 예외의 원인과 생명주기는 무엇이고, CloseableHttpClient의 커넥션 풀이 어디서 설정되는지 확인해야한다. 이는 생성부분의 내부 코드를 확인해야한다.

 

이는 다음 포스팅에 정리하도록 하겠다.

 

- 혹, 글의 내용 중 맞지 않는 부분이나 수정할 사항이 있다면 꼭 댓글 부탁드립니다. 정말 감사히 받아드리겠습니다!

반응형
반응형

1. 개요

 - 클래스에 접근을 제어하는 접근제어자에 대해 복습하였다.


2. 접근제어자

 - 클래스의 멤버변수와 메서드는 접근제어자를 통해 외부에서의 접근을 제어할 수 있다.

 - 접근제어자의 종류와 특징은 다음과 같으며, public과 private가 자주 사용된다.

 

  > public : 클래스의 외부에서 접근이 가능.

  > private : 클래스 내부에서만 접근 가능.

  > default : 동일 패키지에 있는 다른 클래스에서 접근 가능.

  > protected : 동일 패키지의 다른 클래스와 다른 패키지의 하위클래스에서 접근 가능 

 


3. public

 - MyClass 멤버변수의 접근제어자를 public으로 설정하면 외부 클래스인 AccessMain 클래스에서 접근이 가능하다.

 - 멤버변수가 public으로 되어있기 때문에 위 객체의 값을 어디서든 수정, 조회할 수 있다.

MyClass
AccessMain


4. private

 - MyClass 멤버변수의 접근제어자를 private로 설정하면 외부 클래스인 AccessMain 클래스에서 접근이 불가능하다.

 - 예제의 AccessMain 클래스를 보면 7번째 줄에서 컴파일 에러가 발생함을 확인할 수 있다.

 - 접근제어자가 public인 getter, setter 메서드를 생성하면 메서드를 통한 접근이 가능해진다. (AccessMain : 9 line)

 - 만약 특정 객체에 대해 값을 조회하는 기능만 넣고싶다면 public 형식의 getter만 생성하면 된다.

MyClass


5. default

 - MyClass 멤버변수의 접근제어자 설정하지 않으면 default로 설정되며 같은 패키지 내의 클래스에서만 접근할 수 있다.

 - MyClass와 AccessMain 클래스는 같이 패키지에 위치하므로 접근이 가능하다.

 - AccessMain2 클래스는 MyClass의 하위 패키지에 위치하므로 접근이 불가능하다.

MyClass

 

AccessMain

 

AccessMain2


5. protected

 - MyClass 멤버변수의 접근제어자를 protected로 설정하면 같은 패키지 내의 클래스와 자식클래스에서 접근할 수 있다.

 - MyClass와 AccessMain 클래스는 같이 패키지에 위치하므로 접근이 가능하다.

 - AccessMain2 클래스는 MyClass의 하위 패키지에 위치하지만 MyClass를 상속받은 자식클래스이므로 접근할 수 있다.

MyClass

 

AccessMain

 

AccessMain2


6. 마치며

 접근제어자에 대한 복습을 하니, 머릿속에 한번 더 정리되는 느낌이 들어 좋다. default는 있는지도 몰랐는데... 신기했으나 앞으로 쓸일은 없을것같다!

반응형
반응형

1. 개요

 스터디로 File 업로드, 다운로드 로직을 작성하던 도중, 특정 부분에 빨간줄이 등장했다. 자연스럽게 마우스 가져가보니 Exception!. 별생각 없이 add thrwos declaration 클릭. 상황종료.

이처럼 예외 처리에 큰 의미나 생각을 부여하지 않았다.

두둥등장한 에러.

 그러던 어느날, 고객사측에서 에러 문의가 들어와 로그를 확인해보니 딱 봐도 DB 예외로 보이는 ERDBException이 발생하고 있었다.(커스텀 익셉션인 것!) 그래서 DB 예외라는 필터를 머릿속에 장착한 뒤 로직을 확인해보니... 이게웬걸.. 모든 예외에 대한 처리를 ERDBException으로 던지고있었다.

 그리고 깨달았다. 예외처리를 제대로 하지 않으면 유지보수 시 혼란을 초래할 수 있다는 것을...


2. 예외? 에러?

 예외에 대한 기본 개념을 정립하기 위해 구글링을 하니 "예외와 에러의 차이" 가 눈에 들어왔다. 사실 이 두 개념은 비슷하다고 생각했으나, 막상 확인해보니 예외 공부에 아주 중요한 키포인트가 됐던 내용이었다.

 정리하면 에러는 시스템 레벨에서 발생하는 아주 심각한 수준의 문제이다. 예를들면 서버의 과부하가 걸려 발생하는 OutOfMemory 같은 것이다. 이러한 에러는 프로그래머가 미리 예측하지 못하며 로직으로 처리할 수 없다.

 이에반해 예외는 프로그래머가 작성한 로직으로 인해 발생하는 문제이다.  미리 예측하여 처리할 수 있기 때문에 올바른 처리방법을 통해 핸들링하는 것이 중요하다.

예외와 에러의 계층구조 (출처 : https://velog.io/@new_wisdom/Exception)


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문으로 예외를 던진다.

throw

위 예제는 main 메서드에서 myException메서드를 호출하고, 여기서 throw를 통해 Exception을 강제로 발생시키고 있다.

때문에 catch 블럭으로 처리가 위임되고, 여기서 예외를 처리하고 있다. 이는 콘솔 로그를 통해 알수있다.

 

 - throws는 예외가 발생하면 상위메서드로 예외를 던진다.

throws

일반적으로 throws를 사용하면 try, catch 구문이 생성되지 않는 것을 확인할 수 있는데, 이 이유는 throws구문에 의해 예외에 대한 처리를 호출부로 위임하기 때문이다.

throw를 통해 예외를 발생시키고 throws는 이 예외를 밖으로 던져버리고 있다.

추가적으로 이 두가지를 합친 방식도 있다.

 

throw + throws는 예외처리를 catch문에서도 하고, 호출부로 예외를 던진다.

throw + throws

22줄에 throw e를 통해 예외를 발생시키면 throws에 의해 상위 메서드로 예외를 던지게 된다.

예외처리를 예외 발생지와 호출부에서 하고싶다면 이와 같은 방법을 사용하면 된다.

반응형
반응형

1. 개요

 서버에서 API 통신이나 HTTP 통신에 대한 응답 값으로 Json 형식의 문자열 데이터가 오는 경우가 있다. 이때 데이터의 특정 key에 해당하는 값에 접근하기 위해 String 클래스에서 제공하는 메서드를 사용할 수도 있으나, 데이터가 복잡해지고, Node 가 많아질 수록 데이터 조작 및 접근에 한계를 느끼게 된다.

 이를 해소할 수 있는 방안으로 문자열 데이터를 JsonObject로 변환하는 방식이 있다. 이를 사용해보자.

 

2. 로직

 간단한 구조에서는 아래 4가지만 기억하면 된다. 

 

 1) String 형식의 JSON 문자열

 2) JSONParser

 3) To Object

 4) To JSONObject

 

 위 4가지를 기억하고 아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public static void jsonTest() throws ParseException{
        //1. Json 문자열
        String strJson = "{\"userId\":\"sim\", "
                        + "\"userPw\":\"simpw\","
                        + "\"userInfo\":{"
                            + "\"age\":50,"
                            + "\"sex\":\"male\""
                            + "}"
                        + "}";
        
        //2. Parser
        JSONParser jsonParser = new JSONParser();
        
        //3. To Object
        Object obj = jsonParser.parse(strJson);
        
        //4. To JsonObject
        JSONObject jsonObj = (JSONObject) obj;
        
        //print
        System.out.println(jsonObj.get("userId")); //sim
        System.out.println(jsonObj.get("userPw")); //simpw
        System.out.println(jsonObj.get("userInfo")); // {"sex":"male","age":50}
 
    }
cs

 1) Json 문자열을 준비한다. (3 line)

  - API를 통해 받아왔다고 가정하고 String 형식의 userId, userPw와 Object 형식의 userInfo 데이터를 준비한다.

  - Json 형식은 {key:value} 형식이며, key값은 큰따옴표(")로 묶여있다. Parser 는 이 큰따옴표를 통해 파싱 처리를 하기 때문에 키 값에 \" 문자열을 추가해주었다.

 

 2) JSONParser (12 line)

  - 문자열을 Json 형식에 맞게 Object로 파싱할 수 있는 Parser를 생성한다. 

 

 3) To Object (15 line)

  - jsonParser를 통해 Json 문자열을 Object 형식으로 파싱한다.

 

 4) To JSONObject (18 line)

  - Object 형식의 데이터를 JSONObject 형식으로 형변환한다.

 

3. 테스트 결과

 JSONObject 형 객체를 통해 String, Object 등의 모든 Json 형식의 데이터에 접근할 수 있다.

반응형
반응형

1. 개요

 quartz 를 사용하여 1초마다 로그 클래스를 실행시키고, 발생하는 로그를 1분마다 Rolling 하여 파일로 남겨보자!


2. 환경

 1) maven

 2) JDK 1.8

 3) Tomcat 8.0


3. 준비

 3.1) Maven 라이브러리 추가

  스프링 라이브러리와 quartz 라이브러리를 추가해주자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <!-- dispatcherServlet 지원 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
        
    <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.3.0</version>
    </dependency>
cs

 

 3.2) web.xml 파일 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  
      <!-- Controller가 공유하는 Bean들을 포함하는 Spring Container를 생성한다. 
             공통 설정파일은 여기에 생성한다. 즉 quartz 또한 이곳에 생성하는 것이 좋다.
      -->
      
      <context-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>
              /WEB-INF/spring/quartz-context.xml
          </param-value>
      </context-param>
      
      <listener>
          <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
      
      
     <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    
    <!-- /의 형식으로 시작하는 url에 대하여 UTF-8로 인코딩 -->
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
 
</web-app>
cs

 contextConfigLocation을 /WEB-INF/spring/quartz-context.xml 로 설정하고, UTF-8 인코딩 설정한다(인코딩 부분은 딱히 필요없지만 넣었다.)

 

 3.3) quartz-context.xml 파일 생성

 WEB-INF에 spring 폴더를 생성하고 내부에 quartz-context.xml 파일을 생성하자.

요로코롬.

이제 기본적인 준비가 끝났다. 실제 동작을 위한 코드를 삽입하자.


4. 클래스 생성 및 코딩

 - TestJob.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package quartz;
 
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
 
public class TestJob implements Job{
    
    private Logger logger = LogManager.getLogger(TestJob.class);
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // TODO Auto-generated method stub
        logger.debug("this is log and for you");
        logger.trace("this is log and for you");
        logger.info("this is log and for you");
        logger.warn("this is log and for you");
        logger.error("this is log and for you");
    }
}
 
cs

quartz를 적용시키려면 반드시 Job 인터페이스를 상속받아야 한다.

그리고 execute에 실행시킬 로직을 삽입한다.

보는것과 같이 log를 level 별로 찍고있다.

 

 - CronTrigger.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package quartz;
 
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
 
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
 
public class CronTrigger {
    
    public CronTrigger() {
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        try {
            Scheduler scheduler = schedulerFactory.getScheduler();
            
            JobDetail job = newJob(TestJob.class)
                    .withIdentity("jobName""jobGroup")
                    .build();
            
            Trigger trigger = newTrigger()
                    .withIdentity("triggerName1""triggerGroup1")
                    .withSchedule(cronSchedule("0/1 * * * * ?"))
                    .build();
 
            scheduler.scheduleJob(job, trigger);
            
            scheduler.start();
        }catch(Exception e){
            System.out.println(e.getMessage());
        }
        
    }
}
cs

 

JobDetail 에는 스케줄링 하려는 클래스 정보를, Trigger에는 cron 설정을 통해 스케줄링 시간을 설정한다.

* "0/1 * * * * ?" : 1초마다 실행

 

29번 줄을 통해 스케줄러 잡에 job, trigger를 등록시키고, 31번줄에서 start 시킨다.

결과적으로 1초마다 TestJob의 execute 메서드가 실행된다.


5. quartz-context

 앞서 생성한 quartz-context 설정을 통해 CronTrigger 객체를 bean으로 등록하도록 한다.

 bean으로 등록하는 과정에서 기본생성자인 CronTrigger()를 호출하고, 이 생성자를 통해 설정이 세팅 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
 
    <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
    
    <beans:bean id = "cronTrigger" class = "quartz.CronTrigger"/>
    
</beans:beans>
cs

quartz.CronTrigger 객체를 bean으로 등록한다.

 

그 후 서버 tomcat 서버를 실행시키면 아래와 같은 라이프 사이클을 통해 quartz가 시작된다!

 

web.xml load -> quartz-context load -> CronTrigger bean 객체 생성 -> 기본생성자 호출 -> quartz 세팅 및 start!

 

실행 결과 아래와 같은 화면이 출력된다면 quartz 구현에 성공한 것이다.

console 로그

 

추가적으로! 앞의 게시글에서 설정했던 Rolling 파일을 확인해보자.

생성된 Rolling 폴더 및 log 파일
Rolling 파일

매 분마다 폴더가 생성되고, 그 시간동안 쌓였던 로그데이터가 해당 폴더로 들어가 zip 파일로 남게되는 것을 확인할 수 있을 것이다!

 

만약 이를 1일단위로 설정한다면 1일마다 쌓인 로그들이 압축파일 형태로 저장되기 때문에 로그로 인한 저장공간 사용량을 줄일 수 있고, 필요할 때마다 해당 일자의 로그를 살펴볼 수 있기 때문에 로그 관리의 효율성이 높아진다!

 

 

------끄읕------

반응형
반응형

1. 개요

 로그는 시스템의 오류 파악이나 로직 분석을 위해 꼭 필요하다.

 실무를 접하면서 로그가 얼마나 중요한지를 많이 느끼고 있기 때문에 로그 개념을 다시 정리해보고자 글을 쓴다.

 그리고! 그냥 남기면 기억에 오래 남지 않으므로, 좀 특이한? 방식으로 로그를 남겨보았다.


2. 환경

  1) maven

  2) JDK 1.8

  3) Tomcat 8.0 


3. 준비

 1) Maven 라이브러리 추가

 log4j2 관련 라이브러리는 다음과 같다. pom.xml에 추가해주자!

1
2
3
4
5
6
7
8
9
10
11
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.13.0</version>
      </dependency>
      <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.13.0</version>
      </dependency>
     </dependencies>
cs

 2) log4j2.xml 파일 생성

 class 경로인 src/main/resources 경로에 log4j2.xml 파일을 생성한다.

log4j2.xml 파일 생성

 이로써 아~주 기본 환경설정이 벌써 끝나버렸다. @_@;


4. console log

 4.1) 필요 패키지, 클래스 생성

  콘솔에 로그를 찍어보자. 일단, 동작에 필요한 패키지와 클래스를 아래와 다음과 같이 생성한다.

 

패키지, 클래스 생성

   - Main, test 패키지 생성

   - test 패키지에 LogTest 클래스 생성

   - Main 패키지에 MainClass 객체를 생성

   - quartz 패키지도 일단 생성 (활용 부분에서 quartz를 사용해 로그를 예정이며, console 로그를 출력할 때는 사용 X)

 

  4.2) log4j2.xml 파일 코드

   console에 로그를 찍기 위해 아래 코드를 삽입한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
 
<configuration status="debug">
 
    <Appenders>
        <!-- 콜솔 -->
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd hh:mm:ss} %5p [%c] %m%n"/>
        </Console>
    </Appenders>
    
    <loggers>
        <root level="debug" additivity="true">
            <AppenderRef ref="console"/>
        </root>
    </loggers>
 
</configuration>
cs

 

 3번 줄의 configuration status 구문은 이 설정파일이 로드될 때 발생하는 로그에 대한 레벨을 설정하는 부분이다.

 말이 어렵지 실행화면을 보면 이해가 갈것이다. 

 

configuration status = "DEBUG"

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 status ="info" And Appenders -> Appenderss

 

* configuration 부분을 설명하기 위해 먼저 MainClass와 LogTest를 구현하였다. 현재 MainClass와 LogTest 를 생성만 한 상태에서는 위처럼 테스트가 불가능하다. 이런 설정이구나 라고만 이해하고 넘어가자. 

 

Appenders 태그 안에 실질적인 로그 설정 코드를 삽입한다.

현재 콘솔에 로그를 출력시키기 위해 <Console> 태그 관련 코드를 삽입하고 로그로 찍히는 패턴을 PatternLayout 태그를 통해 설정한다.

%d는 로그 시간에 관한 설정을 나타내는데 괄호 안의 형태로 포멧시킬 수 있다.

%p는 로그 레벨, %c는 로그가 발생한 클래스 경로, %m은 로그 메시지, %n은 개행이다. 

%5p는 로그 레벨이 출력되는 기본 문자열 길이를 5로 설정한다는 의미이다.

WARN 앞에 공백 보이시죠?

 

12번 줄의 loggers 는 설정한 로그 코드를 적용하는 부분이다.

root 태그를 사용하면 현재 시스템에서 발생하는 모든 로그를 찍어낼 수 있고, level을 debug로 설정하여 debug 이상의 로그만 출력되도록 한다. Appender를 이용해서 앞서 설정한 console을 적용시키면, 결과적으로 기동하는 시스템내에서 발생하는 모든 로그 중 debug레벨 이상은 모두 찍히게 된다.

 

4.3) 클래스 파일 코드 작성

 - LogTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package test;
 
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
 
public class LogTest {
    
    private Logger logger = LogManager.getLogger(LogTest.class);
    
    public void printLog() {
        logger.debug("[debug] log!");
        logger.info("[info] log!");
        logger.warn("[warn] log!");
        logger.error("[error] log!");
    }
}
 
cs

로그 객체를 설정하고, printLog() 메서드에 debug, info, warn, error로그를 출력하는 코드를 삽입한다.

 

 - MainClass.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package Main;
 
import test.LogTest;
 
public class MainClass {
 
    public static void main(String[] args) {
        
        LogTest logTest = new LogTest();
        logTest.printLog();
    }
}
 
cs

앞서 만든 LogTest 객체 생성 후 printLog() 메서드를 호출하는 코드를 삽입한다.

 

이제 MainClass를 실행시켜보자!

이러한 로그가 출력되면 성공!

만약 에러 로그만 출력하고 싶다면 log4j2.xml 의 root level을 error로 설정하면 된다.

 

5. RollingFile log

로그를 파일로 남기는 방법 중 하나로, 날짜, 시간 등에 따라 로그파일을 생성하여 보관하고 싶거나, 로그파일의 용량이 너무 커 압축파일 형태로 로그를 남기고싶다면 이 설정을 사용하면 된다.

 

5.1) log4j2.xml 파일 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
 
<configuration status="DEBUG">
 
    <Appenders>
        <!-- 콜솔 -->
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd hh:mm:ss} %5p [%c] %m%n"/>
        </Console>
        
        <!-- 파일  -->
        <RollingFile name ="RollingFile">
            <FileName>C:/log/mylog.txt</FileName>
            <FilePattern>C:/log/%d{yyyy-MM-dd-hh-mm}/mylog.zip</FilePattern>
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} %5p [%c] %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy interval = "1" modulate = "true"/>
            </Policies>
        </RollingFile>
        
        <RollingFile name ="RollingFileError">
            <FileName>C:/log/mylog_error.txt</FileName>
            <FilePattern>C:/log/%d{yyyy-MM-dd-hh-mm}/mylog_error.zip</FilePattern>
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} %5p [%c] %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy interval = "1" modulate = "true"/>
            </Policies>
        </RollingFile>
        
    </Appenders>
    
    <loggers>
        <root level="debug" additivity="true">
            <AppenderRef ref="console"/>
            <AppenderRef ref="RollingFile"/>
            <AppenderRef ref="RollingFileError" level = "ERROR"/>
        </root>
    </loggers>
    
</configuration> 
cs

코드가 갑자기 늘어났는데, 천천히 파악해보자.

일단 이 코드의 기능목표는 다음과 같다.

 가) 모든 로그가 찍히는 mylog.txt 파일 생성

 나) 에러 로그만 찍히는 mylog_error.txt 파일 생성

 다) 1분마다 찍힌 로그파일을 압축파일로 저장

 

 5.2) 코드 설명

 RollingFile 태그를 사용해야한다.

 FileName은 로그가 찍히게 될 파일경로,

 FilePattern은 파일이 Rolling 될때, 즉 조건에 의해 파일이 말릴 때의 파일 패턴. >> 날짜폴더/mylog.zip 형태

 Pattern은 찍히는 로그 형태

 TimeBasedTriggeringPolicy는 Rolling 조건을 시간으로 설정하고, interval은 1분 간격으로 Rolling한다는 의미.

 

 * interval은 앞서 설정한 FilePattern에 따라 1분이 될수도, 1시간이 될수도, 1일이 될수도 있다.

%d{yyyy-MM-dd-hh-mm}.log.zip interval = 1  1분마다 롤링
%d{yyyy-MM-dd-hh}.log.zip interval = 1  1시간마다 롤링
%d{yyyy-MM-dd}.log.zip interval = 1  1일마다 롤링

 

일반적으로 1일마다 롤링을 하지만, 로그 파일이 롤링되는 테스트 결과를 내일 확인할 수 없으니.. 1분마다 생성되도록 설정하였다.

 

위와 같이 일반 로그, 에러 로그에 대한 Rolling 로그 설정을 만들고, 

39 ~ 40번 줄처럼 RollingFile, RollingFileError 를 AppenderRef 태그를 통해 실제로 적용시켜보자.

단, RolingFileError는 ERROR 레벨 이상의 로그만 찍혀야 하기 때문에 level을 따로 ERROR로 설정한다.

 

이제 MainClass를 실행 시켰을 때 설정한 경로로 파일이 생성되어 로그가 찍히고 1분마다 zip파일로 남을까??

 

안남는다. 파일은 생성되고 순간의 로그는 파일에 찍히겠지만, 그게끝이야! 로그가 계속 찍히지 않으니까!

 

때문에 날짜 형식으로 폴더 하나가 생성되고, mylog, mylog_error 코드 안에 각각의 로그가 들어가면 성공이다.

 

"그럼 로그를 계속 찍어주려면 어떻게할까?? 반복문 돌려도 된다! while!

하지만 좀 더 의미있게 해보자...! 1초마다 찍히게 하는거야! 반복문을 통해서가 아닌 잡 스케줄러를 이용해서!!"

 

해서 활용편으로 quartz라는 스케줄러를 사용하여 1초마다 로그를 찍도록 해보겠다. @_@;

 

이 부분은 스프링에 대한 조금의 지식을 갖고 있다면 어렵지 않게 구현할 수 있다.

 

다음 글에서 진행하도록 하겠다.

 

 

반응형

+ Recent posts