유지보수 가능한 코딩의 기술: 자바편

소프트웨어 유지 보수의 4대 유형

- 버그를 발견하고 고친다(교정형 유지보수, corrective maintenance)
- 운영 환경의 변화에 따라 시스템을 변경한다(적응형 유지보수, adaptive maintenance)
- 시스템 사용자의 요구를 반영한다(완료형 유지보수, perfective maintenance)
- 품질을 높이고, 버를 방지할 방안을 모색한다(예방형 유지보수, preventive maintenance)

유지보수성 가이드라인


코드 단위를 짧게 하라
목표 : 코드 단위는 15라인을 어가지 않게 작성한다.
  • 메서드 추출(Extract Method)
  • 메서드를 메서드 객체로 대체(Replace Method with Method Object) : 길어지는 코드를 객체로 분리
코드 단위는 간단하게 짜라
목표 : 단위당 분기점은 4개로 제한한다
  • Map 을 이용해 분기문 사용을 줄일 수 있다
  • 케이스별로 구분해서 각각 return 문을 넣으면 중첩 조건문 제거에 도움이 된다. (중첩문 보호철로 대체, Replace Nested Conditional with Guard Clauses)
  • 중첩 조건문을 별도 메서드로 추출하면 덜 복잡한 코드가 된다.
코드는 한 번만 작성하라
목표 : 코드를 복사하지 않는다.
  • 코드를 재사용 가능한 일반적인 형태로 작성하거나 기존 메서드를 대신 호출한다.
  • 상위 클래스 추출(Extract Superclass) 를 이용해 원 클래스의 상위 클래스로 생성하여 추출
단위 인터페이스를 작게 하라
목표 : 단위당 파라미터 개수는 4개 이하로 제한한다.
  • 파라미터를 객체로 추출한다
  • 세터가 전원 this를 반환하게 하여 흘러가는 인터페이스(fluent interface)를 만들 수도 있다
관심사를 모듈로 분리하라
목표 : 모듈 간결합을 느슨하게 하기 위해 큰 모듈은 삼가한다.
  • 개별 모듈로 나누어 일을 시키고 구현 상세는 인터페이스 안으로 감춘다.
  • 클래스를 나누어 관심사를 분리한다
  • 특정 구현부는 인터페이스 안에 숨긴다
  • 커스텀 코드를 서드파티 라이브러리/프레임워크로 대체한다
아키텍처 컴포넌트를 느슨하게 결합하라
목표 : 최상위 수준의 컴포넌트 간 결합도를 낮춘다
  • 다른 컴포넌트 모듈에 호출을 받는 형태로 공개도니 모듈 내부의 상대적인 코드량을 최소화 한다.
  • 모듈의 크기를 제한한다
  • 상위수준에서 컴포넌트 인터페이스를 추상화 한다(컴포넌트 경계를 넘나드는 요청 가짓수를 제한)
  • 스루풋 코드는 쓰지 않는다
아키텍처 컴포넌트의 균형을 잡아라
목표 : 최상위 수준 컴포넌트 개수와 상대적인 크기를 균형잡느다
  • 컴포넌트 개수가 9개(6~12개) 정도 되도록 소스 코드를 조직화 하고 컴포넌트 크기를 대략 균등하게 맞춘다
코드베이스를 작게 하라
목표 : 코드베이스를 가능한 작게 한다
  • 코드베이스가 커지는 걸 막고 시스템 크기를 적극적으로 줄인다
  • 코드를 복사하지 않는다
  • 기존 코드를 리팩터링 한다
  • 서드파티 라이브러리/프레임워크를 사용한다
  • 대규모 시스템을 분리한다
테스트를 자동화하라
목표 : 테스트를 자동화 한다
  • 테스트 프레임워크로 자동화한 테스트를 작성한다
클린 코드를 작성하라
목표 : 클린 코드를 작성한다
  • 규칙 1: 단위 수준의 코드 악취를 남기지 말라(커밋하기 전에 리팩터링을 마쳐야 한다)
  • 규칙 2: 나쁜 주석을 남기지 말라
  • 규칙 3: 주석 안에 코드를 남기지 말라
  • 규칙 4: 죽은 코드를 남기지 말라
  • 규칙 5: 긴 식별자 이름을 남기지 말라
  • 규칙 6: 매직 상수를 남기지 말라(매직상수, magic constant : 의미가 불분명한 숫자나 리터럴 값, 상수값은 명시적으로 정의해서 사용하는 것이 좋다.)
  • 규칙 7: 제대로 처리 안 한 예외를 남기지 말라

서버 성능 테스트 Tips – if(kakao)dev 2019

  • 카카오 게임즈 발표
  • 성능 테스트의 목적
    • 시스템의 성능 기준을 정의
      – 예를 들어 ‘8만 tps에서 동접 1만이 수용 가능하고 그 상태에서 서버 cpu가 70%까지 사용 해야 한다’ 라는 기준
    • 실제 사용자의 액션과 유사한 시나리오를 작성하여 실제 유저의 부하와 유사한 부하 유입
      – 1. 이용자의 다양한 액션 누락 참조
    • 시스템의 병목 구간을 찾아서 튜닝 완료
      – 2. 최종 시스템의 성능테스트 누락
    • 임계값을 기반으로 이용자 증가/감소에 대응할 수 있는 데이터 제공
    • 시스템의 무결성을 검증하여 예상하지 못한 장애를 예방 및 대응
      – 3. 예외 상황에 대한 테스트 누락 참조
  • 성능 테스트 진행하면서 겪었던 시행착오 및 사례
    • 알고 있었지만 실수로 겪었던 사례
      1. 이용자의 다양한 액션 누락
        • 발생 문제 : 예상 동접은 한참 남았는데 PVP 서버 CPU 95%까지 증가.
        • 원인 : 이용자들은 실제로 한가지씩 차례대로가 아닌 다양한 형태로 컨텐츠를 소비한다.
          ( 그렇기 때문에 하나의 테스트 시나리오로는 실제 유저와 유사한 부하를 발생시킬수 없다.)
        • 해결 : 성능 테스트 방식을 변경.
          ( 기존 테스트 방식 : 하나의 성능 테스트 시나리오를 작성한 후 API를 순서대로 나열하고 한번씩 호출하며 반복적으로 부하를 주는 식으로 테스트 진행.
          변경한 테스트 방식 : 우선 다양한 이용자의 흐름에 맞춘 테스트 시나리오를 작성할 수 있도록 인원을 확보하여 각각 다른 스타일로 게임을 진행하게 한다. 그렇게 진행했던 내용들을 시나리오 문서로 작성하도록 한 후, 그 문서를 기준으로 실제 유저들이 동일한 플레이 시간이어도 API별로 호출 횟수가 다양하게 나온다는걸 확인할 수 있었고 그것을 기반으로 실제 유저가 이용 가능한 유사한 시나리오들을 작성할 수 있었고 그 시나리오들을 바탕으로 성능테스트를 진행하면서 기존 보다는 좀 더 실제 유저와 가까운 부하를 발생 가능하게 됨.)
      2. 최종 시스템의 성능 테스트 누락
        • 발생 문제 :
          – 서비스 런칭 후 동시 접속자 3만에서 장애
          – Memcached 응답속도가 느려지는 현상 발생
          – PHP-fpm의 프로세스에서 지연 현상 발견
          – 이후 응답 없음 현상이 발생함
        • 원인 : 분산 Memcached 구조에서의 친구 리스트 호출 횟수가 비정상적으로 증가하면서 php-fpm이 설치된 장비의 memory full.
        • 해결 :
          – Memcached 스케일 업(scale-up) 진행
          – Memcached에 있는 데이터들을 분할하는 작업 진행
          – 비정상적으로 호출되었던 친구 리스트 알고리즘 수정
        • 왜 성능 테스트 때는 발견되지 않았나?
          • 성능테스트를 진행 할 때 다수의 서버에서 하나의 memcached로 구성되어 테스트를 진행했으며 정상적인 결과를 확인했으나 성능 테스트 진행 후 런칭 전까지의 2~3주의 시간동안 5대의 분산구조로 memcached 구조 변경됨. 사실상 런칭스펙에 대해서는 성능테스트가 전혀 진행이 되지 않고 오픈이 된 것.
          • 이 문제는 기술적 부분보다는 성능 테스트 프로세스의 개선이 필요하다 판단됨
        • 개선 : 성능 테스트 프로세스 개선
          • 시스템에 대한 확장성 및 변경 가능성에 대해 리뷰 강화
          • 성능 테스트 진행 전 테스트 환경에 대해 담당 개발자 검증 단계 도입
          • 최종 런칭 스펙과 동일한 구성으로 성능 테스트 진행하도록 프로세스 강화
          • 성능 테스트 종료 후 시스템의 구조 변경은 서비스 담당자들이 합의하도록 변경
      3. 예외 상황에 대한 테스트 누락
        • 발생 문제 : 유저의 이탈 경로와 상세 분석을 위해 로그를 상세하게 넣었고 이 부분에 대해 우려를 했으나 성능 테스트 진행시에 별 이상은 없었음. 하지만 런칭 이후 로그 서버 CPU 95%, memory 100%로 로그 서버 다운되고 API서버도 이어서 다운. 이후에 다운된 서버를 올리는 작업을 진행하였으나 순차적으로 계속 다운.
        • 문제 해결을 위해 계속 서버(API서버와 로그서버는 연관관계가 없다고 생각하여 문제 발생 원인을 다른 곳에서 찾느라 해결이 더 늦어졌음)
        • 원인 : API 서버에서 로그를 썼는데 로그 서버가 죽어있어서 응답 대기 상태로 빠지는 현상 발생.
        • 개선 : 모든 구성 서버들에 대해 예외 상황 테스트 진행
          – 구성 서버들을 나열후에 각 서버들을 강제로 발생 시켜서 장애가 발생한 서버에서 어떠한 장애가 발생하는지 그 현상 파악 그리고 해당 서버에서 장애가 발생한 이후 다른 서버에서는 어떠한 현상들이 발생이 되는지, 정상적으로 동작을 하는지에 대한 내용 파악.
          – Auto scaling 및 DB Failover에 대해서도 사전에 검증 진행
  • 더 좋은 성능 테스트를 위해 준비하고 있는 것.
    • 수집된 데이터들을 바탕으로 시스템 구성 별 성능 예측
      : 성능 테스트를 하며 얻어지는 결과 데이터들을 분류 및 저장하고 있으며 시스템의 기본 성능에 대한 기준을 만들고자 하고 있다. 계속해서 쌓이고 있는 이 데이터들이 나중에는 빅데이터처럼 성능테스트 진행전 시스템 구성이나 서버 인스턴스만 보고도 대략적인 기준을 제시할 수 있지 않을까.
      – [ex]
      – AWS의 EC2 instance 종류 별 적정 TPS, 혹은 수용 가능한 동시 접속자 수
      예시) C4.xlarge(aws 서버타입)는 1000TPS 혹은 C4.xlarge 한대 기준 동시 접속자 3000명
      – 시스템 구성에 대한 적정 TPS
      예시) Front와 API로 구성되어 있고, PHP-FPM과 Nginx이니까 2000TPS
    • 현재의 성능 테스트를 개선하여 더 나은 성능 테스트 체계 구축
      : 현재 성능 테스트 툴로는 웹서버를 기준으로 하고 있고, 웹서버를 대상으로 응답을 주고받거나 http통신 그리고 웹소켓 으로 한정되어 있는 것이 현실.
      이에 그동안 (ngrinder를 사용하여)성능 테스트를 진행하며 원하는 데이터 확인을 위해 커스터마이징 했던 스크립트들을 오픈소스로 공개해서 많은 개발자들과 내용 공유를 하고 Library 개선하고 및 사례들을 수집 하기 위해 준비 중.
  • 사용하고 있는 성능 테스트툴
    • nGrinder
    • QnA 1 :
      성능테스트 툴로 ngrinder를 사용하는 이유 : 스크립트를 커스터마이징면서 사용이 가능하며 원하는 결과 값을 확인 가능하기 때문.
    • QnA 2 :
      nGrinder를 통해 모든 것을 모니터링 하는 것은 불가능 할텐데 그 외의 모니터링은 어떻게 진행하고 있는지 : 대상 서버에 대해서는 nGrinder를 사용해서 모니터링 하는 것이 불가능하기 때문에 이 부분은 개발사에서 제공하는 Zabbix 툴을 사용하여 모니터링하고 있음.

Junit을-이용한-단위테스트

JUnit

JUnit은 자바용 단위 테스트 작성을 위한 산업 표준 프레임워크다.

JUnit assert 주요 메서드 및 사용예시

assert 메서드설명
assertArrayEquals(a, b);배열 A와 B가 일치함을 확인한다.
assertEquals(a, b);객체 A와 B가 일치함을 확인한다.
assertSame(a, b);객체 A와 B가 같은 객임을 확인한다. assertEquals 메서드는 두 객체의 값이 같은가를 검사는데 반해 assertSame메서드는 두 객체가 동일한가 즉 하나의 객인 가를 확인한다.(== 연산자)
assertTrue(a);조건 A가 참인가를 확인한다.
assertNotNull(a);객체 A가 null이 아님을 확인한다.

JUnit Annotation 사용 예시

  • 스프링 프레임워크 기반의 JUnit 테스트를 위한 세팅
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={“file:WebContent/WEB-INF/classes/applicationContext*.xml”})

Spring 기반의 테스트 코드 작성을 위해 테스트 클래스 상단에 @RunWith(SpringJUnit4ClassRunner.class) 구문을 추가한다.
Spring 프레임워크 context 파일을 테스트 수행시에도 동일하게 로딩하기 위해 @ContextConfiguration(locations={“file:WebContent/WEB-INF/classes/applicationContext*.xml”}) 과 같은 형태로 프로젝트의 스프링 설정파일을 설정해 준다.

  • 메서드 수행시간 제한하기
@Test(timeout=5000)

단위는 밀리초이며 이 메서드가 결과를 반환하는데 5,000밀리초가 넘긴다면 테스트는 실패한다.

  • Exception 테스트
@Test(expected=RuntimeException.class)

해당 클래스는 RuntimeException이 발생해야 한다. 만약 테스트에서 RuntimeException이 발생하지 않을 경우 실패한다.

  • 테스트 건너뛰기
@Test(timeout=5000)
@Ignore(value=”여기는 테스트 안할거야”)

@Ignore 어노테이션을 추가하면 해당 메서드는 테스트를 건너뛰게 되며 JUnit4는 성공 및 실패 개수와 함께 건너뛴 테스트 수도 포한된 결과 통계를 제공한다.