입사하고 인수인계를 받으면서 이건~ 원본소스도 없구요 예전에 개발된건데 가끔 ftp 연결 key만 바꿔주면 되는거에요~ 라고 "???" 물음표 3개가 생기는 프로젝트를 얼렁뚱단 받아버린게 하나 있었는데, 이번회사에서 약 1년정도 있다보니 역시나 장애가 터졌다...
가장 큰 문제점은 역시... "스케줄러 프로그램인데 원본소스가 없어요~" 이부분 일 것이다,
일단 무작정 서버에 붙어서 확인을 해보기 시작했다. 다행히 인수인계 문서에 해당 서버 접속정보는 있었다.
(인수인계 제대로 안받은 내잘못이지 ㅠㅠ)
윈도우 서버고 서비스중인 프로세스를 확인해보니 톰캣이 떡하니 있다.
오 톰캣으로 뭔가 하는구나! 설치된 톰캣 위치로 가서 webapps에 가보니 war파일이 5년전에 배포 된게 있고, 실행중으로 판단되는 디렉토리 하나가 있다.
압축파일로 가져와서 개발PC 디컴파일러(jd-gui-windows-1.4.0)를 하나 설치하고 압축상태로 열어본다.
구조를 보니 스프링 구조.. view페이지도 있고;; 단순 스케줄러로 파일 내려주는 것만 있다고 들었는데 심각하다. 분명히 웹의 기능은 없다고 들었는데... 컨트롤러에 필요없는 vo 서비스 dao 등등 그냥 어디서 구조하나 가져와서 스케줄링을 위해 quartz만 설정해서 쓰고 있는것으로 판단되었다.
그래 뭐 다 좋다...
스케줄링이 도는 동작시간을 확인하고 log를 확인하기 위해 log4j가 설정된 부분을 찾아봤다...
우왁... 파일 하나당 10기가 16기가 8기가 엄청나다;
당연히 열리지도 않는다.
gvim, gsplit 등 에디터와 파일쪼개는 프로세스를 활용해서 로그를 분석해본다...
로그가 미친듯이 쌓인 이유를 찾았다. mybatis 설정부분인 Connection, Statement, PreparedStatement, ResultSet 모든 레벨이 다 debug로 되어있다. 실서버 등록하면서 개발자가 까먹은건지... 몰랐던건지 그냥 다 때려박도록 되어있다.
세월아 네월아 천천히 로그들을 분석해보니 특정 쿼리가 도는데 where절이 없이 데이터가 9만개 가량 들어있는 테이블을 조회하고 있다.
테이블 자체에 데이터도 많지만 컬럼양도 약 70개가 넘어가고 중간에 Text형도 있기에 모든 결과행을 출력하면 데이터가 어마어마하다.
한 27만줄씩 찍어대는거 같다..
그런데 그런짓을 한번 돌때마다 9~10번씩 하니 로그만 30분이 넘게 찍다가 OutOfMemory가 뜨고 서버 hdd에도 용량이 심심하면 꽉차고 난리가 난 것 같다...
일단 찾은 부분까지 상황을 팀장님께 보고를 드리고 대기하고 있었다.
그리고 답변이 왔다 "우리가 조치할 수 방법이 뭐가 있나요..?"
원본 소스가 없는데 뭘 어떻게 조치하란 말인가... 프로세스도 미친듯한 절차식들로 인해 분석도 힘들고 심지어 디컴파일러를 통해 보다보니 제대로 안된부분도 많았다.
반나절 정도 고민해보고 소스와 로그를 분석해 본 결과
1. log4j.xml의 저 쓸데없는 로그 레벨을 낮추자
-> IO를 엄청나게 쓰면서 부하 및 용량 이슈를 해결 할 수 있다.
-> 다만 원본소스가 없어서 뭐가 어떻게 동작하는지 모르니 ResultSet만 error로 바꾸자
2. where절이 없이 돌아가는 mapper부분을 분석해보자
-> 다행이 해당 쿼리를 타는 부분을 역추적하여 찾아보니 mapper에서 사용하는 key값이 processId라면 Map에 담아주는 key값이 processid인 것을 확인하였다. 당연히 자바 소스 수정은 불가능하여 choose when으로 mapper부분의 xml을 바꾸기로 하였다. 그럼 8만개를 검색하여 40초걸리던 부분이 약 1초로 줄어들것으로 판단 되었다.
3. tomcat jvm을 강제로 50퍼정도까지 사용하도록 설정하기
이정도였고 실서버에 해당 3가지 내용을 적용하고 엄청난 부하가 오던 이슈가 해결이 되었다.
ps. 사실 중간에 스킵된 내용이 많은데 log분석이 정말 너무 힘들었다... 장애가 터졌던 날의 로그파일이 12GB였는데 500MB씩 쪼개어 23개 가량의 로그파일들을 하나하나 읽어보면서 찾기기능을 쓴다고 해도 어떤 키워드로 어떤 장애가 터졌을지 감도 안왔고, 너무 힘들어 선임님의 도움으로 java로 디렉토리의 모든 파일들을 쭈르륵 읽어들여 특정 키워드가 발생한 문장만 따로 가져오도록 프로세스도 짜고 아주 신박한 경험을 해보았다.
객체를 JSON처리하기 위해 GSON과 SIMPLE JSON 라이브러리의 힘을 빌렸습니다.
처음에 구성은 Simple Json만 사용하여 구성해봤지만, 단순 Array에 담은 데이터를 넘기는건 문제가 없지만 Vo객체를 JSONObject에 넣어서 넘기게되면 "key" 처리가 정상적으로 되지 않아 파싱부분에서 에러가 발생하는 것을 발견하였고, 넘기기전에는 Gson을 통해 JSON화하여 넘기도록 하였습니다.
JSON처리하여 소켓통신하기
Server
이번엔 Gson과 simple Json을 사용할 예정이므로 pom.xml에 아래 정보를 추가해야 합니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.json.simple.JSONObject;
import com.google.gson.Gson;
public class SocketThreadServer extends Thread {
private static final Logger logger = Logger.getLogger(SocketThreadServer.class);
private Socket socket;
public SocketThreadServer(Socket socket){
this.socket=socket;
}
private static final InterlockDao interlockDao = InterlockDao.getIntstance();
//JSON 데이터 넘기기
public void run(){
BufferedReader br = null;
PrintWriter pw = null;
try{
String connIp = socket.getInetAddress().getHostAddress();
System.out.println(connIp + "에서 연결 시도.");
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream());
// 클라이언트에서 보낸 문자열 출력
String returnMessage = br.readLine();
System.out.println(returnMessage);
Gson gson = new Gson();
JSONObject jo = gson.fromJson(returnMessage, JSONObject.class);
List<Map<Object, Object>> list = (ArrayList<Map<Object, Object>>) jo.get("list");
for(int i=0; i<list.size(); i++) {
System.out.println(list.get(i).toString());
}
// 클라이언트에 문자열 전송
pw.println("수신되었다. 오버!");
pw.flush();
HashMap<String, Object> params = new HashMap<String, Object>();
List<Map<String, Object>> test = interlockDao.selectTest(params);
for(int i=0; i<test.size(); i++) {
System.out.println(test.get(i));
}
}catch(IOException e){
logger.error(e);
}finally{
try{
if(pw != null) { pw.close();}
if(br != null) { br.close();}
if(socket != null){socket.close();}
}catch(IOException e){
logger.error(e);
}
}
}
}
이번에도 마찬가지로 데이터를 먼저 받고 응답을 하는 서버 코드입니다.
작성하게될 클라이언트에서 JSONObject에 ArrayList<vo> 컬렉션을 "list" 키에 담아서 발송하는 코드를 작성 예정인데, 파싱하는 부분은 Map형태로 되어있습니다. JSON으로 파싱하면서 VO 객체를 단순 Map의 컬렉션처럼 key, value화 시켰기 때문입니다. 같은 VO로 파싱하려고 하면 파싱에러가 발생하는것을 볼 수 있습니다.
App.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.log4j.Logger;
import kr.or.kisa.ktoaInterlock.socket.SocketThreadServer;
public class App {
private static final Logger logger = Logger.getLogger(App.class);
private static final int PORT_NUMBER = 4432;
public static void main(String[] args) throws IOException{
logger.info("::: :::");
logger.info("::: Socket Application Process Start :::");
logger.info("::: :::");
try(ServerSocket server = new ServerSocket(PORT_NUMBER)){
while(true){
Socket connection = server.accept();
Thread task = new SocketThreadServer(connection);
task.start();
}
}catch(IOException e){
logger.error(e);
}
}
}
작성 후 소켓서버를 동작시킵니다.
Client
클라이언트 프로젝트도 별도로 빼셨다면 server 코드에서 추가한 pom.xml 정보를 입력하여 Gson과 simple Json을 추가해주세요.
Client3.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import org.json.simple.JSONObject;
import com.google.gson.Gson;
public class Client3 {
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
public Client3(String ip, int port) {
try {
// 서버에 요청 보내기
socket = new Socket(ip, port);
System.out.println(socket.getInetAddress().getHostAddress() + " 연결됨");
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream());
ArrayList<BoardVO> list = new ArrayList<>();
for(int i=0; i<5; i++) {
BoardVO vo = new BoardVO();
vo.setTitle(i+"번째 제목입니다!");
vo.setContent("1234567890_testtest String 문자열 테스트 컨텐츠츠츠");
vo.setIdx(i);
vo.setWriter("홍길동");
list.add(vo);
}
JSONObject jo = new JSONObject();
jo.put("list", list);
//VO 메시지 발송
pw.println(new Gson().toJson(jo));
pw.flush();
//발송 후 메시지 받기
System.out.println(br.readLine());
} catch (IOException e) {
System.out.println(e);
} finally {
// 소켓 닫기 (연결 끊기)
try {
if(socket != null) { socket.close(); }
if(br != null) { br.close(); }
if(pw != null) { pw.close(); }
} catch (IOException e) {
System.out.println(e);
}
}
}
}
ArrayList<BoardVO> 형태의 컬렉션 리스트에 데이터를 담고 JSONObject 키값 list에 담은 후 Gson을 활용하여 JSON화한 문자열을 발송합니다.
App.java
public class App{
public static void main( String[] args ) {
String ip = "서버의 IP";
int port = 서버의 포트;
new Client3(ip, port);
}
}
작성이 완료되었으면, 서버를 동작시키고 클라이언트에서 JSON을 발송해본다.
동작결과
서버에 정상적으로 vo별로 담은 정보가 파싱되는것을 볼 수 있습니다.
클라이언트에서도 결과 문자열이 받아진것을 볼 수 있습니다.
주의사항
JSON파싱 로직시 GSON의 도움 없이 Simple JSON만으로 처리시 아래처럼 데이터가 들어오는것을 볼 수 있습니다.
JSONObject jo = new JSONObject();
jo.put("list", list);
pw.println(jo.toJSONString());
꼭 Gson의 toJSON메소드를 활용하여 전체적으로 JSON형태가 될 수 있게 처리하여 발송하도록 해야 받는 곳에서 파싱하는 경우 문제가 없습니다.
JSONObject jo = new JSONObject();
jo.put("list", list);
pw.println(new Gson().toJson(jo));