사용자 도구

사이트 도구


web:신규서비스

목차

신규 Web 서비스시 고려해 볼 사항

그 동안 웹서비스를 개발해오면서 많은 실수들을 했고 그럴 때마다 다시 처음 부터 시작한다면 이렇게 해보는게 좋을 것 같아…라고 생각 했던 것을 정리해 본다. Back-End 위주의 정리이다.

나의 생각은 경험이 증가하고 기술 환경이 변화함에 따라 함께 계속 변화한다.

프로그래밍 언어의 선택

  • 정적(static) 언어 혹은 타입 힌팅(type hinting)을 지원하는 언어를 선호한다.
    • IDE를 통한 Refactoring과 편리한 소스 탐색
    • Static Analysis 의 도움을 통한 버그 줄이기 가능
  • 동적(dynamic) 언어는 언어 문법의 간결성, (비 Java 계통의 경우 재시작이 필요없는 방식으로 인한) 높은 개발 생산성 덕분에 초기 개발 속도가 빠르지만 정말로 서비스가 성공해서 개발자 수가 증가하고 변화 대응을 위한 리팩토링을 할 때는 발목을 잡는 경향이 보인다. 즉, 초반은 개발을 빠르게 할 수 있지만 중 후반에서는 정적 언어보다 개발 속도가 떨어질 수 있고 버그의 가능성도 더 높아질 수 있다.
  • 최근에 나오는 정적 언어들도 기존 동적 언어들에 필적하거나 그를 능가하는 문법적 간결함을 이루어내었기 때문에 더 이상 이 부분은 동적 언어가 우위에 있다고 보기 힘들다.
  • 최근 일부 동적 언어들이 타입 힌팅(static type hinting) 기법 도입 혹은 정적 언어 transpiler 등으로 정적인 방식의 개발이 가능해 진 것도 있다. Python 3.x Type hinting TypeScript, Groovy 의 경우 선택형 정적 언어
  • Microservices 로 개발한다면 API 서버(실질적인 비즈니스 처리 담당)는 정적 언어로, Web View 컨트롤러 처리 부분은 가급적 정적 언어를 선호하긴 하겠지만 필요하다면 동적 언어로 분담시키는 방식을 사용.
  • 과도하게 개발자 풀이 적은/새로운 언어/프레임워크를 사용해서는 안 된다. 서비스 초창기에는 괜찮지만 서비스가 커질 경우 해당 언어나 프레임워크에 익숙한 개발자를 구하기 어려우며, 그로 인해 신규 유입된 개발자들이 언어/프레임워크에 대한 이해 부족으로 코딩을 잘못하게 되고 이는 개발자들의 자질/언어의 좋고 나쁨과 상관없이 해당 언어가 나쁜 언어로 매도되고, 결국 그들에게 익숙한 언어로 다시 작성하느라 서비스 발전의 발목을 잡는 요인이 된다.
  • 따라서 개발자 시장의 현실을 무시하지 말 것이며, 꼭 개발자 풀이 적은 언어를 사용해야 할 경우에는 회사 내에서 해당 언어의 교육과 올바른 언어 사용 컨벤션 정립 등에 대해서도 많은 신경을 써야 하며(이 경우 Pair 프로그래밍이 좋은 방법 같다. 하지만 폭발적으로 개발자가 증가할 경우 잘 안 통할 듯 함) 채용시에도 채용 속도를 천천히 그리고 높은 채용기준을 가지고 하는 것이 좋을 것 같다.

신기술 선택

  • 다음과 같은 용어를 확인해볼것.
  • 단점, 주의할 점, 위험성, 실수, cons, pitfall, gotchas, warns, when not to use, should not use, avoid

용어 통일

  • 팀 내/외 소통시 통일된 용어는 매우 중요하다.
  • 잘못된 용어 사용으로 회의 결과가 전혀 다르게 이해되고 전혀 잘못된 결과물이 나올 수 있다.
  • 또한 서로 다른팀간의 연동을 할 때, 변수, 메소드, 클래스 이름등이 너무 많이 달라서 이들간의 연동을 할 때 혼란이 매우 커진다.
  • 프로젝트를 시작할 때 한국어 용어, 그에 대응하는 영어 단어(이를 사용해서 클래스,변수, 메소드 등을 정의) 그리고 그 정확한 의미를 모두 정리하는 문서화 작업을 하고 이를 전사적으로 공유한다.
  • 용어를 정의할 때 하나의 용어가 상황에 따라 여러가지 의미를 가지게 하면 안된다. 즉, 용어가 맥락까지 포함해야 한다.
    • 예) “배달비” : 맥락에 따라 고객에 가게 주인에게 배달비를 지급할 때도 있고, 가게 주인이 배달원에게 지급할 때도 있다. 따라서 두 상황에 맞는 별도의 용어를 정의해야 한다.
  • 약자 사용 금지 업계의 보편적인 약자라도 문제 직군에 따라 그 이해가 달라진다. 약자는 최대한 자제하고 사용하더라도 문서화 돼 있어야 한다.
  • 프로젝트 이름을 nickname 으로 만들지 말것. 프로젝트에 대해 직관적인 이름으로 짓지 않으면 다른 팀/신규 입사자와 소통할 때 매번 설명이 필요해진다.

영웅 의존 금지

  • 프로그래밍 언어 선택에 이어지는 것인데, 비즈니스의 핵심이 아닌 영역에 대해 영웅 한 두명에 의존하게 시스템을 구성하면 안된다.
  • 일군의 그룹이 모두 시스템을 이해하고 관리할 수 있어야 하면 그렇지 못한 솔루션은 아예 도입하지 않는 것이 낫다.
  • 특히 배포 시스템 처럼 비즈니스의 핵심은 아닌데 이게 없으면 비즈니스가 돌아갈 수 없는 요소들이 있다. 배포 시스템을 복잡하고 한 두명만 이해 할 수 있는 것으로 만들면 그 한 명이 아프거나 퇴사했을 때 모든 시스템이 배포가 불가능해지면서 비즈니스 전체가 멈추게 된다.

몽키 패치 금지

  • 동적 언어 혹은 AOP같은 유사기능 사용시 기존 기능을 런타임에 전혀 다르게 바꾸는 행동 하지 말 것. 온갖 알 수 없는 버그의 온상이 됨.
  • 또한 오픈소스 라이브러리/프레임워크/툴을 사용하다보면 부족한 기능이나 배그를 패치할 일이 생기는데 그 때 회사나 팀에서만 패치 하지 말고, 공식적으로 오픈소스에 코드를 반영시키는 것이 좋다. 나중에 해당 코드가 업그레이드 될 때 팀에서 변경한 사항이 반영이 안 된 상태라서 보안이나 기능에 구멍이 되어 버린다.

프로젝트 구성과 Microservices Architecture

  • Microservices Architecture(MSA) 로 프로젝트를 시작할 필요도 없고 스타트업의 경우 MSA 적용은 오히려 개발 생산성을 떨어뜨리게 된다. 하지만 그로부터 배운바를 적용해서 프로젝트를 구성하는 것이 좋다.
  • 프로젝트의 구성 - 단일 프로젝트 멀티 모듈. ecommerce 프로젝트가 있다고 할 때, 프로젝트를 업무 도메인 단위 모듈로 만든다. 모듈만 분화 됐을 뿐 API 호출이 아닌 실제 코드를 직접 호출하는 방식의 Monolithic Architecture로 구성한다.
    • ecommerce-project : 이커머스 프로젝트 - 크게 비즈니스, 유틸리티, 사용자 접점 세가지 종류의 모듈로 구성한다.
      • product : 상품 도메인 비즈니스 모듈
      • order : 주문 도메인 비즈니스 모듈
      • delivery : 배송 도메인 비즈니스 모듈
      • account : 사용자 계정 도메인 비즈니스 모듈
      • admin-account : 관리자(내부 사용자) 계정 도메인 비즈니스 모듈
      • email-sender : 메일 발송 모듈. 비즈니스는 아니지만 별도 분할해 두어 추후 기능 확장에 대비
      • user-web : 일반 사용자가 사용하는 웹 서비스 모듈. 위에 있는 도메인 비즈니스/유틸리티 모듈들에 의존한다.
      • user-app-api : 일반 사용자 App 에서 호출하는 API 모듈. 위에 있는 도메인 비즈니스/유틸리티 모듈들에 의존한다.
      • admin-web : 관리자 웹 서비스 모듈. 위에 있는 도메인 비즈니스/유틸리티 모듈들에 의존한다.
  • 절대 하지 말아야 할 일 : ecommerce-core 혹은 ecommerce-common 형태의 여러 도메인 비즈니스 로직을 모아둔 공통 모듈을 만들면 절대로 안 된다.
  • 다만, 비즈니스 로직이 절대로 존재하지 않는 공통 유틸리티성 모듈은 어느 정도 허용 가능하다. 하지만 이 모듈이 불필요하게 거대 의존성을 갖지 않도록 잘게 쪼개도록 한다.
  • 각각의 도메인 로직은 interface 와 구현체로 만들고 도메인 로직을 호출하는 측에서는 항상 interface 기반으로 소통한다.
  • interface 기반 도메인 로직 호출은 추후 아키텍쳐 변화나 MSA로 전환시에 그 구현체만 갈아끼우는 최소한의 변경으로 확장을 가능하게 해준다.
  • Microservices Architecture 로 가는 시점
    • 꼭 갈 필요가 없는데 MSA로 가는 것은 지양한다.
    • MSA로 가야하는 상황이 되면 위의 각 비즈니스 도메인 모듈을 하나씩 떼어 신규 프로젝트로 구성하고 Interface 구현체를 API를 호출하게 변경한다.
    • MSA 전환은 정말로 서비스가 크게 성공하고 개발자의 증가, 프로젝트 규모의 증가로 더이상 Monolithic으로는 관리가 불가능해지는 시점에 간다.
    • 기본적으로 모듈화가 잘 돼 있다면 모듈 분화가 안 돼 있을 때보다 개발자간 Ownership 논쟁이 다소 적은 편이겠지만, 개발자수가 증가하면 회의 때마다 ownership 논쟁으로 어느팀이 어디까지 해야하는지 모호해지는 시점이 온다. 이 때부터 MSA에 대한 고민이 필요해보인다.
    • 개발자간 의사 소통이 안되고 소스 코드 버전관리 시스템(VCS)에서 충돌이 너무 자주 일어나고 이는 버그로 이어지며 그에 대한 해결이 매우 어렵다면, MSA를 고려해본다.
    • 프로젝트를 실서버와 개발자 PC에서 띄우는데 너무 오랜시간이 걸려(JVM 기반의 경우) 개발 생산성이 급격히 낮아지고, 수정해야 할 코드를 찾는 시간, 배포 시간 등이 지나치게 오래 걸리는 시점이 오게 된다. 이 때는 MSA로 분할하는것이 Monolithic 보다 개발 생산성이 더 높아지게 된다.
    • MSA 로 갈 때는 팀간에 공통 라이브러리를 만들기 보다는 공통의 규약을 정립하는 것이 나아 보인다. 프로젝트간 의존성이 묶이는 것은 피해야 각 팀별로 다양한 언어, 기타 라이브러리 의존성 업그레이드 등이 가능해 진다.

Team 이 아닌 Project 단위 구성

  • APM, Wiki 공간, 각종 로깅 시스템 구분, Resource Tagging 등을 Team 기준으로 하지 말고 Project 기준으로 해야한다.
  • Team 은 지속적으로 바뀌지만 Project 는 프로젝트 자체가 폐기 될 때까지 잘 바뀌지 않는다.
  • 가급적 애플리케이션 관련 모든 정보 조직과 행위의 기준을 Project 를 단위로 가져가고 Project 에 하위 정보로 Team 을 넣는 방식을 취한다.
  • 이렇게 하지 않고 팀을 기준으로 하면 얼마 안가 고아가 된 정보들이 양산되게 된다.

API Gateway

  • 내부 애플리케이션 API를 외부(브라우저, 앱)로 노출시키는 역할에 API Gateway Framework를 사용하지 말라.
    • 이는 치명적인 보안 위협이 된다. API Gateway Pattern에는 API Gateway가 없다 - YouTube
    • 특히 인증만 하고, 권한 검사를 제대로 안하고 노출시키는 일이 빈번해서, 인증 받은 특정 사용자가 API Gateway 로 열려있는 내부 API에서 본인이 아닌 다른 사용자의 정보를 읽을 수 있게 된다.
    • Netflix 의 Zuul 을 보면, 내부 API를 노출시키는 용도가 아니라, 외부 전용 API 서비스인데, 인증 방식이 다른 것들을 API Gateway 로 노출하가게 구성하는 식으로 만든 것이다.
  • 즉, 외부 노출용 API는 그 안에서 사용자를 인증하고, 다른 API를 호출하고, 권한을 검사하는 로직을 다 직접 작성하는 것이 낫다.
  • 이게 싫다고 내부용 API 에 권한 로직을 넣는 것도 하지 말라.
    • 권한은 외부 노출시에 작동하는 것이다. 외부 노출은 보통 “일반 사용자”, “내부 관리자”, “외주 직원”, “입점 업체 운영자” 등 그 종류가 매우 다양하다.
    • 이러한 권한을 내부 MSA 서비스 API에 전가하기 시작하면 MSA 서비스 갯수 * 권한 갯수의 권한 체크 로직이 분포하게 된다.
    • 게다가 그 와중에 또다른 권한 군이 들어오게 되면 모든 MSA 서비스가 그것들을 모두 신경써가면서 개발해야하게 되어 개발 속도를 떨어뜨리고 보안 위협이 시작되게 된다.
  • 외부 노출 API가 인증과 권한을 전담해야 한다.
  • 하나의 API 애플리케이션은 하나의 인증만 처리한다. 인증이 여러 종류가 섞이는 순간 혼동으로 인해 보안 사고가 발생할 확률이 높아진다. 특히, Front 서버에 내부용 API를 넣는 행동은 절대로 해서는 안된다.

약한 결합도 높은 응집도의 Inteface 기반 개발

  • 각각의 모듈간의 호출은 interface 기반으로 약한 결합도 높은 응집도를 유지해야 한다.
  • 여기서 interface 기반이란 단순히 Java의 interface/class 분리를 뜻하는 것이 아니라(그렇게 하는 것도 매우 좋음), 구현의 구체적 정보가 담기지 않은 설계를 의미한다.
  • 안 좋은 예 : EmailService.sendEmail(String smtpServer, String userName, String password, String subject, String contents, String from, String to)
    • 메소드에 들어간 smtpServer,userName,password
    • Email 발송에 필요한 설정 정보가 메소드 호출이 일어나는 모든 위치마다 여러 군데 퍼지게 되어 응집도가 낮은 안 좋은 설계이며
    • EmailService 의 구체적인 구현이 SMTP 서버를 통함을 밖으로 드러내어 호출자와 구현체간의 결합도를 높이게 된다.
  • 나은 예 : EmailService.sendEmail(String subject, String contents, String from, String to)
    • 호출자 입장에서 봤을 때 Email 발송에 필요한 핵심 정보만 있고 구현에 대한 정보는 없는 인터페이스로
    • 결합도가 낮아서 Email 발송방식이 추후에 API 호출 혹은 MQ 기반으로 변경되더라도 호출자측에서는 아무 변경이 일어나지 않으며
    • 응집도가 높아서 처음에는 SMTP 서버 정보가 EmailService 한 곳으로 몰리게 되고, SMTP 서버 주소가 변경 혹은 아예 MQ/API 기반으로 변경하더라도 그 변경이 EmailService 한 곳에서만 일어나게 된다.
  • 이러한 Interface 기반의 약한 결합도 높은 응집도의 코딩은 추후 서비스의 규모가 커져서 구현 방식을 바꿀 때나 Microservices Architecture로 가야할 때 훨씬 그 변경을 용이하게 해준다.
  • SQL, HTML Template 같은 근본적으로 Logic 처리를 위한 것이 아니지만 일부 코딩 요소가 들어갈 수 있는 것들이 있는데, 여기에 Logic을 넣지말고 항상 프로그램 로직은 프로그래밍 언어에서 처리하도록 한다. 비 프로그래밍 언어 요소에 넣은 Logic은 가독성/유지보수성이 매우 떨어지며 변경 대응과 디버깅이 어렵다.

계층간 침범을 하지 말것

하위 계층(Layer)에서 상위 계층을 사용하지 말라. 대략 다음과 같은 레이어가 있다고 할 때(위에 있을 수록 상위 레이어) 상위 레이어는 하위 레이어를 사용할 수 있지만 하위 레이어는 상위 레이어의 존재를 몰라야 한다.

  • Web Layer(Spring의 경우 Controller, Filter, Interceptor, ..)
  • Service
  • Repository
  • Domain Object

항상 의존성은 위에서 아래로 흘러야 한다.

Web Context 를 비즈니스까지 끌고가지 말 것

  • 모듈화가 잘 안된 프로젝트에서 저지르는 흔한 실수 중의 하나가 로그인 사용자 객체를 자동으로 도메인 객체에 넣어주고 싶다던지의 이유로 Web Layer의 객체를 Domain Object에 넣는 경우가 있는데 이렇게 하면 Batch 등 전혀 다른 목적에서 Domain Object를 사용할 때 엉뚱한 값이 주입되는 등 심각한 부작용이 발생한다.
  • UI단의 컨텍스트를 Method Parameter를 통하지 않고서 비즈니스 코드에서 직접 사용하는 일이 없게 해야한다. (보통은 이럴 때 Thread Local을 사용함)
  • JPA Auditing 같은 것.
  • 단, 이런 상황이더라도, 시작점과 사용지점이 같은 레이어라면 상관 없다. 예) Servlet Filter 에서 뭔가 설정한 것을 도메인 코드에서는 사용하지 않고 Controller 에서만 사용하는 경우(JSON 응답 형태를 변경한다던가).

웹 서비스 모듈의 분화

  • Web Service 모듈의 경우 각 기능별로 Path 지정에 주의한다.
  • 초기 웹 서비스는 하나의 모듈로 만들어지겠지만 기능이 증가하면서 단일 웹 서비스 모듈로는 버티기가 힘들어 결국 이것도 쪼개게 된다. 이때 기능별로 Path를 잘 만들어 두면, 각 path 단위로 모듈을 분할할 수 있게 된다.
  • 예: user-web 모듈의 기능별 URL Path 를 다음과 같이 만들면
    • /accounts/* : 계정 로그인, 계정 관리
    • /products/* : 상품 정보 열람
    • /orders/* : 주문 처리
  • 위의 각 Path 별로 URL을 잘 분할해 두면 URL 변경 없이 user-account-web/accounts 담당, user-products-web/products 담당 , .. 등의 형태로 모듈 분할이 어느정도 쉽게 가능해지게 된다.
  • 서로 다른 권한을 가진 사이트를 하나의 모듈로 만드는 것 금지 이는 프로젝트 복잡도를 높이고 자칫 잘 못 작성된 코드로 인해 서로 다른 권한 체계를 넘나들 수 있다. 예) 사내직원 사이트와 외부 고객 사이트를 하나의 모듈로 만들고 도메인 네임만 다르게 해서 처리하는 등의 행위 금지! → 보안 부분 확인

SSL(https)과 Domain

  • 상용 서비스는 사용자 인증 등으로 인해서 결국에는 SSL을 붙여야 한다. 다중 도메인으로 갈 경우 인증서 비용이 비싸지고 유지 보수성이 떨어지므로 단일 도메인을 유지할 수 있게 전략을 짜는 것이 좋다.
  • 기왕이면 그냥 전체 서비스를 SSL로 해버린다. 이렇게 해야 이미지나 CSS, Javascript 등이 HTTP와 HTTPS를 넘나들 때 잘못 적용되는 실수를 막을 수 있다.
    • 단, 이 경우 이미지 중심의 서비스의 경우 CDN 비용이 증가할 수도 있다.
  • 한 서비스에 고객에게 노출되는 여러개의 프로젝트를 띄울 경우 컨텍스트에 따라 도메인이 아닌 URL을 변경하는 것이 좋다. 다중 도메인 SSL 인증서는 비용적으로도 불리할 뿐만 아니라 관리도 힘들어 질 수 있다.
    • Why use www : 애플리케이션은 www 하나로 통일하고, 정적 이미지 등 전혀 다른 것들은 다른 서브 도메인으로 서빙하면 된다.
  • 도메인을 하나로 가져가면 각 프로젝트간의 Ajax 호출 시 권한 문제가 전혀 발생하지 않게 된다.
  • 외부 연동시(특히 결제?) 도메인 정보를 서로 고정하는 경우가 있는데, 이럴 때도 도메인을 단일로 유지해야 확장성에 대비가 된다.
  • 개발용 도메인을 example.dev, 통합 테스트 도메인 example.test 처럼 전혀 다른 최상위 도메인으로 끝나게 하면 실서비스와 쿠키 정보가 섞이지 않아 편해질 수 있다. 또한 실서비스에서 테스트인 줄 착각하는 일도 줄어든다.
  • 내부망용 도메인 네임 규칙과 외부망용 도메인 네임 규칙을 명확히 가져가야 보안 이슈 발생시 대응이 쉽다.
  • domain name 을 모두 취합하고, 갱신 일자를 정리하고 관리해야한다. 많은 회사들이 회사 핵심 domain name 갱신일을 놓쳐서 서비스가 중단되는 사태를 겪는다.

안정성

  • SPoF(Single Point of failure)를 제거하라.
  • 어떠한 서버도 한 대만 두지 않고 항상 최소 2대로 한 쪽이 무너져도 곧바로 복구 될 수 있게 구성해야 한다.
  • 이는 개발환경에서도 마찬가지이다. 배포, 소스 저장소 등도 모두 이중화 돼 있어야 한다.

확장성 고려

  • 선형 증가 데이터와 지수적 증가 데이터를 잘 구분하여 애초에 저장소를 분리한다.
    • 선형 증가 데이터 : 상품, 사용자 등
    • 지수적 증가 데이터 : 주문, 배송 등 상품에 대해 사용자가 행위하는 것
    • 선형 증가 데이터는 RDBMS(혹은 상황에 따라 다른 적합한 것), 지수적 증가 데이터베이스는 RDBMS sharding 혹은 NoSQL을 고민한다.
    • 초반부터 지나친 확장성 대비는 오히려 악영향을 줄 수 있는 것은 사실이지만,
    • 현재(2013) 상황으로 봤을 때 처음부터 NoSQL을 도입하는 것이 RDBMS를 도입하는 것과 비교해서 특별히 더 어렵지도 않게 되었다.
    • RDBMS 샤딩은 어려운 것은 사실이지만 NoSQL 도입은 초반 부터 고려해도 괜찮아 보인다. 혹은 샤딩을 안하더라도 지수적 증가 데이터의 PK 혹은 Unique Key 설계시 샤딩 가능하게 설계하는 것도 좋아보인다.
  • RDBMS와 유사하게 MQ, Cache 등도 도메인별로 분할하라. 처음부터 분할하면 서버 대수가 너무 많이 늘어나게 되는데, 그 경우에는 최소한 Major 도메인에 대해서만이라도 분할하거나 혹은 물리 장비는 하나로 묶더라도 논리적으로는 분할해 두어 추후 Scale Up이 필요할 때 물리적 분할로 인한 코드 변경이 필요없게 해주는게 좋다.
  • Sharding은 꼭 필요한 경우에만 어쩔 수 없을 때 하고, 선형적 증가 데이터라면 되도록 기능별 DB 분할을 고려하는 것이 낫다.
  • 평소에는 잠잠하다가 사용자의 폭발적 업데이트가 발생할 가능성이 있는 것은(클릭 횟수 카운트 같은 종류)는 Redis, memcached, CouchBase 등을 고려한다.

Primary Key는 처음부터 Long으로

  • 서비스 초창기에는 21억(Integer Max 값 근사치)에 도달하는게 불가능해 보이지만 성장기에 들어서면 21억 정도는 금방이다. 이 때 int → long 변환에는 엄청난 비용이 들며, 안정성 훼손의 요인이 된다.
  • 특히, 컨트롤이 쉽지 않은 외부 업체와의 연동에 PK 가 사용될 때는 int → long 변환시 외부 업체쪽이 처리를 하지 못해서 심각한 장애를 일으킬 수 있다.
  • 따라서 기왕이면 처음부터 주요 PK Sequence의 시작값을 절대로 Integer로는 표현할 수 없는 최소 100억 혹은 1조, 1경 같은 값으로 해서 외부 연계 시스템 등이 연동 PK를 Integer로 설계 하는 실수를 애초부터 불가능하게 하는 것이 좋아보인다.
  • 숫자 Sequence(값 증가형) PK 중에는 그 자체가 외부 노출이 되면 안되는 경우가 있다.
    • 예를들면 가입자 정보의 PK 혹은 주문정보의 PK 같은 것인데, 이것이 노출되면 가입자 수나, 현재 주문 갯수 등을 외부에 노출시키는 것이 된다.
    • 이럴 때는 PK 값을 일정 규칙에 따라 짧은 문자열로 바꿔주는 약한 변환을 해주거나,
    • 아예 PK와 함께 UUID 같은 완전 특수한 새로운 값을 Unique Index를 걸어서 생성해서 이를 Natual Key 형태로 사용하면 보안성을 강화할 수 있다.
  • 이는 반대로 말하면 다른 시스템으로부터 PK로 숫자로 받는다면, 무조건 Long 으로 간주하는게 좋다.

운영체제 OS

  • 시간으로 인한 문제가 심각하게 발생한다. 철저하게 대비하도록 한다.
    • Timezone 설정을 확인하고 통일한다.
    • 시간 동기화(ntp)를 철저하게 수행한다.
    • Java Web Server의 경우에는 JVM Heap 영역이 Swap 이 발생하지 않도록 Linux Performance 참조하여 swappiness=0 혹은 1 정도로 설정한다.

Database / 저장소

  • RDBMS 설계시에도 비록 처음 서비스시에는 물리적으로 하나의 시스템에 올린다 하더라도 그 의미가 분할되는 테이블은 서로 다른 database에 두고 서로간의 join이 불가능한 구조로 구성한다. 그래야 나중에 서비스가 폭발적으로 성장했을 때 기능별 DB 분할이 쉬워진다. → 서로 다른 도메인 영역 Table간에는 비록 하나의 물리 DB에 있더라도 Join하지 말라. → 이는 프로젝트 모듈 구조와 비슷하게 만든다고 보면 된다.
  • SQL에서 데이터를 생성하지 말라. 예를들어 now(), password() 같은 것 사용금지. 데이터 생성/변형은 애플리케이션에서 일관되게 처리한다. 그렇지 않으면 추후 확장시 문제요소가 될 수 있다.
  • Production 서버 애플리케이션에서 사용할 계정은 최소 권한만으로 생성한다. 안그러면 개발자 실수로 drop table, 혹은 hibernate hbm2ddl.auto 옵션에 의해 DB를 날릴 수 있다.
    • DDL 권한은 없어야 한다.
    • DML(update,delete)도 where 조건이 없으면 실행 불가로 하는 것이 좋다.
  • 특별한 이유가 없다면 무조건 ORM 혹은 이에 준하는 솔루션을 사용하라. DB 쿼리 튜닝보다는 ORM으로 객체지향적이고 유지보수성 높은 코드를 짜고서, Cache 등으로 거시적 튜닝을 하는 것이 좋다.
  • 저장소에 Script Code 성 데이터(Stored Procedure나 이에 준하는 것들)를 넣지 않는다.
    • 데이터와 코드의 경계가 불분명해지고, 코드의 커버리지, 히스토리(VCS) 관리, 리팩토링 등이 불가능해지고,
    • 배포 단계(develop, integration, production ..)에 따라 서로 다른 코드가 주입되기 때문에 올바른 테스트가 어려워 진다.
    • 또한 코드가 데이터 저장소에 들어가면서 production에서 실 데이터로 일부 서버만 적용하여 테스트해보거나 하는 분할 배포를 할 수가 없게 되어 production 과 동일 데이터를 사용하는 staging 환경 테스트가 불가능해진다.
  • SQL에 로직 넣지 말자. SQL에서 충분히 가능한 간단한 연산도 애플리케이션 코드에서 처리해서 최종 결과를 적절한 이름의 변수에 설정해서 넘겨줘야 코드 유지보수성이 높아진다.
  • 자연키(Natural Key)를 주키(Primary Key)로 사용해서는 안 된다. 가급적 인공키(Surrogate Key)를 PK로 사용한다.
    • 자연키는 그 “자연적 속성상 변화에 취약하다”. PK는 변하면 안된다. 자연키가 변하는 예로..
    • 주민등록번호는 중복이 없을 것 같지만 동일 주민등록번호를 가진 사람이 존재할 수 있고, 중간에 법규가 바뀌어서 DB에 주민등록번호를 저장할 수 없게 되었다.
    • 도서 ISBN, 우편번호(6자리 → 5자리) 등도 바뀌게 되었다.
  • 비록 인공키를 PK로 사용하더라도 자연키를 Unique로 지정하고, 해당 자연키를 조회의 기본요소로 사용해도 된다. 예를들면 사용자정보가 있을 때 숫자가 PK라 하더라도 실제 조회조건으로는 email 같은 중복 불가능한 Unique Key를 사용할 수 있다. 특히 개발환경과 운영환경이 공유하는 데이터의 경우 인공키를 조회 조건키로 사용하면 insert 순서에 따라 서로 PK 값이 달라져서 불편하다. 이 때는 해당 row를 나타내는 일종의 자연키(게시판을 예로들면 게시물 제목)를 만들어 조회 키로 사용하면 개발과 운영에서 동일 조회키를 사용할 수 있게 된다.
  • 돈/가격 정보는 가급적 number(BigDecimal) 로 못해도 long으로 일괄 적용한다. integer 로 할 경우 가격 overflow 에 시달리거나, 나눗셈 등의 연산에서 취약해질 수 있다. DECIMAL(19,2) 추천.
  • 항상 올바른 타입을 사용하려고 노력한다. 날짜는 날짜 타입, boolean, 숫자 등 항상 적합한 타입을 사용해야 쓰레기 데이터를 막을 수 있다.
  • PK가 아닌 숫자값과 boolean 타입은 모두 NOT NULL로 설계한다. 그렇지 않으면 숫자 연산에 대해 0인 상태와 NULL인 상태 모두에 대해 항상 조건을 걸거나 NULL → 0 변경을 수행해야만 하게 된다. 또한 boolean도 상태가 true/false/null 세가지 상태가 되어 버린다.
    • 만약에 숫자값의 null이 의미가 있다면 정확하게 주석을 단다.
  • 모든 테이블에는 insert 시간과 modify 시간을 모두 기록한다(createdAt, modifiedAt).
  • 중요 테이블의 경우 최종 수정된 내용만 가지고 있고 그에 대해 제약 조건을 모두 지키도록 설계한다. 다만, 변경시마다 중요 변경사항을 history 테이블을 따로 두어 남기도록 한다. 그래야 서비스 유지보수시 알 수 없는 오류에 대한 참고 데이터로 삼아 고객의 요구에 대해 올바로 대응 할 수 있다. history는 RDBMS 가 아니라 NoSQL로 남기는 것도 좋다.
  • 컬럼의 의미를 바꾸지 말 것. DB의 역사가 오래될 수록 컬럼의 의미를 중간에 바꾸는 경우가 있는데, 컬럼의 의미를 변경하려면 기존 데이터를 모두 마이그레이션 하던지, 새로운 컬럼을 만들어서 새로운 의미를 부여하든지 한다. 기존 데이터를 그대로 남겨둔 상태로 컬럼의 의미를 바꾸면 매핑되는 객체 구조 설계에도 문제가 생기고 쿼리 결과를 처리하는 모든 구문에 상황에 따른 조건문이 계속 추가되어 개발 부담을 가중시키게 된다.
  • DB 자체의 타입 enum 을 사용하지 말라. 또한 프로그래밍 언어 enum 을 사용할 때 절대로 순서 숫자값(ordinal)로 저장하면 안된다. 이 둘은 모두 enum 항목들의 순서 변경이 발생하는 순간 엄청난 마이그레이션을 수행해야한다. 그에 비해 성능 향상은 그리 크지 않아보인다. MySQL의 enum은 겉보기와는 달리 ordinal 로 작동하기 때문에 enum 의 순서가 변경되면 단순 alter table로 문제가 해결되지 않고 기존 데이터를 모두 마이그레이션 해야 한다. MySQL 의 enum 은 ordinal 로 저장은 하지만 alter table 시 순서 변경을 자동 보정해주긴한다.
  • 과도한 동적 쿼리를 생성하지 말라. - 특히 iBatis/MyBatis/문자열 기반으로 쿼리 짤 때 null 을 체크해서 조건문을 넣었다 빼었다 하는 경우
    • 상태에 따라 WHERE 절이 나타났다 사라졌다하는 식의 동적 쿼리는 자칫 실수하면 모든 조건이 없어져서 엄청난 부하를 일으키는 SELECT 쿼리나 모든 데이터를 삭제하는 DELETE 문이 될 수 있다.
    • 상태에 따라 WHERE 절이 변하는 동적 쿼리는 되도록 피한다.
    • 필요한 쿼리는 항상 상황별로 다 따로 만든다.
  • Backup/Fail Over 전략을 수립해둔다. : DB 생성 초기 부터 백업/복구/FailOver 전략을 각 DB에 맞게 수립해둬야 한다.
  • 페이징으로 데이터를 불한/연속 조회 할 경우에는 항상 더보기 방식(ID 기반 페이징)을 사용한다. API 설계 부분 참조.
    • offset/limit 조회는 offset이 뒤로 갈수록 성능이 계단형으로 느려지게 된다. offset을 계산하려면 그보다 앞에 있는 데이터들의 조건을 모두 검사해야하기 때문이다.
    • 또한 offset/limit페이징 중에 데이터가 추가 될 경우 그 다음페이지에서 앞에 나왔던 데이터가 다시 나와 중복처리되는 문제가 발생할 수 있다. UI 에서는 큰 문제가 안되지만 batch 나 기타 내부 처리용 데이터 조회시에는 심각한 문제가 될 수 있다.
    • 처리처럼 데이터를 분할해서 가져갈 경우 항상 order by PK asc를 해줘서 페이징의 순서를 명확히 해줘야한다.
    • 대체로 PK asc를 하는게 DB에서 가장 성능이 좋다. MySQL 8.0 descending indexes can speedup your queryds
    • offset/limit 방식(페이징)이 아니라 nextPk,limit을 조회 조건으로 거는 더보기 방식을 사용해야 성능저하 없이 균일한 응답성을 보장받을 수 있다.
  • 사용자 ID 같은 경우에는 소문자로통일해서 받는것이 좋다. 대문자로 입력해도 소문자로 변환한다. 사용자는 자신이 대소문자를 어떻게 입력했는지 혼동하는 경우가 많다. 단 email 같은 외부 값을 받는 경우는 있는 그대로 넣어야 한다.
  • DB Connection Pool 의 connectionTimeout 값(커넥션풀에서 커넥션을 가져오는데 걸리는 최대시간)을 2~3초 정도로 짧게 가져간다. 또한 JDBC 사용시, connectionTimeout(실제 DB에 접속하는데 걸리는 시간)도 1초 이내로 짧게 가져가야한다. 그래야 DB 서버 Fail Over 에 빠르게 반응하게 된다. → 이 부분은 부하가 증가할 경우 오히려 문제가 될 수도 있다. TODO 좀 더 확인
  • Write 트랜잭션(Transaction)은 최소로 유지해야한다. 만약 write 트랜잭션 내에서 다른 데이터의 과도한 조회 등이 발생한다면 write 를 위한 선행 데이터 조회와 write 그 자체의 트랜잭션을 따로 분할해서 DB Lock 을 줄여야 한다.
  • 위의 상황과 유사하지만 비록 순수 Read라 하더라도 트랜잭션을 짧게 가져가야 한다. Long Transaction을 일으키는 큰 문제중의 하나가 DB 트랜잭션 내부에서 다른 API 호출하는 것이다. 외부 API 호출등 DB와 무관하고 긴 시간이 걸리는 행위는 트랜잭션 안에서 하지 말아야한다. API 호출 지연이 발생하면 실제로 DB 작업을 하지 않아도 Connection Pool 이 고갈돼 버려서 API를 호출하지 않는 다른 DB 호출까지 모두 연쇄 지연되는 현상이 발생한다.
  • MySQL 은 항상 증가하는 값을 PK로 사용해야 한다(BIGINT AUTOINCREMENT). Insert 시에 PK를 기준으로 지속적으로 물리적 정렬를 하기 때문에 정렬되지 않은 PK insert 가 발생하면 성능이 저하된다. 만약에 비즈니스키가 증가하는 값이 아니라면 항상 인공키로 BIGINT AUTOINCREMENT로 잡아준다.
  • DB 에 직접 실행하는 update/delete 쿼리는 항상 PK기반으로 한다.
    • PK가 아닌 조건을 기반으로 할 경우 실수할 경우 그 충격이 너무크고(where 조건 하나 잘못짜면 전체 데이터를 모두 삭제할 수도 있음)
    • PK 기반으로 해야 실수 했을 때 어느 PK의 데이터를 되돌려야할지 확인이 쉽고,
    • 처음에 쿼리를 짜던 시점과 실제 실행 시점 사이에 뭔가 변경된 데이터가 있을 때 그걸 알아채기도 쉬움.
    • 또한 롤백시 롤백이 매우 오래걸릴 수도 있다. 한 방쿼리는 지양한다.
  • 여러건 조회시 항상 정렬 조건(order by)를 지정해야한다. 정렬을 넣지 않고 어도 PK로 정령돼 있는 경우들이 있는데 이걸 믿고 있다가 나중에 순서가 예상과 다르게 나와서 장애가 나는 경우가 있다(특히 non-clustered index 조회일경우). 가급적 PK기준으로 지정한다.
    • 유사하게 Stream의 findAny() 같은것도 유사한 문제를 일으킨다.
  • 기본적으로 Slow Query 모니터링을 걸고 알람을 한다. slow query 시간에 따라 알람 레벨을 주는 것도 좋을 것 같다.
  • in 조건 등의 쿼리 생성시 항상 in 안에 들어오는 값의 갯수가 제한될 수 있도록 한다.
    • 이는 bulk insert/update 에서도 마찬가지이다.
    • SQL 문자열의 길이에 제한이 있다는 사실을 잊지말고, SQL 길이가 무제한 커질 수 있는 쿼리가 절대 만들어질수 없게 한다.
    • in의 경우 Guava Lists.partition 으로 나눠서 조회해서 합치는 방식등을 사용한다.
    • 한방 in 절은 중복을 제거하고 결과를 반환하지만 in의 키를 분리조회 할 경우에는 중복 조회가 발생하지 않게 조심해야 한다. 미리 중복을 제거하고 조회한다.
    • 정렬이 필요한 경우가 있다면 이 경우에는 할 수 없이 애플리케이션 코드에서 정렬한다.
  • 즉 모든 데이터 조회는 상방이 무제한 커질 수 없게 제한을 걸어야한다. 그래야 서비스의 증가에 따라 장애가 나는 일이 없어진다.
  • Soft Delete(삭제 여부를 나타내는 컬럼을 두고 마킹만 하기) / Hard Delete (실제로 삭제)
    • Hard Delete 선호한다.
    • unique index 등을 걸기도 편하다.
    • 관건은 삭제된 데이터 히스토리를 남기는 것인데, Hard Delete 를 하고 history table 을 두는게 차라리 나아 보인다. Hibernate Envers 참조.
    • Soft Delete 는 삭제 여부 컬럼을 true/false 로 둘 수 없고 PK 와 동일 값으로 지정되면 삭제된걸리 간주하는 식으로 해서 unique index 를 걸수도 있긴하지만 너무 복잡하고 개발자들이 자주 실수한다.

사용자에게 전달하는 메시징 솔루션

  • Email, SMS, Mobile Push 등의 Messaging은 초반에는 적을 수 있으나 서비스의 증가에 따라 시스템의 부하 요소가 될 수 있다.
  • 초반에는 메시징 인터페이스를 잘 구축해 해당 인터페이스만 사용하도록 하고, 사용자 증가 등이 일어나서 웹 서버만으로는 버티기 힘들어질 때 메시징 서버를 따로 두고 기존 인터페이스는 메시징 서버를 통해 메시지를 보내도록 변경할 수 있는 준비를 해 둔다.
  • 중요한 것은 모든 메시징은 각 종류별(Email, SMS, ..)로 하나의 인터페이스만으로 구체적 구현에 관한 의존없이 소통하게 만드는 것.
  • 메시징은 보통 즉시성보다는 전송 보장성이 더 중요한 경우가 많다. 별도 서버로 구축할 때 MQ 등의 도입을 고려하는 것이 좋다.
  • MQ의 경우 전송할 메시지의 용량을 최소화 할 수 있도록 한다. 아무리 비동기로 실제 업무를 처리한다해도 MQ 메시지 전송 자체는 동기이다.
  • 다중 서버가 되면 Session 관리가 문제가 된다.
  • Session에 대한 고려가 제대로 안되면 세션에 일시적으로 저장하는 데이터를 쿠키에 다 넣다 보니 쿠키가 지저분해지고 HTTP 헤더 크기가 폭증하는 문제가 발생할 수 있다.
  • 쿠키에 안 넣더라도 불필요하게 DB를 사용하게 하여 DB에 큰 부담을 지우고 불필요해진 데이터까지 중요한 데이터베이스 공간을 차지하는 경우가 발생할 수 있다.
  • Sticky Session은 100% 신뢰하기 힘들다는게 경험적 법칙.
  • Java 서비스라면 Spring Session 프로젝트 - Spring 4.x 이상, memcached-session-manager 같은 것을 고려해 볼만 하다.
  • Cookie 생성을 중앙 관리한다. 개발자가 늘어나게 되면서 Cookie에 대해 서로 의사소통없이 마구 만들게 되다보면 Cookie 폭증으로 인해 사고가 발생한다. Cookie 에 관한 중앙 집중 관리가 되지 않으면 서비스의 크기가 커지면서 장애로 이어질 수 있다.
    • 도메인 기반 Cookie는 가급적 최소화하고 쿠키의 필요에 따라 특정 Path 에 종속되도록 만드는 것이 좋다.

최적화

  • 거시적 최적화를 먼저한다. 미시적 최척화는 유지보수성을 해치지 않고 사용자의 요구 사항을 만족시키는 선까지만 한다.
  • 성능을 측정하지 않은 최적화는 최적화 한게 아니다. 실제로 최적화의 결과를 보면 성능이 떨어진 경우가 매우 많다. 잘못된 튜닝 포인트를 잡았거나, 설정상의 오류가 있어서, 혹은 캐시 등을 붙일 경우 실제 환경에서는 Cache hit 율이 너무 낮아서 최적화가 오히려 병목이 되기 도 한다.
  • 성능 측정시, 다양한 샘플에 대한 측정과 단일 샘플에 대한 측정을 모두 해본다.
    • 예를들어 게시판 글 읽기 테스트를 한다고 할 때,
    • 아주 많은 종류의 여러글을 동시에 읽을 때의 테스트와
    • 한 건의 글을 집중적으로 읽을 때를 테스트 해본다. - 해당 게시글을 가리키는 Row의 집중적인 lock으로 인해 이 경우 성능이 떨어질 수도 있다.
  • 성능 측정시 실 데이터 갯수 / 구조 기준으로 한다. 적은 데이터 셋으로 하는 성능 테스트는 무의미하다.
  • CSS는 위로 Javascript는 아래로, 개발시에는 코딩상태 그대로, 실서비스에서는 minify 상태로.
  • 라이브러리는 별도 관리하여 전반적인 라이브러리 업그레이드가 한 군데만 고치면 될 수 있는 구조로 만들 것.
  • 이미지를 많이 사용할 경우 이미지 최적화에 대해서도 고민해야함.
  • Web Load Balancer 의 경우 최초에 서버를 띄운 직후에 분산 weight를 낮게 줬다가 차츰 올리는 설정이 있는 것이 좋아보인다. 서버 띄운 직후에 초기화를 하면서 서버 부담이 지나치게 크게 올라가서 최초 접속자들에게 응답이 매우 늦을 가능성이 보이고 서버 불안이 야기된다.
  • 전체 갯수를 가지고 계산하는 페이징 UI는 말들지 말아야 한다. 결국엔 전체 갯수 계산으로 인해 서비스가 사용할 수 없을 정도로 느려진다. “이전”, “이후”, “첫페이지”, “마지막페이지” 로 가는 버튼만 만들고 전체 갯수는 “마지막페이지” 버튼을 눌렀을 때만 계산하고 다른때는 전혀 신경쓰지 않게 처리한다.
  • 화면에 보이는 것만 로딩하고 순차 로딩/페이징(더보기 방식)하는 형태로 만들어야 한다. 화면에 나오지도 않는 데이터를 한 번에 다 로딩하는 방식으로 개발을 시작할 경우 추후 성능 저하 대응이 매우 어렵다. 특히 모바일 앱의 경우에 개선이 쉽지 않다.

Excel Download / 통계 제공 / bulk 작업

  • Excel 다운로드나 통계 제공 혹은 인자를 받아서 일괄적으로 수많은 데이터를 갱신하는 등의 bulk 작업은 실시간 쿼리로 API 서버에서 실행해서는 안 된다.
  • 이런 대용량 작업은 개발 시작 시점에는 데이터가 적어서 괜찮지만 나중에 데이터 증가후 전체 장애 유발 요인이 된다.
  • 보통 DB 커넥션을 장시간 점유해서 connection 고갈을 일으키거나
  • 혹은 Excel 다운로드의 경우 비록 Apache POI MS Office DocumentSXSSFWorkbook 를 통해 메모리를 최소화 하더라도 DB에서 가져온 데이터를 트랜잭션 단위로 잘 분할하지 않으면 memory 과점으로 OutOfMemory를 일으키기도 한다.
  • 이런 작업은 항상 batch 를 통해 비동기로 처리하도록 한다.

Backend 캐시

  • Local 캐시는 변화가 적고, 일시적인 값 Mismatch는 상관 없고 성능이 더 중요할 때
  • 분산 캐시를 사용하면 성능은 떨어지만 expire등에 대해 전 서버가 일제히 대응이 가능해져 일관성이 보장 될 수 있다.
  • 분산 캐시 사용시에 제일 중요한 것은 내부 네트워크 망의 대역폭(bandwidth)이다. 이를 잘 분산 시킬 수 있어야 한다.
  • memcached는 데이터 저장 용량 큰 것으로 적은 대수 보다는 메모리가 작더라도 여러 대로 많이 잘게 배치 하는 것이 부하 분산 측면과 안정성에서 더 나은 것 같다.
  • 분산 캐시는 value type 이 배포중간에 바뀌는 경우 심각한 오류에 직면할 수 있다. 이게 중요한 곳에는 분산 캐시를 사용하면 안 된다.
  • 분산 캐시의 캐시된 value 에 대한 type 은 DB schema 처럼 매우 중요하게 다뤄야한다. 변경된 캐시 value 타입이 배포되는 동안 에러가 발생하는 현상이 잦기 때문에, 호환성을 항상 염두에 둬야한다.
  • Java Serialization 직렬화으로 분산 캐시 데이터를 저장할 경우 필드 변경, package 변경등의 문제가 지속적으로 발생한다. 또한 런타임 바이트코드를 변환하는 도구가 주입돼도 직렬화에 변경이 생겨서 문제가 되기도 했다.
  • 가급적 JSON 등의 plain 한 방식을 취하고, 필드나 타입이 변경되면 단순히 null 로 처리하고 에러는 안내는 방식을 취해야 한다.
    • 하지만 이 경우에도 문제가 있는데, 필드를 null 처리 할 경우 해당 필드를 꼭 사용해야하는 코드가 배포되는 중간에 있을 때 심각한 문제가 발생할 수 있다. 따라서 이 경우에는 cache key 자체를 변경해야 한다.
  • JVM 언어의 경우 memcached 등이 없어도 ehcache, Infinispan, Hazelcast 등의 JVM 기반 분산/Replication 캐시를 구축할 수도 있다. 이 경우 Local Cache의 성능 분산 캐시의 일관성이라는 장점을 얻을 수도 있다. 하지만 설정 복잡도가 높아질 것이다.
    • 애플리케이션 배포가 일어날 때 in memory cache 만 사용하면 복제하는 시간이 들어갈 수도 있다.
  • 특정 Cache invalidation 시 해당 cache 에 대한 요청이 여러서버에서 동시에 발생할 경우 원천 데이터(보통 DB)에 대한 트래픽이 폭증할 수 있다. 매우 빈번하게 사용되는 캐시는 invalidation 이 안일어나게 주기적 갱신을 해주는 별도 프로세스를 두는게 좋고, 그렇지 않아면 동시 요청이 오지 않게 할 방안을 마련해야한다.

Schemaless 저장소

  • 분산 캐시(distributed cache), document db, elasticsearch, json 저장소 등 스키마가 없는 저장소에 대해서 코드의 데이터 타입(class 등)을 통해서 스키마를 관리할 때는 데이터 정합성이 코드 배포 흐름에 따라 문제가 안생기는지 항상 확인해야 한다.

이미지와 정적 리소스 서빙 시스템 image, static resources

  • CDN 같은 정적 리소스 서빙 시스템을 두자.
  • CDN이 아니더라도, 별도의 도메인에서 정적 리소스를 통합 서비하는 것이 좋다.
  • CDN도 이중화 해야한다.
  • JS, CSS 등은 버저닝하여 정적 리소스 서빙 시스템에 미리 배포한다.
  • 이렇게 해야 리소스 로딩 속도가 조금이라도 빨라지고(깔끔한 HTTP 헤더와 별도 도메인으로 인한 다중 로딩 지원때문), 배포 중간에 발생하는 일시적 CSS, JS mismatch 현상을 조금이나마 줄일 수 있다(Sticky 세션 사용시 정적 리소스 미스매치 현상은 완화 가능할 수도).
  • 단, 개발시에는 로컬 개발 환경에서 정적 리소스를 로딩하고, 배포시에만 별도 서비스에 일괄 배포하여 사용한다.
  • 컨텐츠 이미시 썸네일(thumbnail)의 경우 업로드시 생성보다는 실시간 생성후 캐싱이 나아보인다.
    • 컨텐츠는 시간이 지나면 점점 액세스가 줄어든다.
    • 접근이 적은 썸네을을 삭제하는 방식으로 용량을 줄일 수 있고, 업로드 시간도 확보 가능할 것으로 보임.
  • Javascript 등을 캐시하면, 코드를 고쳐 배포해도 사용자가 캐시로 인해 변경된 프로그램을 못 사용한다.
    • 따라서 filehashing 혹은 timestamp 등을 파라미터로 붙이는 기법을 각 JS framework 별로 제공해주므로 해당 방법으로 배포시마다 캐시를 무력화해줘야 한다.

HTML

  • HTML과 유사하게 꺽쇠(<>) 기반의 커스텀 태그를 사용하는 템플릿 엔진은 사용하지 말라. HTML과 템플릿 코드가 섞여보여서 유지보수성이 현저히 저하된다(JSP, Freemarker 등 쓰지 말라는 얘기).
  • HTML Escape를 기본으로 하는 엔진을 선택하라(Jade, Handlebars.js 등). 안그러면 Cross Site Scripting에 너무 쉽게 노출된다.
  • 레이아웃은 무조건 처음부터 사용하며 Layout 상속 기능을 지원하는 템플릿 엔진을 선택한다. 별도의 레이아웃 프레임워크가 필요한 상황은 피한다.
  • Logic 처리가 적은 템플릿 엔진을 선택하라. 완전히 로직 표현이 불가능한 엔진은 사실 개발하기 매우 힘들지만, 그래도 logic을 넣기 힘든 템플릿 엔진을 선택하고 로직은 애플리케이션 프로그래밍 언어 코드로 표현하게 해야 유지보수성이 높아진다.
  • 가능하면 Jade 같은 HAML류의 HTML Validation을 어기는 것이 불가능한 템플릿 엔진을 사용해야 HTML의 유지보수성이 보장된다(하지만 UI개발자들은 별로 안 좋아한다).
  • Jade/haml류가 안 된다면 가급적 HTML과 구분되는 템플릿 문법에 HTML의 정합성을 깨지 않는 Java HTML Template Engines을 사용한다(Handlebars.js, Pebble 등).

데이터와 View 대한 독립성 - 특히 HTML escape

  • 데이터 저장소는 항상 원천 데이터포맷을 명확히 하고 해당 포맷으로만 저장한다.
  • 원천 데이터 포맷은 Text 라면 데이터 저장소에는 그냥 텍스트로 저장해야 한다. 이를 HTML 로 escape 해서 저장하지 말아야 한다.
  • 예) 원천 데이터가 우리는 대박 할인해요 >.< !! 라는 데이터가 있을 때 이 데이터의 근본 형식은 TEXT 이다.
  • 그런데, HTML로 외부에 저장할 때 이 Text 의 >.< 등이나 HTML 태그가 입력될 수 있다고 해서 원천 데이터 저장 자체를 HTML Escape 해서 저장하고자 하는 욕구를 느낄 수 있는데, 절대해서는 안된다.
  • 사용자 측이 항상 HTML Browser 라는 보장도 없고, 검색엔진 등도 색인할 때 원천 데이터의 형식을 Text 로 해야할지 HTML로 해야할지 혼란스럽게 된다.
  • 이로 인해 가끔씩 앱이나 일부 웹 애플리케이션에서 &lt;, &gt; &amp; 같은 문자열들이 노출되게 된다.
  • HTML escape 의 책임은 데이터 저장소단이 아닌 View 단에서 해야한다.
  • 즉, 데이터는 그 데이터의 포맷에 적합하게 저장돼야 하며 view 에 의존적이면 안된다. 데이터 저장은 view 에 대해 독립적이어야 한다.

보안

  • 사내 직원을 위한 서비스 모듈은 사내망 혹은 VPN으로만 접근하게 한다. 절대로 외부망에서 접근할 수 없게 한다.
  • 데이터를 암호화해 저장할 경우, Key를 소스코드에 담지 말고 별도로 담을 수 있도록 한다.
  • 암호화 Key는 수시로 바뀔 수 있어야 한다. 암호화된 데이터를 저장할 때 Key의 버전도 함께 저장하고, 암호화 코드는 Key 버저닝을 지원해야 한다.
  • 사용자의 비밀번호(password)는 절대로 복호화 불가능한 방식으로 암호화 해야한다.
  • 잦은 이직, 손쉽게 소스를 전달할 수 있는 시스템들(github, ..), 개발자들이 노트북 사용한 원격 근무, 노트북 도난 등 대부분의 회사에서 소스코드를 도난 등 회사의 소스 코드는 회사만의 것이라고 볼 수 없다. 보안을 생각할 때 항상 소스는 노출되어 있다라는 생각으로 해야만한다.
    • 비밀번호, 암호화 Key, Hash Key 등을 소스에 넣지 말고 원격접속을 통해 어딘가에서 값을 가져오게 한다.
  • 회사 규모가 작을 때는 상관없지만 커지면 Github 같은 외부 노출된 저장소 사용시 Key 노출로 인한 보안문제가 발생할 수 있으므로, 사내망에서만 접속 가능한 Source Code Repository를 구축한다.
  • 코드에서 Validation, 돈 등의 계산은 항상 요청을 받는 서버에서 해야한다. 브라우저등의 클라이언트측 Validation은 단순히 고객 편의를 위해서일 뿐이며, 최종 검사는 항상 서버에서 이뤄져야 한다. Client의 요청은 언제든지 조작 가능하다.
  • 요즘엔 다중 기기에서 로그인 유지 상태로 지속적으로 애플리케이션을 사용하는 경우가 있는데, 이 때 사용자의 계정 해킹등이 발생하거나 혹은 기타 다른이유로 해당 사용자로 인증된 모든 기기의 인증을 해제시킬 수 있어야 한다.
  • 뭔가를 막아야할 때는 blacklist 보다는 whitelist 방식 권장. blacklist 는 또 다시 막아야할 것이 발견될 가능성이 높다.
    • 문자 필드에 어떤 문자를 사용할 수 없게 해야하는 상황일 경우에도, 한글,영문 대소문자, 숫자, _, … 식으로 whitelist 를 지정하는게 낫다.
  • 인증 토큰(token) 은 변조가 불가하면서 애플리케이션 사용에 필요한 최소한의 데이터를 담도록 한다. JWT 등을 암호화 하는 방법을 권장함.
  • DDoS 공격 방어를 위해 WAF(Web Application Firewall, 웹방화벽) 설정을 해야한다.
    • 서비스 런칭 시점부터 하는게 좋다. 나중에 붙이다가 그동안 매우 많이 사용되던 막아서는 안되는 것을 막는 일이 일어나기 쉽다.
    • 이 때 IP 차단시 동일 NAT 를 사용하는 사용자가 많이 존재할 수 있음을 염두에 두어야 한다. 명백하게 해당 NAT IP 를 사용하는 자가 공격일 경우에만 차단한다.
  • 규모가 큰 시스템이고 보편적이지 않은 흐름에서 데이터 소유주 본인이 아닌 사용자에 의해 admin 등을 통한 데이터 조작 행위가 일어날 경우 이를 트래킹하는 저장소 시스템을 만들고, 각 admin 혹은 데이터 변경을 일으키는 시스템에서 해당 시스템으로 행위자와 행위 관련 데이터(뭐를 어떻게 언제 변경시켰는지)를 전송하고 트래킹할 수 있는 시스템을 구축하는 것이 좋다.

서버 운영

  • 절대로 여러 사람이 공유하는 공용 계정으로 서버를 관리하지 말라(AWS 등 포함). 이는 치명적인 보안 사고로 이어진다.
  • 전체 공용 계정을 만들게 되면 해당 권한이 있는 디렉토리가 난장판이 되고 누가 무슨 일을 했는지도 구분하기 힘들다.
  • 서버 이름에 대한 공통 규칙을 미리 만들어놔야 한다. - Cloud 를 사용하지 않을 경우
    • 프로젝트 이름
    • 서버 번호는 2~3자리로 구성 : 예) xxx-01 ~ xxx-20, 혹은 xxx-001,…
    • 가상 서버와 물리 서버간의 구분
  • 내부망도 IP 주소 기반으로 운영하지 말고 Domain Name 기반으로 운영하는 것이 좋다. 가끔씩 IP 자체를 바꾸지 않으면 안되는 사태가 발생하는데 이 경우 Domain Name으로 항상 접속정보를 구성하면 DNS 서버에서만 주소를 바꿔주면 된다.
    • 이 경우 DNS Resolve 자체가 부담이 될 수 있으므로 DNS Resolve 성능 최적화에 주의할것.
  • 내부 서버간의 인증시에 IP Address 기반 인증을 하지 말고, 동적 서버 증가에 대비한 인증 수단을 마련해야 한다.
    • AWS 같은 동적 서버 증가가 가능한 상태에서 IP 기반 인증은 서비스 확장의 유연성을 떨어뜨린다.

설정

  • 개발팀에서 제어할 수 없는 외부 연동 정보가 있을 경우 해당 정보(특히 API 접속 주소라든가) 등은 Spring Cloud Config 등을 사용하여, 설정 변경을 재배포없이 즉시 적용 가능하게 하는게 좋다.
  • 제어 불가능한 외부 환경의 경우 어떤일이 일어날지 알 수없고, 대응이 언제 될지등도 알 수 없다(예, 갑자기 외부 시스템 운영자의 실수로 API 서버 IP 주소가 바뀌어버렸다).
  • SpringBoot 처럼 자동 설정이 많고, 업그레이드시 자동 설정값이 바뀌는 경우가 있는 프레임워크 사용시에, 원하는대로 설정이 된 상태로 실서비스가 실행되는지 검증하는 통합 테스트(integration test)를 작성해두는 것이 버전업을 안전하게 할 수 있는 방법이다.

서버 모니터링

  • 애플리케이션에 대한 가시성 확보는 장애 탐지, 장애 발생 후 분석등에 있어 매우 중요하다.
  • 서버 모니터링 툴을 통해 초반부터 모니터링을 강화한다.
  • 서버 운영체제 상태(collectd) 뿐만 아니라 가능하면 사용하는 언어에서 제공되는 해당 언어 VM 모니터링 기능을 적극 활용한다.
  • 장애 상황시 사태파악을 빠르게 하려면 모니터링이 꼭 필요하다.
  • 특정 서버에 접속해서 Java ThreadDump 등을 떴을 때 이를 쉽게 개발자 PC로 전송받을 수 있는 시스템을 구축해야한다.
  • Spring Boot Actuator 같은 애플리케이션 서버 모니터링 URL을 외부로 노출시켜서는 안된다.
  • 모든 팀원은 주기적으로 장애상황에 대한 훈련을 해야하고 장애시 곧바로 모니터링 시스템을 활용할 수 있게, 각 서버에 접속하여 상황파악(dump 등)을 할 수 있게 익숙한 상태를 유지해야 한다.
  • 반복적일 알람에 대해서 모니터링 알람, On Call (Opsgenie 등의 강력한 전화 알람기능)등이 갖춰져 있고, 작동여부 테스트도 돼 있어야 한다.
  • 주문 지표 처럼 중요 비즈니스 지표가 전날 혹은 전주 동일요일 등을 기준으로 알람이 지정돼 있어야 한다. 비록 에러가 안 발생한 상황이더라도 지표가 급격히 떨어졌다면 문제 소지가 있는 것이다.

Logging

  • 항상 로깅 프레임워크(Java 계통의 경우 Slf4j)를 사용한다.
  • HTTP/HTTPS 요청은 Web Server에 의해 요청 로그가 남지만 MQ 를 통한 요청은 남지 않아 문제가 될 수 있다. MQ등 자동으로 요청 로그가 남지 않는 계통은 항상 INFO Level 등으로 실서비스에서 요청로그를 상세히 남기도록 해야 나중에 대응할 수 있다.
  • 남긴 로그는 비동기로 중안 서버로 전송하여 개발자들이 한 곳에서 편하게 모니터링 할 수 있게 해준다.
    • 동기식으로 중앙 수집할 경우 서비스가 커지거나 특정 시점에 트래픽이 몰릴 때 Logging 자체가 병목이 되어버린다.
    • GrayLog2, fluentd, Logstash, ELK Stack 등을 고려한다.
  • 에러 로그 메시지는 최대한 에러를 유발한 요청 파라미터를 상세하게 남기도록 한다. 안그러면 디버깅할 때 상하단의 모든 로그를 다 확인해야 하고, 그나마 요청 정보 로그도 존재하지 않으면 아예 새로 로그를 남겨 배포해야만 확인 가능해진다.
  • 로그를 검색하기 쉽게 만들것
    • 코드상에서 로그를 봤을 때 로깅 시스템에서 해당 로그를 빠르게 검색할 수 있게 만들어야 한다.
    • 동적으로 로그 메시지를 시작하지 말것 : log.info(“{}”, message)
    • 로그 메시지를 prefix 고정으로 작성해야 검색이 쉽다.
      • log.info(“주문 {} 취소”, orderNo)log.info(“주문 취소건 orderNo: {}”, orderNo)
      • 이렇게 해야 주문 취소건 이라는 검색어로 주문 취소와 관련된 연관 로그를 한 번에 검색 가능해진다.
    • 절대로 예외를 먹지 말것. 정말 특별한 경우가 아니면 항상 예외의 stacktrace까지 모두 남길것. 특별한 경우에는 왜 특별한지 주석 달 것.
  • UI 관점에서 사용자 행위 로그를 꼭 남기도록 하고 이를 분석하여 개선 방향을 도출할 수 있도록 한다.

Production Server ACL

  • 운영 시스템에 대한 ACL은 개발 초기부터 망 분리 등을 통해 운영시스템에서만 접속가능하도록 하고 절대 개발자 PC, 테스트 시스템 등에서는 접속이 불가능하도록 구성한다(여기서 말하는 접속은 서버에 대한 SSH 접속이 아니라 DB,Redis,MQ 같은 시스템, API 서버 등에 대한 접속을 뜻한다).
  • 그 뒤에 필요할 경우 필요한 서버에게만 ACL을 하나씩 열어준다. ACL은 모두 막아 놓고 열어야지, 모두 열린 상태에서 막으면 개발자들이 혹시나 실수로 중요한 애플리케이션을 비 운영서버에서 돌릴경우(ACL이 되니까) 추후 ACL 제약시 문제가 될 수 있다.

배포

  • 배포 스크립트는 중앙 집중형으로 만들지 말고 프로젝트별로 독립적으로 만들어, 중앙에서 각 프로젝트의 배포 스크립트를 호출하도록 한다.
  • 중앙 집중형으로 만들면 초기에는 편하지만 프로젝트가 증가하면 관리가 어렵고 하나 고치다가 다른데 영향을 주거나 하기 쉽다. 또한 중단된 프로젝트에 관한 설정을 제거할 때도 망설이게 되어 계속 크기가 증가만 하게 된다.
  • Blue/Green 배포(Blue Green Deployment)를 가능하게 설계한다.

배포 Profile 통합

  • 배포 프로필을 전사적으로 통일하지 않으면 여러 Micro Service에 대해 각종 테스트시 혼란이 유발된다.
  • local, dev, qa, prod, staging 등의 의미를 통합해서 구축하고 항상 동일한 명칭 동일한 의미로 사용하게 한다.

Mobile App

  • 항상 최신버전을 유지할 수 있는 웹과는 달리 Mobile App은 그럴 수 없다.
  • 이는 추후 변화에 대한 대응을 불가능하게 만든다. 가급적 처음 만들 때 앱 자체에 강제 업데이트를 가능하게 만들 필요가 있어보인다. 안그러면 추후 서버 쪽 코드의 발전을 클라이언트가 못 따라가서 서버쪽에 클라이언트 버전별로 코드가 늘어나게 될 수 있다.
  • 앱의 반응성이 조금 떨어져도 상관없다면 모바일 앱은 거의 껍데기만 존재하고 웹뷰를 통한 비즈니스 처리를 고려해볼 필요도 있어보인다.

Code 품질 관리 - 정적 분석도구 도입 (Static code analysis)

  • 서비스 개발 초기부터 정적 분석도구 (java의 경우 checkstyle, PMD, findbug, js는 jslint 등)를 도입하여 코드 컨벤션을 지키도록 하는 것이 좋다.
  • 정적 분석 도구가 오류를 보고하면 CI 툴에서 빌드를 아예 실패하도록 한다. 그리고 통과 기준을 시간이 지날수록 높여간다. 기준이 낮아지는 일은 없게 한다.
  • 정적 분석도구를 통한 분석을 프로젝트가 한참 지난후에 적용하려면 매우 고통스럽고 모든 분석된 오류를 다 해결할 수 없어서 쉬운 규칙만 적용하는 경향이 생긴다. 하지만 처음부터 적용하면 별 문제가 아니다.
  • 프로그래밍 언어와 프레임워크, 라이브러리는 지속적으로 업그레이드된다. 성장하는 서비스에서 고객의 요구 사항을 만족시키기 위해서는 결국 언젠가 언어/프레임워크/라이브러리를 업그레이드 할 수 밖에 없는 순간이 온다. 그 때 안정적인 업그레이드를 보장하는 가장 나은 방법은 테스트 코드 커버리지를 100%에 가깝게 유지하는 것이다.
  • 사용중인 도구(프레임워크/라이브러리 등)의 학습 테스트도 함께 코드상에 넣어두는 것도 좋아 보인다. 테스트 코드를 리뷰하며 팀원들이 함께 학습할 수 있고 도구의 버전업시 변경 사항에 대한 인지도 쉬워진다.
  • 또한 EditorConfig 등을 통해 처음부터 코드 포맷을 통일 시킨다.

Load Balancer 설정 확인

  • DB Connection Pool, HTTP Keep-Alive Connection 등에 원인을 알 수 없는 잦은 끊김 등의 이상 동작이 발생한다면 Load Balancer(LVS, L4,L7, HAProxy, LVS, Keepalived…) 를 의심해봐야한다.
  • Load Balancer가 중간에서 설정에 의해 커넥션의 유효성 여부를 체크하고 유효하지 않을 경우, 혹은 장시간 미사용일 경우 강제로 끊어버릴 수 있는데 그 설정값이 Connection Pool의 설정과 매칭이 안되면 의도치 않은 순간에 접속이 끊길 수 있다.
  • 가장 손쉬운 확인 방법은 Load Balancer 없이 직접 커넥션을 맺고서도 동일 문제가 발생하는지 확인해 본다.
  • Load Balancer 가 각 인스턴스를 활성화시키는 health check URL은 해당 인스턴스의 애플리케이션이 완전히 초기화가 완료된 뒤에 true 를 반환하게 해야한다.
    • 그렇지 않으면 요청이 너무 일찍들어와서 일찍 들어온 요청들은 모두 오류가 발생하게 된다.
    • Spring MVC 등을 사용히 health check url 용 컨트롤러를 만들어서 해당 컨트롤러는 Spring 의 필수 bean 들이 모두 초기화 되고, DB 커넥션풀 등도 맺어진 상태에서 수행될 수 있게 한다. (@PostConstruct로 초기화??)

API 설계

  • API 설계시 다중건 결과를 내는 경우 Paging 기반으로 요청을 받지 말고 더보기 방식으로 요청을 받는 것이 낫다.
    • 더보기 방식은 항상 Primary Key를 기준으로 하여 PK asc 로 정렬을하고, 앞서 1~10 의 결과를 읽었다면 다시 파라미터로 앞의 마지막 PK 10을 받은 뒤 pk > 10 limit pageSize로 그 다음 조회를 한다.
    • 이 경우 페이지가 뒤로 늘어나도 전혀 성능저하가 발생하지 않고 균일하다.
    • 다음 페이지의 존재 여부를 알고자 한다면 limit이 10이면 11개를 쿼리해서 결과가 11개가 나오면 다음페이지가 존재하고, 아니면 여기서 끝인 것으로 판단하면 된다.
    • offset/limit 방식도 전체 카운트를 하는 Paging 보다는 낫지만 offset 값이 커질 수로 조건에 맞는 그 앞 데이터에 대한 카운팅을 해야해서 성능이 점점 느려진다.
      • offset/limit은 요청자 측에서 페이징 방식으로 자기네가 알아서 감싸는 것이 쉽다. 즉, 두가지 방식을 모두 요청자측에서 결정해서 할 수 있다.
  • API 의 경우 호출자에 대한 정보를 HTTP Header 등에 주입하고 로그로 자세히 남기는 것이 좋다. 이는 SQL 구문도 마찬가지 이다.
    • 호출자 서비스 이름
    • 호출자 서비스의 구체적 기능(컨트롤러 이름이나 배치 이름 등)
    • 인증의 역할
  • 수정/삭제/생성 등의 API를 만들 때 기초 데이터를 FORM/JSON 형태의 body로 실어보내면 access log에 정보가 제대로 남지 않는다. 가급적 중요한 핵심 정보는 URL에 Path Variable이나 Query Parameter로 남을 수 있게 해야 나중에 로그 확인이 쉽다.(예: member 수정시 /members 에 POST로 모든 정보를 담기보다는 /members/1 로 memberId를 URL에 남기고 나머지 데이터를 POST Body로 전송)
  • 다른 API를 호출한 결과는 항상 값이 null 일 수도 있다고 간주한다.
  • API 호출 내부에서 다른 API를 호출해서 결과를 합치는 일을 하는 것은 좋지 않다. 내부 호출 API의 장애가 전체적으로 다 전파 돼 버린다.
    • 최초의 API호출자는 여러 API 호출이 필요하면 다른 API에게 또 다른 API호출을 요청하기 보다는 스스로 모든 요청을 하는게 장애 포인트를 줄여나가는 방법이다.
  • 오류가 나면 오류를 발생시키는게 낫다(4xx, 5xx 응답 권장).
    • 오류 발생시 API 응답은 200 으로 정상으로 내리면서, 응답 데이터에 오류 코드등을 넣게 설계하는 경우가 있는데
    • 이렇게 되면 모든 호출자가 응답에 오류코드가 있는지 일일이 확인해야 한다.
    • 이를 확인하지 않고 데이터를 사용할 경우 에러가 즉각 발생했으면 오히려 문제가 덜 발생했을 것을 더 큰 문제로 커지게 된다.
    • 예를들어 에러코드는 존재하고, 응답 데이터는 empty 로 주는데, 이때 오류를 확인하지 않고 empty 로 온 데이터를 직렬화하면 응답 객체의 필드가 null 혹은 기본값으로 채워지게 되고, 이를 가지고 정상응답으로 착각하고 나머지 프로세스를 타면 아주 치명적인 문제가 될 수 있다.
  • API Gateway 와 연계하여, 작업중인 API 가 Presentation 계층용도인지 Business 계층 용도인지 명확히 인식하고 이 둘을 하나의 시스템에 섞지 말 것.
  • PUT 으로 전체 데이터를 수정하는 것은 MSA 에서는 문제가 될 수 있다. 수정 대상 Entity 에 필드 변화가 생겼을 때 이를 호출하는 서비스에서 그 필드 변경을 제대로 인식하지 못하고 PUT 으로 전체 데이터 수정 요청시 신규 필드의 데이터 유실이 발생할 수 있다.
    • JSON Patch 혹은 JSON Merge Patch 를 사용하는게 낫다.
    • 필드 변경을 반영한 UI와 Presentation 계층 API가 동시 배포된다면 Presentation 계층에서는 괜찮을 수 있다.
  • enum 에 대해 주의가 필요하다.
    • Java Enum 으로 API 요청을 받는데, 해당 Enum 이 외부에서 관리되는 것이라면 배포 타이밍에 따라 Enum 값 갱신이 안 된 상태일 수 있다.
    • Enum 공급자(변경자) 측은, enum 추가시 사용처를 모두 확인해서 공지해주고 배포 타이밍을 다른 팀에 모두 Enum 을 추가한 뒤로 맞춰줘야 한다.
    • Enum 소비자측은
      • Enum 으로 뭔가 작업이 필요할 때는 해당 타입도 Enum 으로 하여 Enum 이 올바로 추가 안됐을 때 차라리 에러를 내는게 나을 수 있다.
      • 혹은 String 으로 받되 해당 시스템이 파악하고 있는 Enum 인지 여부를 검사하고 정확한 에러를 내게 처리하는게 나을 수도 있다.
      • enum 값을 단지 DB에 저장하거나 다른 API 호출에 사용만 하고 실제 이를 가지고 비즈니스 로직을 처리하지 않는 경우는 String 으로 받아서 불필요하게 의존성이 안생기게 하는게 낫다.

MQ 등을 통한 비동기 처리 / event driven

  • 비동기 처리는 반응 속도를 높이고 전송 신뢰도를 높여주는 등 좋은 점이 있지만 단점들도 많으므로 확실히 이해해야 한다.
  • 메시지 전송이 실패하는 경우(클라이언트측 버그로 메시지를 먹어버리고 종료되는 경우, 프로듀서는 보냈다고 됐지만 네트워크 단절 등으로 안 가는 경우등이 발생함)에 대비히 철저하게 메시지 재전송이 가능하도록 솔루션을 만들어야 한다.
  • RabbitMQ의 경우 Exchange까지는 갔지만 해당 Exchange의 일부 Queue 들에만 메시지가 전송 안되는 경우도 발생했다. 특정 Queue에만 메시지를 재전송할 수 있는 기능도 필요하다.
  • 비동기 타이밍 문제도 심각하다. 이 경우 비동기를 사용하면 안되는데 비동기를 사용한 경우일 수 있다. A → B로 메시지를 보냈는데 B 가 메시지를 Consuming 하기 전에 A 혹은 C 등의 다른 서비스가 B가 메시지를 받았다는 가정하에 어떤 행위를 할 경우 오류가 발생한다. 매우 빈번히 발생하는 문제이다. 비동기를 사용하면 안 되거나 B가 메시지를 받았는지 확인할 수 있는 방안을 강구해야 한다.(JMX의 경우에는 Consume 확인 기능이 존재함)
  • MQ Consumer 서버를 API 서버와 분리한다. MQ Consumer 가 DB 커넥션 쓰레드나, 애플리케이션 쓰레드를 점유해버릴 경우 API가 원인 모르게 느려지고 작동을 안하는 현상이 발생한다. 이 둘을 분리해서 장애시 원인이 MQ Consumer 인지, 특정 API 호출인지를 명확히 구분할 수 있다.
  • 모니터링
    • 메시지 발생 실패를 모니터링 / 알람 해야 한다.
    • 발행은 성공했으나 consumer 처리 적체가 발생할 수 있으므로 Queue 에 대기중인 메시지가 너무 많을 경우에 대해서도 모니터링 알람해야한다.

Event PayLoad 정책

ᅟZero Payload

  • Consumer 측에서 이벤트에 대해서 Event Source 데이터를 최신화하기만 하면 되는 경우에는 zero payload 가 유리해 보임.
  • 즉, 데이터의 최종 상태 동기화만 하면 되는 경우(Eventually Consistency 보장)다.
  • 예시) 업체 정보, 고객 정보 등의 변경을 consumer 측에서 최신 데이터로 계속 갱신만 하면 되는 상황
  • Event body 에 모든 데이터를 심는 방법은 이벤트 전송 부담을 키운다. 이벤트에는 이벤트를 발생시킨 데이터의 종류(상품,사용자,..)와 해당 Primary Key, 그리고 이벤트 발생 요인(생성,수정,삭제)만 전달하고, 실제 데이터는 이벤트를 받은 시스템에서 API 등으로 호출하여 확인하게 한다. 그리고 상황에 따라 API로 봐야하는 데이터가 너무 크다면 그 중에 어떤 데이터가 변경됐는지(상품중에서 가격이 변경 됐는지 여부?)를 함께 담아 보낸다(zero payload).
  • zero payload 상황에서 consumer는 producer 에게 실제 데이터를 API로 요청해야하는데, 이 때 DB replication이 돼 있을 경우 replica 에 데이터 복제 지연이 발생해서 못 읽는 경우가 있을 수 있다. MQ의 전송지연 기능으로 3~5초 정도 지연해서 메시지를 받게 해주면 이 문제가 줄어든다.
  • zero payload가 이벤트를 받는측에서 데이터를 조회해야해서 producer 측에 DB부담을 가중시킬 가능성도 있다. 혹은 어떤 상황에서는 이벤트 발행 그 시점의 데이터가 필요할 때도 있다. 따라서 데이터를 넣어서 전송해야 할 일이 있다면 그 정책을 producer가 payload 생성으로 인해 쓰레드가 길어지거나 너무 많은 데이터 조회로 부담되지 않게 잘 정의해줘야 한다.

Full Payload

  • Consumer 측에서 event source의 상태 변경에 대해서 서로 다른 행위를 해야하는 경우에는 full payload 가 아니면 장애가 날 수 있다.
  • 이 경우 순서 보장도 매우 중요하다.
  • 보통 이 경우는 event source 데이터의 최신화는 목적이 아니다.
  • 예시) 주문 정보의 변화 이벤트는 이 이벤트를 받는 측에서 주문 정보를 최신으로 저장하는 것에는 관심이 없고 주문이 생성되면 “배달 시작”, 주문이 취소 되면 “배달 취소” 형태로 하는 행위가 달라진다.

Single Page Application?

  • 2020년 현재 대부분의 Front End Framework 이 SPA 기반이다. SPA를 사용하지 말라는 것은 유효하지 않다.
  • 관리툴은 최대한 단순함을 유지하는 것이 낫다.
  • SPA 적용시 링크를 새창으로 여는 기능등, SPA가 아닐 때 작동하던 UI 가 모두 잘 작동하게 작성할 수 있도록 최선을 다해야 한다. 그렇지 않을 경우 오히려 사용성이 매우 떨어지게 된다.

예외

  • 예외는 먹는게 아니다.예외 로그를 찍거나 다른 예외로 감쌀 때, 항상 원인 예외를 함께 전달한다.
  • 예외(Exception)에 해당 예외의 근본 원인을 찾알 수 있는 정확한 정보를 남겨준다. 예를들어 사용자의 전화번호가 잘못된 포맷으로 입력되었다면, 단순히 new IllegalArgumentException(“잘못된 전화번호”)가 아니라 new IllegalArgumentException(String.format(“사용자 %s의 전화번호(%s)가 잘못되었습니다”, userId, phoneNumber)) 형태로 구성한다. 실무에서 예외가 발생했을 때 조금이라도 정확하고 빠르게 대응 가능해진다.
  • 예외 메시지를 UI 에 노출하는 경우가 있는데, 여기에 너무 많은 메시지를 주면 불필요하게 시스템의 보안 이슈가 될 수도 있다. 따라서 UI 노출용 메시지와 로깅용 예외 메시지를 분할하는게 좋다.
  • API나 Web 애플리케이션의 경우 예외 발생을 적절한 JSON + HTTP Code 로 변환해주는 예외 핸들러를 두는 것이 좋다. 자동으로 예외가 JSON 으로 변환되 나가도록 하며 그 때, UI 노출용 예외 메시지도 함께 준다. (Spring MVC 에서 @ControllerAdvice, @ExceptionHandler 등을 사용.)

Batch Job

  • 배치 애플리케이션은 DB 커넥션을 최소로 맺으며 시작하고 필요에 따라 늘려가게 한다. API와는 달리 DB 커넥션 맺는 속도가 문제가 되지 않는다. 오히려 DB 커넥션을 과점유하면 여러 배치 애플리케이션이 동시에 돌 때 문제가 된다.
  • DB를 읽을 때 reader/writer 를 올바로 읽는지 테스트를 잘 해야한다. batch 때문에 write node가 불필요한 부담을 동시에 받는일이 생기지 않게 한다.
  • Batch 가 올바로 시작됐는지, 혹은 올바로 종료됐는지를 모니터링 한다. 시작 모니터링도 매우 중요할 수 있다. 시작조차 안되면 실패 알람조차도 안오기 때문에 아예 배치가 시작 안됐음을 모르고 며칠이 지나는 경우도 생길 수 있다. Batch / Scheduled / Cron JobsGrafana 혹은 Kibana 로 관련 모니터링이 가능하다.
    • 혹은 시작은 됐으나 기존에 10분이면 끝나던 작업이 갑자기 하루 종일 걸리는 현상
    • 누군가가 실수로 batch job 실행시간을 잘못된 시간으로 옮겨버리는 행위 등을 감지할 수 있어야 한다.
  • 배치 실행 실간을 가정으로 만들지 말 것. 예를들어 앞선 배치가 1시간 걸릴테니 그에 관한 후속 배치는 2시간 이후 실행되게 했는데, 앞선 배치가 2시간이 넘게 걸리는 등의 현상 발생. Job 들간 의존 관계가 있을 경우 명확하게 의존 관계를 코드나 스케줄로 표현할 것.

날짜 / 시간

  • 한 달(28일, 29일, 30일, 31일), 1년(365일, 366일) 이라는 용어는 매우 주의가 필요하다. 상황에 따라 실질 날수가 달라지기 때문이다.
    • 가급적 의사 소통을 월/년 으로 하지말고 날짜수로 해야한다.
    • 1 주는 7일 고정이라 무관.
    • 정말로 의도가 월/년이 맞는 경우에는 항상 로직을 작성할때 +- month, +- year 로직으로 짜야지 +- 30, +-365 이런식으로 작성하면 안된다.
  • 날짜 ↔ 문자열간 변환이 많이 필요한데, 처음부터 포맷을 결정하고 간다.
    • 날짜시간, 날짜, 시간 세가지 종류가 필요하다.
    • 가급적, 밀리초까지 포함한다. 초까지만 지정하면 추후에 밀리초를 사용할 일이 생겼을 때 매우 당황스러워질 수 있다.
  • 시각(시분초) 만으로 기간을 설정해야하는 경우가 있다면 시작과 끝을 시각으로 만들면 큰 혼란이 온다.
    • 10:00~10:00, 10:00~03:00 를 어떻게 해석해야할까? (다음날 10시, 다음날 3시 등의 표현)
    • 이렇게 되면 이 범위를 다루는 코드가 시작시각보다 종료시각이 더 큰 값인 경우는 당일 처리, 더 작은 값이나 같은 값인 경우는 익일 처리 로직을 짜야한다.
    • 더 심각한 것은 시작은 10:00~11:00 인데 데이터 입력자의 의도가 다음날 11:00 인 경우가 발생할 수도 있다.
    • 시각기준의 범위를 지정할 때는 시작 시각 + 유지 시간 형태로 하는게 나아보임.

기타

  • 네트워크 연결 되는 설정의 경우 Connection Timeout과 Read Timeout 두 가지가 존재한다. 항상 이 둘을 적절히 설정해야 한다.
  • UI 에 페이징을 사용하지 않는 것이 좋다. 페이징은 전체 결과 갯수를 필요로 하는데 이로 인해 DB 부하가 매우 크다. Elastic Search 같은 검색엔진 기반이라면 써도 된다. 하지만 DB 기반으로 하는 대부분의 애플리케이션은 페이징 UI 말고 offset/limit 방식을 사용해서 페이징을 구현한다. 안그러면 서비스가 성장했을 때 UI가 사용이 불가능할 정도로 느려진다.
  • 현재 시간(Java 에서는 LocalDateTime.now() 같은 것을 하나의 비즈니스 로직 흐름에서 여러번 호출해서는 안된다. 호출 순서에 따라 시분초 중이 시가 바뀌가나 아예 날짜가 바뀌어버릴 수 있다. 하나의 비즈니스트랜잭션에서는 이 값을 로직의 최초 시작점 혹은 호출자측에서 현재시간을 구해서 인자로 넘겨주고 그 후속 로직은 모두 이 인자를 사용해야 한다. 그렇지 않으면 하나의 트랜잭션안에 현재시간을 나타내는 값이 모두 다르게 들어가서 장애가 날 수 있다.

부하 예상상황

  • 대규모 포털 광고나 서비스에 영향을 주는 이벤트 일정을 항상 공유하고 그에 대해 대비돼 있는지 체크리스트를 확인하는 절차를 가지고 있어야 한다.
  • 부하 상황에서는 서비스에 크리티컬하지 않은 기능을 일시적으로 꺼버리는 옵션을 두는 것이 좋다. 즉, 서비스의 핵심 기능만 작동하고 불필요하게 부하를 일으키는 부가 기능을 꺼버려서 대응할 수 있게 하는게 좋다.

전사 가이드

  • 신규 서비스 런칭/변경 등에 대해 각 개발자가 알아서 처리하면 회사 직원들의 노하우가 묻히게 되고 보안 구멍이 많이 생기게 된다.
  • 각 분야별(OS, 보안, 각 개발 언어, DB, AWS, …) 등에 대해 전사 표준 가이드를 만들고 Checklist 를 수립하고 신규 서비스 런칭/변경시마다 체크리스트를 각 분야 전문가 한명씩 참가한 상태로 체크하도록 한다.
  • 장애 대응시 5 WhysPokayoke 포카요케를 적용한다.

마이그레이션

  • 서비스를 리팩토링하면서 재구축할 때 실서비스 기준으로 마이그레이션을 먼저 시뮬레이션 해보는게 좋다. 그래야 현재 실제 존재하는 데이터들의 구성을 정확히 파아할 수 있다.

프로젝트 관리 : 기본적으로 애자일

  • 오픈 예정일이 굉장히 밀도 높고 여러 팀이 엮인 프로젝트를 진행해야 할 경우
  • Jira(혹은 관련 이슈 트래커)에 여러 팀별 이슈를 하나로 묶은 보드를 생성한다. (묶음의 조건을 만족시킬 수 있는 이슈 레이블 등이 필요할 듯)
  • 모든 팀을 통합하여 스프린트를 일괄로 잡는다.
  • Sprint 를 1주 단위 정도로 잘게 쪼개는 게 좋다. 목표를 명확히 가시화 한다.
  • 스프린트당 통합 테스트를 목표로 정하는게 좋다. 허접해도 통합해서 뭔가를 보는게 좋다.

외부 연결 정보와 인증서 관리

  • 외부 API 연동 Key 혹은 HTTPS 인증서 등의 목록을 면밀히 관리해야 한다.
  • 특히 인증서 자체의 숫자가 늘어나서 관리가 안 될 수 있기 때문에 소스코드에 API Key 등을 두지 말고, 특정 저장소에서 일관되게 사용할 수 있게 해야 한다.
  • HTTPS SSL 인증서의 경우 만료일 관리 스케줄링을 하고, 관리 주체를 명확히 가져가도록 한다.

Test 환경

  • DB 테스트나 각종 솔루션에 대해 Docker docker-compose 를 통해 실제 서비스를 local 에 띄워서 처리하게 한다.
  • DB 테스트를 실운영 DB가 아닌 embedded DB 로 하지 않는다. 실 DB로 해야 정확하게 테스트된다. embedded 에서 되게 맞추느라 불필요한 작업을 하거나 혹은 embedded 에서 잘 된다고 배포했다가 실DB 에서는 장애나는 경우가 있다.

신규 개발 조직 구축시 먼저 할 일

  • AWS, Jenkins 등 써드파티 솔루션의 인증과 사내 인증(ldap 등)을 자연스럽게 연동 해 줄 수 있는 OAuth 서비스나 기타 SSO - Single Sign On 솔루션을 확보한다.
  • nexus 같은 의존성 저장소를 구축한다.
    • 사내 라이브러리 올리기
    • 외부 망에 의존하지 않아서 빠른 속도로 의존 라이브러리 가져오기
  • 비슷한 의미로 Docker Registry 도 구축
  • 회사가 극초반이 아니라면 https://github.com 같은 외부 저장소는 오픈소스 전용으로 사용하고, 회사 내부용 별도의 git 저장소를 구축한다.
    • Github Enterprise
    • Gitlab Enterprise 등.
    • 이유 : 공개 저장소를 사용하면 항상 보안 문제가 발생한다. 특히 개발자 한명의 실수로 전체 소스코드 유출이 가능해진다.
    • 무것보다 소스코드에 DB 접속 정보, 개인정보 등이 있을 때 큰문제가 된다.
    • 근본적으로 어딘가의 접속 정보는 소스코드 저장소에 넣지 않는게 제일 좋지만 이걸 안지키는 경우가 지속적으로 발생한다.
  • Branch 전략을 수립한다. gitflow 는 사용하지 말 것. 이건 백포팅이 필요한 package software/library 용 전략임.
  • Local 개발 환경을 구성한다.
    • 개발자가 개발시에 여러 팀에 함께 테스트(통합 테스트 환경)하는 DB 등의 리소스를 사용하지 않게 해야, 다른 팀의 개발환경을 깨뜨리지 않고 안정적이고 빠르게 개발할 수 있다.
    • Local 개발환경이 구축돼 있지 않으면 신규 기능이 DB 스키마 변경등을 유발할 때 통합 테스트 환경을 변경하면서 일시적 장애 상황에 빠지게 되고 이런 조직에서는 통합 개발환경이 자주 망가져서 전체 팀이 일을 못하게 되는 상황이 자주 발생한다.
    • Docker, LocalStack, Vagrant 등을 활용하여 개발자 전용 개발 환경을 만들어주고
    • Flyway Java Database Migration, Liquibase, Terraform, Ansible같은 Infrastructure As Code 툴로 운영환경을 Local 에서 복구하는 것이 자동화 돼 있어야 한다.
  • 개발 시간도 아니면서 개발 시간이 부족하게 만드는 요소들(배포 복잡도로 인한 배포 시간이 너무 오래걸린다던가, 매우 반복적인 통계 생성 요청이나 기타 운영 요청을 받아주느라 개발을 못한다던가)이 있는지 확인하고 개선하여 개발 시간을 확보하고 또한 이를 계속 반복 확인한다.
  • 최대한 IasC 로 작업한다.
  • 기술 선언서 / 개발조직 Ground Rule 을 만든다.
    • 장애대응 방법과 태도(장애를 막기보다는 관리한다, 비난하지 말고 함께 문제를 해결한다던가)
    • 테스트 커버리지
    • CI 필요성
    • 문서화 수준 요구사항..
    • 지금 이 문서 같은 것을 해당 조직의 기술에 맞게 정리
    • false alarm 에 철저히 대응한다. false alarm 이 많아지면 정작 중요한 alarm 을 놓치게 된다.
    • 최소한의 기술 요구사항등을 정리해 둔다.
      • 프로젝트 구성
      • 개발 환경구성방법
      • CI 구성 요구사항
      • Test 방법
    • 그리고 지속적인 회고로 기술 선언서를 개선한다.
    • 모든 Rule 에는 그것이 추구하는 가치(이유)를 함께 명시해야한다.
      • 변화의 흐름에 따라 규칙이 오히려 생산성을 저해시키는 경우가 생기는데
      • 그럴 때 왜 이런 규칙이 생겼는지 그 규칙이 추구하는 가치가 무엇인지를 확인하고 규칙을 변경할 수 있어야 한다.

git push force 금지

  • git rebase 하는 습관이 있는 개발자들로 인해 지속적인 conflict 가 발생한다.
  • conflict 발생은 그 자체로 개발 시간을 잡아먹는 요소이고
  • 무엇보다 문제는 conflict 해결중에 잘못 해결해서 소스코드가 사라지거나 꼬여버리는 문제로 버그가 양산된다.
  • 아예 rebase 를 금지시키고, rebase 시에 하게 되는 push force도 금지시킨다.
  • rebase 대신 리포지토리 fork 후 개발이 끝난 것을 squash merge request 방식으로 해소하도록 한다. 작업 완료 후 fork 한 리포지토리는 무조건 삭제하고 그걸 받았던 local 리포지토리도 삭제한다.

지속적인 업그레이드

  • 프로그래밍 언어와 Framework 를 지속적으로 업그레이드 해줘야한다.
    • 비즈니스가 발전하면서 생기는 요구사항을 빠르고 정확하게 해내는 기술도 계속 발전하는데 프로그래밍 언어와 프레임워크가 지나치게 구버전이거나 혹은 반대로 지나치게 신버전이면 이러한 흐름을 따라갈 수 없다.
    • 따라서 한발자국 뒤 정도 수준으로 꾸준히 언어와 프레임워크를 업그레이드해줘야 한다.
    • 이는 또한 개발조직의 개발자 만족도와도 직결되는 문제이다.
    • 지나치게 구버전은 신기술을 원하는 개발자들의 욕구를 채워줄 수 없고
    • 지나치게 신버전은 시스템의 안정성을 해친다.
  • 안정적 업그레이드의 필수 요소
    • 넓은 코드 커버리지로 촘촘하게 짜여진 Unit Test 와 통합 테스트
    • 언어/프레임워크/라이브러리의 학습 테스트도 권장한다.
      • 프로젝트에 학습 테스트를 넣으면 업그레이드시에 조기 문제 발견이 쉬워질 수 있다.
      • 단, 비즈니스 코드 테스트와 너무 섞여서 비즈니스 코드 테스트를 실행하기 어렵게 하지 않게 잘 분리 해본다.

참조

web/신규서비스.txt · 마지막으로 수정됨: 2024/03/08 11:26 저자 kwon37xi