📌 1장 : 들어가기

  • 컴포넌트 : 개별 메서드부터 여러 패키지로 이뤄진 복잡한 프레임워크까지 재사용 가능한 모든 소프트웨어 요소
  • 자바 자료형(type)
    • 참조 타입(reference type) : 인터페이스, 클래스, 배열
    • 기본 타입(primitive type)
  • annotation : 인터페이스의 일종
  • enum : 클래스의 일종

📌 2장 : 객체 생성과 파괴

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

  • 생성자 : 클라이언트가 클래스의 인스턴스를 얻는 수단
  • 클래스는 생성자 대신에 정적 팩토리 메소드(static factory method)를 제공할 수 있다.
  • 정적 팩토리 메소드의 장점
    1. 이름을 가질 수 있다.
      • 생성자보다 객체의 특성을 이름으로 묘사할 수 있다.
    2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
      • 인스턴스 통제 클래스 : 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지 통제할 수 있다.
      • 인스턴스를 통제하면, 클래스를 싱글톤 패턴 or 인스턴스화 불가로 만들 수 있다.
      • 또한, 불변 값 클래스에서 동치 인스턴스가 단 하나임을 보장할 수 있다.
      • ex. a == b 일 때, a.equals(b)가 성립
    3. 반환 타입의 하위 타입 객체를 반화할 수 있는 능력이 있다.
      • 구현 클래스를 공개하지 않아도, 그 객체를 반환할 수 있어 API 생성을 할 때 작게 유지 가능하다.
    4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    5. 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
      • 서비스 제공자 프레임워크 3개의 핵심 컴포넌트
        • 서비스 인터페이스(service interface) : 구현체의 동작을 정의
        • 제공자 등록 API(provider registration API) : 제공자가 구현체를 등록할 때 사용
        • 서비스 접근 API(service access API) : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
      • 종종 서비스 제공자 인터페이스(service provider interface)라는 네 번째 컴포넌트도 쓰인다.
        • 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명해준다.
      • ex. JDBC
        • 서비스 인터페이스 : Connection
        • 제공자 등록 API : DriverManager.registerDriver
        • 서비스 접근 API : DriverManager.getConnection
        • 서비스 제공자 인터페이스 : Driver
  • 정적 팩토리 메소드 패턴의 단점
    1. 상속을 하려면 public, protected 생성자가 필요하다. 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
    2. 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
      • 알려진 규약을 따라 이름을 짓는 것으로 문제점을 완화한다.
      • from : 매개 변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드
        Date d = Date.from(instant);
      • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드
        Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
      • valueOf : from과 of의 자세한 버전
        BigInteger prime = BingInteger.valueOf(Integer.MAX_VALUE);
      • instance 혹은 getInstance : (매개 변수를 받으면) 매개 변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
        StackWalker luke = StackWalker.getInstance(options);
      • create 혹은 newInstance : instance, getInstance 와 같지만 매번 새로운 인스턴스를 생성해 반환한다.
        Object newArray = Array.newInstance(classObject, arrayLen);
      • getType : getInstance 와 같지만, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다.
        FileStore fs = Files.getFilesStore(path);
      • newType : newInstance 와 같지만, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다.
        BufferReader br = Files.newBufferReader(path);
      • type : getType 과 newType 의 간결한 버전
        List<Complaint> litany = Collections.list(legacyLitany);

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라

  • 정적 팩토리, 생성자는 둘 다 선택적 매개변수가 많으면 적절히 대응하기 힘들다.
  • 점층적 생성자 패턴(telescoping constructor pattern)
    • 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개만 받는 생성자, ..., 형태들로 선택 매개변수를 전부 다 받는 생성자 까지 늘려가는 방식
  • 점층적 패턴을 사용할 수 있지만, 매개변수의 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려워진다.
  • 선택 매개 변수가 많은 경우, 두 번째 대안 -> 자바빈즈 패턴(JavaBeans pattern)
    • 매개변수가 없는 생성자로 객체를 만들고, setter 으로 원하는 매개변수 값을 설정하는 방식
    • 하지만, 객체 하나를 만들기 위한 여러 메소드 호출의 필요성과 객체 생성 전까지 일관성이 무너진다.
  • 세번째 대안, 빌더 패턴(Builder pattern)
    • 점층적 생성자 패턴의 안전성 + 자바빈즈 패턴의 가독성
    • 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수 만으로 생성자(혹은 정적 팩토리)를 호출해 빌더 객체를 얻는다.
    • 그 후에 빌더 객체가 제공하는 setter 으로 원하는 선택 매개변수를 설정
    • 마지막으로, 매개 변수가 없는 build 를 호출해서 필요한 객체를 얻는다.
    • 플루언트(fluent) API 혹은 메소드 연쇄(method chaining)
      • 빌더의 setter 들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
      • 메소드 호출이 흐르듯 연결된다는 의미
public class NutritionFacts { 
    private final int servingSize; 
    private final int servings; 
    private final int calories;
    private final int fat;
    private final int sodium; 
    private final int carbohydrate;
    
    public static class Builder {
    // 필수 매개변수
        private final int servingSize; 
        private final int servings;

        // 선택 매개변수 - 기본값으로 초기화한다.
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
    
        public Builder(int servingSize, int servings) { 
            this.servingSize = servingSize; 
            this.servings = servings;
        }
        
        public Builder calories(int val)
            { calories = val; return this; }
            
        public Builder fatdnt val)
            { fat = val; return this; }
        public Builder sodium(int val)
            { sodium = val; return this; }
        public Builder carbohydrate(int val)
            { carbohydrate = val; return this; }
            
        public NutritionFacts build() { 
        return new NutritionFacts(this);
        } 
   }
   
   private NutritionFacts(Builder builder) { 
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
   }
}        
  • 빌더 패턴 사용한 클래스를 사용하는 클라이언트 코드 NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build ();
  • 잘못된 매개변수는 빌더의 생성자, 메소드에서 검사하는 것이 좋다.
  • 불변성을 위해서는 빌더로부터 매개변수를 복사하고 해당 객체 필드들을 검사해야 한다.
  • 어떤 매개 변수가 어떻게 잘못되었는지는 IllegalArgumentException를 사용한다.
  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다.
public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } 
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T» { 
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); 
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        
        abstract Pizza build();
        // 하위 클래스는 이 메서드를 재정의(overriding)하여 
        // "this"를 반환하도록 해야 한다.
        protected abstract T self();
    }
 
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // 아이템 50 참조
    }   
}
 

아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

  • 싱글톤(singleton) : 인스턴스를 오직 하나만 생성할 수 있는 클래스
    • 전형적인 예시 : 무상태(stateless) 객체, 설계상 유일해야 하는 시스템 컴포넌트
    • 클래스를 싱글톤으로 만들면 클라이언트가 테스트하기 어려울 수 있다.
  • 싱글톤을 만드는 방식 : 생성자는 private 으로 감추고, 유일하게 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 만든다.
  • 방식1 : public static 멤버가 final 필드
  • 방식2 : 정적 팩토리 메소드를 public static 멤버로 제공
    • 장점 : API를 바꾸지 않아도 싱글톤이 아니게 변경 가능하다. 정적 팩토리를 제너릭 싱글톤 팩토리로 만들 수 있다. 정적 팩토리 메소드 참조를 공급자(supplier)로 사용할 수 있다.
    • 장점이 필요하지 않으면 방법1이 더 좋다.
  • 싱글톤 클래스 직렬화
    • Serializable 을 구현하는 것 만으로는 부족하다.
    • 모든 인스턴스 필드를 일시적(transient)으로 선언하고, readResolve 메소드를 제공해야 한다.
    • 이렇게 하지 않으면, 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어진다.
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... } 
}
public class Elvis {
    private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... }
    public static Elvis getlnstance() { return INSTANCE; }
    public void leaveTheBuilding() { ... } 
}

아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라

  • 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만든다.
  • 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
  • 컴파일러가 기본 생성자를 만드는 경우는 명시된 생성자가 없을 때 뿐이기 때문에
  • private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

  • 정적 유틸리티를 잘못 사용한 예시
public class Spellchecker {
  private static final Lexicon dictionary = ...;
  private SpellChecker() {} // 객체 생성 방지
  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... } 
}
  • 싱글톤을 잘못 사용한 예시
public class Spellchecker {
  private final Lexicon dictionary = ...;
  private Spellchecker(...) {}
  public static Spellchecker INSTANCE = new SpellChecker(••.);
  public boolean isValid(String word) { ... }
  public List<String> suggestions(String typo) { ... } 
}
  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 유틸리티 클래스나 싱글톤 방식이 적합하지 않다.
  • 인스턴스를 생성할 때 생성자가 필요한 자원 넘겨주는 방식
  • 의존 객체 주입의 한 형태 : 유연성과 테스트 용이성을 높여준다.
public class Spellchecker {
  private final Lexicon dictionary;
  public Spellchecker(Lexicon dictionary) {
    this.dictionary = Objects.requireNonNull(dictionary); 
  }
  public boolean isValid(String word) { ... }
  public List<String> suggestions(String typo) { ... } 
}

아이템 6. 불필요한 객체 생성을 피하라

  • 생성자 대신 정적 팩토리 메소드를 제공하는 불변 클래스는 정적 패곹리 메소드를 사용해 불필요한 객체 생성을 피할 수 있다.
  • ex. Boolean(String) 생성자 대신에 Boolean.valueOf(String) 팩토리 메소드를 사용하는 것이 좋다.
String s = new StringC'bikini"); // 따라 하지 말 것!
String s = "bikini";
  • 생성 비용이 아주 비싼 객체도 있다. 자신이 만드는 객체가 비싼 객체인지 알 방법이 명확하게 없기 때문에
  • 정규 표현식을 사용해 성능을 끌어올릴 수 있다.
  • 하지만, String.matches 메소드를 사용하기 때문에 성능이 중요한 상황에서 반복해 사용하기 적합하지 않다.
  • Pattern 인스턴스는 한 번 쓰고 버려지기 때문에 가비지 컬렉션 대상이 된다.
  • Pattern 은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.
static boolean isRomanNumeraI(String s) { 
  return s.matchesC'^?#. ()*CM[MD] |D?C{0,3})"
    + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
  • 성능을 개선하기 위해서, 필요한 정규 표현식을 표현하는 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱하고
  • 나중에 메소드가 호출될 때마다 이 인스턴스를 재사용한다.
public class RomanNumerals {
  private static final Pattern ROMAN = Pattern.compile(
    u("*C(?[=M.)DM]|D?C{0,3})"
      + "(X[C니 |L?X{0,3})(I[XV]|V?I{0,3})$");
  static boolean isRomanNumeral(String s) { 
   return ROMAN.matcher(s).matches();
  } 
}
private static long s니m() { 
  Long sum = 0L; // Long으로 선언해서 불필요한 Long 인스턴스가 2^31개 만들어진다.
  for (long i = 0; i <= Integer.MAX_VALUE; i++) 
    sum += i;
  return sum; 
}
  • 불필요한 객체를 만드는 또 다른 예시 : 오토박싱(auto boxing)
  • 박싱된 기본 타입보다 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하기

아이템 7. 다 쓴 객체 참조를 해제하라

  • 메모리 누수가 일어나는 위치
  • stack은 element 배열로 저장소 풀을 만들고 원소들을 관리하기 때문에
  • 가비지 컬렉션은 비활성 영역에 대해 알 수 없다.
  • 비활성 영역이 되는 순간 null 처리 해준다.
public Object pop() { 
  if (size = 0)
  throw new EmptyStackException(); 
  return elements[—size];
}

// 메모리 누수 개선
public Object pop() { 
  if (size = 0)
  throw new EmptyStackException(); 
  Object result = elements[—size]; 
  elements[size] = null; // 다 쓴 참조 해제 
  return result;
}
  • 객체 참조를 null 처리하는 일은 예외적이어야 한다.
  • 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.
  • 자기 메모리를 직접 관리하는 클래스는 항시 메모리 누수에 주의한다.
  • 캐시도 메모리 누수를 일으키기 쉽다. 객체 참조를 캐시에 넣고 놔두는 경우가 있다.
    • weakHashMap 사용 : 다 쓴 entry는 즉시 자동 제거
    • Scheduled ThreadPoolExecutor 같은 백그라운드 스레드를 사용하거나 LinkedHashMap은 removeEldestEnry 메소드를 사용

아이템 8. finalizer 와 cleaner 사용을 피하라

  • 자바의 두 가지 객체 소멸자
    • finalizer : 예측 불가능, 상황에 따라 위험할 수 있어 일반적으로 불필요
    • cleaner : finalizer보다 덜 위험하지만, 예측 불가능, 느리고 일반적으로 불필요하다.
  • finalizer, cleaner 로는 제때 실행되어야 하는 작업은 절대 할 수
  • 상태를 영구적으로 수정하는 작업에서는 절대 finalizer, cleaner 에 의존해서는 안된다.
    • ex. 데이터베이스 같은 공유 자원의 lock 해제를 맡기면 분산 시스템 전체가 서서히 멈추게 된다.
  • finalizer 는 가비지 컬렉터의 효율을 약 50배 떨어뜨리기 때문에 성능 저하 문제가 있다.
  • finalizer, cleaner 대신에 사용할 방법 : AutoCloseable
  • AutoCloseable
    • 클라이언트에서 인스턴스를 다 쓰고, close 메소드를 호출하면 된다.

아이템 9. try-finally 보다는 try-with-resources 를 사용하라

  • 자바에서는 close 메소드를 호출해서 직접 닫아줘야하는 경우가 있다.
  • try-finally 를 사용해서 자원을 안전하게 닫을 수 있지만, 자원이 둘 이상이면 지저분해진다.
  • 자바 7에서 try-with-resources 덕에 문제 해결
    • 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
static void copy(String src, String dst) throws lOException { 
  try (Inputstream in = new FilelnputStream(src);
    Outputstream out = new FileOutputStream(dst)) { 
      byte[] buf = new byte[BUFFER_SIZE];
      int n;
      while ((n = in.read(buf)) >= 0)
        out.write(buf, 0, n);
  } 
}

📌 11장 시스템

시스템 제작과 시스템 사용을 분리하라

  • 소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 연결하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
  • 초기화 지연(Lazy Initialization), 계산 지연(Lazy Evaluation)
    • 장점
      • 실제 필요하기 전까지 객체를 생성하지 않기 떄문에 불필요한 부하가 걸리지 않는다. 애플리케이션 시작 시간이 빨라진다.
      • 어떤 경우에도 null 포인터를 반환하지 않는다.
    • 단점
      • 메소드 호출 이전에 적절한 테스트 전용 객체를 sevice 필드에 저장해야 한다.
  • 책임이 둘이라는 말은 메소드가 작업을 두 가지 이상 수행하는 것 -> 단일 책임 원칙(SRP) 위반

Main 분리

  • 시스템 생성과 시스템 사용을 분리하는 방법
  • 생성과 간련된 코드는 모두 main이나 main이 호출하는 모듈로 옮기고
  • main 함수에서 시스템에 필요한 객체를 생성한 후 애플리케이션에 남긴다.

팩토리

  • 객체가 생성되는 시점을 애플리케이션이 결정해야하는 경우
  • 추상 팩토리 패턴 사용 : 주문처리 시스템에서 애플리케이션이 LineItem 인스턴스를 생성해서 Order에 추가
  • LineItem 생성 시점은 애플리케이션이 결정하지만, 생성 코드는 애플리케이션이 모른다.

의존성 주입(Dependency Injection)

  • 사용과 제작을 분리하는 강력한 메커니즘
  • 제어의 역전(Inversion of Control, IoC) 기법을 의존성 관리에 사용한 메커니즘
    • 제어의 역전 : 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다.
    • 새로운 객체가 넘겨받은 책임만 맡기 때문에 단일 책임 원칙(SRP)을 만족한다.
    • 의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임을 지지 않는다.
    • 초기 설정으로 시스템 전체에서 필요하기 떄문에 컨테이너를 사용한다.
  • DI 컨테이너는 (대개 요청이 들어올 때마다) 필요한 객체의 인스턴스를 만들고, 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다.
  • 실제 생성 객체 유형은 설정 파일에서 지정하거나 특수 생성 모듈에서 코드로 명시한다.
  • 스프링 프레임워크의 경우
    • 자바 DI 컨테이너 사용, 객체 사이 의존성은 XML 파일에 정의
    • 자바 코드에서는 이름으로 특정한 객체를 요청한다.

횡단(cross-cutting) 관심사

  • 영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다.
  • 모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다.
  • ex. 특정 DBMS나 독자적인 파일 사용, 테이블과 열은 같은 명명 관례, 일관적인 트랜잭션
  • 횡단 관심사 : 온갖 객체로 흩어진 영속성 구현 코드들을 모듈화한다.
    • 영속성 프레임워크 모듈화
    • 도메인 논리 모듈화
  • AOP(Aspect Oriented Programming, AOP) 관점 지향 프로그래밍
    • 횡단 관심사에 대처해 모듈성을 확보하는 일반적인 방법론
    • 특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꾸어야 한다.
    • Ex. 영속성
    • 영속적으로 저장할 객체와 속성을 선언하고, 영속성 책임을 영속성 프레임워크에 위임한다.
    • AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다.

자바 프록시

  • 개별 객체나 클래스에서 메소드 호출을 감싸는 경우가 좋은 예시
  • JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다.
  • 클래스 프록시를 사용하려면 바이트 코드 처리 라이브러리가 필요하다.
    • ex. CGLIB, ASM, Javassist
  • 프록시의 단점 : 많은 코드의 양과 크기 -> 클린 코드 작성의 어려움

순수 자바 AOP 프레임워크

  • 자바 프레임워크(스프링 AOP, JBoss AOP 등등)는 내부적으로 프록시를 사용한다.
  • POJO
    • 스프링 비즈니스 논리를 POJO로 구현
    • 순수하게 도메인에 초점을 맞춘다.
    • 엔터프라이즈 프레임워크에 의존하지 않는다.
    • 상대적으로 단순하기 때문에 유지보수가 편하다.

  • Bank 도메인 객체는 DAO(Data Accessor Object)으로 프록시 되었다.
  • DAO 객체는 JDBC 자료 소스로 프록시되었다.
  • 클라이언트는 Bank 객체의 getAccounts()를 호출한다고 생각하지만, 실제로는 Bank POJO 기본 동작을 확장한 중첩 DECORATOR(데코레이터) 객체 집합의 가장 외곽과 통신한다.

AspectJ 관점

  • AspectJ 언어 : 관심사를 관점으로 분리하는 가장 강력한 도구
  • 관점 분리의 여러 도구 집합을 제공하지만, 새 도구를 사용하고 문법과 사용법을 익혀야 하는 단점이 있다.

결론

  • 깨끗하지 못한 아키텍처는 도메인 논리를 흐리고, 제품 품질이 떨어진다.
  • 버그가 생기기 쉽고, 기민성이 떨어지면 생산성이 낮아지고 TDD 제공 장점이 사라진다.
  • 모든 추상화 단계의 의도는 명확히 표현해야하기 때문에 POJO를 작성하고, 관점 혹은 관점과 유사한 매커니즘을 사용하여 각 구현 관심사를 분리해야한다.

📌 12장 창발성

창발적 설계로 깔끔한 코드를 구현하자

  • 단순한 설계 규칙
  1. 모든 테스트를 실행해라
    • 테스트 케이스를 많이 작성할수록 DIP와 같은 원칙을 적용하고 DI, 인터페이스, 추상화 같은 도구를 사용해 결합도를 낮춘다.
  2. 중복을 없앤다.
    • 템플릿 메소드 패턴(Template method)으로 중복 제거
abstract public class VacationPolicy { 
    public void accrueVacation() {
        calculateBaseVacationHours() ; 
        alterForLegalMinijnums() ; 
        applyToPayroll();
    }
    
    private void calculateBaseVacationHours() .{*/./.}; 
    abstract protected void alterForLega!Minimums(); 
    private void applyToPayrolK) .{*/./.};
 }
    
public class USVacationPolicy extends VacationPolicy { 
    @Override protected void alterForLegalMinimums() {
    // 미국 최소 법정 일수를 사용한다. 
    }
}   
  
public class EUVacationPolicy extends VacationPolicy { 
    @Override protected void alterForLegalMinimums() {
    // 유럽연합 최소 법정 일수를 사용한다. }
    }
}
  1. 프로그래머 의도를 표현한다.
    • 좋은 이름 선택
    • 함수와 클래스 크기 줄이기
    • 표준 명칭 사용
    • 단위 테스트 작성
  2. 클래스와 메소드 수를 최소로 줄인다.

📌 13장 동시성

동시성이 필요한 이유?

  • 동시성은 결합(coupling)을 없애는 전략이다.
  • 무엇(what), 언제(when)를 분리하는 전략
  • 작업 처리량(throughput)을 개선 해야 하는 경우, 다중 스레드 알고리즘으로 수집기 성능을 높일 수 있다.
  • 한 번에 한 사용자를 처리하는 시스템인 경우, 사용자가 늘어나면 시스템 응답 속도가 느려진다. 많은 사용자를 동시에 처리하면 시스템 응답 시간 개선 가능
  • 동시성은 항상 성능을 높여주지 않는다.
    • 동시성은 여러 개의 프로세서를 동시에 처리할 독립적인 계산이 많은 경우에만 성능이 높아진다.
  • 동시성을 구현하면 설계가 변한다.
    • 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다.
    • 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없지 않다.
    • 컨테이너 동작 원리, 동시 수정 방법, 데드락 등과 같이 문제를 회피할 수 있는 지 알아야 한다.
  • 동시성은 다소 부하를 유발한다.
    • 성능 측면 부하, 코드의 추가
  • 동시성은 복잡하다.
  • 일반적으로 동시성 버그는 재현하기 어렵다.
  • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

동시성을 구현하기 어려운 이유

public class X {
    private int lastldllsed;
    
    public int getNextId() { 
        return ++lastldllsed;
    } 
}
  • astldUsed 필드를 42으로 설정하고, 두 스레드가 같은 변수를 동시 참조하면
  • 한 스레드는 43를 받고, 다른 스레드도 43을 받는다. lastIdUsed는 43이 된다.

동시성 방어 원칙

  • 단일 책임 원칙(Single Responsibility Principle)
    • 주어진 메소드, 클래스, 컴포넌트를 변경할 이유가 하나여야 한다.
    • 동시성 관련 코드는 다른 코드와 분리한다.
  • 따름 정리(corollary) : 자료 범위를 제한하라
    • 공유 객체를 사용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호한다.
    • 자료를 캡슐화하고, 공유 자료를 최대한 줄여라
  • 따름 정리 : 자료 사본을 사용하라
    • 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.
    • 하지만 사본으로 동기화를 피하게 되면, 사본 생성과 가비지 컬렉션이 성능 부하를 상쇄할 수 있다.
  • 따름 정리 : 스레드는 가능한 독립적으로 구현하라
    • 다른 스레드와 자료를 공유하지 않는다.
    • 각 스레드는 클라이언트 요청 하나를 처리한다.
    • 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.

라이브러리를 이해해라 - 스레드 환경에 안전한 컬렉션

  • java.util.concurrent 패키지가 제공하는 클래스
  • 다중 스레드 환경에서 사용해도 안전하고, 성능이 좋다.
  • ConcurrentHashMap은 거의 모든 상황에서 HashMap보다 빠르다.

실행 모델을 이해해라

  • 기본 용어


실행 모델 종류

  • 생산자/소비자(Producer-Consumer)
    • 하나 이상 생산자 스레드가 정보를 생성해 버퍼(buffer)나 대기열(queue)에 넣는다.
    • 대기열은 한정된 자원
    • 생산자 스레드는 대기열에 빈 공간이 있어야 정보를 채우고, 소비자 스레드는 대기열에 정보가 있어야 가져온다.
    • 생산자 스레드와 소비자 스레드는 서로 대기열 정보에 관련한 시그널을 보낸다.
  • 읽기/쓰기(Readers-Writers)
    • 읽기 스레드를 위해 공유 자원을 사용하게 되면, 쓰기 스레드가 이 공유 자원을 갱신할 때 처리율(throughput) 문제가 생길 수 있다.
    • 처리율을 강조하면 기아(starvation) 현상이 생기거나 오래된 정보가 쌓인다.
    • 쓰기 스레드가 버퍼를 오랫동안 점유하게 되면 읽기 스레드는 버퍼를 기다리느라 처리율이 떨어진다.
  • 식사하는 철학자들(Dining Philosophers)

동기화하는 메소드 사이에 존재하는 의존성을 이해해라

  • 공유 객체 하나에는 메소드 하나만 사용하기
  • 공유 객체 하나에 여러 메소드가 필요한 경우
    • 클라이언트에서 잠금 : 클라이언트에서 첫 번째 메소드를 호출하기 전에 서버를 잠근다. 마지막 메소드를 호출할 때까지 잠금 유지
    • 서버에서 잠금 : 서버에서 "서버를 잠그고 모든 메소드를 호출한 후 잠금을 해제하는" 메소드를 구현한다. 클라이언트가 이 메소드를 호출한다.
    • 연결(Adpated) 서버 : 잠금을 수행하는 중간 단계를 생성한다.

동기화하는 부분을 작게 만들어라

  • 자바에서 synchronized 키워드를 사용하면 락을 설정한다.
  • 락은 스레드를 지연시키고 부하를 가중시키기 때문에 키워드를 남발하지 않는다.
  • 임계영역 개수를 최대한 줄이는 것이 좋지만, 필요 이상의 임계영역 크기는 스레드 간의 경쟁이 늘어나고 프로그램 성능이 떨어진다.

올바른 종료 코드는 구현하기 어렵다

  • ex. 데드락
    • 부모 스레드가 자식 스레드를 여러 개 만들고, 모두 끝나면 자원을 해제하고 종료하는 시스템
    • 자식 스레드 하나가 데드락에 걸리면 부모 스레드는 계속 기다리게되고, 프로그램은 종료할 수 없다.

스레드 코드 테스트하기

  • 테스트가 정확성을 보장하지는 않지만, 위험도를 낮출 수 있다.
  • 문제를 노출하는 테스트 케이스를 작성하라
  • 구체적인 지침
    • 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라
    • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
    • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드르 ㄹ구현하라
    • 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
    • 프로세서 수보다 많은 스레드를 돌려보라
    • 다른 플랫폼에서 돌려보라
    • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라
    • 직접 구현하기
    • 자동화

📌 14장 점진적인 개선

점진적으로 개선하다

  • 테스트 주도 개발(Test-Driven Development)
    • 언제 어느 때라도 시스템이 돌아가야 한다.
    • 내용이 변경되어도 시스템 변경 전과 똑같이 돌아가야 한다.
  • 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다.
  • 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 쉬워진다.

📌 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를 사용해도 된다.

https://www.hackerrank.com/challenges/big-sorting/problem?isFullScreen=true 

 

Big Sorting | HackerRank

Sort an array of very long numeric strings.

www.hackerrank.com

 

기존 풀이 방법

  • String -> int 
  • int -> String 하는 과정에서 시간초과로 실패 
    public static List<String> bigSorting(List<String> unsorted) {
        List<BigInteger> sorted = unsorted.stream()
                .map(BigInteger::new).sorted().collect(Collectors.toList());
        Collections.sort(sorted);
        List<String> answer = sorted.stream()
                .map(String::valueOf)
                .collect(Collectors.toList());
        return answer;
    }

 

새로운 풀이 방법

  • compareTo를 통해서 자릿수를 비교해가면서 오름차순 정렬
    public static List<String> bigSorting(List<String> unsorted) {
        Collections.sort(unsorted, (x, y) -> {
            if(x.length() == y.length()){
                return x.compareTo(y);
            } else {
                return x.length() - y.length();
            }
        });
        return unsorted;
    }

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를 제한하는 것
    • 사전에 지정된 도메인이나 범위가 아니면, 리소스를 가져올 수 없게 제한한다.

+ Recent posts