1장. 인프라 아키텍쳐를 살펴보자

  • Scale-up: 서버의 자체 성능을 증가시키는 것 = "수직 스케일"
  • Scale-out: 기존의 서버와 같은 사양 또는 비슷한 사양의 서버 대수를 증가시키는 것 = "수평 스케일"

집약형 아키텍쳐 <=> 분할형 아키텍쳐(분산 시스템)

  • 수직 분할: 서버별로 다른 역할 담당하는 경우
    • 클라이언트 - 서버형: ex) 클라이언트측(PC, 스마트폰, 태블릿 등)에 전용 SW 설치
      • 클라이언트: 화면 표시나 단순 계산
      • 서버: 데이터 제공
    • 3계층형 아키텍쳐
      • 프레젠테이션 계층: 사용자 input을 받고 웹 브라우저 화면 표시
      • 애플리케이션 계층: request에 따라 데이터 처리
      • 데이터 계층: 애플리케이션 계층에 따라 데이터 입출력
  • 수평 분할: 용도가 같은 서버를 늘려나가는 방식
    • 단순 수평 분할형 아키텍쳐: 샤딩/파티셔닝 ex) 서울 지사 시스템과 부산 지사 시스템을 나눈다
    • 공유형 아키텍쳐: 단순 분할형과 달리 일부 계층(데이터 계층)에서 데이터 교환(동기화 등)이 이루어진다.

 

위 수직/수평 분할을 적절히 조합하여 아래 아키텍쳐를 만들 수 있다.

  • 지리 분할형 아키텍쳐: 업무 연속성 및 시스템 가용성을 높이기 위해 지리적으로 분할
    • 스탠바이형 아키텍쳐: 물리 서버를 최소 두 대를 준비하여 한 대가 고장나면 가동중인 소프트웨어를 다른 한 대로 옮겨서 운영하는 방식
    • 재해 대책형 아키텍쳐: 위와 마찬가지로 동일한 환경의 서버를 별도 사이트에 배치하고 재해가 발생하면 다른 사이트에 있는 정보 사용한다. 애플리케이션 최신화와 데이터 최신화를 주의해야 한다.

클라우드형 아키텍쳐: 가상화: 집약형+분할형 양쪽의 장점을 취하는 방법

  • 물리 서버를 가상화 기능으로 여러 대의 가상 서버로 분할

 

 

2장. 서버를 열어보자

서버는 rack에 장착되고, 랙에는 서버 외에도 HDD, 네트워크 스위치 등이 붙어있다. 

  • CPU(=코어): 서버 중심에 위치하며 연산 처리(1초에 10억회 정도)를 한다. 코어는 각자가 독립된 처리를 할 수 있다.
    • 프로세스/사용자IO가 OS에 명령 -> OS가 CPU에 명령을 내림
    • CPU 자체에도 메모리가 있다. (레지스터, L1/L2/L3 캐시 등)
    • 레지스터: CPU 구조의 일부로 CPU에서 연산에 사용하는 데이터를 기억하는 소규모 기억장치
  • 메모리: CPU 옆에 위치하며 데이터를 저장하거나, 처리 결과를 받는다. 속도가 빠르지만 서버를 재시작하면 정보가 없어짐.
    • L1/L2/L3캐시 등으로 메인 메모리로부터 데이터 처리 시간을 줄일 수 있다. L1/L2는 각 코어 전용, L3는 CPU 전체가 공유
    • 컴퓨터는 캐시로부터 데이터를 읽어들여 레지스터에 저장한 후, 레지스터 사이로 데이터를 전달하면서 연산을 수행한다.
    • 메모리 인터리빙(memory interleaving): 메모리를 여러 개의 모듈로 나누어 메모리에 접근하더라도 block 되지 않고 단위시간에 여러 메모리로 동시에 접근이 가능하도록 하는 기법 => 다음에 필요한 데이터를 미리 가져다놓는다
      • 상위 인터리빙: 메모리 주소의 상위 비트에 의해 모듈 선택, 하위 비트에 의해 모듈 내 주소 선택
        • 장점: 모듈간의 독립성 -> 한 모듈에 에러가 나더라도 해당 모듈만 영향
        • 단점: 동시 접근을 통한 성능 향상이 어렵다
      • 하위 인터리빙: 메모리 주소의 하위 비트에 의해 모듈 선택, 상위 비트에 의해 모듈 내 주소 선택
        • 장점: 다수의 모듈이 동시 동작 가능
        • 단점: 새로운 메모리 뱅크 추가 시, 전체에 영향을 주게 된다
      • 혼합 인터리빙: 메모리 뱅크를 몇개의 모듈로 나누어 구성하는데, 메모리 뱅크 선택은 상위 인터리빙 방식을 따르고, 뱅크 선택 후, 뱅크 내 모듈 간에는 하위 인터리빙 방식을 따른다.
    • 메모리 ~ CPU 간 데이터 교환은 채널을 통해 이루어진다.
  • IO 장치
    • HDD(하드디스크): 장기 저장 목적의 데이터  저장 장소
      • 자기 원반이 여러 개 들어 있음, 고속으로 회전해서 read/write 처리 => 회전 구조 때문에 물리 법칙에 좌우되며, 메모리처럼 순식간에 access 할 수 없다.
      • 서버와 IO 시에는 캐시를 통해 데이터를 교환한다.
        • Read => 캐시에 없는 경우 디스크에서 읽은 후 캐시에 올리고, 캐시를 서버에 반환
        • Write Through => 캐시와 저장소에 기록하고 IO 종료 여부를 서버에 알림
          • 장점: 캐시와 메모리에 업데이트를 같이 해버리는 방식이기때문에 데이터 일관성 유지, 안정적
          • 단점: 속도가 느린 저장소에 데이터를 기록할 때, cpu block 시간이 필요
        • Write Back => 캐시에 기록하고 IO 종료 여부를 서버에 알림(캐시 내에 일시적으로 데이터 저장할 때 사용)
          • 장점: 빠르다
          • 단점: 데이터 일관성이 깨진다
      • 대형 저장소와 연결할 때에는 일반적으로 FC(Fibre Channel) 케이블을 사용해서 SAN(Storage Area Network) 네트워크를 경유한다. 서버 사이에 FC 포트가 없는 경우, 서버 사이의 통신을 위해 PCI 슬롯에 HBA 카드를 삽입하기도 한다.
      • c.f. 최근에는 SSD(반도체 디스크): 물리적인 회전 요소를 사용하지 않는 디스크
    • 네트워크 인터페이스: 서버와 외부 장비를 연결하기 위한 것(외부 접속용)
    • IOH/ICH: IO를 제어한다
      • IOH(IO 핸들러): 이전에는 메모리 IO가 주 역할이었지만, CPU에 그 역할이 옮겨간 후, 고속 처리가 필요한 PCI Express나 네트워크 IO를 제어한다. 또는 CPU간 데이터 전송 제어를 하기도 한다.
      • ICH(IO 컨트롤러): 속도가 느려도 괜찮은 DVD/USB 등의 IO를 제어하거나, IOH간의 데이터 전송 제어를 한다.
    • 아래는 다양한 I/O 예로, HDD의 I/O와 DVD의 I/O 경로가 물리적으로 다르다는 것을 알 수 있다.

 

  • 버스: 서버 내부 컴포넌트들을 연결시키는 회선으로, 전송능력(대역: 전송폭+전송횟수 = throughput)이 중요하다.

 

 

3장. 3계층형 시스템을 알아보자

3계층형 시스템의 전체 구성

  • 프로세스 및 스레드: 프로그램이 서버 내부의 디스크 상에 설치된 후, 실행 요청이 들어오면 커널이 프로세스를 실행 시키고 프로세스 시작 시에 요청 분량만큼 메모리를 할당한다. 즉, 프로세스 및 스레드는 프로그램 실행 후 OS 상에서 실행 되면서 어느 정도 독립성을 가지며 동작하는 것이다. 
    • 프로세스: 전용 메모리 공간을 이용해서 동작한다. (웹서버) => 독자 메모리 공간을 가지고 있기 때문에 생성 시 CPU 부하가 스레드와 비교할 때 높아진다. 따라서 멀티 프로세스 애플리케이션에서는 프로세스 생성 부담을 낮추기 위해 미리 프로세스를 시작시켜 둔다.(풀링 pooling)
    • 스레드: 다른 스레드와 메모리 공간을 공유하고 있다. (AP 서버) => 메모리 공간을 공유하기 때문에 의도하지 않는 데이터 읽기/쓰기가 발생할 수 있다.
    • 프로세스가 메모리 공간을 공유할 수 없는 것은 아니다. (오라클 DB) 여러 프로세스가 공유 메모리 공간을 상호 이용할 수 있는 방법도 있다. 이와 별도로 프로세스 별로 독자 메모리 영역도 있어서 용도별로 나누어 사용할 수 있다. 캐시 데이터를 일반적으로 프로세스 간에 공유하기 때문에 공유 메모리 상에 둔다.

 

  • OS 커널: OS 처리는 원칙적으로 커널을 통해 이루어진다. 커널은 다른 Layer(?)에서 이루어지는 처리를 은폐하고 편리한 인터페이스를 제공한다. 커널의 역할은 아래 6가지로 정리할 수 있다.
    • 시스템 콜 인터페이스: 프로세스나 스레드로부터(애플리케이션) OS 를 통해 어떤 처리를 하고 싶으면 시스템 콜로 커널에 명령을 하고, 이 명령이 인터페이스를 통해 전달된다. (키보드나 마우스 입력은 인터럽트로 처리)
    • 프로세스 관리: 가동되고 있는 프로세스 관리와 CPU 이용 우선순위 등을 스케쥴한다. => CPU 코어 고려
    • 메모리 관리: 서버 상의 메모리를 단위 크기의 블록으로 분할하여 프로세스에 할당하거나, 메모리 독립성 등을 관리 => 물리 메모리 공간 고려
    • 네트워크 스택: 네트워크를 관리 => 6장
    • 파일 시스템 관리: 디렉터리 구조 제공, 액세스 관리, 고속화, 안정성 향상 등의 기능을 제공함으로써 애플리케이션이 `파일` 단위로 데이터를 작성하거나 삭제할 수 있음
    • 장치 드라이버: 디스크, NIC, HBA 등의 물리 장치용 인터페이스 제공 => 이 장치 드라이버가 해당 OS 의 표준 장치로서 커널을 경유해 이용할 수 있도록 한다.
    • 커널 설계 및 구현 방식
      • monolithic 커널: OS의 주요 구성 요소를 모두 하나의 메모리 공간을 통해 제공한다. ex) UNIX 계열의 OS, 리눅스
      • micro 커널: 최소한의 기능만 커널이 제공하고, 그 외 기능은 커널 밖에서 제공한다. ex) Mac OS X

 

  • 웹 데이터 흐름
  •  

'작성중...' 카테고리의 다른 글

Java Reflection이란?  (0) 2021.01.23
Java HashMap vs LinkedHashMap vs TreeMap  (0) 2020.12.19
Web Development w/ Google’s Go (golang) Programming Language:  (0) 2020.12.17
Go: 기본 문법  (0) 2020.12.17

1장. 화폐 예제

  • 객체를 만들면서 시작하는 것이 아니라 테스트를 먼저 만들어야 한다.
  • 테스트를 작성할 때는 메소드의 완벽한 인터페이스에 대해 상상 해보는 것이 좋다.
  • 테스트와 코드 사이의 결합도를 낮추기 위해, 테스트하는 객체의 새 기능을 사용한다.

 

TDD를 할 때에는 아래와 같은 순서로 코드를 짜게 된다.

1. 테스트를 작성 -> fail
2. 컴파일 되게 하기
3. 테스트 pass
4. 중복 제거 (추상화)

화폐 예제를 위 순서에 따라 조금씩 기능을 추가하는 것을 따라가다 보니 어느새 필요한 여러 기능이 구현되어있었다.

`어떤 테스트들이 추가로 필요할까?` 생각하는 것, 그리고 `중복을 제거하기 위한 리팩토링`을 예제를 통해 볼 수 있는 챕터였다.

 

2장. xUnit

xUnit 챕터는 테스트 프레임워크를 만드는데, 그 것을 위한 테스트 케이스를 작성하고 있다.

테스트 프레임워크에서 테스트 케이스 진행 순서는 아래와 같다.

1. 테스트 메서드 호출
2. setUp - 자원 할당
3. tearDown (실패하더라도) - 자원 반납
4. 여러개의 테스트 실행
5. 결과 수집 및 출력
  • 테스트 커플링을 만들지 말아야 한다. -> 테스트(객체) 간에 영향을 주지 않도록 해야 함

나도 테스트 툴을 만들었던 경험이 있는데, 이 챕터를 읽었다면 좀 더 쉽게 구조를 잡을 수 있지 않았나 싶은 생각이 들었다.

큰 테스트를 작게 쪼개며 테스트를 격리하는 것에 대해 생각해 보게 되었다.

 

3장. 테스트 주도 개발의 패턴

  • 테스트 주도 개발 패턴
    • 테스트 데이터의 경우 산발적으로 쓰지 말자. (1과 2 중에 선택해야 한다면 1)
    • 테스트 데이터로는 여러 의미를 담는 동일한 상수를 쓰지 말자. (1+1 보다는 1+2를 사용)
  • 빨간 막대 패턴: 테스트를 언제 어디에서 작성할 것인지, 테스트 작성을 언제 멈출지
    • 오퍼레이션이 아무 일도 하지 않는 경우를 가장 먼저 테스트 하도록 하자
  • 테스팅 패턴
    • 큰 테스트 케이스는 작은 테스트케이스들로 나누자 (A, B, C를 한번에 동작하게 하기 보다는 A->B->C 순서대로 동작하도록)
    • 비용이 많이 들거나 복잡한 리소스에 의존하는 객체(ex. DB)를 테스트 할 땐, Mock 객체를 만들자.
      • Mock 객체: 스스로 검증할 수 있는 능력이 있고, 외부에서 결과값을 조정할 수 있다.
    • 한 객체가 다른 객체와 올바르게 대화하는지 보기 위해서는 Self Shunt 패턴을 이용 하자. 즉, 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 한다. => 테스트 케이스가 Mock 객체가 됨
      • Self Shunt 패턴의 경우 테스트 케이스가 구현 할 인터페이스를 얻기 위해 인터페이스 추출 과정이 필요하기 때문에, 인터페이스 추출이 쉬운지 존재하는 클래스를 블랙박스로 테스트 할 것인지 정해야 한다.
    • 로그 문자열 등을 가지고 있다면 테스트 시 문자열 집합을 저장하고 있다가 집합 비교 등을 수행하면 된다. 로그 문자열은 Self Shunt 패턴과도 잘 동작한다.
    • 발생하기 힘든 에러 상황의 경우, 그냥 예외를 발생시키는 특수한 객체를 익명 내부 클래스 등으로 만들어 이를 호출하도록 하면 된다.
      • 파일 시스템의 여유 공간이 없을 경우 발생 할 문제 = 실제로 파일을 채우기 보다는 fileSystemError() 등을 호출하도록
  • 초록 막대 패턴: 코드가 테스트를 통과하게 만들기 위해서
    • 일단은 깨진 테스트에 대해 상수를 반환하게 하자
    • 컬렉션을 다루는 연산은 먼저 컬렉션 없이 구현하고, 그 다음 컬렉션을 사용하게 하자
  • xUnit 패턴
    • 판단 결과는 true/false
    • 객체를 세팅하는 코드가 여러 테스트에 걸쳐 동일 할 때에는 테스트 fixture을 만들어 두는 것을 선택하는 것도 괜찮다 = setUp()
      • setUp을 만든다면 메서드가 자동으로 호출된다는 점과, 어떻게 초기화 되는지 기억해야 한다.
      • fixture 중 외부 자원이 있을 경우 tearDown()으로 해제 함으로써 테스트가 실행되기 전화 후의 외부 세계가 동일하게 유지되도록
  • 디자인패턴
    • 커맨드: 간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요할 때 => 계산 작업에 대한 객체를 생성하여 이를 호출하도록 (ex. Java Runnable)
    • 값 객체: 널리 공유해야 하지만, 동일성은 중요하지 않을 때 => 객체가 생성될 때, 객체의 상태를 설정 후 상태가 변할 수 없도록 + 연산 결과는 항상 새로운 객체를 반환. 즉, 객체를 값처럼 행동하도록 만든다.
    • 널 객체
    • 템플릿 메서드: 작업 순서는 변하지 않지만 각 작업 단위에 대한 미래의 개선 가능성을 열어두고 싶은 경우 => 다른 메서들을 호출하는 내용으로만 이루어진 메서드를 만든다. => setUp, run, tearDown 구현
    • 플러거블 객체: 변이를 표현할 때 가장 간단한 방법은 명시적인 조건문을 사용하는 것이다. 그런데 이 명시적인 조건문은 중복될 수 있는데, 이 때 중복을 제거하기 위해 플러거블 객체를 사용하게 된다.
    • 플러거블 셀렉터: 인스턴스별로 다른 메서드가 동적으로 호출되게 하려면, 메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출하면 될 것이다. 플러거블 셀렉터란 이러한 경우 리플렉션을 이용하여 동적으로 메서드를 호출하는 것이다. (리플렉션이므로 남용하진 않도록 하자)
    • 팩토리 메서드: 새 객체를 만들 때 유연성을 원하는 경우
    • 임포스터: 기존의 코드에 새로운 변이를 도입하기 위해 기존의 객체와 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가한다. 이를 임포스터라고 하는데, 이 예로서는 널 객체와 컴포지트 패턴이 있을 수 있다.
    • 컴포지트 패턴: 하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행도하게 만들기 위해서는 객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현하게 된다. 이를 컴포지트 패턴이라고 한다. 
      • 중복이 나타나는 순간 컴포지트패턴을 도입해 보자
    • 수집 매개 변수: 여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집해야 할 때에는 결과가 수집될 객체를 각 오퍼레이션의 매개변수로 추가해 주면 된다.
      • ex. java.io.Externalizable - writeExternal 메서드는 객체와 그 객체가 참조하는 모든 객체를 기록한다. 이를 위해서 메서드는 수집 매개 변수로서 ObjectOutput을 전달한다

 

  • 리팩토링
    • 메서드 추출하기
    • 메서드 인라인
    • 인터페이스 추출/객체 추출하기
    • 메서드 객체 - 여러 개의 매개 변수 및 지역 변수를 갖는 복잡한 메서드의 경우 꺼내서 객체로 만든다

 

느낀 점

일단 책이 말하는 식으로 진행이 되어서 읽기 쉬웠고, 양도 얼마 되지 않아서 금방 다 읽었다.

내용 자체는 무엇을 말하려는지 알 것 같은데.. 흠,, 프로젝트에 직접 적용해 보아야 좀 더 이해한 내용이 명확해 질 것 같다.

+ Recent posts