본문 바로가기
회사

회사 성능개선 회고

by 개발고구마 2024. 9. 22.

성능개선 회고
3주내내 야근하면서 성능개선 회고록

그룹장님이 이전에 성능개선을 하면서 쌓은 경험을 들려주시며 그 포인트를 중점으로 성능개선을 진행했다
서버(DB 제외) 에서 포인트 3가지가 있다
체크해야할 포인트를 3순위까지 정해 비즈니스 로직의 과정을 처음부터 끝까지 분석하며 수정한 내용들을 적어봤다

1순위. 루프가 많은경우

가장 먼저 해야 할 것은 본인이 성능개선하고자 하는 소스에 루프들을 찾는 것이다.
그 후 찾은 루프들 중에 개선할 수 있는 부분을 찾는다
특히 중첩루프는 피하는 것이 좋다

사례1

첫번째 사례는 들어온 데이터 중 기존 데이터가 있는지 판단하는 거였다.
문법 무시

const list1 = [1,2,3,4,5];
const list2 = [1,2,3]; 
const new_list = [];
list1.map(item => {
	list2.map(item2 => {
      if(item == item2){
        new_list.push(item)
      }
    })
})

위와 비슷한 구조로 되어있었다.
비즈니스 로직으로 왜 저렇게 만들었는지 구체적인 설명은 불가능하지만 변경할 수 있는 방법은 있었다
list2가 select로 불러오는 로직이기에 다음과 같이 바꿨다

const list1 = [1,2,3,4,5];
const set = new Set([1,2,3,]);
list1.map(item => {
  if(set.has(item)){
        new_list.push(item)
  }
})

위 사항 개선으로(물론 여기서 select부분도 고쳐야 했고 데이터 비교의 변경사항도 있었지만 자세한 내용은 생략)
비교하던 로직이 측정 기준 38초 -> 2초로 개선할 수 있었다
그리고 데이터가 더 많으면 많을수록 개선효과는 당연히 커질수밖에 없어 성능 개선에 큰 효과를 봤다.

사례2

아래같은 상황들로 사용하는 경우가 빈번한데 이건 그리 효과가 크진 않았지만 여러 개가 쌓이다보니 효과가 어느정도 있었다
TS기준

const item = items.filter(item => item.key == '1')
if(item){
  // Do Something
}

특정 키 값을 만족하는 데이터가 있는지 판단 후 있으면 특정 행동을 하도록 만든 로직이 있다
저기서 써야하는건 filter보단 some이다
filter와 some의 차이를 모르겠다면 아래 참고 링크가 도움이 될 것 같다
참고 : https://jh-7.tistory.com/11
filter는 배열의 모든 데이터를 확인해보고
some은 조건을 만족하는 순간 결과를 내뱉는다
결국 배열을 끝까지 돌 필요가 없는 상황에서는 그에 맞춰 함수를 사용하자.

2순위. 문자열

문자열이 왜 서버에 부하를 주냐? 라고 할 수 있는데
문자열이 생성되는 원리부터 봐야한다
문자열은 불변객체라 할 수 있는데 (자세한 내용은 검색을 해보면 아주 많이 나올 것이다)

결국 이 문자열이 불변 객체라 문자열에 대한 연산이 이루어질 때마다 새로운 문자열 객체가 생성된다
저 문자열 객체는 메모리 사용량을 증가시킨다

고로 무분별한 문자열 연산을 성능개선할 때 고려할 대상으로 염두해두자
문자열이 여러 파싱을 통해 한 결과물을 얻어낼 때,
중간 과정에 연산이 많다면 중간 불필요한 문자열이 모두 생긴다.

그럴때는 결과물만 뱉어내 줄 수 있는 StringBuilder 같은걸 사용하자

3순위. 잦은 인스턴스 생성

위와 비슷한 문제이다.
잦은 인스턴스 생성은 힙영역 메모리를 증가시키는 원인이 된다.
이에 더해 잦은 GC 발생으로 GC의 부하를 높일 수 있다.

 

더보기

+추가
이전에 변수 위치를 반복문 안이 아닌 밖에다 하고 인스턴스를 동일한 곳에 생성해야 효율이 좋다는 말을 들었던 것 같은데 찾아보니 꼭 그렇지만은 않은 것 같다
참고 : http://stackoverflow.com/questions/8803674/declaring-variables-inside-or-outside-of-a-loop
결국 인스턴스는 생성 위치가 중요하지 변수 할당의 위치는 크게 상관 없는 것으로 보인다

 

물론 인스턴스를 한번만 만들 수 있는걸 여러 번 만드는 건 당연히 안좋다

아래는 생성에 대한 개선 사례다

사례

특정 로직을 통하면 A 데이터가 사용자가 설정한 매핑으로 B데이터로 변경을 해준다.
예를 들면, B.name을 A의 A.id + A.name 으로 사용자가 설정했다면 여러 데이터를 동일한 방식으로 매핑되도록 변경해주는 로직이다
그럼 로직 중간에 B.name = A.id + A.name 이라는 설정이 담겨있는 객체가 있었다 (이하 mapper라는 변수로 호칭)
문제는 데이터가 100건이라면 mapper가 100번 호출되고 있었다.
mapper는 맨 처음 호출할 때 한번만 하면 되는데 그게 안에서 만들어지고 있었던 것.

datas.map(data : A =>{
  const mapper = new Mapper();
  const new_data : B = {};
  B.properties.map(property =>{
    result[property] = mapper.mapping(A, property);
  })
  result_list.push(new_data);
})

 

대충 위와 비슷한 식으로 되어있었다.
물론 이 소스만 보면 왜 저렇게 만들었냐 하겠지만 당연히 진짜 로직은 엄청나게 복잡하고 길었다.
해당 부분을 찾았을 때도 바로 찾은게 아니라 여러 번 보며 찾게 되었다.
문제는 저런 경우가 꽤 많아 모두 다 밖으로 빼서 변경하게 되었다.

setting_map = this.init(); // 여러가지 초기화가 필요한 애들을 묶어서 한방에 처리
datas.map(data : A =>{
  const new_data : B = {};
  B.properties.map(property =>{
    result[property] = setting_map.get('mapper').mapping(A, property);
  })
  result_list.push(new_data);
})

 

위와같이 변경해 init에서 설정에 관련된 모든 애들을 처리한 후 필요한 애들을 꺼내서 사용하도록 변경했다.
기존 한개의 데이터를 처리할때 0.4초(100개일 때 40초) 걸리는 로직이 0.4초(100개일 때 10초)로 걸리도록 변경했다. 단순 선언 변경으로는 이정로도 큰 효과를 보지 못했을 텐데 설정 객체를 만들기위한 DB select 및 중첩문도 100 -> 1번으로 줄어들어 큰 효과를 보았다

 

위 내용 말고도 여러 가지 개선 부분이 있었지만, 크게 효과를 본 것들 대부분 위 내용이었고 진짜 포인트를 위주로 고치다 보니 개선 효과가 커 뿌듯했다.