본문 바로가기

UCLA CS 130: SWE Capstone project

[Midterm] 구글 개발자에게 듣는 소프트웨어 엔지니어링 기초 [상]

* 본 자료는 제가 UCLA 학부 시절 CS 130: Software Engineering 수업을 수강하면서, 절반 가량의 시간이 지난 시점까지의 강의 중 일부를 중간고사를 대비하기 위해 모두어 둔 내용입니다.

* 참고로 해당 수업은 UCLA에서 제공하는 컴퓨터 공학과 학부 대상 커리큘럼의 끝에 있는 Capstone 수업으로, 3명의 강사진 모두 구글 현직 개발자들입니다.

 

Source Control

= Version control. 코드 변경 사항을 추적하고 관리하는 활동.

 

1 여러개의 revision control system의 각각의 원리를 알고 장단점 비교하기

  1. Git:
    • 유형: 분산 버전 관리 시스템 (DVCS)
    • 특징:
      • 로컬과 원격 리포지토리 개념을 모두 사용합니다.
      • 변경 내용은 '커밋'으로 저장되며 각 커밋은 이전 버전과의 차이를 나타냅니다.
      • 브랜치와 병합을 통해 여러 개발자와의 협업이 용이합니다.
      • GitHub, GitLab과 같은 플랫폼과 연동하여 사용될 때 효과적입니다.
    • 장점: 매우 유연하며 대규모 프로젝트와 팀에 적합합니다.
    • 단점: 초보자에게 다소 복잡하고 학습 곡선이 가파릅니다.
  2. Subversion (SVN):
    • 유형: 중앙집중식 버전 관리 시스템 (CVCS)
    • 특징:
      • 중앙 서버에 모든 파일의 버전 정보가 저장됩니다.
      • 클라이언트는 변경사항을 중앙 서버에 커밋하여 다른 사용자와 공유합니다.
      • 권한 관리와 디렉토리 버전 관리가 가능합니다.
    • 장점: 설정과 관리가 비교적 간단하며 작은 팀에 적합합니다.
    • 단점: 중앙 서버에 의존하기 때문에 서버에 문제가 생기면 작업이 중단될 수 있습니다.
  3. Mercurial:
    • 유형: 분산 버전 관리 시스템 (DVCS)
    • 특징:
      • Git과 유사하게 로컬과 원격 리포지토리 개념을 사용합니다.
      • Python으로 작성되어 있으며, 크로스 플랫폼 지원이 우수합니다.
    • 장점: 사용이 비교적 간단하며, Git보다 학습 곡선이 완만합니다.
    • 단점: Git에 비해 커뮤니티와 지원이 작습니다.

 

2 git

 

Git의 사용

  • 기본 사용법: 파일을 로컬에서 수정하고 git add, git commit을 사용하여 변경사항을 커밋합니다.
  • 원격 리포지토리와 협업: Gerrit 같은 코드 리뷰 도구를 사용하여 코드의 리뷰를 받고, git push와 git pull --rebase를 사용하여 원격 리포지토리와 동기화합니다.
  • 기본적인 커맨드들:
    1. git init
      • 설명: 새로운 Git 저장소를 초기화합니다. 이 명령을 실행하면 현재 디렉토리에 .git 디렉토리가 생성되며, 이곳에는 모든 버전 관리 정보가 저장됩니다.
      • 사용 예: 프로젝트 시작 시 최초 한 번 실행하여 Git 저장소를 생성합니다.
    2. git add
      • 설명: 하나 이상의 파일을 스테이징 영역(커밋하기 전 상태)에 추가합니다. git commit을 실행하기 전에 git add를 사용하여 변경된 파일들을 선택합니다.
      • 사용 예: git add filename 혹은 변경된 모든 파일을 추가하려면 git add .를 사용합니다.
    3. git commit
      • 설명: 스테이징 영역에 있는 파일들을 저장소의 새로운 버전으로 커밋합니다. 커밋 시 -m 옵션을 사용하여 커밋 메시지를 함께 지정할 수 있습니다.
      • 사용 예: git commit -m "Initial commit"
    4. git status
      • 설명: 현재 작업 디렉토리와 인덱스(스테이징 영역)의 상태를 보여줍니다. 수정된 파일, 스테이징된 파일, 커밋되지 않은 변경 사항 등을 확인할 수 있습니다.
      • 사용 예: 어떤 파일이 변경되었는지 확인할 때 사용합니다.
    5. git push
      • 설명: 로컬 브랜치의 변경사항을 원격 저장소에 업로드합니다. 일반적으로 git push origin main과 같은 형식으로 사용되며, origin은 원격 저장소, main은 브랜치 이름을 의미합니다.
      • 사용 예: 로컬의 변경사항을 GitHub 같은 원격 저장소에 반영할 때 사용합니다.
    6. git checkout
      • 설명: 다른 브랜치로 전환하거나, 특정 파일의 특정 버전으로 돌아갈 때 사용합니다. -b 옵션과 함께 사용하면 새 브랜치를 생성하면서 해당 브랜치로 전환합니다.
      • 사용 예: git checkout -b new-branch로 새 브랜치를 생성하고 전환합니다.
    7. git pull
      • 설명: 원격 저장소에서 최신 변경사항을 가져와 현재 브랜치와 병합합니다. 이는 git fetch와 git merge의 단축 명령입니다.
      • 사용 예: 원격 저장소의 변경사항을 로컬에 적용할 때 사용합니다.
    8. git merge
      • 설명: 두 브랜치의 변경사항을 합칩니다. 보통 현재 브랜치로 다른 브랜치를 병합하는 용도로 사용됩니다.
      • 사용 예: git merge feature-branch로 feature-branch의 변경사항을 현재 브랜치와 합칩니다.
    9. git rebase
      • 설명: 한 브랜치의 변경 사항을 다른 브랜치로 가져와 재배치합니다. tree 형태로 엉망이 된 히스토리를 선형으로 만들 수 있습니다. 보통 아래의 두 경우에 사용합니다.
      • 사용 예:
        • 로컬 브랜치를 최신 상태로 유지: 작업 중인 로컬 브랜치를 main 브랜치의 최신 상태로 업데이트하고 싶을 때 rebase를 사용할 수 있습니다. main 브랜치의 최신 커밋들을 현재 브랜치의 시작 지점으로 옮긴 후, 현재 브랜치의 변경사항을 그 위에 다시 적용합니다.
        • 풀 리퀘스트 또는 병합 전에 충돌 해결: 다른 브랜치로 병합하기 전에 현재 브랜치의 커밋들을 재정렬하거나 충돌을 사전에 해결하기 위해 사용합니다.

소스 컨트롤의 전반적인 관리

  • 효율적인 협업: Git을 사용하여 효과적으로 협업하고, 코드의 품질과 프로젝트의 건강을 유지합니다. Git은 버전 관리, 변경 추적 및 협업을 용이하게 합니다.
  • 소스 컨트롤의 중요성 강조: 프로젝트의 모든 중요한 코드는 소스 컨트롤에 의해 관리되어야 하며, 이는 리스크 감소, 사용자 문제 해결 가능성 증가 및 법적 책임에서의 보호를 제공합니다.
  • 코드 변경 효율화: Git은 리포지토리에서 어떤 부분을 변경하면, 변경된 코드 전체를 어떻게 하는 것이 아니라, 기존의 것들과 비교하여 달라진 부분에 한해서 코드를 수정, 저장합니다.

 

Testing

 

Picking Good Test Cases

  • 테스팅은 공개 API를 대상으로 해야 하며, 일반적인 사용 사례를 먼저 테스트한 다음, 에러 처리와 특별히 복잡한 코드를 테스트해야 합니다.
  • 경계 조건, 사전 및 사후 조건, 방어적 시나리오, 그리고 에러 조건에 중점을 둬야 합니다.
  • API 문서에서 좋은 테스트 사례를 도출할 수 있으며, 각 공개 메소드의 동작이 검증되도록 해야 합니다.
  • 예를 들어, List 클래스에서 add(int index, E element) 메소드를 테스트할 때, 다양한 위치의 인덱스(시작, 중간, 끝), 다양한 유형의 요소들, 그리고 리스트의 다른 상태(비어있는, 채워진, 거의 가득 찬)에서 테스트해야 합니다.

Unit Testing, Using Fixures and Mocks

  • 유닛 테스트는 코드의 개별 단위를 격리하여 검증하고, 테스트하는 코드와 같은 언어로 작성되어야 합니다.
  • 복잡한 실제 객체의 동작을 모방하는 데 사용되는 mock 객체를 사용할 수 있으며, 실제 객체를 테스트에 포함시키기 어렵거나 불가능할 때 특히 유용합니다.
  • GUnit(구글 테스트) 같은 프레임워크를 사용하여 유닛 테스트와 픽스처를 구조화하세요.

Refactoring for Testability

  • 테스트 가능하도록 코드를 리팩토링해야 합니다. 이는 전역 변수, 길고 복잡한 메소드, 상태가 많은 객체, 긴밀한 결합, 그리고 나쁜 추상화와 같은 문제를 해결하는 것을 포함합니다.
  • 리팩토링은 테스트하기 쉬운 소프트웨어를 처음부터 설계하는 것을 의미하기도 합니다. 이는 깔끔한 인터페이스를 만들고 부작용을 최소화하는 것을 포함합니다.

Integration Testing

  • 이는 애플리케이션의 여러 구성 요소를 함께 테스트하여 그룹으로 제대로 작동하는지 확인하는 것을 포함합니다.
  • 예를 들어, 웹 서버와 구성 파서의 상호 작용을 테스트하여 서버가 주어진 구성 설정으로 올바르게 초기화되는지 확인합니다.

Other Kinds of Testing

  • 유닛 테스트와 통합 테스트 외에도 회귀 테스트, 시스템 테스트, 수용 테스트와 같은 다른 형태의 테스팅이 있습니다.
  • 각 테스트 유형은 다른 목적을 가지며 애플리케이션의 신뢰성과 성능에 대한 다양한 통찰을 제공합니다.

General Best Practices and Expectations

  • 코드는 기본적인 사용 사례와 일반적인 에러 조건에 대한 유닛 테스트를 가져야 합니다.
  • 거의 모든 클래스와 소스 파일에는 해당하는 유닛 테스트가 있어야 합니다.
  • 바이너리에는 적어도 하나의 통합 테스트가 있어야 합니다.

 

Code Reviews

코드 리뷰의 목적

  • 재사용성의 증명: 코드가 재사용 가능하다는 가장 좋은 증거는 리뷰를 통해 다른 사람이 그 코드를 가치 있고 이해할 수 있다고 판단하는 것입니다. 다른 사람이 스택에 통합하고 사용할 수 있어야 합니다.
  • 에러 발견: 코드 리뷰는 오류를 60-90% 포착할 수 있으며, 첫 번째 리뷰어와 리뷰가 가장 중요합니다.

코드 리뷰의 방법

  • 코드 리뷰 준비: 변경 사항을 작고 집중적으로 유지하고, 작업 진행 상황을 조기에 리뷰로 보내며, 자신의 작업을 스스로 리뷰해야 합니다.
  • 변경 사항 설명: 변경 사항이 무엇인지, 왜 변경을 했는지, 그리고 변경을 달성하기 위한 방법을 포함해야 합니다. 새로운 테스팅이 이루어졌는지도 고려해야 합니다.
  • 코드 리뷰 중에: 실행 오류, 불분명한 문서화, 오타, 스타일 위반, 나쁜/누락된 테스트, 버그를 지적해야 합니다. 신중한 사고를 통해 오류를 찾아내고, 코드의 목적에 대한 공유된 이해를 개발하며, 작은 변경 사항도 목표가 변할 수 있으므로 각 코드 리뷰는 수정할 기회를 제공합니다.

코드 리뷰 시 고려할 사항

  • 코드 리뷰는 경쟁이 아닙니다: 개인적인 공격을 피하고, 말을 신중하게 선택해야 합니다. 그러나 너무 쉽게 리뷰를 해서도 안 됩니다. 코드는 자신이 아니며, 리뷰는 개인적인 것이 아니라 코드의 질을 향상시키기 위한 과정입니다.
  • 리뷰 방법들: 회의에서 코드를 프로젝션하는 것, 페어 프로그래밍, 풀 리퀘스트, 코드 리뷰 도구 등 여러 방법이 있습니다. Gerrit, GitHub의 풀 리퀘스트 등이 리뷰 도구로 사용될 수 있습니다.

 

Web Server

  • Status Cats: HTTP 상태 코드를 좀 더 친근하게 표현한 것입니다. 예를 들어, "404 Not Found" 오류를 귀여운 고양이 이미지로 나타내는 인터넷의 유행을 의미할 수 있습니다.
  • HTTP Error Codes: 클라이언트 요청에 대한 서버의 응답을 나타내는 코드입니다. 이러한 코드들은 200번대(성공), 300번대(리다이렉션), 400번대(클라이언트 에러), 500번대(서버 에러)로 분류됩니다.
  • Error codes, Statelessness, Request verbs, Disconnect, Relative urls, Upgrade: HTTP 프로토콜의 핵심 개념들입니다. 에러 코드는 요청이 실패했을 때의 상황을 나타냅니다. 무상태성(Statelessness)은 HTTP가 이전 요청의 상태를 기억하지 않는다는 것을 의미합니다. 요청 동사(Request verbs)는 HTTP 메소드(GET, POST, PUT, DELETE 등)를 가리킵니다. Disconnect는 연결을 끊는 것을, 상대적 URL(Relative urls)은 전체 URL이 아닌 부분적인 경로를 사용하는 것을, Upgrade는 프로토콜 버전을 업그레이드하는 것을 의미합니다.
  • Cookie, REST, Keep-alive, Host: HTTP 헤더의 일부입니다. 쿠키(Cookie)는 사용자의 상태를 유지하는 데 사용되고, REST는 웹 API 디자인의 한 스타일을, Keep-alive는 연결을 지속적으로 유지하는 것을, Host는 요청이 전송되는 서버의 도메인을 의미합니다.

Basic Functionality of a Web Server:

  • 웹 서버의 기본 역할은 HTTP GET 요청에 대응하여 클라이언트에 정적 파일을 반환하는 것입니다.
  • 설정 값을 수락하고 처리할 수 있어야 합니다.
  • 클라이언트 요청을 다른 서버로 전달하는 역방향 프록시로서 기능할 수 있어야 합니다.

Additional Capabilities of a Web Server:

  • 동적 콘텐츠를 관리하고 인터랙티브한 웹사이트를 위한 HTTP POST 요청을 처리할 수 있어야 합니다.
  • 성능 향상을 위해 자주 접근하는 리소스의 복사본을 저장하는 여러 수준의 캐싱 메커니즘을 구현합니다.
  • 데이터 전송 크기를 줄여 로드 시간과 대역폭 사용을 개선하기 위한 압축 기술을 사용합니다.
  • 사이버 위협으로부터 보호하고 데이터 무결성 및 개인 정보를 유지하기 위한 견고한 보안 조치를 확보합니다.

 

Build Systems (CMake vs. Google build system)

일단 들어가기 전에,

- Build는 코드를 compile 하는 것: source code -> executable binary

- Deploy는 실행파일을 실행하는 것: run an executable

라고 정의한다고 한다.

 

1. CMake 설명

  • CMake는 소프트웨어 빌드 프로세스를 자동화하기 위한 도구입니다. 단일 설정 파일인 CMakeLists.txt와 함께 작동하며, 프로젝트의 빌드를 설정하고 관리하는 데 필요한 모든 지시사항을 포함합니다 .
  • 예를 들어, CMake를 사용하여 라이브러리를 정의하고(add_library) 실행 파일을 추가(add_executable)할 수 있으며, GTest를 사용한 테스트를 구성하는 방법을 제공합니다 .
  • CMake는 여러 출력 형식을 지원하고, 크로스 플랫폼 빌드를 용이하게 하는 기능을 갖추고 있습니다.

2. GNU 빌드 시스템 설명

  • GNU 빌드 시스템은 Autoconf와 Automake를 포함하며, 플랫폼 간 호환성을 제공하기 위해 Makefile을 자동으로 생성합니다 .
  • configure 스크립트를 실행하여 시스템 환경을 검사하고 적절한 Makefile을 설정하는 방식으로 동작합니다. 이후 make 명령어를 사용하여 빌드를 실행합니다 .
  • 주요 파일로는 Makefile.am, Makefile.in, configure, config.h 등이 있습니다.

3. CMake와 GNU 빌드 시스템 비교

  • 유사성:
    • 두 시스템 모두 복잡한 빌드 프로세스를 단순화하고 자동화합니다.
    • 프로젝트 빌드를 위한 설정 파일(CMakeLists.txt와 Makefile.am)을 사용합니다 .
  • 차이점:
    • CMake는 단일 도구를 사용하여 구성되며, 다양한 IDE와 통합될 수 있는 반면, GNU 빌드 시스템은 여러 도구의 조합으로 이루어져 있습니다.
    • GNU 빌드 시스템은 시스템에 설치된 패키지와 컴파일러를 탐지하고 이를 기반으로 Makefile을 생성하는 반면, CMake는 빌드 디렉토리 내에서 다수의 생성된 파일을 통해 이를 관리합니다 .
    • CMake는 구성이 간편하고 여러 출력 형식을 지원하는 반면, GNU 빌드 시스템은 GNU 표준에 맞는 Makefile을 생성하는 데 중점을 둡니다 .

 

Static vs. Runtime Analysis

 

Static Analysis: examining source code before a program is run - a.k.a. during compilation

 

컴파일러 경고

  • 컴파일러에서 경고를 활성화(-Wall은 GCC에서 사용)하여 사용하지 않는 변수나 함수와 같은 잠재적 문제를 찾습니다.
  • -Werror을 사용하면 경고를 오류로 바꿔 컴파일이 안 되게 하여, 경고가 있는 코드로 컴파일을 방지하고 깨끗한 코드를 유도합니다.

정적 타입 검사

  • 컴파일 시간에 타입 오류를 감지합니다. 예를 들어, 문자열을 정수 변수에 할당하려는 시도를 잡아냅니다.
  • C++과 같은 언어는 auto 키워드를 사용하여 컴파일 시간에 변수의 타입을 추론할 수 있는 기능을 제공합니다.

린터(Linters)

  • clang-tidy와 같은 도구는 코드를 검사하여 포맷 이상의 오류를 찾습니다—잠재적인 버그, 수상한 구조물 및 기술적으로 언어에서 허용되지만 실수일 가능성이 높은 타이포 등을 검사합니다.

정적 분석이란?

  • 코드를 실행하지 않고 분석하는 것을 말합니다. 종종 제어 흐름 그래프를 구축하고 문제를 나타낼 수 있는 패턴을 찾습니다.
  • 정적 분석은 use-after-free, 버퍼 오버런, 데드락 같은 다양한 오류를 코드 경로와 데이터 흐름을 검사하여 찾아낼 수 있습니다.

정적 분석의 작동 방식

  • 추상 해석(Abstract Interpretation): 코드가 실행될 때 무엇이 참인지를 추적하는 이론적 프레임워크입니다. use-after-free나 초기화되지 않은 변수와 같은 버그를 잡는 데 도움을 줍니다.
  • 데이터 흐름 분석(Data Flow Analysis): 변수가 프로그램의 특정 지점에서 가질 수 있는 가능한 값들의 집합을 추적하여 그 값들에 대한 잘못된 연산을 식별합니다.

정적 분석의 한계

  • 프로그램을 통한 경로의 수가 기하급수적이며, 이로 인해 잠재적인 거짓 긍정 또는 모든 가능한 오류를 잡지 못할 수도 있습니다.
  • 정적 분석 도구는 종종 많은 거짓 긍정과 실제 버그를 잡지 못하는 것 사이에서 균형을 찾아야 합니다.

한계 극복 방법

  • 언어 확장이나 주석 사용과 같은 방법으로 정적 분석기가 코드를 더 잘 이해할 수 있도록 도와, 더 정확한 결과를 얻을 수 있습니다.
  • TypeScript나 Java의 @VisibleForTesting 주석과 같은 도구는 분석을 수행하기 위한 더 많은 컨텍스트를 제공합니다.

사용 가능한 정적 분석 도구

  • 타입 검사(Type Checking): 변수가 선언된 타입과 일치하는 방식으로만 할당되고 사용되는지 확인합니다.
  • Lint 도구: clang-tidy

 

Runtime Analysis: examining the program while it is running

 

계측 런타임(Instrumented Runtimes)

  • 프로그램 실행 중에 오류를 검사하는 가상 머신이나 런타임입니다.

런타임 분석 작동 방식

  • 종종 프로그램의 실제 상태를 추적하고 프로그램이 실행되는 동안 문제가 발생하는 위치를 정확히 지적할 수 있습니다.

런타임 분석의 한계

  • 느린 속도와 제한된 테스트 커버리지입니다. 테스팅 동안 특정 코드 부분이 실행되지 않으면 런타임 분석은 거기에 있는 잠재적 문제를 잡아내지 못할 수 있습니다.

사용 가능한 런타임 분석 도구

  • gcov, asan, tsan, gdb, valgrind와 같은 도구들은 메모리 누수, 스레드 안전성, 코드 커버리지 등 프로그램 실행의 여러 측면을 모니터링하고 테스트할 수 있습니다.

 

예외 처리 + logging 등

로깅(Logging)에 대한 자세한 설명

 

1. 로깅의 목적

  • 개발(Development): 개발 중에 임시 로깅을 활용하여 프로그램의 흐름을 추적하거나, 진짜 기능을 구현하기 전에 로깅 문장을 임시 기능으로 사용할 수 있습니다.
  • 디버깅(Debugging): 지속적인 로깅은 서버 상태를 드러내고 오류 정보를 제공하여 문제 해결에 도움을 줍니다.
  • 보안(Security): 로그는 침입 시도나 악의적 활동을 감지하는 데 사용될 수 있습니다.
  • 모니터링(Monitoring): 서버 부하, 요청 및 응답 시간 등을 보고할 수 있습니다.
  • 사용자 통찰력(Usage Insights): 서버나 제품과의 사용자 상호작용을 기록하여 사용 분석(예: 사용자 위치, 일일 고유 사용자 수 등)을 수행할 수 있습니다.

2. 로깅 방법

  • Boost.Log 활용 예시:
#include <boost/log/trivial.hpp>
#include <iostream>

int main(int, char*[]) {
  BOOST_LOG_TRIVIAL(info) << "This is some info";
  BOOST_LOG_TRIVIAL(warning) << "Not great...";
  BOOST_LOG_TRIVIAL(error) << "OH NOOOO";
  BOOST_LOG_TRIVIAL(fatal) << "eff this I'm out";
  return 0;
}

 

3. 언제 로깅을 해야 하는가?

  • 중요한 사건이 발생할 때마다 로깅을 수행해야 합니다. 예를 들어, 웹 서버가 준비되거나 요청을 처리하거나 오류가 발생하거나 서버가 종료될 때 등이 해당됩니다.

4. 로깅 위치

  • 로그는 콘솔(stdout/stderr), 디스크 파일, 원격 서버 등 다양한 위치에 저장할 수 있습니다. 로그 파일은 수동 검사가 가능하며, 도구로 파싱할 수 있어야 하고, 디스크를 가득 채우지 않아야 합니다. 또한 재시작이나 충돌 후에도 로그가 유지되어야 하며, 하드웨어 실패에 대한 내구성을 가져야 합니다.

예외 처리(Exception Handling)에 대한 자세한 설명

 

1. 예외 처리의 중요성

  • 예외 처리는 오류가 발생했을 때 프로그램이 적절히 반응하도록 하는 중요한 기능입니다. 이는 애플리케이션이 예기치 않은 상황에서도 안정적으로 작동하도록 보장하는 데 필요합니다.

2. 오류 처리 전략

  • 오류가 발생했을 때, 적절한 로깅을 통해 오류를 기록하고, 필요한 경우 사용자에게 오류 정보를 제공하며, 오류의 원인을 분석하여 향후 비슷한 오류가 발생하지 않도록 조치를 취할 수 있습니다.

3. 예외 처리 방법

  • 시스템 오류(System Errors): 하드웨어 오류나 시스템 리소스 문제와 같은 심각한 오류는 대부분의 경우 복구가 불가능하며, 이 경우 프로그램을 안전하게 종료하는 것이 최선일 수 있습니다.
  • 애플리케이션 오류(Application Errors): 잘못된 입력이나 파일 누락과 같은 문제는 사용자에게 오류를 알리고, 적절한 조치를 취할 수 있는 기회를 제공하여 해결할 수 있습니다.

이러한 로깅과 예외 처리 방법은 프로그램의 안정성을 유지하고, 오류 발생 시 신속하게 대응할 수 있도록 도와줍니다. 이러한 기술들은 개발자가 효과적으로 오류를 관리하고, 사용자 경험을 향상시키는 데 중요한 역할을 합니다.