MS-SQL 2012 이전 (11.X 미만)

예전의 MS-SQL (2012 이전)에서는 페이징 쿼리를 하기 위해서는
보통 ROW_NUMBER() 를 사용하여 정렬순으로 번호를 부여한 다음
중첩 쿼리을 이용하여 정렬번호를 기준으로 페이지에 해당하는 글을 불러오는 방법을 주로 썼습니다.

# ROW_NUMBER() + 중첩 쿼리를 이용한 기존 방식
SELECT T.MEMBER_ID
  FROM (
        SELECT ROW_NUMBER() OVER (ORDER BY MEMBER_ID) AS ROWNUM,
               MEMBER_ID
          FROM MEMBER_TABLE WITH (NOLOCK)
       ) T
 WHERE T.ROWNUM BETWEEN ({pageNo} - 1) * {pageSize} + 1 AND {pageNo} * {pageSize}

위 쿼리에서 MEM_ID로 우선 정렬한 후 ROW_NUMBER()를 이용해 ROWNUM을 부여하고,
BETWEEN 등을 사용하여 탐색하려는 페이지의 첫 번째 ROWNUM부터 페이지의 마지막 ROWNUM 사이를 가져오는 식으로
탐색했습니다.

# 페이지 당 10개 씩 가져올 때
# 1페이지
SELECT T.MEMBER_ID
  FROM (
        SELECT ROW_NUMBER() OVER (ORDER BY MEMBER_ID) AS ROWNUM,
               MEMBER_ID
          FROM MEMBER_TABLE WITH (NOLOCK)
       ) T
 WHERE T.ROWNUM BETWEEN 1 AND 10
 
# 5페이지
SELECT T.MEMBER_ID
  FROM (
        SELECT ROW_NUMBER() OVER (ORDER BY MEMBER_ID) AS ROWNUM,
               MEMBER_ID
          FROM MEMBER_TABLE WITH (NOLOCK)
       ) T
 WHERE T.ROWNUM BETWEEN 41 AND 50

 

MS-SQL 2012 이후 (11.X 이상)

MS-SQL 2012 이후에 추가된 ORDER BY 절의 OFFSET - FETCH 문을 통해
중첩 쿼리 없이 한 번에 가져올 수 있습니다.

# 양식
ORDER BY {정렬 대상 컬럼}
    OFFSET {건너 뛸 행의 수} ROWS
    FETCH NEXT {가져올 행의 수} ROWS ONLY

ORDER BY를 통해 정렬 기준을 정한 뒤
OFFSET을 통해 건너 뛸 행의 수를 먼저 설정하고
(1부터 가져올 경우 OFFSET 0 ROWS, 11부터 가져올 경우 OFFSET 10 ROWS)

FETCH NEXT 에서 몇 개의 행을 가져올 지 결정하게 됩니다.
(한 페이지 내에 10개를 가져올 경우 FETCH NEXT 10 ROWS ONLY)

# OFFSET - FETCH 를 사용한 예
SELECT MEMBER_ID
  FROM MEMBER_TABLE WITH (NOLOCK)
 ORDER BY MEMBER_ID
       OFFSET ({pageNo} - 1) * {pageSize} ROWS
       FETCH NEXT {pageSize} ROWS ONLY
       
# 1페이지
SELECT MEMBER_ID
  FROM MEMBER_TABLE WITH (NOLOCK)
 ORDER BY MEMBER_ID
       OFFSET 0 ROWS
       FETCH NEXT 10 ROWS ONLY
       
# 5페이지
SELECT MEMBER_ID
  FROM MEMBER_TABLE WITH (NOLOCK)
 ORDER BY MEMBER_ID
       OFFSET 40 ROWS
       FETCH NEXT 10 ROWS ONLY       

 

중첩 쿼리를 쓰지 않아서 실행계획도 훨씬 간단해집니다.

그림 1) ROW_NUMBER() + 중첩쿼리 사용한 경우 실행 계획

 

그림 2) OFFSET - FETCH 적용하여 단일 쿼리로 수행한 경우 실행 계획

 

쿼리도 간단해지고 성능 상의 이점도 가질 수 있습니다.

참고
Microsoft Docs: docs.microsoft.com/ko-kr/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-ver15

※ 여기 있는 내용은 구글링을 통해 수집한 정보를 바탕으로 기존의 소스들을 재구성한 내용입니다.

참고한 소스의 URL은 글 하단에 표기하였습니다.

 

기존의 운영되던 프로젝트를 확인하다 레거시 쪽에 API를 호출하는 부분을 살펴 보게 되었습니다.

한 화면에 여러 결과를 한꺼번에 모아 보여주다 보니 페이지 접속 시 API 호출이 5번 발생하게 되더군요.

Apache의 Http Component 4.5.X를 사용하고 있는데,

좀 더 자세히 살펴 보니 API를 호출할 때 마다 Apache HttpClient를 생성하고 자원을 반환하고 있었습니다.

 

표 1) 기존 소스

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
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
 
try {
    URI uri = new URIBuilder()
            .setScheme("http")
            .setHost(...)
            .setPath(...)
            .build();
    HttpPost httpPost = new HttpPost(uri);
    httpClient = HttpClients.createMinimal();
    response = httpClient.execute(httpPost);
    ...
catch (URISyntaxException | IOException exception) {
    throw new ApiCallException("API 조회에 실패하였습니다.", exception);
finally {
    if (response != null) {
        try {
            response.close();
        } catch (IOException ignore) {
            LOGGER.debug("HttpResponse Close 에러", ignore);
        }
    }
    if (httpClient != null) {
        try {
            httpClient.close();
        } catch (IOException ignore) {
            LOGGER.debug("HttpClient Close 에러", ignore);
        }
    }
}
 

여기서는 HttpClients.createMinimal()를 사용하고 있는데, createMinimal()의 소스를 보면

HttpClient를 객체를 새로 생성하고 PoolingHttpClientConnectionManager를 등록하고 있습니다.

 

표 2) HttpClients 소스 일부

1
2
3
public static CloseableHttpClient createMinimal() {
    return new MinimalHttpClient(new PoolingHttpClientConnectionManager());}
}

결국 API 호출 후 HttpClient 자원을 반환하면서 Connection Pool 도 같이 반환하게 되어

결과적으로 Connection Pool을 전혀 사용하지 못 하는 상황이 되었습니다.

 

우선 PoolingHttpClientConnectionManager를 등록해서 사용할 수 있도록 Customizing 된 HttpClient가 필요했습니다.

 

표 3) HttpClient 빈 등록

1
2
3
4
5
6
7
8
9
10
@Configuration
public class HttpClientConfig {
 
    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .build();
    }
 
}

먼저 HttpClient 객체를 재사용할 수 있도록 Bean으로 등록하였습니다.

 

이어서 해당 HttpClient에서 사용할 PoolingHttpClientConnectionMananger 등록이 필요했습니다.

 

표 4) PoolingHttpClietConnectionManager 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class HttpClientConfig {
 
    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(poolingHttpClientConnectionManager())
                .build();
    }
 
    private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        return connectionManager;
    }
 
}
 

HttpClient에 Pooling Connection Manager까지 추가했습니다.

그런데 PoolingHttpClientConnectionManager는 기본값으로 Max Connection은 20, Route 당 Connection 수는 2였습니다.

실제 운영환경에서는 접속자가 많아질 경우 Connection 할당이 되지 않는 경우가 생길 거 같네요.

적당한 값으로 조정했습니다.

 

표 5) Max Connection 수 및 Route 당 Connection 수 할당

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class HttpClientConfig {
 
    private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
    private static final int MAX_CONNECTIONS_TOTAL = 100;
 
    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(poolingHttpClientConnectionManager())
                .build();
    }
 
    private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
        connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL);
        return connectionManager;
    }
 
}
 

이렇게 해서 Pooling Connection Mananger가 적용된 HttpClient Bean이 만들어졌습니다.

이제 호출해서 잘 쓰기만 하면 되겠네요.

 

저희 서비스는 Spring 환경에서 구축되었기 때문에 HttpClient Bean을 Injection 하여 사용하였습니다.

 

표 6) Customizing된 HttpClient Bean을 이용한 API 호출

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
private CloseableHttpClient myHttpClient;
 
@Autowired
public void setMyHttpClient(CloseableHttpClient myHttpClient) {
    this.myHttpClient = myHttpClient;
}
 
...
 
// 실제 비즈니스 코드 부분
CloseableHttpResponse response = null;
 
try {
    URI uri = new URIBuilder()
            .setScheme("http")
            .setHost(...)
            .setPath(...)
            .build();
    HttpPost httpPost = new HttpPost(uri);
    response = myHttpClient.execute(httpPost);
    ...
catch (URISyntaxException | IOException exception) {
    throw new ApiCallException("API 조회에 실패하였습니다.", exception);
finally {
    if (response != null) {
        try {
            response.close();
        } catch (IOException ignore) {
            LOGGER.debug("HttpResponse Close 에러", ignore);
        }
    }
}
 
 

이제 Connection Pool이 적용된 HttpClient를 사용하여 API를 호출하게 되었습니다.

이제 매번 Socket Open을 하지 않아도 되게 되었습니다.

 

여기까지만 해도 코드는 잘 작동합니다.

그런데 관련 내용을 좀 더 찾다 보니 Idle Connection에 대한 자원 반환 이슈가 있더군요.

Socket 통신을 할 경우 정상적으로 종료되지 않을 경우 TIME_WAIT에 걸리며 자원이 제대로 반환되지 않는 현상이 발생하게 되고

TIME_WAIT이 점점 늘어나다 보면 나중에는 할당할 수 있는 자원이 남지 않게 되지요.

 

만료되거나 비정상적으로 종료된 Connection에 대한 반환 작업이 필요했습니다.

 

주기적으로 사용하지 않는 Connection을 반환하기 위해서는

스프링의 스케줄링 기능을 이용하여 자원 반환을 지속 수행하도록 해야 했습니다.

 

물론 앞서 만들었던 Connection Manager도 Bean으로 등록하여

자원 반환 Thread에서 Injection 하여 사용하는 것이 필요했습니다.

 

표 7) 사용하지 않는 Connection 반환 스케줄러 추가

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
45
@Configuration
@EnableScheduling
public class HttpClientConfig {
 
    private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
    private static final int MAX_CONNECTIONS_TOTAL = 100;
 
    private static final int IDLE_TIMEOUT = 30 * 1000;
 
    @Bean
    public CloseableHttpClient myHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(poolingHttpClientConnectionManager())
                .build();
    }
 
    @Bean
    public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
        connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL);
        return connectionManager;
    }
 
    @Bean
    public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
        return new Runnable() {
            @Override
            @Scheduled(fixedDelay = 30 * 1000)
            public void run() {
                try {
                    if (connectionManager != null) {
                        LOGGER.info("{} : 만료 또는 Idle 커넥션 종료.", Thread.currentThread().getName());
                        connectionManager.closeExpiredConnections();
                        connectionManager.closeIdleConnections(IDLE_TIMEOUT, TimeUnit.MILLISECONDS);
                    } else {
                        LOGGER.info("{} : ConnectionManager가 없습니다.", Thread.currentThread().getName());
                    }
                } catch (Exception e) {
                    LOGGER.error(Thread.currentThread().getName() + " : 만료 또는 Idle 커넥션 종료 중 예외 발생.", e);
                }
            }
        };
    }
}
 
 

ConnectionManager를 검사하여 이미 만료되었거나 Idle 상태인 Connection들을 종료해 주는 Bean을 만들어 30초(fixedDelay) 주기로 수행되도록 하였습니다.

IDLE_TIMEOUT은 30초로 설정하여 30초 동안 Connection을 재사용하지 않으면 Idle Connection으로 판단하여 종료되도록 하였습니다.

 

이제 Spring의 Scheduling을 이용할 Bean도 만들었으니 추가로 Scheduling을 관리하는 Configuration도 필요하겠죠.

 

표 8) Spring Scheduing Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
 
    private static final int POOL_SIZE = 10;
 
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(POOL_SIZE);
        taskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        taskScheduler.initialize();
 
        scheduledTaskRegistrar.setTaskScheduler(taskScheduler);
    }
}
 

이렇게  HttpClientConfig와 SchedulingConfig를 @Configuration으로 등록하고

비즈니스 로직에서 HttpClient Bean을 사용하여 Connection Pool을 사용하여 HttpClient를 사용하는 방법을 정리해 봤습니다.

 

구글링을 통해 수집한 정보를 제 나름대로 해석하여

필요에 맞게 가공하여 정리했기에 완벽하지 않습니다.

 

완전하지는 않지만 작업한 내용을 정리하기 위해 우선 기록해 둡니다.

수정되는 내용이 발생하거나 의견을 주시면 계속해서 수정, 반영해 나가도록 하겠습니다.

 

감사합니다.

 

참고

Tutorial - Connection management: http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html

HttpClient Examples - Threaded request execution: https://hc.apache.org/httpcomponents-client-4.5.x/httpclient/examples/org/apache/http/examples/client/ClientMultiThreadedExecution.java

Apapche HttpComponent 제대로 사용하기: https://inyl.github.io/programming/2017/09/14/http_component.html

Spring RestTemplate + HttpClient configuration example: https://howtodoinjava.com/spring-boot2/resttemplate/resttemplate-httpclient-java-config/

How to Schedule Tasks with Spring Boot: https://www.callicoder.com/spring-boot-task-scheduling-with-scheduled-annotation/

웹 어플리케이션이 아닌, 1회성 심플 자바 어플리케이션이 급히 필요하여

IntelliJ 에서 jar 파일 만들기에 도전해 봤습니다.


우선 샘플로 HelloWorld 프로젝트를 하나 만들었습니다.




FIile -> Project Structure 로 들어갑니다.




Artifacts 선택 후 Add에서 JAR -> From modules with dependencies 를 추가합니다.




JAR 생성 창이 나오는데요, Main Class (JAR 실행 시 처음으로 실행될 class)를 선택해 줍니다.






JAR 만들 준비가 다 되었네요.




이제 JAR를 만들어 볼까요?

Build -> Build Artifacts... 로 들어갑니다.




화면 한 가운데에 Build 옵션이 나오네요.



Build를 선택합니다.



좌측 프로젝트 내비게이터에 out 디렉토리가 추가된 게 보입니다.




탐색기로 확인해 보겠습니다.



jar 파일이 생성되었네요.



이제 제대로 생성되었는지 실행 한번 해보겠습니다.



Hello World 가 잘 보입니다.



어쩌다 한번씩 자바 어플리케이션 필요할 때가 있는데, 참고하려고 정리했습니다.


조금이나마 도움 되시길~

개발하다 보면 SSH 접속 툴로 Putty를 많이 사용하게 되는데요,


Putty의 단점이 바로 커넥션 관리가 쉽지 않다는 거죠.


이 때문에 많은 분들이 커넥션 매니저와 같은 별도의 프로그램을 많이들 쓰시는데요,


저는 커넥션 매니저 프로그램들이 좀 불편하다고 많이 느껴서 빠른 실행 아이콘을 만들어서 많이 사용하는 편입니다.


저도 매번 새로 설치하면서 빠른 실행 아이콘 만드는데 이게 만들때보다 좀 번거로워서요,


이번 기회에 옵션들을 좀 정리해 봤습니다.


우선 Putty를 설치해 보겠습니다.


https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html



최근에는 윈도우 버전은 Installer(msi) 버전으로 올라오네요.


다운 받아서 설치해 보겠습니다.



프로그램 목록에 추가 되었습니다.




이제 단축아이콘을 하나 만들어 보지요.




새로 만들어진 단축 아이콘의 속성을 열어보겠습니다.




이제 바로 가기 대상 뒤에 옵션을 붙여 주면 됩니다.


제가 주로 사용하는 옵션들은 다음과 같습니다.


 옵션

용도 

-ssh

SSH 프로토콜 사용 

-P

접속 포트 

-l 

로그인 사용자 이름(계정) 

-pw

패스워드


"C:\Program Files\PuTTY\putty.exe" -ssh 192.168.10.1 -P 8888 -l myid -pw mypassword


위와 같은 식으로 입력한 후 실행해 보면 곧바로 접속되는 것을 확인할 수 있습니다.



더 많은 옵션들은 Putty Manual에서 확인하실 수 있습니다.


https://the.earth.li/~sgtatham/putty/0.69/htmldoc/Chapter3.html#using-cmdline


조금이나마 도움 되셨길 바랍니다 ^^

+ Recent posts