📌 7장 오류처리

오류 코드보다는 예외를 사용하라

  • if else 문으로 오류 코드를 호출하지 않고, try catch 으로 오류 발생시 예외 던지기

미확인(unchecked) 예외를 사용하라

  • 확인된 예외는 OCP(Open Closed Principle)을 위반한다.
    • 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다.

예외에 의미를 제공하라

  • 오류 메시지에 정보를 담아 예외와 함께 던진다.
  • 실패한 연산 이름과 실패 유형도 언급한다.
  • 애플리케이션이 로깅 기능을 사용하면, catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

호출자르 고려해 예외 클래스를 정의하라

  • 오류가 발생한 컴포넌트로 분류한다.
  • 유형으로도 분류가 가능하다. ex. 디바이스 실패, 네트워크 실패, 프로그래밍 오류
  • 여러 catch 문을 사용하는 것 보다, 호출하는 라이브러리 API를 감싸서 예외 유형 하나를 반환해준다.
LocalPort port = new LocalPort(12); 
try {
    port.open();
} catch (PortDeviceFailure e) {
    reportError(e);
    logger. log(e.getMessage(), e);
} finally {}

public class LocalPort {
    private ACMEPort innerPort;
    public LocalPort(int portNumber) { innerPort = new ACMEPort(portNumber);
}
    public void open () { 
        try {
            innerPort.open ();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e); } 
        catch (GMXError e) {
            throw new PortDeviceFailure(e);
        } 
    }
}
  • 다른 라이브러리로 갈아탈 경우 비용이 적어진다.
  • 프로그램 테스트가 쉬워진다.

정상 흐름을 정의하라

  • 외부 API를 감싸 독자적인 예외를 던지고, 코드 위에 처리기를 정의해 중단된 계산을 처리한다.
  • 중단이 적합하지 않은 경우 : ex. 비용 청구 애플리케이션
    • Special Case Pattern 특수 사례 패턴
    • 클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식 -> 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.
    • 예외가 논리를 어렵게 만드는 경우, 특수 상황을 처리할 필요가 없으면 더 개선될 것이다.
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); 
    m_total += expenses.getTotaK);
} catch(MealExpensesNotFound e) { 
    m_total += getMealPerDiem();
}

// 개선 방법 : ExpenseReportDAO를 언제나 MealExpense 객체를 반환하도록 수정
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotaK);

public class PerDiemMealExpenses implements MealExpenses { 
    public int getTotal() {
// 기본값으로 일일 기본 식비를 반환한다.
} }

null 을 반환하지 마라

  • 메서드로 null 을 전달하는 코드와 반환하는 코드는 피한다.

결론

  • 깨끗한 코드는 읽기 좋은 코드 + 높은 안정성
  • 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하자

📌 8장 경계

외부 코드 사용하기

  • Map은 제공하는 기능성과 유연성이 있지만, 다른곳으로 넘길 경우 clear 메소드로 Map 사용라면 누구나 Map 내용을 지울 권한이 있는 위험이 있다.
  • Map의 인터페이스가 변하게 되면, 수정할 코드가 많아진다.
  • Map을 깔끔하게 사용한 코드. 제네릭스의 사용 여부는 Sensor 안에서 결정한다.
public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(String id) { 
        return (Sensor) sensors.get(id);
}
// 이하 생략 }

경계 살피고 익히기

  • 타사 라이브러리를 사용할 경우
    • 하루나 이틀 이상의 시간동안 문서를 읽고 사용법 결정
    • 코드를 작성 후 라이브러리가 예상대로 동작하는지 확인
  • 학습 테스트
    • 코드를 작성해 외부 코드를 호출하는 것보다 먼저 간단한 테스트 케이스 작성 후 외부 코드를 익히는 방법

log4j 익히기

  • log4j를 사용해서 패키지를 내려받고, "hello"를 출력하는 테스트 케이스
@Test
public void testLogCreate() {
    Logger logger = Logger.getLoggerC'MyLogger");
    logger, infoC'hello"); // Appender가 필요하다는 오류 발생
}

@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger"); 
    logger.removeAllAppenders(); 
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n"),
        ConsoleAppender.SYSTEM_OUT)) ;
    logger. infoC'hello");
}

학습 테스트는 공짜 이상이다

  • 이해도를 높여주는 정확한 실험이다.
  • 패키지 새 버전이 나오면 학습 테스트를 돌려 차이를 확인한다.
  • 패키지가 예상대로 도는지 검증한다.

아직 존재하지 않는 코드를 사용하기

  • 경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계
  • 지식이 별로 없는 상태에서 시스템을 개발하려고 하는 경우, 하위 시스템과 먼 부분부터 작업을 진행
  • 송신기 API에서 CommunicationsController 분리
  • 다른 팀에서 송신기 API 정의 후에 TransmitterAdapter를 구현해 간극을 매운다.
  • ADAPTER 패턴(어댑터 패턴) 으로 API 사용을 켑슐화 해서 API가 바뀔 때 수정할 코드를 한 곳으로 모았다.

깨끗한 경계

  • 경계에 위치하는 코드는 깔끔하게 분리한다.
  • 기대치를 정의하는 테스트 케이스도 작성한다.
  • 외부 패키지에 의존하는 것보다 우리 코드에 의존하는 편이 훨씬 좋다.
  • 외부 패키지를 호출하는 코드를 가능한 줄인다.
    • 새로운 클래스로 경계를 감싸거나 어댑터 패턴을 사용해서 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환한다.

📌 9장 단위 테스트

TDD 법칙 세 가지

  • 첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  • 둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  • 셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

깨끗한 테스트 코드 유지하기

  • 지저분한 테스트 코드가 있는 것보다 테스트가 없는 것이 낫다.
  • 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

깨끗한 테스트 코드

  • 중복되는 코드, 자질구레한 사항이 너무 많을 경우 표현력 저하
  • BUILD-OPERATE_CHECK 패턴
    • BUILD : 테스트 자료를 만든다.
    • OPERATE : 테스트 자료를 조작한다.
    • CHECK : 조작한 결과가 올바른지 확인한다.

도메인에 특화된 테스트 언어

  • DSL(도메인 특화 언어) 으로 테스트 코드를 구현하는 기법
  • 보통 시스템 조작 API를 사용하지만, 대신에 API 위에 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용한다.
    • 테스트 코드 작성과 읽기가 쉬워진다.
  • 이중 표준
    • 실제 환경에서는 안되지만 테스트 환경에서는 문제 없는 방식
    • ex. StringBuffer는 효율적이지만 테스트 환경은 자원이 제한적일 가능성이 낮다.

테스트 당 assert 하나

  • given-when-then 이라는 관례를 사용해서, 테스트 코드를 읽기 쉽게 작성
  • 테스트를 분리하게 되면 중복 코드가 생기게 된다.
  • 방법1 : TEMPLATE METHOD(템플릿 메소드) 패턴을 사용하면 중복 제거 가능
    • given/when 부분을 부모 클래스에 두고, then 부분을 자식 클래스에 둔다.
  • 방법2 : 독자적인 테스트 클래스를 만들어서, @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣는 방법

테스트 당 개념 하나

F.I.R.S.T

  • 깨끗한 테스트를 위한 다섯가지 규칙
  • Fast(빠르게) : 테스트는 빨리 돌아야 한다.
  • Independent(독립적으로) : 각 테스트는 서로 의존하면 안된다.
  • Repeatable(반복가능한) : 어떤 환경에서도 테스트는 반복이 가능해야 한다.
  • Self-Validating(자가검증하는) : 테스트는 bool 값으로 결과를 내야 한다.
  • Timely(적시에) : 단위 테스트는 테스트를 하려는 실제 코드를 구현하기 직전에 구현한다.

📌 10장 클래스

클래스 체계

  • 변수 목록
    • static public 변수
    • static private 변수
    • private instance 변수
    • public 변수가 필요한 경우는 거의 없다.
  • 공개 함수
  • 비공개 함수 : 자신을 호출하는 공개 함수 직후
  • 캡슐화
    • 변수와 유틸리티 함수는 가능한 공개하지 않는다.
    • 테스트 코드에 접근을 허용할 수 있도록 protected 으로 선언하는 경우가 있다.
    • 하지만 비공개 상태를 유지할 방법을 생각한 후, 캡슐화를 풀어주는 것은 마지막 수단으로 한다.

클래스는 작아야 한다!

  • 클래스 이름에 해당 클래스 책임을 기술한다.
  • 클래스 설명은 if, and, or, but 등을 제외한다. 그리고 25단어 내외로 가능해야 한다.
  • SRP 단일 책임 원칙(Single Responsibility Principle)
    • 클래스는 책임이 하나여야 한다.

응집도 Cohesion

  • 클래스는 인스턴스 변수의 수가 작아야 한다.
  • 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
  • 변수를 많이 사용할수록 메소드와 클래스는 응집도가 더 높다.
  • 응집도를 유지하면 작은 클래스 여럿이 나온다

변경으로부터 격리

  • 구체적인 클래스 : 상세한 코드를 포함
  • 추상 클래스 : 개념만 포함
  • 인터페이스와 추상 클래스를 사용해서 구현이 미치는 영향을 격리한다.
  • 시스템의 결합도를 낮추면 유연성과 재사용성을 높일 수 있다.

📌 4장 주석

  • 주석 사용 이유 : 코드로 의도를 표현하지 못해, 실패를 만회하기 위해 사용
    • 코드는 변화하기 때문에, 주석이 점점 코드에서 분리될 수 있다. 주석을 가능한 줄이도록 노력한다.

주석은 나쁜 코드를 보완하지 못한다

코드로 의도를 표현하라

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)){}

// 코드로 의도 표현
if (employee.isEligibleForFullBenefits())

좋은 주석

  • 법적인 주석 : 저작권 정보, 소유권 정보
  • 정보를 제공하는 주석 : ex. 정규 표현식이 시각과 날짜를 뜻하는 것을 명한다.
// kk:mm:ss EEE, IWI dd, yyyy 형식이다. Pattern timeMatcher = Pattern.compile(
:"*\\d W,*\\w,*\\d "*\)\;d
  • 의도를 설명하는 주석
  • 의미를 명료하게 밝히는 주석
  • 결과를 경고하는 주석 : ex. 특정 케이스(여유 시간이 충분하지 않은 경우)를 꺼야 하는 이유를 설명
    • 굳이 주석을 사용하지 않아도 @Ignore 속성으로 테스트 케이스를 끌 수 있다.
  • TODO 주석 : 앞으로 할 일
  • 중요성을 강조하는 주석
  • 공개 API에서 Javadocs
  • 나쁜 주석
    • 대다수 주석은 나쁜 주석
    • 주절거리는 주석
    • 같은 이야기를 중복하는 주석
    • 오해할 여지가 있는 주석
    • 의무적으로 다는 주석
    • 이력을 기록하는 주석
    • 있으나 마나 한 주석
    • 무서운 잡음
    • 함수나 변수로 표현할 수 있으면 주석을 달지 마라
    • 위치를 표현하는 주석
    • 닫는 괄호에 다는 주
    • 주석으로 처리한 코드
    • HTML 주석 : 읽기 어렵다.
    • 전역 정보 : 주석은 근처에 있는 코드만 기술
    • 너무 많은 정보
    • 모호한 관계
    • 함수 헤더
    • 비공개 코드에서 Javadocs

📌 5장 형식 맞추기

형식을 맞추는 목적

  • 코드 형식은 의사소토의 일환

적절한 행 길이를 유지하라

  • 대부분 200줄의 코드인 파일로도 커다란 시스템 구축이 가능하다.

신문 기사처럼 작성하라

  • 위에서 아래로 내려갈수록 의도를 세세하게 묘사한다.

개념은 빈 행으로 분리하라

  • 빈 행 : 새로운 개념을 시작하는 시각적 단서

세로 밀집도

  • 서로 밀집한 코드 행은 세로로 가까이 놓아야 한다.
public class ReporterConfig {
/**
* 리포터 리스너의 클래스 이름
*/
private String m_className;
/**
* 리포터 리스너의 속성
*/
private List<Property> m_properties = new ArrayList<Property>(); public void addProperty(Property property) {
m_properties. add (property);
}

// 변경 후
public class ReporterConfig {
  private String m_className;
  private List<Property> m_properties = new ArrayList<Property>();
  public void addProperty(Property property) { m_properties. add(property);
  } }

수직 거리

  • 서로 밀접한 개념은 세로로 가까이 위치하도록 한다.
  • 멀리 떨어져 있으면 소스 파일과 클래스를 여기저기 뒤져야하는 불편함이 생긴다.
  • 변수 선언 : 변수를 사용하는 위치에 최대한 가까이 선언
  • 인스턴스 변수 : 클래스 맨 처음에 선언
  • 종속 함수 : 종속되는 2개의 함수는 세로로 가까이 배치한다.
    • 가능한 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
  • 개념의 유사성 : 개념적인 친화도가 높을수록 코드를 가까이 배치한다.
  • 세로 순서 : 가장 중요한 개념을 먼저 표현한다.
  • 가로 형식 맞추기 : 하나의 행의 가로 길이는 짧을수록 좋다. 최대 120자가 넘지 않도록 한다.
  • 가로 공백과 밀집도 : 공백을 사용해서 밀접한 개념과 느슨한 개념을 표현한다.
  • 들여쓰기 : scope(범위)를 표현하기 위해 코드를 들여쓴다.

팀 규칙

  • 어디에 괄호를 넣을지
  • 들여쓰기는 몇 자리로 할지
  • 클래스, 변수, 메서드 이름 규칙

📌 6장 객체와 자료구조

자료 추상화

  • 변수를 의존하지 않게 만들기 위해 private 사용
  • get(조회), 설정(set)은 왜 public 인지?
  • 변수를 private 으로 선언해도, 조회와 설정을 공개하면 구현을 외부로 노출하게 된다.
    • 구현을 감추기 위해서는 추상화 필요. 추상 인터페이스 제공

자료/객체 비대칭

  • 객체 : 추상화 뒤로 자료를 숨기고 자료를 다루는 함수만 공개
  • 자료구조 : 자료를 그대로 공개하고 별다른 함수는 제공하지 않는다.

디미터 법칙

  • 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙
  • 객체는 자료를 숨기고, 함수를 공개한다.
  • 기차 충돌 : 여러 함수를 이어서 호출한 것은 이어진 기차로 보여진다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

// 기차 충돌 개선
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
  • 하지만, 위의 ctxt, Options, ScratchDir가 객체면 디미터 법칙을 위반한다.
  • 아래처럼 구현하면 디미터 법칙을 거론할 필요가 없다.
final String outputDir = ctxt.options.scratchDir.absolutePath;

자료 전달 객체

  • 자료 구조체, 자료 전달 객체 DTO(Data Transfer Object) : 공개 변수만 있고, 함수가 없는 클래스
  • 데이터 베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체
  • 빈(Bean) : 구조 좀 더 일반적인 형태
    • private 변수를 조회/설정 함수로 조작한다.
  • 활성 레코드 : DTO 의 특수한 형태
    • save, find 와 같은 탐색 함수도 제공한다.
    • 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한다.
    • 활성 레코드는 자료 구조로 취급한다.
    • 비즈니스 규칙을 담고, 내부 자료를 숨기는 객체는 따로 생선한다.

결론

  • 객체는 동작을 공개하고 자료를 숨긴다.
  • 어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체
  • 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 적합하다.

📌 1장 깨끗한 코드

깨끗한 코드란?

  • 우아하고 효율적인 코드 : 의존성을 줄여야 유지보수가 쉬워지고, 오류를 처리하고, 성능을 최적으로 유지해야 한다.
    • 오류 처리 : 메모리 누수, 경쟁 상태(race condition)
  • 단순하고 직접적인, 가독성이 좋은 코드
  • 가독성이 좋고, 다른 사람이 고치기 쉬운 코드 : 각 의존성을 명확하게 정의한다.
  • 주의 깊게 작성한 코드
  • 켄트 백 코드 규칙
    • 모든 테스트를 통과한다.
    • 중복이 없다.
    • 시스템 내 모든 설계 아이디어를 표현한다.
    • 클래스, 메소드, 함수 등을 최대한 줄인다.
  • 읽으면서 짐작한 대로 돌아가는 코드

📌 2장 의미 있는 이름

의도를 분명히 밝혀라

  • 변수(혹은 함수나 클래스) 의 존재 이유, 수행 기능, 사용 방법을 주석 없이 이름으로 의도를 드러낸다.
  • 아래의 코드는 하는 일을 짐작하기 어렵다.
    • theList에 무엇이 들었는지?
    • theList에서 0번째 값이 어째서 중요한지?
    • 값 4는 무엇을 의미하는지?
    • 함수가 반환하는 리스트 list1은 어떻게 사용하는지?
public List<int[]> getThem() {
List<int[]> listl = new ArrayList<int[]>();
 for (int[] x : theList) if (x[0] = 4)
listl.add(x); return listl;
}
  • 이름 변경후
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>(); for (Cell cell : gameBoard)
if (cell.isFlagged()) flaggedCells.add(cell);
return flaggedCells; }

그릇된 정보를 피하라

  • 의미가 있는 단어를 다른 의미로 사용하지 않는다.
    • ex) hp, aix, sco
  • 실제 List가 아니면, accountList라고 명명하지 않는다.
    • accountGroup, bunchOfAccounts, Accounts라 명명한다.

의미 있게 구분하라

  • 컴파일러를 통과해도 연속된 숫자를 덧붙이거나 불용어(noise word) 를 추가하는 방식은 적절하지 않다.
    • ex) a1, a2, a3, ... , aN -> source와 destination와 같이 정보를 제공할 수 있는 이름을 사용한다.
public static void copyChars(char al[], char a2[]) { for (int i = 0; i < al.length; i++) {
a2[i] = al[i]; }
}

발음하기 쉬운 이름을 사용하라

검색하기 쉬운 이름을 사용하라

  • 숫자 7을 사용하게 되면, 7이 들어가는 모든 이름들이 검색된다.
  • MAX_CLASSES_PER_STUDENT 와 같이 검색하기 쉬운 이름을 사용한다.

인코딩을 피하라

자신의 기억력을 자랑하지 마라

클래스 이름

  • 클래스 이름과 객체 이름 : 명사, 명사구
  • 적절한 예시 : Customer, WikiPage, Account, AddressParser
  • 부적절한 예시 : Manager, Processor, Data, Info와 같은 이름, 동사

메소드 이름

  • 메소드 이름 : 동사, 동사구
  • 적절한 예시 : postPayment, deletePage, save
  • 접근자(Accessor), 변경자(Mutator), 조건자(Predicate)는 javabean 표준에 따라 값 앞에 get, set, is를 붙인다.

기발한 이름은 피하라

한 개념에 한 단어만 사용하라

  • 추상적인 개념 하나에 단어 하나를 선택한다.
  • 똑같은 기능의 메소드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.

말장난을 하지 마라

  • 한 단어를 두 가지 목적으로 사용하지 마라
  • ex) 기존에 add를 사용한 메소드가 두 개를 더하거나 이어서 새로운 값을 만드는 기능이라 가정했을 경우
    • 값 하나만 추가하는 메소드의 이름은 add보다 insert나 append를 사용하는 것이 적당하다.

해법 영역에서 가져온 이름을 사용하라

  • 익숙한 기술 개념에는 기술 이름이 적합한 선택

문제 영역에서 가져온 이름을 사용하라

  • 적절한 용어가 없으면 문제 영역에서 이름을 가져온다.

의미 있는 맥락을 추가하라

  • state -> addState

불필요한 맥락을 없애라

  • 고급 휘발유 충전소(Gas Station Deluxe) 라는 애플리케이션을 만들었을 때, 모든 클래스 이름을 GSD로 시작하는 것은 바람직하지 않다.

📌 3장 함수

작게 만들어라

  • 블록과 들여쓰기
    • 함수에서 들여쓰기 수준은 1단이나 2단을 넘으면 안된다.

한 가지만 해라

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

함수 당 추상화 수준은 하나로

  • 위에서 아래로 코드 읽기 : 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
  • switch 문은 작게 만들기 어렵다.

서술적인 이름을 사용하라

  • ex) testableHtml -> SetupTeardownlncluder 함수가 하는 일을 좀 더 잘 표현할 수 있다.
  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.

함수 인수

  • 함수에서 이상적인 인수 개수 0개(무항)
  • 3개 이상은 피하는 것이 좋다.
  • 플래그 인수
    • 함수로 부울 값을 넘기는 것은 좋지 않다.
    • ex) render(boolean isSuite) 보다는 renderForSuite(), renderForSingleTest()이라는 함수로 나눠야한다.
  • 출력 인수 : 일반적으로 출력 인수는 피한다.

명령과 조회를 분리하라

오류 코드보다 예외를 사용하라

  • try/catch 블록 뽑아내기
    • try/catch 블록은 정상 동작과 오류 처리 동작을 뒤섞기 때문에, 별도 함수로 뽑아낸다.
    • 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하기 수정하기 쉬워진다.
    • 오류 처리도 한 가지 작업이다.

반복하지 마라

  • 알고리즘이 여러 함수에서 반복되지 않도록 작성한다.
  • 객체지향 프로그래밍은 코드를 부모 클래스에 몰아 중복을 없앤다.

구조적 프로그래밍

  • 루프 안에서 break, continue, goto를 사용하지 않는다.
  • 함수를 작게 만드려면, return, break, continue를 사용해도 된다.

HTTP(HyperText Transfer Protocol)

  • HTML 문서와 같은 리소스들을 가져올 수 있도록 해주는 프로토콜
  • 웹에서 이루어지는 모든 데이터 교환의 기초
  • 클라이언트-서버 프로토콜 : 클라이언트 요청을 생성하기 위해 연결을 연 다음 응답을 받을 때 까지 기다리는 모델
    • 데이터 스트림이 아닌, 개별적인 메시지 교환으로 통신
  • request(요청) : 클라이언트(브라우저)에 의해 전송되는 메시지
  • response(응답) : 서버에서 응답으로 전송되는 메시지
  • 애플리케이션 계층 프로토콜
  • 신뢰 가능한 전송 프로토콜을 사용한다. 
    • 주로 TCP TLS(암호화된 TCP) 사용

 

프록시(Proxy)

  • 웹 브라우저와 서버 사이에 있는 많은 컴퓨터와 머신이 HTTP 메시지를 이어 받고 전달한다.
  • 애플리케이션 계층에서 동작하는 컴퓨터/머신
  • 프록시로 다양한 기능 수행 가능
    • 캐싱
    • 필터링
    • 로드밸런싱
    • 인증
    • 로깅

 

HTTP 특징

  • 간단한 사용 
  • 확장 가능 : HTTP 헤더를 통해 확장 가능
  • 무상태(Stateless), 세션
    • HTTP 쿠키를 통해 상태가 있는 세션을 만든다.
    • 헤더 확장성을 사용해서 동일한 컨텍스트 또는 동일한 상태를 공유하기 위해 각각의 요청들에 세션을 만들도록 HTTP 쿠키 추가

 

HTTP 통신 과정

1. TCP 연결을 연다. : 요청을 보내거나 응답을 받는 데 사용

2. HTTP 메시지 전송(request)

- HTTP2는 캡슐화되어 직접 읽기 불가능

GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr

3. 서버가 보낸 응답을 읽는다.

HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html

<!DOCTYPE html... (here comes the 29769 bytes of the requested web page)

4. 연결을 닫거나 다른 요청을 위해 재사용

 

 

HTTP Message 구조

  • 시작 줄(start-line) : HTTP 요청 / 요청에 대한 성공 또는 실패
  • HTTP 헤더 : 요청에 대한 설명 / 메시지 본문에 대한 설명
  • 빈 줄 : 요청에 대한 모든 메타 정보가 전송되었음을 알린다. (헤드와 본문 사이)
  • 본문(optional) : 요청과 관련된 데이터(HTML form) / 또는 응답과 관련된 문서가 선택적으로 들어간다.

HTTP Requset

  • 클라이언트가 서버로 전달하는 메시지
  • request line : HTTP method + url + http version
  • header(헤더)
    • request header
      • client의 IP, 사용 언어(accept language), content type(파일 형식), referer(이전 페이지 주소)
    • general header : 메시지 전체에 적용, connection(네트워크 접속 유지할지 유무)
    • entity header : 요청 본문에 적용, content-length(요청과 응답 메시지의 본문 크기)
  • request body : query string의 정보

 

HTTP Resonse

  • Status line(Response Line)
    • http/1.1 (프로토콜 버전) + status code 상태 코드(ex.200) + 상태 텍스트(ex.OK)
  • response header
    • 결과 데이터에 대한 설명 : Encoding, content-type(MIME(HTML, Image, video), size, server 설명(아파치, 톰캣 등등)
  • response body
<HTML>
<HEAD>
<BODY>
...

 

 

HTTP Request Method

  • GET : 리소스를 받기 위해 사용. URI 형식으로 서버측에 리소스 요청
  • POST : 내용 및 파일 전송. 클라이언트에서 서버로 어떤 정보를 제출하기 위해 사용
  • PUT : 리소스 갱신. 
  • DELETE : 리소스 삭제 -> 실제로는 클라이언트에게 리소스 삭제 권한 X
  • HEAD : 메시지 헤더 정보를 받기 위해 사용. 응답 메시지에 Body는 비어있고 header 정보만 받는다.
  • CONNECT : 클라이언트와 서버 사이의 중간 경유를 위해 사용. 보통 proxy으로 SSL 통신할 때 사용
  • OPTIONS : 서버 측 제공 메소드에 대한 질의를 위해 사용. 웹 서버에서 지원하는 메소드를 알기 위해 사용
  • TRACE : 요청 리소스가 수신되는 경로를 보기 위해 사용
  • PATCH : 리소스의 일부분을 갱신하기 위해 사용. PUT과 유사하지만 모든 데이터 갱신이 아니라 일부분만 수정할 때 사용

 

HTTP Status Code

  • 응답 상태 코드로 성공/실패 여부 확인
  • 10x : 정보 확인
  • 20x : 통신 성공
  • 30x : 리다이렉트
  • 40x : 클라이언트 오류
  • 50x : 서버 오류

 

성공 응답

상태코드 이름 의미
200 OK 요청 성공(GET)
201 Created 생성 성공(POST)
202 Accepted 요청 수신O, 리소스 처리X
배치 프로세스를 하고 있는 경우를 위해 사용
204 No Content 요청 성공O, 내용 없음

 

 

리다이렉션 메시지

상태코드 이름 의미
300 Multiple Choice 요청 URI에 여러 리소스가 존재
301 Move Permanently 요청 URI가 변경되었음을 의미
304 Not Modified 요청 URI의 내용이 변경X
캐시 목적으로 사용한다. 클라이언트는 계속해서 응답된 캐시 버전을 사용할 수 있다.

 

 

클라이언트 오류

상태코드 이름 의미
400 Bad Request 잘못된 요청. API에서 정의되지 않은 요청
401 Unauthorized 인증 오류
403 Forbidden 권한 밖의 접근 시도
(401와는 다르게 서버는 클라이언트가 누구인지 알고 있음)
404 Not Found 요청받은 리소스를 찾을 수 없음
(리소스를 숨기기 위해 403으로 전송할 수도 있음)
405 Method Not Allowed API에서 정의되지 않은 메소드 호출
429 Too Many Requests 요청 횟수 상한 초과

 

 

서버 에러 응답

상태코드 이름 의미
500 Internal Server Error 서버 내부 오류
자세한 오류를 알 수 없는 상황
502 Bad Gateway 게이트 웨이 오류
503 Service Unavailable 서비스 이용 불가
504 Gateway Timeout 게이트웨이 시간 초과

 


Socket(소켓)

  • 두 프로그램이 서로 데이터를 주고 받을 수 있도록 양쪽에 생성되는 통신 단자
  • 양방향 통신 : 서버와 클라이언트 양방향 연결이 이루어지는 통신
  • 클라이언트도 서버로 요청을 보낼 수 있고, 서버도 클라이언트로 요청을 보낼 수 있다.
    • 실시간 서비스 : 영상 스트리밍, 실시간 채팅
  • 웹 소켓 프로토콜 : 최초 연결을 요청할때만 HTTP 프로토콜 위에서 Handshaking 하기 때문에 http hedaer 사용한다.

 

HTTP

  • 파일을 전송하는 프로토콜
  • JSON, Image 등등 전송
  • 단방향 통신 : 클라이언트가 요청을 보내고 서버가 응답을 하는 방식의 통신

CSRF(Cross Site Request Forgery)

  • 웹 애플리케이션 취약점 중 하나
  • 인터넷 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(modify, delete, register) 등 특정한 웹사이트에 request 하도록 만드는 공격 기법
  • ex. SNS의 광고성 글
  • 자동 로그인으로 이런 피싱 사이트에 접속하게 되면서 피해를 입는 경우가 많다.

대응 기법

  • 리퍼러(Refferer) 검증
    • 백엔드 단에서 Refferer 검증을 통해 승인된 도메인으로 요청시에만 처리하도록 한다.
    • request header 안에 있는 데이터를 가져와서 referer 값 확인
String referer = request.getHeader("Referer");
  • Security Token 사용
    • 사용자의 세션에 임의의 난수 값을 저장하고, 사용자의 요청시 해당 값을 포함하여 전송시킨다.
    • 백엔드 단에서는 요청을 받을 때 세션에 저장된 토큰 값과 요청 파라미터로 전달받는 토큰 값이 일치하는지 검증 과정을 거치는 방법

 

XSS(Cross Site Scription)

  • 웹 애플리케이션 취약점 중 하나
  • 관리자가 아닌 권한이 없는 사용자가 웹 사이트에 스크립트를 삽입하는 공격 기법
  • 악의적으로 스크립트를 삽입해서 이를 열람한 사용자의 쿠키가 해커에게 전송시키며, 쿠키를 통해 세션 하이재킹 공격을 한다.
  • 해커는 세션 ID를 가진 쿠키로 사용자의 계정에 로그인

대응 기법

  • 입출력 값 검증 : XSS 필터링 적용 후 스크립트 실행되는지 테스트
  • XSS 방어 라이브러리, 확장앱 : 서버단에서 방어 라이브러리 추가 또는 사용자들이 확장앱 설치
  • 웹 방화벽 : 다양한 injection 방어 가능
  • CORS, SOP 설정 
    • CORS(Cross-Origin Resource Sharing), SOP(Same-Origin-Policy)를 통해 리소스의 Source를 제한하는 것
    • 사전에 지정된 도메인이나 범위가 아니면, 리소스를 가져올 수 없게 제한한다.

API Key

  • 서비스 확대로 기능들이 분리되면서, Module이나 Application들간의 공유와 독립성을 보장하기 위한 기능들이 생겨남
  • 동작 방식
    • 1. 사용자는 API Key를 발급받는다.
    • 2. 해당 API를 사용하기 위해 Key와 함께 요청을 보낸다.
    • 3. Application은 요청이 오면 Key를 통해 User정보를 확인해서 누구의 key인지 확인
    • 4. 해당 key의 인증과 인가에 따라 데이터를 사용자에게 반환
  • 문제점 : key가 유출되면 대비하기 힘들다. -> key 업데이트의 번거로움

 

 

OAuth(Open Authorization)

  • 인터넷 사용자들이 비밀번호를 제공하지 않고 
  • 다른 웹 사이트 상의 자신들의 정보에 대해
  • 웹 사이트나 애플리케이션의 접근 권한을 부여할 수 있는 개방형 표준
  • 간편 로그인
  • API Key 단점 보완
  • 사용 용어
    • 사용자 : 계정을 가지고 있는 개인
    • 소비자 : OAuth를 사용해 서비스 제공자에게 접근하는 웹사이트 or 애플리케이션
    • 서비스 제공자 : OAuth를 통해 접근을 지원하는 웹 애플리케이션
    • 소비자 비밀번호 : 서비스 제공자에서 소비자가 자신임을 인증하기 위한 키
    • 요청 토큰 : 소비자가 사용자에게 접근 권한을 인증받기 위해 필요한 정보
    • 접근 토큰 : 인증 후에 사용자가 서비스 제공자가 아닌 소비자를 통해 보호 자원에 접근하기 위한 키 값
    • 토큰의 종류
      • Access Token : 만료시간 존재. 다시 요청 가능
      • Refresh Token : 만료되면 다시 처음부터 진행
  • 문제점
    • Token이 규칙 없이 발행되기 때문에 증명확인이 필요하다.

인증 과정

1. 사용자가 App의 기능을 사용하기 위한 요청을 보낸다.

2. App은 해당 사용자가 로그인이 되어있는지 확인

3. App은 사용자가 로그인이 되어있지 않으면 사용자를 인증서버로 Redirection

4. 간접적으로 Autorize 요청을 받은 인증 서버는 해당 사용자가 회원인지 인증서버에 로그인 되어있는지 확인

5. 사용자가 최초의 요청에 대한 권한이 있는지 확인(GRANT)

6. 사용자가 Grant 요청을 받게 되면 사용자는 해당 인증정보에 대한 허가를 준다.

7. 인증서버에서 인증과 인가에 대한 과정이 모두 완료되면 App에게 인가코드

8. 인가코드의 유효시간은 짧기 때문에, App은 해당 코드를 Request Token으로 사용하여 인증 서버에 요청을 보낸다.

9. 인증서버는 자신의 저장소에 저장한 코드와 일치한지 확인하고 실제 리소스에 접근하게 될 Access Token 발급

10. 이제 App은 Access Token을 사용해서 업무 처리 가능

 

 

JWT(JSON Web Token)

  • 토큰 작성에 대한 규약
  • 웹 표준으로, 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 방식으로 정보를 안정성 있게 전달
  • 인증 여부 확인을 위한 값, 유효성 검증을 위한 값 그리고 인증 정보 자체를 담고 있기 때문에 인증 서버에 묻지 않아도 사용 가능
  • 문제점
    • 토큰 자체가 인증 정보를 가지고 있기 때문에 민감한 정보는 인증 서버에 다시 접속하는 과정이 필요하다.

 

JWT 구성요소

 

  • 3가지 문자열로 구성
  • 1. header(헤더) : JWT 서명에 사용된 알고리즘을 담는다. 
    • typ : 토큰의 타입
    • alg : 해싱 알고리즘 
  • 2. payload(정보) : 토큰에 담긴 주체들을 담는다. 
    • 토큰을 담을 정보
    • iss : 토큰 발급자 (issuer)
    • sub : 토큰 제목 (subject)
    • aud : 토큰 대상자 (audience)
    • exp : 토큰 만료 시간(expiration)
    • nbf : 토큰의 활성 날짜(Not before)
    • iat : 토큰이 발급된 시간(issued at)
    • jti : JWT 고유 식별자
  • 3. signature(서명) : 헤더와 페이로드를 각각 base64로 인코딩하고 콤마로 이어 붙인다. 그리고 헤더에 있는 알고리즘으로 암호화된 값을 담는다.
    • 클라이언트에서 받은 토큰의 데이터 조작 여부를 확인하는 곳 
    • 헤더의 인코딩 값과 정보의 인코딩 값을 합친 후 주어진 비밀키로 해쉬하여 생성한다. (header+payload+salt = hash)
    • 이렇게 만든 해쉬는 base64 형태로 나타낸다.
    • base64 : 8비트 이진 데이터를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식. 통신과정에서 binary data 손실을 막기 위해 사용한다.

 

로그인 인증과 JWT

  • 유효기간이 짧은 토큰은 사용자 입장에서 자주 로그인을 해야해서 번거롭다.
  • 유효기간이 긴 토큰은 편하지만 토큰을 탈취당할 약점이 있다.
  • Refresh Token : 위의 단점을 보완하기 위해 사용
    • Access Token의 유효기간이 만료되었을 때, Refresh Token이 새로 발급
    • Refresh Token이 만료되면, 새로 로그인
  • 토큰 저장 장소
    • Access Token : 클라이언트(cookie)
    • Refresh Token : 서버(DB)

 

 

 

 

JWT와 OAuth 2.0의 차이

- 둘 다 인증 방식이라는 공통점이 있지만, 서로 목적이 다르다.

- OAuth 2.0 : 하나의 플랫폼의 권한(아무 의미없는 무작위 문자열 토큰)

다양한 플랫폼에서 권한을 행사할 수 있게 해줌으로써 리소스 접근이 가능하게 하는데 목적을 두고있다.

 

- JWT는 Cookie, Session을 대신하여 의미있는 문자열 토큰

로그인 세션이나 주고받는 값이 유효한지 검증할 때 주로 사용

아파치 카프카(Apache Kafka)

  • 아파치 소프트웨어 재단이 슼라라로 개발하나 오픈 소스 메시지 브로커 프로젝트
  • 실시간 데이터 피드를 관리하기 위해 통일된, 높은 처리량, 낮은 지연시간을 지닌 플랫폼을 제공하는 것이 목표
  • 분산 트랜잭션 로그로 구성된, 확장 가능한 pub/sub 메시지 큐로 정의 가능
  • 스트리밍 데이터를 처리하기 위한 기업 인프라를 위해 적절하다.

 

pub/sub 모델(발신자/수신자)

  • 발신자 : 카프카에게 토픽(Topic)이라는 주소로 전송하는 역할
  • 수신자 : 카프카에게 원하는 토픽을 구독

 

개발 배경

  • 기존 링크드인 데이터 처리 시스템은 각각의 파이프라인 별 dependency가 높았고, 확장하기 어려운 상황이였다.
  • 카프카 개발 전 링크드인 데이터 처리 시스템

  • 카프카 적용
    • 프로듀서와 컨슈머의 분리
    • 영구 메시지 데이터를 여러 컨슈머에게 적용
    • 높은 처리량을 위한 메시지 최적화
    • 트래픽 증가에 따른 스케일 아웃이 가능한 시스템

 

 

카프카 구성 요소

  • 프로듀서(Producer) : 메시지를 생산하고, 브로커의 토픽으로 전달하는 역할
  • 컨슈머(Consumer) : 브로커의 토픽으로부터 저장된 메시지를 가져오는 역할
  • 브로커(Broker) : 카프카 내의 서버 또는 노드를 지칭한다. -> 실행된 카프카 서버. 메시지를 저장하고 관리한다.
  • 주키퍼(Zookeper) : 분산 어플리케이션 관리를 위한 코디네이션 시스템
    • 분산된 노드 정보(메시지 큐의 메타 정보) 중앙 처리 및 구성 관리, 동기화 등을 수행한다.

 

 

카프카 동작 방식

  • 프로듀서는 메시지를 카프카에 전달한다.
  • 전달된 메시지는 브로커의 토픽(Topic)이라는 구분에 의해 저장된다.
  • 컨슈머는 구독한 토픽에 접근하여 pull 방식으로 가져온다.

 

 

카프카의 특징과 장단점

  • 특징과 장점
    • 디스크에 메시지를 저장해서 영속성(persistency) 보장
    • 프로듀서와 컨슈머 분리로 멀티 프로듀서와 멀티 컨슈머 지원 -> pub/sub 구조의 확장성
    • 메시지 배치 처리가 가능해서 네트워크 오버헤드 감소
    • 높은 성능과 고가용성, 확장성 : 일부 노드가 죽어도 다른 노드가 일을 지속
    • 메시지 보장 여부 선택 가능
  • 단점
    • 직접 통신이 아니기 때문에 잘 전달됬는지 파악 X
    • 중간 메시징 플랫폼을 거쳐서 전달 속도가 상대적으로 느리다.

 

쿠키(Cookie)

  • 인터넷에 접속할 때 웹 사이트가 있는 서버에 의해 사용자의 컴퓨터에 저장되는 정보
  • 주로 로그인 정보, 장바구니 정보를 저장한다.
  • 쿠키와 다르게 캐시는 사운드, 이미지 파일을 일시적으로 저장해서 로딩을 빠르게 하는 것
  • 기존에 쿠키는 암호화가 되지 않았지만, 최근에는 암호화가 이루어져서 과거처럼 매우 낮은 보안성을 가지지 않는다.
  • 쿠키는 최적화 프로그램으로 자주 삭제하거나 브라우저의 자동 삭제 기능을 사용한다.
  • 쿠키를 차단할 수 있지만, 차단하면 사이트가 동작하지 않을 수 있는 단점이 있다.

 

세션(Session)

  • 쿠키를 기반으로 일정 시간동안 같은 브라우저로부터 들어오는 요구를 하나의 상태로 보고 그 상태를 유지하는 기술
  • 쿠키는 클라이언트
  • 세션은 웹 서버에 저장
  • 둘 이상의 통신 장치나 컴퓨터와 사용자 간의 대화나 송수신 연결상태를 의미하는 시간대
  • 세션은 연결상태 유지보다, 안정성이 더 중요하다.

 

쿠키 vs 세션

  • 쿠키
    • 클라이언트에 저장, 파일로 관리되기 때문에 브라우저의 종료와 상관없이 만료시간까지 존재
    • 관련 정보들이 쿠키 내부에 존재해서 빠른 속도 
    • 클라이언트가 서버로 요청을 보낼 때 요청 메시지를 스나이핑 당할 위험이 있다.
  • 세션
    • 서버에 저장되어 브라우저 종료 혹은 세션 만료시간 이후 삭제된다.
    • 관련 정보들이 서버에 저장되서 느린 속도를 가진다.
    • 세션은 쿠키를 통해 세션 Id만 저장하고 나머지 정보는 서버에 처리해서 상대적으로 보안성을 갖고 있다.

 

  쿠키(Cookie) 세션(Session)
저장위치 클라이언트(Client) 서버(Server)
저장형식 Text Object
만료시점 쿠키 저장시 설정 설정 시간
리소스 클라이언트의 리소스 서버의 리소스
용량제한 한 도메인 당 20개, 한 쿠키당 4KB 제한 없음

 

저장 위치 

  • 쿠키 : 클라이언트의 웹 브라우저가 지정하는 메모리나 하드디스크
  • 서버 : 서버의 메모리

 

만료 시점

  • 쿠키 : 저장할 때 expires 속성을 정의한다.
  • 세션 : 클라이언트 로그아웃 or 설정 시간 -> 정확한 시점을 알 수 없다.

 

리소스

  • 쿠키 : 클라이언트에 저장, 서버 자원 사용 X
  • 세션 : 서버에 저장, 서버 메모리에 로딩되고 세션이 생길 때마다 리소스 차지

 

용량 제한

  • 쿠키 : 쿠키로 인해 문제가 발생하는 것을 방지하고자 한 도메인당 20개, 한 쿠키당 4KB
  • 세션 : 클라이언트가 접속하면 서버에 의해 생성된다. -> 제한 없음

 

만약 로드밸런서와 세션을 사용한다면?

  • 세션은 서버에 저장된다. 
  • 여러 서버가 분산되어 있고, 로드밸런서를 사용할 때 세션은 공유되지 않는다.
  • 클라이언트가 로그인을 위해 서버A에 요청을 보내면 서버 A의 세션 저장소에 저장된다.
  • 클라이언트가 서비스를 이용하기 위해 서버B에 요청을 보내면 서버 B에는 해당 클라이언트의 세션 정보가 없기 때문에 로그인 페이지로 리다이렉트 하는 문제 발생
  • 해결 방법
    • Sticky Session
      • 첫 요청에 대한 응답을 준 서버에게 sticky하게 붙어서 이후의 모든 요청들을 해당 서버로만 보내는 방법
      • 단점 : 로드 밸런성 효율 저하
    • Session Clustering(세션 클러스터링)
      • 여러 대의 서버를 하나의 서버처럼 운영한다. 각 서버의 세션 저장소를 하나로 묶어서 관리한다.
      • 모든 서버가 동일한 세션을 공유하게 되기 때문에 하나의 서버가 죽어도 세션 정보를 잃어버릴 위험이 없어진다.
      • 단점 : 모든 세션 저장소 업데이트, 메모리 필요 -> 성능 저하
    • Session Server
      • 세션만 관리하는 별도의 서버를 두는 방식
      • 모든 서버의 업데이트 필요 X, Redis같은 In-memory(인메모리) 데이터 저장소를 사용해서 빠른 세션 조회 가능
      • 단점 : 하나의 세션 서버로 관리하기 때문에 서버가 죽으면 모든 세션 데이터를 잃어버린다.
        • 하지만 레디스를 사용하면 다른 서버의 메모리에 실시간으로 복사본을 저장하거나 디스크에 직접 저장하여 백업 가능
        • Master-Slave 형식으로 서비스 유지 가능
        • 단점 : 데이터 별도 저장의 메모리 필요

+ Recent posts