5부 - 소스코드 최적화

@Soo · February 20, 2022 · 23 min read

1. 기본 방향

  • 소스코드 최적화 분석 시 중점적으로 체크할 항목

    • 단순 정보성 로깅을 하는 부분이 있는가?
    • 로깅을 하지 않을 때 로깅을 위한 문자열이나 데이터가 만들어지고 있는가?
    • 전문 파싱이나 환경설정 로딩과 같은 작업이 반복 수행되는가?
    • 락 범위를 최소화하거나 락을 회피할 수 있는가?
    • 집합 데이터에 대한 검색 방식은 적절한가?
    • 문자열 처리에 오버헤드가 존재하는가?
    • 반복 처리 로직이 존재하는가?
    • 기능이 단순한 함수이지만 호출 횟수가 많아 성능에 영향을 미치고 있는가?
    • 영업일, 계정과목, 공통코드 같은 소량 테이블에 빈번한 조회가 발생하는가?
    • 배치 같은 경우 결과가 한정적인 데이터에 대한 반복조회가 많이 있는가?
    • 포털 화면에서 보여지는 게시물 목록 같은 것을 캐시할 수 있는가?
    • 프로그램 흐름상 병목이 존재하는가?
    • 대량건 조회는 가능한가?
    • 송수신 전문 포맷과 크기는 시스템 특성과 네트워크 환경에 적합한가?
    • 한 서비스 내에서 연계 시스템과 빈번하게 통신하는가?

2. 불필요한 작업 제거

2.1 로깅

  • 로그는 시스템 관리자와 개발자에게 수행된 프로그램에 대한 추가 정보를 제공하기 위해 있는 것이지 로그가 있느냐 여부가 핵심 기능에 영향을 주는 것은 아니다.

2.1.1 잘못 사용된 로깅 수준

  • Log4J의 경우 전체(ALL), 추적(Trace), 디버그(Debug), 정보(info), 경고(Warn), 에러(Error), 심각(Fatal), 끄기(Off)라는 8가지 수준이 제공된다.
  • 운영 시에는 경고, 에러 수준으로 로깅을 설정한다.

    • 운영 시스템의 로그 수준이 디버그로 낮게 설정돼 있으면 로깅을 위한 오버헤드 코드와 파일 입출력 병목으로 응답시간이 느려질 수 있다.
  • 추적이나 디버그, 정보 성격의 로그를 에러나 심각으로 남기지 않는다.

2.1.2 로깅을 위한 불필요한 메시지 생성

// CASE 1
for (int i=0; i<n; i++){
	Log.debug.println("파일 서버에 전송할 입력 매개변수 + " + output);
}

// CASE 2
for (int i=0; i<n; i++){
	if(Log.debug.isEnabled()){
		Log.debug.println("파일 서버에 전송할 입력 매개변수 + " + output);
	}
}
  • CASE 1은 println 내부에서 디버그에 대한 로깅 여부를 확인해 출력하지 않아도 자바에서는 “String” + Object.toString() 문자열을 만드는 작업이 수행된다.
  • CASE 2if(Log.debug.isEnabled()) 조건문으로 로깅 코드를 감싸서 로깅 수준이 비활성화 상태면 문자열을 만드는 작업이 수행되지 않는다.

2.1.3 로깅을 효율적으로 하기 위한 개선

  • 개발 서버에 로깅하는 양이 너무 많아 원하는 로그를 찾는데 시간이 걸리고 성능 저하가 발생한다면 개발 서버도 기본 로그 수준을 경고로 설정하고, 테스트하는 구간만 디버그 수준으로 설정할 수 있다.
  • 서비스 요청 단위로 로깅을 관리하면 특정 서비스 요청만 디버그 수준으로 설정해 해당 서비스 전체의 성능 저하 없이 셀제 운영환경에서 디버그를 수행할 수 있다.

2.1.4 트랜잭션 저널 로그

2.2 불필요한 로직

  • 불필요한 로직은 공통 모듈을 사용할 때 주로 나타난다.

    • 대체로 공통 모듈은 다양한 경우에 대비해 여러 가지 값을 제공한다.
    • 어떤 로직에서는 필요하지 않은 정보도 함께 읽어오게 된다.
  • 이를 방지하기 위해 포괄적인 정보 외에 사용 패턴을 고려해 세분화된 정보를 제공하는 함수도 다양하게 제공해야 한다.

2.3 반복 로직

  • 성능에 영향을 주는 부분이 반복 작업으로 분석됐다면 맨 처음에 수행된 결괏값을 저장해 뒀다가 재사용할 수 있게 변수나 캐시를 둠으로써 성능 저하를 해결할 수 있다.
  • 반복의 예

    • 통신전문에서 동일한 항목을 읽어내는 것임에도 파싱 작업을 한다.
    • for, while 문 안에서 매번 동일한 값으로 동일한 계산식을 수행하는 부분이 있다.
    • 외부 인터페이스를 호출할 때마다 관련 환경설정 파일을 매번 읽어 들인다.

2.4 불필요한 초기화 과정

  • 내부적으로 초기화 과정이 복잡하고 시간이 오래 걸리는 작업임에도 개발자 입장에서 코드를 작성하는 부분은 너무 단순해 모르고 넘어가는 경우가 있다.

    • 자바의 JAXB와 스프링 프레임워크의 RestTemplate등 이 있다.
  • 스레드 안전한 클래스라면 싱글톤으로 선언하여 재사용할 수 있다.
  • 스레드 안전하지 않다면 ThreadLocal 같은 기능을 사용해 스레드별로 만들어 사용할 수도 있다.

3. 로직 최적화

3.1 락 최소화

  • 애플리케이션에서도 여러 프로세스나 스레드가 동일 데이터에 접근함으로써 발생할 수 있는 오류를 방지하기 위해 락을 사용한다.
  • C/C++는 세마포어, 뮤텍스, 크리티컬 섹션 등으로 락을 제공
  • 자바는 synchronized, wait/nofity 등 제어 구문 외에도 동시성을 제어하는 클래스를 제공한다.

3.1.1 락 범위 최소화

  • 캐시를 구성할 때 보통 데이터 종류별로 하나의 메모리 구조로 설계하는 것이 일반적이다.
  • 이 경우 해당 메모리 전체에 대해서 싱글포인트락이 존재하는 경우 동시 대량 처리 시 락 경합이 발생할 수 있으므로 복수 개의 메모리 구조로 쪼개서 락 경합을 분산할 수 있다.

3.1.2 락 제거

3.2 문자열 처리 개선

3.2.1 String.format 메서드

  • StringBuilder 로 처리하는 것에 비해 String.format이 10배정도 느리다.

3.2.2 String.replaceAll 메서드

  • replaceAll 내부는 정규 표현식을 사용하는 형태로 구현돼 있어서 복잡하다.

3.2.3 문자열 합치기

  • 자바 초기에는 “String” + 변수 + “String” 형태로 문자열을 처리하면 합치는 횟수만큼 객체가 생성되어 성능이 좋지 않았다.
  • 하지만 자바 버전이 올라가면서 컴파일러가 코드 최적화를 통해 StringBuilder로 대체하므로 개선 대상으로 고려하지 않아도 된다.

3.3 리플렉션 호출 제거

  • 리플렉션 호출은 프로그램에 유연성과 확장성을 제공하지만 이를 구현하기 위해 객체를 생성하고 메서드를 호출하기 위한 준비 작업 하나하나가 직접 메서드를 호출하는 것과 맞먹는 비용이 든다.

3.4 채번

  • 애플리케이션 코드 내에서 채번이 이뤄지는 경우에는 성능에 큰 영향이 없으나 DB 내에서 채번이 이뤄지는 경우에는 성능 저하의 원인이 되는 경우가 많다.
  • DB 내 채번 방식

    • 기존 최대값 조회 MAX+1
    • 동시에 채번이 이뤄지는 경우 동일한 번호가 채번되어 후속 작업에서 에러가 발생할 수 있다.
    • 채번 테이블
    • 채번 테이블에 유형별 레코드로 채번 번호를 관리하는 방식
    • DB 채번 기능
    • DB에서 재공하는 채번 기능을 사용하는 것으로 세 가지 방식 중 가장 성능이 우수하다.
  • 매번 DB에서 채번하게 되면 10,000번 채번에 DB 호출이 10,000번 수행되어야 한다.
  • 하지만 채번 테이블의 채번 캐시 크기를 예를 10,00 단위로 사용하게 되면 10,000번 채번 1번당 DB 채번이 이뤄지므로 성능이 훨등히 개선된다.

3.5 날짜 연산

3.6 시간 문자열 처리

3.7 순차 검색 제거

3.8 파일 입출력 단위

  • 파일 처리는 입출력 단위가 큰 것이 성능에 유리하다.
  • 버퍼를 가지고 있는 입출력 클래스를 사용해 버퍼 단위로 입출력이 발생하게 하는 편이 우수한 성능을 발휘한다.
  • BufferedOutpuStream, BufferedInputStream

3.9 SQL

  • SQL 바인드 변수 처리

    • SQL을 수행할 때는 PreparedStatement 객체를 사용하여 바인드 변수 처리하는 것이 기본 원칙이다.
    • SQL 캐시효과, 하드 파싱, 공유 풀 경합 등
    • 바인드 변수를 사용하지 않으면 필요 이상으로 많은 SQL이 공유 풀에 저장되어 메모리 사용량이 증가한다.
  • 자원 반납

    • SQL을 수행한 후 사용한 JDBC 자원을 반납하지 않으면 성능 저하나 장애가 유발된다.
    • Statement를 반납하지 않으면 DB 프로세스 당 할당된 최대 커서 수에 도달해 더는 SQL을 처리할 수 없는 상황이 되고, 자바 메모리 누수 또한 발생한다.
    • 자원 반납은 ResultSet, Statement, Connection 순으로 수행한다.
  • DB 연결 풀 사용

    • DB는 새로운 연결을 맺을 때 많은 비용이 드므로 일반적으로 연결 풀을 사용한다.
    • 평상시 부하는 연결을 늘리지 않고도 처리할 수 있을 정도로 최소 연결 풀 개수를 유지한다.
    • 더미 쿼리를 사용해 주기적으로 연결을 테스트함으로써 DB 연결이 방화벽이나 운영체제 설정에 의해 끊어지지 않게 한다.

3.10 BigDecimal

  • float 이나 double 숫자 타입은 정확성이 아닌 성능 위주로 설계된 부동 소수 형식을 사용하기 때문에 일부 값이 정확하지 않고 근사값으로 표현된다.
  • BigDecimal을 생성할 때 new BigDecimal(double) 보다는 new BigDecimal(String)을 사용한다.
  • double로 변환해야 하는 경우에는 BigDecimal.valueOf(double) 메서드를 사용한다.

3.11 비대기 입출력 사용

  • 논블럭킹 I/O 를 사용하면 읽어내고자 하는 야보다 데이터가 적거나 전혀 없더라도 현재 있는 데이터 양만큼 읽고 나온다.

3.12 엑셀 처리

  • 엑셀 포맷을 다룰 때 일반적으로 아파치의 POI를 사용한다.
  • POI는 엑셀 파일을 다룰 때 크게 스트리밍과 인 메모리 트리라는 두 가지 방식을 사용한다.

    • 인메모리 트리는 문서 내용 전체를 메모리에 유지하므로 메모리 사용량이 크고, 스트리밍에 비해 속도가 느리다.
  • 버퍼드 스트리밍은 전체 데이터를 메모리에 가지고 있지 않고 일정 건수가 되면 디스크로 내리기 때문에 메모리 제약이 없어 큰 용량의 데이터 처리가 가능하다.

3.13 기타 성능 개선

3.14 코드 성능 측정

4. 적극적인 캐시 사용

  • 성능 튜닝의 한 축은 서비스 간이나 서비스 내에서 반복되는 로직을 제거하는 것 이다.
  • 기존에 작업한 결과를 저장해 뒀다아 이후에 다시 동일한 작업이 수행됐을 때 결과를 재사용하면 반복되는 로직을 제거할 수 있다.
  • 브라우저
  • 도메인명에 대한 주소 캐시
  • 콘텐츠 캐시
  • 웹 서버
  • 환경설정 캐시
  • 콘텐츠 캐시
  • WAS
  • 환경설정 캐시
  • 프로그램 모듈 캐시
  • 애플리케이션
  • 적극적인 사용이 요구되는 부분
  • 프레임 워크
  • 환경설정 캐시
  • 서비스와 프로그램 구성 캐시
  • 송수신 전문 구조 캐시
  • SQL 캐시
  • 애플리케이션을 위한 캐시 제공
  • DBMS
  • 환경설정 캐시
  • 테이블, 인덱스, 권한 등 구성정보 캐시
  • SQL 실행계획 캐시
  • 데이터 캐시

5. 효율적인 아키텍처 구성

5.1 병렬 처리

  • 웹 기반 시스템은 각 구성 서버마다 병렬 처리를 설정할 수 있다.
  • 웹 서버는 사용자의 모든 서비스 요청을 받아들이는 곳으로, 한 사용자가 6개 이상의 병렬 스레드를 사용할 수도 있다.
  • 그러나 요청이 WAS 까지 도달하면 스레드를 한두 개 사용한다.
  • DB 동시 작업은 애플리케이션 처리시간과 DB 연결 사용시간이 거의 일치하면 스레드 풀 설정과 거의 동등한 수준으로 DB 연결 풀을 사용한다.
  • 아키텍처를 스레드 병렬 구조로 만들 때는 반드시 앞쪽에 큐를 둬서 스레드가 작압 단위 간에 쉼 없이 연속해서 처리할 수 있도록 해야한다.

5.2 통신전문

  • XML의 경우에는 메모리 사용량과 전문 크기가 많다.
  • 이를 개선하기 위해 컬럼명을 레코드 첫 부분에 한번만 기술하고 이후 반복되는 레코드에서 컬럼명을 생략하고 데이터만 기술하는 변형된 형태의 JSON을 적용했다.
  • 따라서 고성능 시스템이 필요하거나 사용자가 원격지에 있어 데이터 전송량을 줄여 속도를 개선해야 하는 경우에는 다른 형식의 전문을 고려해야 한다.

5.3 고객정보 조회 이력 로깅과 마스킹

5.4 대량 조회 프레임워크 구성

  • 데이터를 대량으로 조회하다보면 메모리 부족으로 애플리케이션 서버가 다운되는 문제를 경함하게 된다.
  • 아래와 같은 방안으로 대량건 조회를 대처할 수 있다.

    • 프레임워크 DAO에서 일정건이상 조회하면 예외를 발생시킨다.
    • 대량건 조회는 페이징 처리를 통해 메모리에 무리가 없는 단위로 나눠서 처리한다.
    • 대량 조회가 이뤄지지 않도록 조회 건수를 줄이는 필수 조건을 입력하도록 유도한다.
    • 일반 온라인 업무에 영향을 주지 않도록 대량 조회 전용 WAS 인스턴스를 구성한다.
    • 배치 같은 별도 프로세스에서 대량 건을 파일로 생성한 다음 파일을 다운로드하게 한다.
  • 프레임워크에서 대량건 조회를 지원하기 위해서는 자바의 경우 JDBC ResultSet과 전문 생성 부분을 바로 연결해야 한다.

5.5 내부 연계시스템

5.6 수직확장과 수평확장

  • 성능 저하가 발생하면 서버를 늘리는 수평확장보다 CPU나 메모리를 증설하는 수직확장을 선호해 왔다.

    • 수평확장은 서버간 동기화로 인한 성능 저하가 발생할 수 있기 때문에
  • 수평확장이 가능하도록 아키텍처를 구성하려면 다음과 같은 사항을 고려해야 한다.
  • 데이터베이스 샤딩
  • 샤딩은 여러 DB에 데이터를 나눠서 저장하는 기술이다.
  • 파티션은 한 DB에 분산 저장하는 것이고, 샤딩은 복수 DB에 분산 저장한다.
  • 샤딩은 DBMS가 제공하는 기능이 아니므로 애플리케이션에서 로직으로 구현해서 적용한다.
  • DB 샤딩에서 수평확장이 용이하게 하려면 초기 샤딩 아키텍처를 설계할 때 온라인 상태에서 DB 서버를 늘리고 데이터를 옮길 수 있는 방안을 마련해야 한다.
  • DB 서버가 4대에서 6대로 증가할 때 샤딩된 기존 저장된 DB서버를 찾아가고 증가된 DB수에 따라 데이터 재배치가 이뤄질 수 있어야 한다.
  • DB 샤딩의 한계는 구축하기가 어렵고, 샤딩된 데이터로 인해 테이블 간에 조인이 제한적이거나 불가능하다는 것이다.
  • 대량의 데이터 처리를 고려할 때 데이터 저장 솔루션으로 고려하는 것이 NoSQL 이다.
  • NoSQL을 이용하면 자동분할과 노드 추가 시 자동 재분배, 이중화에 의한 장애 대응까지 솔류션이 지원하므로 유연한 수평확장이 가능하다.
  • StatelessProcessing
  • WAS에서 세션을 통해 사용자 인증 정보를 관리하는 경우가 많다.
  • 여러 대의 WAS 서버를 사용하는 경우 세션 클러스터링 기술을 사용한다.
  • 그런데 서버 수가 많아지면 이마저도 성능 저하 요인이 된다.
  • 시스템을 Stateless 아키텍처로 만들어지면 사용자 서비스 요청이 어떤 애플리케이션 서버에서도 처리할 수 있다.
  • 인메모리 데이터 그리드
  • 인메모리 데이터 그리드는 키캆 형식의 데이터 캐시 솔류션이다.
  • DBMS에 저장된 데이터나 사용자 서비스 호출 간에 공유할 데이터를 캐시해서 성능을 개선할 목적으로 사용한다.
  • 상대적으로 데이터의 생명주기가 짧고 변경이 발생하지 않는 데이터에 대해 캐시하는 것이다.
@Soo
RDBMS, NoSQL, 분산 처리에 관심이 많은 백엔드 엔지니어입니다.