1. 컨테이너 기술이란?
- 모놀리스 애플리케이션에서 MSA 으로 독립적 배포가 가능해지면서 발생한 관리 및 종속성 문제 해결
- 격리된 실행 환경 제공, 호스트 OS 에서 독립된 프로세스로 실행되어 자원 소비 및 오버헤드 최소화

 


2. 도커란?
- 컨테이너 기술을 사용해서 애플리케이션을 패키징, 배포, 실행하기 위한 오픈 소스 플랫폼
- 도커 파일로 이미지를 빌드하고, 이미지로 컨테이너를 실행한다.

 


3. 도커 파일, 도커 이미지, 도커 컨테이너의 개념은 무엇이고, 서로 어떤 관계입니까?
- 도커 파일(Dockerfile)
    - 도커에서 이미지를 생성하기 위해 작성하는 파일.
    - 컨테이너 설정을 정의한 것
- 도커 이미지(Docker Image)
    - 실행 가능한 컨테이너의 빌드된 상태
    - 애플리케이션을 실행하기 위한 모든 환경을 포함한다.
- 도커 컨테이너(Docker Container)
    - 도커 기반 컨테이너 이미지에서 생성된 리눅스 컨테이너
    - 실행 중인 컨테이너는 도커를 실행하는 호스트에서 실행되는 프로세스
    - 호스트와 호스트에서 실행 중인 다른 프로세스와 격리되어 있다.

7장 분산 시스템을 위한 유일 ID 생성기 설계

1단계 문제 이해 및 설계 범위 확정

  • ID 특성
    • 유일성, 정렬 가능
  • 새로운 레코드의 ID의 증가 값
    • 증가 값이 1씩 증가하지는 않지만, 과거의 값보다 나중에 만든 값이 더 큰 ID를 갖는다.
  • ID는 숫자로 구성
  • 시스템 규모

2단계 개략적 설계안 제시 및 동의 구하기

  • 분산 시스템에서 유일성이 보장되는 ID 생성 방법
    • 다중 마스터 복제(multi-master replication)
    • UUID(Universally Unique Identifier)
    • 티켓 서버(ticket server)
    • 트위터 스노플레이크(twitter snowflake) 접근법

  • 다중 마스터 복제

  • 데이터 베이스의 auto_increment 기능을 활용한다.
  • 증가 값은 k만큼 증가
  • k = 현재 사용 중인 데이터베이스 서버 수
  • 단점
    • 여러 데이터 센터에 걸쳐 규모를 확장하기는 어렵다.
    • ID 유일성은 보장되지만, 시간 흐름에 맞추어 커지도록 하기에는 힘들다.
    • 서버 추가나 삭제할 때에도 잘 동작하기 힘들다.

  • UUID

  • 컴퓨터 시스템에 저장되는 정보를 유일하게 식별하기 위한 128비트 수
  • 서버 간 조율 없이 독립적으로 생성 가능
  • 각 웹 서버는 별도 ID 생성기로 독립적인 ID 생성이 가능하다.
  • 장점
    • UUID 만드는 것이 간단하고, 동기화 이슈도 없다.
    • 규모 확장이 간단하다.
  • 단점
    • ID가 128비트이기 때문에 길다.
    • ID 시간순 정렬이 어렵다.
    • ID에 숫자가 아닌 값이 포함된다.

  • 티켓 서버

  • auto_increment 기능을 갖춘 데이터베이스 서버(티켓 서버)를 중앙 집중형으로 하나만 사용한다.
  • 장점
    • 유일성이 보장되는 숫자 ID를 간단하게 만들 수 있다.
    • 규모가 적은 애플리케이션에 적합
  • 단점
    • 티켓 서버가 SPOF(Single-Point-of-Failure)
    • 장애가 발생하면, 해당 서버를 사용하는 모든 시스템에 영향을 끼친다.

  • 트위터 스노플레이크 접근법

  • ID를 바로 생성하기 전에, divide and conquer 적용
  • sign : 음수, 양수 구분
  • timestamp : 기원 시각(epoch) 이후로 몇 밀리초가 경과했는지 나타내는 값
  • 데이터센터 ID
  • 서버 ID
  • 일련번호 : 각 서버에서 ID 생성할 때마다 일련 번호를 1씩 증가

📌 대규모 시스템 설계 기초

13장 : 검색어 자동 완성 시스템

  • 검색어를 입력하고 있을 때, 자동으로 완성되는 검색어 만들기
  • 검색어 자동완성(autocomplete, typeahead, search-as-you-type, incremental search)
  • 예제) 검색창에 단어를 입력했을 때 자동완성 되는 검색어들


1단계 문제 이해 및 설계 범위 확정

  • 요구사항 파악하기
    • 자동완성 검색어는 첫 부분인지, 중간 부분인지
    • 몇 개의 검색어를 표시
    • 검색어를 고르는 기준 : 인기순, 최신순
    • 맞춤법 검사 기능을 제공
    • 다국어 지원
    • 대문자, 소문자, 특수 문자 처리 여부
    • 사용자의 수는?
  • 요구사항 정리하기
    • 빠른 응답 속도
    • 검색어의 연관성
    • 정렬 : 계산 결과는 인기도 등의 순위 모델을 사용해야 한다.
    • 규모 확장성 : 트래픽을 감당할 수 있도록 확장 가능해야 한다.
    • 고가용성 : 일부에 장애가 발생하더도, 시스템은 계속 실행되어야 한다.
  • 개략적 규모 추정
    • 일간 사용자 천 만명 가정
    • 한 사용자는 평균적으로 일일 10건 검색
    • 평균적으로 20바이트의 데이터를 입력한다고 가정
      • 문자 인코딩 방법은 ASCII를 사용하면 1문자 = 1바이트
      • 평균적으로 질의문은 4개의 단어로 이루어져 있다.
      • 각 단어는 평균적으로 다섯 글자로 구성
      • 결과 = 질의당 평균 4 X 5 = 20 바이트
    • 검색 창에 글자를 입력하면, 클라이언트는 백엔드에 요청을 보낸다.
    • 평균적으로 1회 검색당 20건의 요청이 백엔드에 전달
    • 계산 = 1000만 사용자 X 10 질의 / 일 X 20자 / 24시간 / 3600초)
    • 초당 24000건의 QPS
    • 질의 중에 20%가 신규 검색어라면, 1000만 사용자 X 10질의 / 일 X 20자 X 20%
    • 0.4GB의 신규 데이터가 시스템에 추가된다.

2단계 개략적 설계안 제시 및 동의 구하기

  • 시스템 구성 정리
  1. 데이터 수집 서비스(data gathering service)
  2. 질의 서비스(query service)

데이터 수집 서비스

  • 사용자가 입력한 질의를 실시간으로 수집하는 서비스
  • 데이터가 많은 애플리케이션에서는 어려운 작업
  • 질의문과 사용빈도를 저장하는 빈도 테이블(frequency table)이 있다고 가정

질의 서비스

  • 주어진 질의에 다섯 개의 인기 검색어를 정렬하는 서비스
  • 주어진 빈도 테이블
    • query : 질의문을 저장하는 필드
    • frequency : 질의문이 사용된 빈도를 저장하는 필드

  • 사용자가 "tw" 를 검색하게 되면, 아래와 같이 자동 완성 검색표가 완성 되어야 한다.
검색어 : tw
twitter
twitch
twilight
twin peak
twitch peak

 

  • 질의문 SQL
SELECT * FROM frequency_table
WHERE query Like `prefix%`
ORDER BY frequency DESC
LIMIT 5

 

  • 하지만 위와 같이 설계한다면, 데이터가 많아질 때 데이터베이스의 병목 현상이 일어날 수 있다.

3단계 상세 설계

  • 트라이(trie) 자료구조
  • 데이터 수집 서비스
  • 질의 서비스
  • 규모 확장이 가능한 저장소
  • 트라이 연산

트라이 자료구조

  • 관게형 데이터베이스를 사용할 때 가장 인기 있는 다섯 개의 질의문을 골라내는 방법은 효율적 X
  • 트라이 자료구조로 해결
  • 트라이
    • 문자열들을 간략하게 저장할 수 있는 자료구조
    • 트리 형태의 자료구조
    • 루트 노드는 빈 문자열
    • 각 노드는 하나의 문자만 저장한다.
  • tree, try, true, toy, wish, win 질의어를 트라이에 보관할 때

  • 빈도를 따라 정렬해야 하기 때문에, 빈도 정보를 저장할 빈도 테이블

  • 빈도 테이블을 트라이 노드에 저장하게 되면

  • 질의어 찾는 방법
    • p(접두어 길이), n(트라이 노드 개수), c(주어진 노드의 자식 노드 개수)
      • 해당 접두어를 표현하는 노드를 찾기 -> O(p)
      • 해당 노드부터 시작하는 하위 트리를 탐색해서, 모든 유효 노드를 찾는다. -> O(c)
      • 유효 노드를 정렬해서 가장 인기 있는 k개를 찾는다. -> O(clogc)
  • 예시 : k = 2, 질의어 'be'
    • 접두어 노드 'be' 찾기
    • 해당 노드의 하위 트리들을 탐색해서 모든 유효 노드를 찾는다.
    • 유효 노드를 정렬해서 2개만 고른다.

  • 최악의 경우에는 전체 트라이를 검색해야 하는 문제 발생
    • 해결 방법
      • 접두어 최대 길이 제한 두기
      • 각 노드에 인기 검색어를 캐시


데이터 수집 서비스

  • 계속 대용량의 데이터를 트라이에 갱신하면 성능 저하
  • 인기 검색어는 자주 바뀌지 않을 것이기 때문에 자주 갱신할 필요가 없다.
  • ex. 트위터는 검색어를 자주 갱신해주어야 하지만, 구글 같은 경우 자주 바꿔줄 필요 X

  • 데이터 분석 서비스 로그 : 검색창에 입력된 질의에 대한 원본 데이터

  • 로그 취합 서버 : 제각각인 데이터 형식들을 잘 집계(aggregation) 해서 쉽게 소비할 수 있도록 한다.
    • 트위터 같이 결과를 빨리 보여줘야 하면 데이터 취합 주기를 짧게, 대부분의 경우 일주일에 한 번 정도 로그를 취합해도 된다.
  • 취합된 데이터 : time, frequency 필드를 통해 해당 쿼리에 대한 시작한 날짜, 해당 주에 사용된 횟수의 합을 저장한다.
  • 작업 서버 : 주기적으로 비동기적 작업을 실행하는 서버 집합
    • 트라이 자료구조를 만들고, 트라이 데이터베이스에 저장하는 역할
  • 트라이 캐시 : 분사 산 캐시 시스템으로, 트라이 데이터를 메모리에 유지하여 읽기 연산 성능을 높인다. 매주 갱신하도록 한다.
  • 트라이 데이터베이스 : 지속성 저장소. 문서 저장소 or 키 값 저장소 형태로 사용
    • 문서 저장소(document store)
      • 새 트라이를 매주 만들어야 해서, 주기적으로 트라이를 직렬화하여 DB에 저장
      • MongoDB 같은 문서 저장소 활용
    • 키 값 저장소
      • 해시 테이블 형태로 변환
        • 트라이에 보관된 모든 접두어를 해시 테이블 키로 변환
        • 각 트라이 노드에 보관된 모든 데이터를 해시 테이블 값으로 변환


질의 서비스

  • 비효율성 개선 설계
  1. 검색 질의가 로드 밸런서로 전송
  2. 로브 댈런서가 해당 질의를 API 서버에 전송
  3. API 서버는 트라이 캐시에서 데이터를 가져와 해당 요청에 대한 자동완성 검색어 제안 응답 구성
  4. 데이터가 트라이 캐시에 없는 경우의 데이터는 데이터베이스에서 가져와서 캐시에 채운다.


트라이 연산

  • 트라이 생성 : 작업 서버 담당. 데이터 분석 서비스의 로그나 데이터베이스로부터 취합된 데이터를 이용
  • 트라이 갱신
    • 매주 한 번 갱신해서 기존 트라이를 대신 하는 방법
    • 각 노드를 개별적으로 갱신하는 방법 -> 트라이가 적을 때만 좋다.
      • 트라이 노드를 갱신할 때 그 위의 상위 노드도 갱신해야 하기 때문

  • 검색어 삭제
    • 폭력적인 질의어는 자동완성에서 제거한다.
    • 트라이 캐시 앞에 필터 계층(Filter layer)을 두고 부적절한 질의어가 반환되지 않도록 한다.
    • 검색어를 물리적으로 삭제하는 것은 다음 업데이트에 비동기적으로 진행


저장소 규모 확장

  • 서비스 크기가 확장될 때 규모를 확장할 수 있도록 해결한다.
  • 첫 글자 기준으로 샤딩(sharding) 해보기
    • 영어는 26개의 알파벳으로 이루어져 있다.
    • 서버 2개인 경우, a부터 m까지 첫 번째 서버, 나머지 두 번째 서버
    • 서버 3개인 경우, a부터 i까지 첫 번째 서버, ...
  • 다른 방법으로는 샤딩을 계측적으로
    • 첫 번째 샤딩에 첫 번째 글자
    • 두 번째 샤딩에 두 번째 글자
  • 하지만 데이터가 균등하지 않게 분배되는 문제가 생길 수 있따.
  • ex. c로 시작하는 단어가 x로 시작하는 단어보다 많은 경우
  • 검색어 대응 샤드 관리자(shard map manager)로 어떤 검색어가 어느 저장소 서버에 저장되는지에 대한 정보를 관리한다.


4단계 마무리

  • 다국어 지원을 하려면?
    • 트라이에 유니코드(Unicode) 데이터를 저장해야 한다.
    • 유니코드 : 모든 문자 체계를 지원하는 표준 인코딩 시스템
  • 국가별로 인기 검색어 순위가 다르면?
    • 국가별로 다른 트라이를 사용
    • 트라이를 CDN에 저장해서 응답속도 개선
  • 실시간으로 변하는 검색어 추이를 반영하려면?
    • 스트림 프로세싱에 필요한 시스템
    • 하둡 맵리듀스, 스파크, 카프카 등이 필요하다.

📌GitHub Actions 실습

CI/CD 란?

  • CI : Continuous Integration
    • 지속적인 통합
    • 코드, 빌드, 테스트 부분을 자동화해서
    • 커밋할 때마다 빌드와 자동 테스트로 동작을 확인해서 문제가 생기지 않도록 보장한다.
    • 개발에 더 많은 시간을 투자 가능
  • CD : Continuous Delivery / Deployment
    • 지속적인 제공, 배포
    • 배포 자동화 과정
    • CI 프로세스를 통과하면 마지막에 배포하는 과정

GitHub Actions

  • Workflow
  • Job
  • Step
  • Action

yaml 파일 예시

name: 'Workflows name'

on: push // on은 script 가 언제 돌아가야하는지에 대한 부분 -> push 가 들어갈 때마다 실행된다.

jobs: // 스크립트가 해야하는 행동
  first-job: // job의 이름이고, 정해진 규칙은 없다.
    name: 'First Job' // name에 지정한 것은 각각 안에서 여러가지 나뉘는데 각각 job에 대한 이름 설정 안하면 위에 셋팅한 이름으로 default

    runs-on: ubuntu-latest // 어느 가상 환경에 올릴 것인지 보통 3가지 - windows mac linux

    steps:
      - name: Say Hello World 1
        shell: bash
        run: |
          echo "Hello World from step 1"

      - name: Say Hello World 2
        shell: pwsh
        run: |
          echo "Hello World from step 2"

 

  • GitHub Actions 관련 문서 링크

https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows


 

GitHub Action 요구사항 해결 방안

  • OS 종류와 Node 버전이 다양한 경우

-> 매트릭스 빌드 사용

  • 빌드할 때마다 테스트 실행이 불편한 경우

-> 템플릿 변경으로 빌드 테스트 분리

  • 다른 job에서 빌드 아티팩트 접근이 안되는 경우

-> Built-in 스토리지 이용


매트릭스 빌드

  • 각 경우의 수에 맞게 빌드 아티팩트를 만들어서 실행
...
  strategy:
    matrix:
      os: [ubuntu-latest, windows-2016]
      node-version: [12.x, 14.x]
...
  • 우분투 12, 14버전
  • 윈도우 12, 14버전 실행

빌드, 테스트 분리

  • build를 build와 test로 분리
jobs:
  build:
    - run: npm ci
    - run: npm run build --if-present
    - run: npm test
jobs:
  build:
  ...
  test:
  ...

 


job 병렬 실행 -> 종속적으로 실행 시키기

jobs:
  build:
    ...
  test:
    needs: build // 이 부분이 실행되기 위해서는 build job이 필요
    ...

 


GitHub Actions 요구 사항 해결 방안

  • main 브랜치 에러 위험

-> 브랜치 정책 설정

  • Pull Request 는 언제 merge?

-> PR 리뷰 강제화

  • 늘어나는 Issue 와 PR 관리의 어려움

-> Custom action 으로 라벨링


Branch 정책 설정 방법

  • Repo 의 Settings
  • Branches 메뉴에서
  • Branch protection rules 에 있는 Add rule

 

 

 

 

  • Require pull request reviews before merging
    • 브랜치를 다른 브랜치와 merge 하려고할 때, PR 요청을 통해서 리뷰되어야만 가능
  • Require status checks to pass before merging
    • merge 전에 status check 를 통과해야 한다.

실습

  • Comment 에 "/close" 커맨드 입력 시 이슈 닫는 워크플로우 작성
name: 'Close Issue Condition Workflows'

on:
  issue_comment: 
    types: [ created ]

jobs: 
  close-issue:
    name: 'Close Issue Job' 
    if: contains(github.event.comment.body, '/close')
    runs-on: ubuntu-latest

    steps:
    - name: Closed Issue
      uses: actions/github-script@v6
      with:
        script: |
          github.rest.issues.update({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            state: 'closed'
          })

 

 

 

 

 

📌 12장 : 직렬화

아이템 85. 자바 직렬화의 대안을 찾으라

  • 1997년, 자바에 직렬화 도입
  • 직렬화의 근본적인 문제
    • 공격 범위가 넓고, 지속적으로 넓어져 방어가 어렵다.
    • ObjectInputStream의 readObject 메서드를 호출하면 객체 그래프가 역직렬화 되기 떄문
  • 가젯(gadget)
    • 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드
  • 역직렬화 폭탄(deserialization bomb)
    • 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하다 서비스 거부 공격에 쉽게 노출될 수 있다.
  • 직렬화 문제 회피 방법
    • 아무것도 역직렬화하지 않는다.
    • 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘을 사용한다.
    • 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)
  • 크로스-플랫폼 구조화된 데이터 표현
    • 자바 직렬화보다 간단하다.
    • 임의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않는다.
    • 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
    • ex. JSON, 프로토콜 버퍼(protocol buffers)
      • JSON : 텍스트 기반이라 사람이 읽을 수 있다.
      • 프로토콜 버퍼 : 이진 표현이라 효율이 좋다.
      • JSON은 데이터를 표현하는 데 사용, 프로토콜 버퍼는 문서를 위한 스키마(타입)을 제공한다.
      • 프로토콜 버퍼는 사람이 읽을 수 있는 텍스트 표현(pbtxt)도 지원한다.

아이템 86. Serializable 을 구현할지는 신중히 결정하라

  • 클래스의 인스턴스를 직렬화 하기 위해서는 클래스 선언에 implements Serializable 붙이기
  • 선언은 쉬워보이지만 Serializable 을 구현하면 릴리즈한 뒤에 수정이 어렵다.
    • Serializable 구현 후, 직렬화된 바이트 스트림 인코딩은 공개 API가 된다.
  • Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다.
    • 객체는 생성자를 사용해 만드는데, 직렬화는 기본 메커니즘을 우회하는 객체 생성 기법
    • 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 노출된다.
  • Serializable 구현은 신버전 릴리즈할 때 테스트할 것이 늘어난다.
    • 직렬화 수정 후, 구버전으로 역직렬화 할 수 있는지 반대도 가능한지 확인해야 한다.
  • 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안된다. 인스턴스도 대부분 안된다.
  • 내부 클래스는 직렬화를 구현하면 안된다.

아이템 87. 커스텀 직렬화 형태를 고려해보라

  • 수정이 어렵기 떄문에 고민한 후에 괜찮다고 판단될 때만 기본 직렬화 형태 사용하기
  • 객체의 물리적 표현과 논리적 내용이 같으면 기본 직렬화 형태도 무방하다.
  • 차이가 클 때 사용시 문제점
    • 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
    • 너무 많은 공간을 차지할 수 있다.
    • 시간이 너무 많이 걸릴 수 있다.
    • 스택 오버플로를 일으킬 수 있다.

아이템 88. readObject 메서드는 방어적으로 작성하라

  • readObject는 어떤 바이트 스트림이 넘어와도 유효한 인스턴스를 만들어야 한다.
  • 안전한 readObject 메서드를 작성하는 방법
    • private 이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사해야 한다.
    • 모든 불변식을 검사해서 어긋나는 게 발견되면, InvalidObjectException 을 던진다.
    • 역직렬화 후 객체 그래프 전체의 유효성을 검사해아 하면, ObjectInputValidation 인터페이스를 사용해라
    • 재정의할 수 있는 메서드를 호출하지 않는다.

아이템 89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라

  • readResolve 를 인스턴스 통제 목적으로 사용하려면 객체 참조 타입 인스턴스 필드는 모두 transient 으로 선언해야 한다.
    • 그렇게 하지 않으면, readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 수도 있다.
  • 직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않는 것을 보장해준다.
  • 열거 타입 싱글턴 예시
public enum Elvis { 
    INSTANCE;
    private String[] favoriteSongs =
        { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System. out.println(Arrays.toString (favoriteSongs));
    } 
}

아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

  • Serializable 으로 구현하면, 생성자 이외의 방법으로 인스턴스를 생성
    • 버그와 보안 문제가 일어날 수 있다.
    • 직렬화 프록시 패턴(serialization proxy pattern) 으로 해결
  • 직렬화 프록시 패턴
    • 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해서 private static 으로 선언한다.
    • 중첩 클래스의 생성자는 1개
    • 바깥 클래스를 매개변수로 받아야 한다.
    • 생성자는 단순히 인수로 넘어온 인스턴스 데이터를 복사
    • 일관성 검사, 방어적 복사도 필요 없다.
    • 바깥 클래스, 직렬화 프록시 모두 Serializable 를 구현한다고 선언해야 한다.
  • 직렬화한 Period 클래스 예시
private static class SerializationProxy implements Serializable { 
    private final Date start;
    private final Date end;
    
    Serializationproxy(Period p) { 
        this.start = p.start; 
        this.end = p.end;
    }
    private static final long serialVersionUID = 
            234098243823485285L; // 아무 값이나 상관없다. (아이템 87)
}

// 바깥 클래스에 추가 : 직렬화 프록시 패턴용 writeReplace 메서드
// 직렬화가 이뤄지기 전에 바깥 클래스 인스턴스를 직렬화 프록시로 변환
private Object writeReplace() {
    return new SerializationProxy(this); 
}

// 직렬화 프록시 패턴용 readObject 메서드
private void readObject(ObjectlnputStream stream) 
        throws InvalidObjectException {
    throw new InvalidObjectException("프록시가 필요합니다.");
}

// Period.SerializationProxy용 readResolve 메서드
// 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환
private Object readResolve() {
  return new Period(start, end); // public 생성자를 사용한다.
}

📌 11장 : 동시성

아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라

  • synchronized
    • 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장
    • 일반적인 동기화
      • 한 객체가 일관된 상태를 가지고 생성, 객체에 접근하는 메서드는 그 객체에 lock 을 건다.
      • lock 을 건 메서드는 객체의 상태를 확인하고 필요하면 수정한다.
    • 동기화의 중요한 기능
      • 일관성이 깨진 상태를 볼 수 없게 해주고
      • 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
  • 동기화는 배타적 실행뿐만 아니라, 스레드 사이의 안정적인 통신에 필요하다.
  • 다른 스레드를 멈추는 올바른 방법
    • 첫 번째 스레드는 자신의 boolean 필드를 폴링하면서 값이 true가 되면 멈춘다.
    • 필드를 false로 초기화하고, 다른 스레드에서 이 스레드를 멈추려고 할 때 true 로 변경
  • 적절한 스레드 종료 방법
public class StopThread {
    private static boolean stopRequested;
    
    private static synchronized void requeststop() { 
        stopRequested = true;
    }
    
    private static synchronized boolean stopRequested() { 
        return stopRequested;
    }
    
    public static void main(String[] args) 
        throws InterruptedException {
            Thread backgroundThread = new Thread(() -> { 
                int i = 0;
                while (!stopRequested()) 
                    i++;
            }); 
            backgroundThread.start();
            
            TimeUnit.SECONDS.sleep(1);
            requestStop();
    } 
}
  • 쓰기 메서드(requestStop), 읽기 메서드(stopRequested) 모두 동기화 해야 한다.
  • 둘 다 동기화되지 않으면, 동작을 보장하지 않는다.
  • volatile
    • 배타적 수행과는 상관없이 항상 가장 최근에 기록된 값을 읽게 해준다.
    • 반복문에서 동기화 하는 비용을 더 빠르게 하는 방법
private static volatile boolean stopRequested;
  • 주의해야 하는 상황
    • 증가 연산자는 코드상으로 하나지만, 실제로 nextSerialNumber 필드에 두 번 접근한다.
    • 먼저 값을 읽고, 증가한 새로운 값을 저장한다.
    • 두 번째 스레드가 이 두 접근 사이를 들어와 값을 읽게 되면 첫 번째 스레드와 같은 값을 받게 된다.
    • 안전 실패(safety failure) : 프로그램이 잘못된 결과를 계산하는 오류
    • 해결 방법 : generateSerialNumber 메서드에 synchronized 한정자를 붙인다.
      • 동시 호출을 해도 이전 호출이 변경한 값을 읽게 해준다.
      • synchronized을 사용하면 nextSeriaINumber 필드에서 volatile 를 제거해주어야 한다.
private static volatile int nextSeriaINumber = 0;

public static int generateSerialNumber() { 
        return nextSerialNumber++;
}
  • 위의 문제들을 피하는 가장 좋은 방법은 처음부터 가변 데이터를 공유하지 않도록 한다.
  • 가변 데이터는 단일 스레드에서만 사용하도록 한다.

아이템 79. 과도한 동기화는 피하라

  • 과도한 동기화는 성능이 저하되고, 교착상태에 빠질 수 있다.
  • 응답 불가와 안전 실패를 피하기 위해서 동기호 메서드나 동기화 블록 안에서 제어를 클라이언트에 양도하지 않기
    • 동기화된 영역 안에 재정의할 수 있는 메서드를 호출하면 안된다.
    • 클라이언트가 넘겨준 함수 객체를 호출하면 안된다.
  • 외계인 메서드 호출을 동기화 블록 바깥으로 옮겨준다.
private void notifyElementAdded(E element) { 
    List<SetObserver<E>> snapshot = null; 
    synchronized(observers) {
      snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer : snapshot)
        observer.added(this, element);
}
  • 열린 호출(open call)
    • 동기화 영역 바깥에서 호출되는 외계인 메서드
    • 외계인 메서드는 언제까지 실행될지 알 수 없는데, 동기화 영역 안에서 호출되면 다른 스레드는 사용 대기를 해야 한다.
    • 열린 호출은 실패 방지 효과 이외에 동시성 효율을 개선해준다.
  • 더 좋은 방법은 자바의 동시성 컬렉션 라이브러리 CopyOnWriteArrayList가 있다.
    • ArrayList를 구현한 클래스
    • 내부의 배열이 수정되지 않아 순회할 때 락이 필요 없어 속도가 매우 빠르다.
    • 순회만 일어나는 관찰자 리스트 용도로 좋다.
private final List<SetObserver<E>> observers = 
        new CopyOnWriteArrayList<>();
public void addObserver(SetObserver<E> observer) { 
    observers.add(observer);
}
public boolean removeObserver(SetObserver<E> observer) { 
    return observers.remove(observer);
}
private void notifyElementAdded(E element) { 
    for (SetObserver<E> observer : observers)
      observer.added(this, element);
}
  • 기본 규칙
    • 동기화 영역에서 가능한 일을 적게 하기
  • 멀티코어가 일반화 된 오늘날에, 과도한 동기화는 락을 얻는 데 드는 CPU 비용이 아니다.
  • 경쟁하는 낭비 시간, 모든 코어가 메모리를 일관되게 보기 위한 지연 시간

아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라

  • java.util.concurrent
    • 실행자 프레임워크라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
    • 간단한 작업 큐 사용 가능
ExecutorService exec = Executors.newSingleThreadExecutor();
exec.execute(runnable); // 실행자에 실행할 태스크를 넘기는 법
exec.shutdown(); // 실행자를 종료시키는 법
  • 큐를 2개 이상의 스레드가 처리하게 하려면, 다른 정적 팩토리로 다른 종류의 실행자 서비스(스레드 풀) 생성하기
  • 스레드 풀 스레드 개수는 고정할 수 있고, 늘어나거나 줄어들게 할 수도 있다.
  • 주로 사용하는 실행자는 java.util.concurrent.Executors 에서 사용
  • ThreadPoolExecutor 클래스를 직접 사용해도 된다.
  • 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다.
  • 태스크 : 작업 단위를 나타내는 개념
  • 태스크 종류 : Runnable, Callable
    • Callable은 Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다.
  • 실행자 서비스 : 태스크를 수행하는 일반적인 메커니즘
  • 자바 7부터 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원한다.
  • fork-join 태스크는 fork-join 풀이라는 특별한 실행자 서비스를 실행한다.
    • fork-join 태스크의 인스턴스는 작은 하위 태스크로 나뉠 수 있다.
    • fork-join 풀을 구성하는 스레드들이 태스크들을 처리한다.
    • 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와서 대신 처리할 수도 있다.
    • 모든 스레드가 움직여 CPU를 최대한 활용한다.
    • 높은 처리량, 낮은 지연시간

아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라

  • 자바 5부터 고수준 동시성 유틸리티가 wait, notify 대신 일을 처리해준다.
  • java.util.concurrent 의 고수준 유틸리티
    • 실행자 프레임워크
    • 동시성 컬렉션(concurrent collection)
    • 동기화 장치(synchronizer)
  • 동시성 컬렉션
    • List, Queue, Map과 같은 표준 컬렉션 인터페이스에 동시성을 추가한 고성능 컬렉션
    • 동기화를 각자 내부에서 수행해 높은 동시성을 갖춤
    • 동시성 컬렉션에서 동시성을 무력화할 수 없다. 외부에서 락을 추가로 사용하면 속도가 느려진다.
  • ConcurrentMap 으로 구현한 동시성 정규화 맵
public static String intern(String s) { 
    String result = map.get(s);
    if (result = null) {
      result = map.putIfAbsent(s, s); 
      if (result = null)
        result = s;
    }
    return result; 
}
  • Collections.synchronizedMap 보다 ConcurrentHashMap 의 성능이 더 좋다.
    • 동기화된 맵 -> 동시성 맵
  • wait, notify 보다 동시성 유틸리티를 사용하는 것이 더 좋지만
  • 레거시 코드를 유지보수해야 하는 경우, wait은 while문 안에서 호출해야 한다.
  • 일반적인 상황에서 notify보다 notifyAll을 사용해야 한다.
    • 응답 불가 상태에 빠질 수 있기 때문에

아이템 82. 스레드 안전성 수준을 문서화하라

  • 메서드 선언에서 synchronized 은 API에 속하지 않는다.
  • 멀티 스레드 환경에서 API를 안전하게 사용하기 위해서 스레드 안전성 수준을 정확히 명시해야 한다.
  • Collections.synchronizedMap API 문서
// synchronizedMap이 반환한 맵의 컬렉션 뷰를순회하려면 반드시 그 맵을 락으로 사용해 수동으로 동기화하라.

Map<K, V> m = Collections. synchronizedMap(new HashMapo()); 
Set<K> s = m.keySet(); // 동기화 블록 밖에 있어도 된다.
        ...
synchronized(m) { // s가 아닌 m을 사용해 동기화해야 한다! 
  for (K key : s)
    key.f();
}
// 이대로 따르지 않으면 동작을 예측할 수 없다.

아이템 83. 지연 초기화는 신중히 사용하라

  • 지연 초기화(lazy initialization)
    • 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
    • 정적 필드, 인스턴스 필드에서 사용
    • 주로 최적화에 사용, 클래스와 인스턴스 초기화에 발생하는 위험 순환 문제 해결
    • 클래스, 인스턴스 생성의 초기화 비용은 줄지만, 필드 접근 비용이 커진다.
    • 지연 초기화가 성능을 느리게 할 수도 있다.
  • 멀티 스레드 환경에서는 지연 초기화 필드를 2개 이상의 스레드가 공유하면 반드시 동기화 해야 하기 때문에 구현이 어렵다.
  • 대부분의 상황에서 일반적인 초기화가 지연 초기보다 낫다.
  • 인스턴스 필드 초기화하는 일반적인 방법
private final FieldType field = computeFieldValue();
// 지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같을 때 synchronized 접근자 사용하기

private FieldType field;
private synchronized FieldType getFieId() { 
    if (field = null)
        field = computeFieldValue(); 
    return field;
}
  • 정적 필드를 지연 초기화해야 할 때, 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구 사용
private static class FieldHolder {
static final FieldType field = compiiteFieldValue();
}
private static FieldType getFieId() { return FieldHolder.field; }
  • 인스턴스 필드를 지연 초기화해야 할 때, 이중검사(double-check) 관용구 사용 
private volatile FieldType field;

private FieldType getFieId() {
        FieldType result = field;
        if (result != null) { // 첫 번째 검사 (락 사용 안 함)
            return result;
            
        synchronized(this) {
          if (field = null) // 두 번째 검사 (락 사용)
            field = computeFieldValue(); 
          return field;
        }
}

아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

  • 여러 스레드가 실행 중일 때, 운영체제의 스레드 스케줄러는 스레드 실행 순서와 시간을 정한다.
  • 구체적인 스케줄링 정책은 운영체제마다 다르다.
  • 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램은 다른 플랫폼에 이식하기 어렵다.
  • 견고하고 빠르고 이식성 좋은 프로그램 작성 방법
    • 실행 가능 스레드의 평균 개수를 프로세서 수보다 크게 많아지지 않도록 해야한다.
  • 실행 가능 스레드 수를 적게 유지하는 주요 기법
    • 각 스레드가 유용한 작업 후, 다음 일이 생기기 전까지 대기하기
    • 당장 처리할 작업이 없으면 실행 X
    • ex. 실행자 프레임워크 : 스레드 풀 크기를 적절히 설정하고 작업은 짧게 유지한다.

📌 10장 : 예외

아이템 69. 예외는 진짜 예외 상황에만 사용하라

  • 잘못된 예외상황 : 무한루프를 돌다가 배열에 끝에 도달해서 ArrayIndexOutOfBoundException 발생했을 때 종료
try {
     int i = 0;
     while(true)
        range[i++].climb();
     } catch(ArrayIndexOutOfBoundsException e){
}
  • JVM은 배열에 접근할 때마다 경계가 넘는지 검사한다.
  • 위의 코드에서 같은 일(경계가 넘는지 확인)이 반복된다.
  • 예외를 사용 vs 표준 관용구 : 예외를 사용한 것이 속도가 더 느리다.
  • 예외는 예외 상황에서만 사용하고, 일상적인 제어 흐름용에서는 쓰지 않는다.

아이템 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라

  • 자바 문제 상황을 알리는 타입(throwable)
    • 검사 예외, 런타임 예외, 에러
    • 호출하는 쪽에서 복구해야 하는 상황이면 검사 예외를 사용
    • 검사 예외일 때 복구에 필요한 정보를 알려주는 메서드를 제공해야 한다.
  • 비검사 throwable
    • 런타임 예외, 에러
    • 프로그래밍 오류를 나타낼 때 : 런타임 예외
    • JVM이 자원 부족, 불변식 깨짐 등 더 이상 수행을 할 수 없는 상황일 때 : 에러 사용
    • Error 클래스를 상속해서 하위 클래스를 만들지 않기
      • 비검사 throwable은 모두 RuntimeException 의 하위 클래스여야 한다.

아이템 71. 필요 없는 검사 예외 사용은 피하라

  • 검사 예외는 발생한 문제를 프로그래머가 처리해서 안전성을 높여준다.
  • 과하게 사용하면 쓰기 불편한 API가 될 수 있다.
  • 검사 예외를 회피하는 가장 쉬운 방법은 적절한 결과 타입을 담은 옵셔널을 반환하는 방법이다.
    • 검사 예외를 던지는 대신에 빈 옵셔널 반환하기
    • 단점 : 예외 발생 원인의 부가 정보를 담을 수 없다.
  • 예외를 사용하면 구체적인 예외 타입, 그 타입이 제공하는 메서드를 활용해 부가 정보를 제공할 수 있다.
  • 옵셔널만으로 상황을 처리해서 충분한 정보를 제공할 수 없을 때만 검사 예외를 사용한다.

아이템 72. 표준 예외를 사용하라

  • 표준 예외를 사용하면 사용이 쉽고, 예외 클래스가 적을수록 메모리 사용량이 줄고 클래스 적재 시간도 적게 걸린다.
  • 가장 많이 사용하는 예외 : IllegalArgumentException
    • 호출자가 인수로 부적절한 값을 넘길 때 던지는 예외
    • 반복 횟수를 지정하는 매개변수에 음수를 건낼 때 사용할 수 있다.
  • 자주 사용되는 예외들 
  • Exception, RuntimeException, Throwable, Error은 직접 재사용하지 말아야 한다.
  • 이 예외들은 다른 예외들의 상위 클래스이기 때문에, 여러 성격의 예외들을 포괄해서 안정적으로 테스트할 수 없다.

아이템 73. 추상화 수준에 맞는 예외를 던지라

  • 예외 번역(exception translation)
    • 메서드가 저수준 예외를 처리하지 않았을 때, 관련 없는 예외가 나오는 문제가 생길 수 있다.
    • 문제를 해결하기 위해서, 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔서 던져야 한다.
try {
... // 저수준 추상화를 이용한다.
} catch (LowerLevelException e) {
    // 추상화 수준에 맞게 번역한다.
    throw new HigherLevelException(...);
}
  • 예외 연쇄(exception chaining)
    • 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 될 때 예외 연쇄를 사용하는 것이 좋다.
    • 문제의 근본 원인(cause)인 저수준 예외를 고수준 예외에 실어 보내는 방식
    • 별도의 접근자 메서드(Throwable의 getCause 메서드)를 통해 저수준 예외를 꺼내서 볼 수 있다.
try {
... // 저수준 추상화를 이용한다.
} catch (LowerLevelException e) {
    // 저수준 예외를 고수준 예외에 실어 보낸다.
    throw new HigherLevelException(cause);
}
  • 고수준 예외의 생성자는 상위 클래스의 생성자에 원인을 보내 최종적으로 Throwable 생성자까지 보낼 수 있다.
class HigherLevelException extends Exception { 
    HigherLevelException(Throwable cause) {
        super(cause); 
    }
}

아이템 74. 메서드가 던지는 모든 예외를 문서화하라

  • 검사 예외는 항상 따로 선언하고, 각 예외가 발생하는 상황을 자바독 @throws 태그를 사용해서 문서화한다.
  • 공통 상위 클래스 하나로 뭉뚱그려 선언하지 않기
    • ex. Exception, Throwable을 던진다고 선언하지 않기
    • 예외 상황 : main은 오직 JVM만 호출하기 떄문에 Exception을 던지도록 선언해도 된다.
  • 메서드가 던질 수 있는 예외는 각각 @throws 태그로 문서화
  • 비검사 예외는 메서드 선언의 throws 목록에 넣지 않기

아이템 75. 예외의 상세 메시지에 실패 관련 정보를 담으라

  • 예외를 잡지 못해 프로그램이 실패하면 자바 시스템은 그 예외의 스택 추적(stack trace) 정보를 자동으로 출력한다.
  • 스택 추적 : 예외 객체의 toString 메서드를 호출해 얻는 문자열
/**
* IndexOutOfBoundsException을 생성한다. *
* @param lowerBound 인덱스의 최솟값
* @param upperBound 인덱스의 최댓값 + 1 * @param index 인덱스의 실젯값
*/
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index){
        // 실패를 포착하는 상세 메시지를 생성한다.
        super(String.format(
        "최솟값: %d, 최댓값: %d, 인덱스: %d",
        lowerBound, upperBound, index));

        // 프로그램에서 이용할 수 있도록 실패 정보를 저장해둔다.
        this.lowerBound=lowerBound;
        this.upperBound=upperBound;
        this.index=index;
}

아이템 76. 가능한 한 실패 원자적으로 만드랄

  • 실패 원자적(failure-atomic) 특성
    • 호출된 메서드가 실패해도, 해당 객체는 메서드 호출 전 상태를 유지해야 한다.
  • 메서드를 실패 원자적으로 만드는 방법
    • 불변 객체로 설계 한다.
      • 불변 객체의 상태는 생성 시점에 고정되어 절대 변하지 않는다.
    • 가변 객체의 경우, 작업 수행에 앞서 매개변수 유효성을 검사한다.
      • 객체 내부 상태를 변경하기 전에 잠재적 예외 가능성을 걸러준다.
    • 객체의 임시 복사본에서 작업을 수행하고, 작업이 성공적으로 완료되면 원래 객체와 교체한다.
      • ex. 정렬 수행 전에 입력 리스트의 원소를 배열로 옮겨 담는다.
      • 배열을 사용하면 정렬 알고리즘의 반복문에서 원소들에 더 빠르게 접근 가능하고, 정렬이 실패해도 입력 리스트는 변하지 않는다.
    • 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성해서 작업 전 상태로 되돌린다.
      • 주로 디스크 기반의 내구성(durability)을 보장해야 하는 자료구조에 쓰인다.

아이템 77. 예외를 무시하지 말라

  • 예외 상황을 무시하지 않도록 해야 한다.
  • 예외를 무시해야 하는 경우도 있다.
    • ex. FileInputStream 을 닫을 때
  • 예외를 무시하는 경우, catch 블록 안에 이유에 대해 주석으로 남기고, 예외 변수 이름을 ignored 으로 변경한다.
// catch 블록을 비워두면 예외가 무시된다. 
try {
        ...
        } catch(SomeException e){    
}
 
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4;
try {
    numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
    // 기본값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다).
}

📌 9장 : 일반적인 프로그래밍 원칙

아이템 57. 지역변수의 범위를 최소화하라

  • 지역변수의 유효 범위를 최소로 줄이면
    • 코드 가독성 증가
    • 유지보수성 증가
    • 오류 가능성 감소
  • 지역변수 범위를 줄이는 방법
    • 가장 처음 쓰일 때 선언하기
    • 대부분 지역변수는 선언과 동시에 초기화한다.
    • 메서드를 작게 유지하고 한 가지 기능에 집중한다.

아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라

  • for-each(enhanced for statement 향상된 for문)
    • 반복자와 인덱스 변수를 사용하지 않는다.
    • 코드가 깔끔하고 오류가 날 일이 없다.
    • 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있다.
  • for-each를 사용할 수 없는 상황
    • 파괴적인 필터링(destructive filtering)
      • 컬렉션을 순회하면서 선택된 원소를 제거해야 하는 경우, 반복자의 remove 메서드를 호출해야 한다.
      • 자바 8에서 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적 순회하는 일을 피할 수 있다.
    • 변형(transforming)
      • 리스트나 배열을 순회하면서 일부 값이나 전체를 교체해야 하는 경우 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
    • 병렬 반복(parallel iteration)
      • 여러 컬렉션을 병렬로 순회해야 하는 경우 각각의 반복자와 인덱스 변수를 사용해야 한다.

아이템 59. 라이브러리를 익히고 사용하라

  • 표준 라이브러리를 사용해서 크게 관련 없는 문제에 시간을 허비하지 않고, 기능 개발에 집중할 수 있다.
  • 자바 7 부터는 Random 사용을 하지 않는게 좋다. ThreadLocalRandom 으로 대체
    • 포크-조인 풀이나 병렬 스트림에서는 SplittableRandom 사용
  • 알아두면 좋은 API
    • java.lang
    • java.util
    • java.io
    • 이외 하위 패키지
    • java.util.concurrent 동시성 관련

아이템 60. 정확한 답이 필요하다면 float와 double은 피하라

  • flaot, dobule 타입은 과학과 공학 계산용으로 설계
  • 부동 소수점연산에 쓰이기 때문에 정확한 결과가 필요한 경우에는 사용하지 않는다.
  • System.out.printIn(1.03 - 0.42); 코드의 출력 결과
    • 0.6100000000000001 출력
  • 금융 계산과 같이 정확한 계산이 필요한 경우에는 BigDecimal, int, long을 사용해야 한다.
    • BigDecimal의 단점
      • 기본 타입보다 쓰기 불편하고 느리다.
      • int 혹은 long 타입을 사용할 수 있다.
  • 숫자를 9 자리 십진수로 표현할 수 있으면 int
  • 18 자리 십진수로 표현할 수 있으면 long
  • 18 자리가 넘어가면 BigDecimal

아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용하라

  • 자바의 데이터 타입
    • 기본 타입 : int, double, boolean
    • 참조 타입 : String, List
  • 박싱된 기본 타입
    • 각각의 기본 타입에 대응하는 참조 타입
    • int : Integer
    • double : Double
    • boolean : Boolean
  • 기본타입과 박싱된 기본 타입의 차이
    • 기본 타입은 값만 가지고 있고, 박싱된 기본 타입은 값 + 식별성(identity)을 갖는다.
    • 기본 타입의 값은 언제나 유효하지만, 박싱된 기본 타입은 null을 가질 수 있다.
    • 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
Comparator<Integer> naturalOrder =
    (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
  • 같은 값 비교시, 0을 출력해야 하지만 1을 출력하는 이유는?
    • 같은 객체를 비교하는 게 아니면 박싱된 기본 타입에 == 연산자를 사용하면 오류 발생
  • 실무에서는 기본 타입을 다루는 비교자가 필요할 때
    • Comparator, naturalOrder() 사용
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> { 
    int i = iBoxed, j = jBoxed; // 오토박싱
    return i < j ? -1 : (i == j ? 0 : 1);
};
  • 기본 타입과 박싱된 기본 타입을 혼용한 연산에서 박싱된 기본 타입의 박싱은 자동으로 해제된다.
  • null 참조를 다시 언박싱 하게 되면 NullPointerException 이 발생한다.

아이템 62. 다른 타입이 적절하다면 문자열 사용을 피하라

  • 문자열이 다른 값 타입을 대신하기 적합하지 않는 경우
    • 열거 타입 : 상수를 열거할 때 문자열보다 열거 타입이 낫다.
    • 혼합 타입 : 각 요소를 개별로 접근하려면 문자열을 파싱해야 하기 때문에 속도가 느리고 오류가 생길 수 있다.
      • 이런 경우, private 정적 멤버 클래스로 새로 선언하는 것이 낫다.
    • 권한을 표현하는 경우

아이템 63. 문자열 연결은 느리니 주의하라

  • 문자열 연결 연산자 (+)
    • 문자열 연결 연산자를 사용한 문자열 n개를 잇는 시간은 n^2 에 비례한다.
    • 성능 개선을 위해 String 보다 StringBuilder 를 사용한다.

아이템 64. 객체는 인터페이스를 사용해 참조하라

  • 매개변수 타입으로 클래스보다 인터페이스가 더 적합하다.
  • 적합한 인터페이스가 있는 경우, 매개변수 말고도 반환 값, 변수, 필드를 전부 인터페이스 타입으로 선언해야 한다.
  • 객체의 실제 클래스를 사용해야 하는 경우는 생성자로 생성할 경우이다.
  • 예시 : Set 인터페이스를 구현한 LinkedHashSet 변수를 선언
// 좋은 예. 인터페이스를 타입으로 사용했다.
Set<Son> sonSet = new LinkedHashSet();

// 나쁜 예. 클래스를 타입으로 사용했다.
LinkedHashSet<Son> sonSet = new LinkedHashSet();

아이템 65. 리플렉션보다는 인터페이스를 사용하라

  • 리플렉션 기능(java.lang.refelct)을 사용하면 프로그램에서 임의의 클래스에 접근할 수 있다.
  • Class 객체가 주어지는 경우, 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있다.
  • 인스턴스들로 클래스의 멤버 이름, 필드 타입, 메서드 시그니처를 가져올 수 있다.
  • 리플렉션 단점
    • 컴파일타임 타입 검사가 주는 이점을 누릴 수 없다.
    • 리플렉션을 이용하면 코드가 지저분해진다.
    • 성능이 저하된다.
  • 리플렉션을 써야 하는 복잡한 애플리케이션이 있지만, 단점 때문에 사용을 줄이고 있다.
  • 리플렉션은 아주 제한된 형태로 사용해야 단점을 피하고 이점을 취할 수 있다.
  • 컴파일 타임에는 알 수 없는 클래스를 사용해야하는 프로그램을 작성해야 할 때
    • 리플렉션은 인스턴스 생성에만 사용하고, 만든 인스턴스는 인터페이스나 상위 클래스로 참조해서 사용한다.

아이템 66. 네이티브 메서드는 신중히 사용하라

  • 자바 네이티브 인터페이스(Java Native Interface, JNI)
    • 자바 프로그램이 네이티브 메서드를 호출하는 기술
    • 네이티브 메서드 : 네이티브 프로그래밍 언어로 작성한 메서드
  • 네이티브 메서드의 쓰임새
    • 레지스트리 같은 플랫폼 특화 기능을 사용한다.
    • 네이티브 코드로 작성된 기존 라이브러리를 사용한다.
    • 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다.
  • 자바는 점점 하부 플랫폼의 기능을 흡수하고 있기 떄문에 네이티브 메서드 사용이 줄어들고 있다.
    • ex. 자바9에서 process API를 추가해 OS 프로세스에 접근하는 길을 열어줌
  • 네이티브 메서드로 성능 개선이 되는 일은 적기 때문에 되도록 사용하지 않는다.

아이템 67. 최적화는 신중히 하라

  • 빠른 프로그램보다 좋은 프로그램을 작성해야 한다.
  • 성능을 제한하는 설계는 피해야한다.
  • API를 설계할 때 성능에 주는 영향을 고려해라
  • 모든 변경 후에 성능을 측정하라

아이템 68. 일반적으로 통용되는 명명 규칙을 따르라

  • 임의의 타입 T
  • 컬렉션 원소의 타입 E
  • 맵의 키와 값에 K, V
  • 예외 X
  • 메서드 반환 타입 R

+ Recent posts