상세 컨텐츠

본문 제목

대용량 데이터를 위한 배치 프로그램 개선

Spring/SpringBatch

by husks 2016. 4. 1. 10:06

본문

반응형


From PL/SQL Batch to Spring Batch

 

온라인 프로그램과 마찬가지로 배치 프로그램에도 유행이라는 것이 존재했었고, 더 나은 방법을 찾기 위한 노력들이 계속되어 왔다. 근래에는 데이터를 저장하고 있는 매체가 대부분 관계형 데이터베이스이다 보니 대량의 데이터를 다뤄야 하는 배치프로그램들도 데이터베이스에 특화된 PL/SQL을 이용해 개발되는 경우가 많은데, 이 글은 이러한 PL/SQL 배치를 개선할 수 있는 한 가지 방법에 대해 얘기한다.

세상에는 많은 프로젝트들이 있지만 프로젝트를 구성하는 프로그램의 종류로만 나눠본다면 크게 두 가지로 구분할 수 있다. 하나는 온라인 또는 C/S와 같이 사용자와의 상호작용을 기반으로 하는 프로그램이고 또 하나는 사용자와의 상호작용 없이 대량의 데이터를 순차적으로 반복 처리하는 배치 프로그램이다.

 

나 대리의 배치 이야기

 

새롭게 이직한 회사로 부푼 꿈을 가지고 출근한 나 대리. 사내에서 맡은 업무는 개발되어 있는 배치 프로그램을 유지 보수하고 필요한 경우 신규 배치프로그램을 개발하는 것이다. 현재 개발되어 있는 프로그램들의 상황을 분석해 보니 DB에서의 작업이 대부분이기 때문에 PL/SQL로 작성된 배치만 개발됐었고 현재까지 유지되고 있다. 빈약한 매뉴얼 대신 직접 프로그램 분석을 위해 작게는 수백 줄에서 많게는 수천 줄이 넘어가는 PL/SQL을 뜯어보면서 수많은 쿼리들을 넘나들다 보니 “여긴 어디? 난 누구?”라는 인터넷 상의 우스갯소리가 자연스럽게 떠오른다. 게다가 뭔가 수정된 듯한 쿼리들이 여기저기 보이지만 어느 곳에서도 그 이력을 찾을 수 없었다. 다행히도 전임자에게 몇몇 PL/SQL에 대한 설명을 들을 수 있었지만 나머지 PL/SQL들을 아무런 설명도 문서도 없이 유지 보수하려니 정신이 아득해진다.

 

그나마 다행인 것은 대부분의 배치 작업이 문제없이 잘 실행된다는 점이다. 물론 가끔씩 예외가 발생하지만 그 빈도가 매우 적은 편이라 처리에 그리 큰 부담이 되지는 않는다. 다만 원인을 찾기가 어려워서 예외가 발생하는 경우 해당 레코드를 수작업으로 찾아서 배치 처리 대상에서 제외하고 다시 배치를 실행시키는 것으로 문제를 해결했다. 나 대리는 이 경험들을 바탕으로 예외가 발생하는 경우에는 로그 테이블에 해당 배치명과 오류의 원인 등을 기록할 수 있도록 추가적인 프로시저를 만들고 모든 PL/SQL과 Function에 적용시키고 로그 테이블을 기초로 해서 예외에 대한 처리 작업을 수행했다.

 

월말이 되자 결산과 같은 업무를 처리하기 위해 많은 수의 배치들이 실행되기 시작했다. 로그 테이블을 확인하면서 배치가 잘 수행되고 있는지 모니터링하고 있는데 갑자기 로그 테이블에 수백만 건의 로그가 쌓이면서 전체적인 성능 저하가 발생했다. 로그 테이블을 확인해보니 모두 같은 Function에서 발생한 예외였다. PL/SQL 내부적으로 Function을 이용하는데 이 Function에서 예외가 발생하다보니 PL/SQL이 처리하는 레코드의 수만큼 테이블에 쌓이게 된 것이었다. 로그 테이블에서 발생한 예외들을 삭제하고 Function을 수정한 후 다시 배치를 실행시키는 것으로 문제를 해결하고 Function에서는 오류 로그를 작성하지 않는 것으로 결정하고 넘어갔다.


그러던 어느 날 갑자기 내부의 자료를 대외기관으로 매 월말 기준으로 보내야 한다는 업무지시가 내려왔다. 나 대리는 늘 하던 배치 업무에 하나가 늘어난 것이라 별로 대수롭지 않게 여기고 있었는데, 잠깐 생각해보니 보통 대외기관으로 보내는 자료들은 파일로 주고받는데 현재 사내에서 주로 사용하는 PL/SQL에서는 패키지를 이용하면 결과를 파일로 생성할 수 있긴 하지만 그 방법이 너무 복잡할 뿐 아니라, 보안적인 정책으로도 PL/SQL에서 DBMS.OUTPUT 등을 이용해 파일로 생성하는 것은 지양하고 있어 사용하기가 쉽지 않을 것 같다. 더욱이 전송해야 할 파일은 일반적으로 구분자로 구분되거나 특정한 고정길이를 포함하는 파일이기 때문에 해당 기관에서 원하는 포맷에 맞춰 파일을 제어하기는 사실상 거의 불가능하다고 판단했다.

 

결국은 대외기관에 전송할 파일 생성을 위한 배치를 새로 개발하기로 하고, 그나마 해본 경험이 있는 자바로 만들기로 했다. 나 대리는 속으로 “PL/SQL 보는 것만으로도 눈이 돌아갈 지경인데 자바 배치 프로그램까지 개발해야 하는 건가…” 라는 생각이 서해안에 밀물 밀려들듯이 머릿속에 몰려왔다. 막상 프로그램을 개발하려니 오렌지나 TOAD와 같은 오라클 클라이언트 프로그램만 사용해 와 쿼리에만 익숙해졌는데 이클립스를 띄워 자바문법을 찾아 삼만리 여행을 할 생각에 눈앞이 캄캄해진다. 얼마를 삽질해야 이 자바 배치프로그램을 완성할 수 있을까? 답답한 마음을 아무나 붙잡고 하소연하고 싶지만 어쩌겠는가? 맡은 바 업무이니 묵묵히 개발을 하는 수밖에.

 

PL/SQL이 갖는 단점

 

나 대리의 이야기는 조금 과장된 내용이지만 현실에서 충분히 발생할 수 있는 상황이다. 서두에도 얘기했듯이 현재 배치 프로그램의 주력은 당연히 데이터를 보관하고 있는 DB에 특화된 PL/SQL이다. 오라클이나 DB2, MS-SQL과 같은 벤더에서 제공해주는 막강한 기능(패키지 포함)을 활용해 내부적인 배치 작업을 하는 데 최적화되어 있다. 하지만 나 대리의 상황에서 볼 수 있듯이 PL/SQL은 만능이 아니며 많은 장점들이 있지만 분명한 단점들도 존재한다는 것이다.

 

개인적으로 PL/SQL을 보다 보면 닷컴이 활황일 때 많이 개발되던 소스 파일 하나가 수천 줄이 넘어가고 각종 기능들이 총망라되어 있었던 MODEL1 방식의 ASP나 PHP, JSP와 같은 웹 프로그램이 떠오른다. 한창 유행하던 MODEL1 방식의 웹 프로그램들이 MODEL2 방식의 다계층화된 아키텍처(Layered Architecture)를 따르게 된 가장 큰 이유는 바로 유지 보수의 이점이 MODEL2 방식이 더 크기 때문이었다. 각각의 계층이 하는 역할을 분리하게 되면서 내가 집중해야 하는 부분만을 생각하고 개발하면 되기 때문에 섞어찌개와 같이 모든 기능이 한데 엮여 있지 않다. 따라서 가독성과 현재 봐야 하는 코드의 범위 제한이라는 측면이 부각되어 유지 보수를 하는 입장에서는 한정적인 소스 코드만을 파악하더라도 프로그램의 오류를 수정하거나 새로운 기능을 추가하는 데 보다 유리해졌으므로 많은 프로그램들이 MODEL2 방식으로 변화해 갔던 것이다. 지금 PL/SQL 배치의 소스 파일이 있다면 그 파일을 열고 쿼리를 천천히 읽어보자. 옛 향수에 젖어 들어가면서 MODEL1 방식의 웹 프로그램 냄새가 솔솔 나는 게 느껴지지 않는가? 이렇게 우리가 사용하고 있는 PL/SQL은 하나의 PL/SQL 안에 수많은 쿼리들을 포함하고 있기 때문에 내용을 파악하는 것도 어렵고 유지 보수에 많은 비용과 시간이 들어가게 되는 것이다.

 

그렇다면 이러한 PL/SQL을 개선하기 위해 필요한 것은 무엇일까? 개발 표준의 준수? 매뉴얼의 상세화? 꾸준한 교육? 개선을 위해서는 필요한 많은 요소들이 존재하지만 근본적인 해결책은 큰 덩치의 PL/SQL을 잘게 쪼개어 처리하게 만들면 된다는 것이다. 크기가 작아지면 내가 집중해야 하는 범위가 줄어드는 것과 마찬가지이므로, 새롭게 PL/SQL을 이용한 업무를 개발하는 사람이나 유지 보수 담당자의 학습비용이 낮아질 수 있는 것이다. 다시 말하자면 많은 사람들이 알고 있는 분할정복법(Divide And Conquer)과 관심사항의 분리(Separation Of Concern)의 원P> 

조금 더 PL/SQL 배치가 갖는 단점에 대해 이야기해 보자. 보통 PL/SQL 배치는 하나의 PL/SQL로 이뤄지는 일보다는 다수의 PL/SQL로 이뤄지는 일이 많다. 생각해 보면 PL/SQL 하나 안에서도 여러 개의 쿼리들이 사용되고 있다 보니 선후 관계나 조건에 따른 처리에 대한 내용 파악이 어려웠는데 명시적으로 표시되지 못하는 PL/SQL들 간의 선후관계나 조건에 따른 처리는 또 어떻게 파악해야 하는 걸까? 물론 PL/SQL을 묶어 호출하는 PL/SQL을 추가로 작성하면 여러 개의 PL/SQL들 간의 관계를 파악하는 것에 도움이 되기는 한다. 하지만 이렇게 작성된 PL/SQL은 또 유지 보수 대상이 늘어나는 것과 마찬가지의 일이 되는 것이다.

 

또한 앞서의 사례에서도 언급되었던 내용이지만 PL/SQL은 그 태생에 맞게 DB 안에서만 구동된다. 이 의미는 DB 이외의 어떤 것과도 데이터를 주고받을 수 없다는 것이다. 처음에 얘기했던 대로 우리가 사용하는 시스템에서 데이터를 저장하고 있는 곳이 DB이다 보니 보안적인 이유로 인해 외부에서의 접근을 애초에 막아야 한다는 것이 사실 가장 큰 이유다. 당연히 외부시스템에서 내부 DB에 접근하려면 IP에서부터 Port, ID와 비밀번호까지 모두 알고 있어야 한다는 의미인데 아무리 조회 권한만 준다곤 하지만 누구나 볼 수 있는 웹 환경에서 이런 정보들을 주고받는다는 것은 도둑에게 문을 열어주고 열쇠까지 쥐어주는 것과 크게 다르지 않다. 이렇게 상황에 따른 제약이 존재하다 보니 보통 주력은 PL/SQL로 배치 프로그램을 작성하고 외부적인 연계나 회사 내의 서브 프로젝트들 사이의 데이터 전송에는 추가적으로 다른 프로그램 언어를 사용한 배치프로그램을 사용하게 된다. 결국 이 말은 각각의 배치를 담당하는 담당자가 따로 존재한다는 것이다.

 



 

규모가 작은 회사라면 나 대리와 마찬가지로 한 사람이 모두 다 개발하고 유지 보수를 해야 하겠지만 보통은 역할을 분리해 본인들의 전문 분야인 PL/SQL 배치나 자바 배치 또는 C 배치를 담당하게 된다. 즉, 대외연계가 필요한 경우 하나의 업무를 수행하기 위해 두 종류의 배치 프로그램이 필요하고 이를 관리하는 담당자도 각각 필요하다. 만약 이 2개의 배치 프로그램에 모두 연관된 테이블이 바뀌거나 전송해야 하는 내용이나 대상이 바뀌게 되면 각각의 담당자들에게 내용이 전달되어야 하고, 프로그램 변경에 대한 체크도 양쪽에서 모두 해야 한다.

 

새로운 물결의 등장

 

2~3년쯤 전부터인가 우리나라에서 스프링 프레임워크(Spring Framework, 이하 스프링)의 인기가 급속도로 올라갔다. 많은 분야에서 활용되고 스프링의 유연함이 최대 장점이 되어 많은 사람들에게 강한 인상을 남기고 있다. 이러한 스프링의 장점을 고스란히 살리면서 배치라는 특정한 도메인 영역에 특화된 프레임워크가 스프링 배치(Spring Batch)이다. 일전의 글에서도 소개했지만 스프링 배치는 배치 영역에서 잔뼈가 굵은 Accenture사의 주도하에 스프링을 기반으로 해 보다 나은 배치 프로그램 개발을 위해 탄생했다. 스프링 배치는 기본적으로 수행해야 하는 배치를 작은 단위로 나눠 처리하려고 한다. 이를 위해 DB나 파일로부터 읽고 가공하고 쓰기 위해 Item이라고 부르는 DB로 따지자면 한 개의 data를, 파일로 따지면 한 줄의 record를 의미하는 도메인 객체를 기반으로 한다. 물론 스프링 배치가 2.x로 버전이 올라가면서 chunk 기반으로 바뀌었다라고 얘기하는데 chunk라는 것은 item들을 처리하기 위한 묶음 단위라고 생각하면 된다. DB에서는 commit을 하는 주기와 그 의미가 통한다.

 

스프링 배치를 알기 위해 간단하게 몇 가지 용어를 얘기하고 넘어가자. Item을 기반으로 해서 수행되는 작업의 단위를 Step이라고 부르는데 기본적으로 읽고 쓰는 작업을 묶어서 Step이라고 생각하면 된다. 이런 Step들을 처리할 업무 프로세스 단위로 묶어 놓은 것을 Job이라고 부른다. 보통 하나의 Job은 여러 개의 Step으로 구성된다. 그리고 Item을 DB나 파일로부터 읽기 위한 ItemReader가 있고 쓰기 위한 ItemWriter가 있다. 그 사이에 필요한 경우 Item을 가공하기 위해 ItemProcessor라는 것도 존재한다.

 

새로운 물결에 몸을 맡기다

 

이제부터 PL/SQL을 스프링 배치로 전환하는 작업을 해보자. 가장 먼저 해야 할 일은 무엇일까? “스프링 배치의 라이브러리를 다운로드하는 것”이라고 얘기하는 독자들도 있겠지만, 그것과는 별도로 변경하고자 하는 PL/SQL 배치를 분석해 이를 Step과 Job이라는 스프링 배치의 틀에 맞춰가는 작업을 먼저 해야 한다. 어떻게 생각하면 이 부분이 가장 중요하고 어려운 부분이지만, 어떠한 기준이나 가이드를 제시하기에는 아직 국내에서 스프링 배치를 이용한 프로젝트 사례도 극히 드물고 공론화되어 많은 사람들의 의견이 오간 적도 없기 때문에 정답이라고 무언가를 제시하기는 매우 어렵다. 개인적으로 Step의 기준으로 삼는 것은 PL/SQL 하나이지만 이렇게 기준을 삼을 수 있는 이유는 이미 PL/SQL을 만들 때 어떠한 하나의 작업 단계의 수준으로 크기를 줄여놓았기 때문이다. 만약 PL/SQL의 크기가 크고 자체적으로 어떠한 업무 프로세스를 모두 처리한다고 하면 우선 프로세스의 단계를 정의하고 각 단계에 맞게 PL/SQL을 분리하는 작업을 선행해야 할 것이다.

 

예를 들어보면 쇼핑몰에서 매일 12시에 지난 하루 동안 있었던 모든 결제 트랜잭션에 대한 일마감 처리를 하는 PL/SQL이 있는 경우 현금 결제를 취합하는 작업 단계가 있고, 신용카드 결제를 취합하는 작업 단계가 있고 휴대폰 소액결제를 취합하는 작업 단계, 그리고 제휴 포인트를 이용한 결제를 취합하는 작업 단계가 있다. 각각의 작업은 결제내역 테이블로부터 일마감 테이블로 해당 내역을 입력하게 되며 이 작업들이 끝나면 일별 통계작업을 진행한다. 이 중에서 작업이라고 얘기한 부분들이 각각 Step이라고 생각하면 된다. 결국 이것을 스프링 배치 형태로 변경하면 <그림 2>와 같이 일마감 업무 프로세스를 일마감 Job으로 설정하고 해당 Job은 총 5개의 Step으로 구성된다.

 



 

Step은 각각 하나의 Item Reader, ItemWriter, Item Pro cessor로 구성된다(사실 배치의 흐름상으로는 ItemReader 다음에 ItemProcessor가 와야겠지만 ItemProcessor는 필수 구성요소가 아니기 때문에 뒤에 표기한다). <그림 3>에서 볼 수 있듯이 ItemReader를 사용해 DB나 파일로부터 Item을 읽고 필요하다면 이를 가공하기 위한 ItemProcessor를 거치게 된다. 가공이 완료된 Item들이 List 형태로 모이게 되는데 이때 스프링 배치에 설정한 commit interval에 따라서 List에 모인 Item들을 한번에 ItemWriter를 이용해 DB나 파일에 쓰거나 갱신 또는 삭제하게 된다. DB로 따지면 매번 insert하는 것이 아니라 batchUpdate를 이용해 쿼리 문에 대한 컴파일 작업 횟수도 줄이고 한꺼번에 처리하는 것이다.

 



 

이제 PL/SQL을 스프링 배치로 변환하는 모습을 살펴보자. 매달마다 관리되고 있는 계좌와 고객을 대상으로 해서 부도상태인 계좌(연체계좌, 건전성 손상기준)와 고객(신용회복 기준, 개인회생 기준)의 정보를 추출해 별도의 부도 상태를 관리하는 테이블들에 입력하고 이 정보들을 이용해 다시 고객정보를 관리하는 테이블의 정보들(부도 관련)을 갱신하는 PL/SQL 배치가 있다. 이를 간단하게 스프링 배치 형태로 도식화하면 <그림 4>와 같이 표현할 수 있다.

 



 

이 배치 작업의 모든 Step은 DB에서 정보를 읽어 다시 DB로 입력하거나 갱신하는 형태이므로 연체계좌 추출 Step을 대표적으로 변환해 보도록 하자. 모든 추출 Step은 SELECT-INSERT 쿼리를 기반으로 구성되어 있다. 해당 Step에서 실제 실행되는 PL/SQL의 일부분부터 확인해 보자.

 



 

하나의 쿼리를 나눈다는 것이 조금 불편해 보이겠지만 이 쿼리를 다시 SELECT와 INSERT로 나눠 보자. 조회를 하는 SELECT는 ItemReader에서 실행되고, INSERT는 Item Writer에서 실행된다. 이제 실제적인 DB를 이용하는 Item Reader와 ItemWriter를 만들어 보자.

 



 

실행할 쿼리 문을 작성하고 전달할 파라미터를 setPrepared StatementSetter에서 설정한다. 파라미터값은 스프링 배치를 실행시킬 때 설정했던 JobParameter에서 추출한다. 이 ItemReader가 상속받고 있는 상위클래스는 반복적인 작업을 줄이기 위해 추상화한 클래스로 원래는 ItemReader가 상속받아야 하는 JdbcCursorItemReader를 상속받아 Job Parameter와 PreparedStatementSetter에 대한 설정작업을 수행한다.

 



 

ItemReader에서 SELECT 쿼리를 수행한 결과인 ResultSet을 Item으로 매핑하기 위해 RowMapper 인터페이스를 구현한 Row Mapper를 작성한다. 여기서 사용하는 Item은 getter와 setter를 메소드로 가진 일반적인 DTO(Data Transfer Object)이다.

이제 Reader에서 읽은 Item을 다시 DB에 입력하기 위해 ItemWriter를 작성한다. ItemWriter 인터페이스를 구현하고 JDBC를 사용하기 위해 JdbcTemplate을 상속한다. Item Writer에서는 write() 메소드가 호출되면 batchUpdate를 사용해 INSERT 쿼리를 실행하게 되는 것이다.

 

마지막으로 Job 설정 파일에 <bean>을 사용해 ItemReader, ItemWriter를 bean으로 등록해 준다. DB를 이용하기 때문에 양쪽 모두 DataSource를 설정하고 reader의 경우 앞서 설명한대로 RowMapper를 추가로 설정해 준다. 이렇게 설정된 bean을 사용해 Job과 Step에 대한 설정을 작성한다. 예제는 Step이 하나만 설정되어 있어 느끼긴 힘들지만 여러 개의 Step으로 구성된 Job은 이 설정 파일을 보면 어떤 Step으로 구성되어 있는지 빠르게 파악할 수 있다.

 

맺음말

 

간단한 변환 예제로 인해 과연 스프링 배치를 개선되는 것이 무엇인지를 느끼기에 부족할 수도 있겠다. 하지만 만약 입출력 대상에 변화가 생기는 경우에 그에 맞는 ItemReader나 ItemWriter를 구현해 설정 상에서 교체해 주기만 하면 동일한 Item을 사용해 처리하는 것이므로 다른 배치에는 아무런 영향을 주지 않고 입출력 대상을 변경할 수 있다. 지면 관계상 입출력 대상이 변경되는 예제를 보이질 못해 아쉬운 마음이 많이 든다. 아직 그 능력이 많이 알려지지 않은 스프링 배치에 더 많은 분들이 관심을 갖게 되어 기존의 레거시 코드를 스프링 배치로 변환하는 한국만의 기준이나 가이드를 만들어 낼 수 있는 날이 오기를 기대해 본다.

 

 

필자소개

 

최한수 cuteimp@gmail.com|아이티와이즈 컨설팅 PE 그룹에서 엔지니어로 일하고 있다. 여전히 체력과 건강에 관심만 갖고 하는 운동이라곤 숨쉬기만 하고 있다. 현재는 보험사 시스템 구축 프로젝트에서 일하고 있다.

 

출처 : 한국 마이크로 소프트웨어 [2010년 1월호]

제공 : DB포탈사이트 DBguide.net


반응형

관련글 더보기

댓글 영역