3.1 람다란 무엇인가?

람다 표현식은 메서드로 전달 할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생 할 수 있는 예외 리스트는 가질 수 있다.

다음은 람다의 특징이다.

1. 익명

 - 보통의 메서드와 달리 이림이 없으므로 익명이라 표현한다.

2. 함수

 - 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식 ,가능한 예외

    리스트를 포함한다.

3. 전달

 - 람다 표현식을 메서드 인수로 전달하거나 변수로 저장 할 수 있다.

4. 간결성

 - 익명 클래스처럼 많은 코드를 구현 할 필요가 없다.

 

람다표현식을 사용시 간결한 방식으로 코드를 전달 할 수있다. 람다가 기술적으로 Java 8 이전의 Java로 할 수 없었던 일을 제공하지는 않는다. 

    	// 기존코드
        Comparator<Apple> byWeight1 = new Comparator<Apple>() {
            public int compare(Apple o1, Apple o2) {
                return Integer.compare(o1.getWeight(), o2.getWeight());
            }
        };
        
        // 람다를 이용한 코드
        Comparator<Apple> byWeight2 =
                (Apple o1, Apple o2) -> Integer.compare(o1.getWeight(), o2.getWeight());

람다 표현식을 이용하면 compare 메서드의 바디를 직접 전달하는 것처럼 코드를 전달 할 수 있다.

3.2 어디에, 어떻게 람다를 사용할까?

3.2.1 함수형 인터페이스

오직 하나의 추상 메서드가 있는(디폴트 메서드 제외)  Predicate, Comparator, Runnable 등 이있다.

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달 할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급

(기술적으로 따지면 함수형 인터페이스를 구현한 클래스의 인스턴스) 할 수 있다.

 

예전에는 함수형 인터페이스의 입, 출력을 다 기억했는데, 간만에 보니 새롭다.🥲

  public static void main(String[] args) {

        Runnable r1 = () -> System.out.println("run() Method 실행-1");

        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("run() Method 실행-2");
            }
        };

        process(r1);
        process(r2);
        process(()-> System.out.println("run() Method 실행-3"));

    }

    public static void process(Runnable r){
        r.run();
    }

3.2.2 함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.

Ex) Runnable의 run메서드는 인수와 반환값이 없으므로(void 반환) 인수와 반환값이 없는 시그니처이다.

 

람다와 함수형 인터페이스를 가리키는 특별한 표기법을 사용한다.

() -> void 표기는 파라미터가 없고, void를 반환하는 함수를 의미한다. 즉, 앞에서 설명한 Runnable이 이에 해당한다.

(Apple,  Apple) -> int 는 두개의 Apple을 인수로 받아 int를 반환하는 함수를 가리킨다.

	 // 정상적인 람다 표현식
        process(()-> System.out.println("run() Method 실행"));
        // 중괄호로 감싼 표현
        process(()->{
            System.out.println("run() Method 실행");
        });
        //한개의 void 메소드 호출은 중괄호로 감쌀 필요가 없다.

✅ @FunctionalInterface 란?

함수형 인터페이스를 가르키는 어노테이션이다. @FunctionalInterface로 인터페이스를 선언했지만, 실제로 함수형 인터페이스가 아니면

컴파일러가 에러를 발생시킨다.

Ex) 추상 메서드가 한개 이상이라면 에러발생

3.3 람다 활용 : 실행 어라운드 패턴

자원처리(예를들면 데이터베이스의 파일처리)에 사용하는 순환패턴은 자원을 열고, 처리한 다음, 자원을 닫는 순서로 이루어진다.

설정과 정리 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두과정이 둘러싸는 형태를 갖는다.

실행 어라운드 패턴

3.3.1 1단계 : 동작 파라미터화를 기억하라

현재 코드는 파일에서 한번에 한 줄만 읽을 수 있다. 한번에 두중을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야할까?

processFile의 동작을 파라미터화하자.

processFile 메서드가 한번에 두 행을 읽게 하려면 아래 코드로 수정이 가능하다.

    // 파일 두행을 읽도록 수정
    String result = processFile((BufferedReader br) -> 
                        { return br.readLine() + br.readLine()});

3.3.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달

함수형 인터페이스 자리에 람다를 사용 할 수 있다. BufferedReader와 일치하는 인터페이스를 정의하자.

@FunctionalInterface
public interface BufferedReaderProcessor {

    String process(BufferedReader br) throws IOException;
}

3.3.3 3단계 : 동작실행

이제 위 코드에서 정의한 BufferedReaderProcessor 인터페이스의 process 메서드의 시그니처 (BufferedReader -> String)와

일치하는 람다를 전달 할 수 있다. 람다 표현식으로 함수형 인터페이스이 추상 메서드 구현을 직접 전달 할 수 있으며 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리된다.

  public String processFile2(BufferedReaderProcessor p) throws IOException {
        try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
            return p.process(br);
        }
    }

3.3.4 4단계 : 람다 전달

    //람다 표현
    String oneLine = processFile((BufferedReader br) -> br.readLine());

    String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.4 함수형 인터페이스 사용

3.4.1 Predicate

Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 Boolean을 반환한다.

    List<String> listOfStrings = new ArrayList<>();
    
    public <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        List<T> results = new ArrayList<>();
        for (T t : list) {
            if (predicate.test(t)) {
                results.add(t);
            }
        }
        return results;
    }

    Predicate<String> nonEmpryStringPredicate = (String s) -> !s.isEmpty();
  
    List<String> nonEmpty = filter(listOfStrings, nonEmpryStringPredicate);

3.4.2 Consumer

Consumer<T> 인터페이스는 제네릭 형식 T객체를 받아서 void를 반환하는 accept라는 추상메서드를 정의한다.

    public void test(){

        forEach(Arrays.asList(1,2,3,4,5),
                (Integer i) -> System.out.println(i));
    }

    public <T> void forEach(List<T> list, Consumer<T> c){

        for (T t : list) {
            c.accept(t);
        }
    }

3.4.3 Function

Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.

    public void test(){

        List<Integer> l = map(
                Arrays.asList("lambdas", "in", "action"),
                (String s) -> s.length());
    }

    public <T, R> List<R> map(List<T> list, Function<T, R> function) {
        List<R> result = new ArrayList<>();

        for (T t : list) {
            result.add(function.apply(t));
        }
        return result;
    }

기본형 특화

자바의 모든 형식은 참조형(Byte, Integer, Object, List 등) 아니면 기본형(int, double, byte, char 등)에 해당한다.

하지만 제네릭 파라미터에는 참조형만 사용 할 수 있다. 제네릭의 내부 구현 때문에 어쩔수 없는 일이다.

자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱이라고한다.

반대로 참조형을 기본형으로 변환하는 동작을 언박싱이라고 한다. 또한,개발자가 편리하게 코드를 구현 할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 오토박싱이라는 기능도 제공한다.

하지만 이런 변환과정은 메모리를 더 소비하며 기본형을 가져올때도 메모리를 탐색하는 과정이 필요하다.

자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.

   IntPredicate evenNumbers = (int i) -> i % 2 == 0;

    Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
    public void test() {
        evenNumbers.test(1000); // true(박싱 없음)

        oddNumbers.test(1000); // false(박싱)
    }

3.5 형식 검사, 형식 추론, 제약

3.5.1 형식 검사

람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론 할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상형식이라고 한다. 

   List<Apple> result = filter(inventory, (Apple apple) 
   					-> apple.getWeight > 150);

1. filter 메서드의 선언을 확인한다.

2. filter 메서드는 두번째 파라미터로 Predicate<Apple> 형식을 기대한다.

3. Predicate<Apple>은 test라는 한개의 추상 메서드를 정의하는 함수형 인터페이스이다.

4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.

5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

3.5.2 같은 람다, 다른 함수형 인터페이스

대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용 될 수 있다.

    Comparator<Apple> c1 = (Apple a1, Apple a2) -> {
        Integer a3 = a1.getWeight();
        Integer a4 = a2.getWeight();

        return a3.compareTo(a4);
    };

    ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> {
        Integer a3 = a1.getWeight();
        Integer a4 = a2.getWeight();

        return a3.compareTo(a4);
    };
    BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> {
        Integer a3 = a1.getWeight();
        Integer a4 = a2.getWeight();

        return a3.compareTo(a4);
    };

3.5.3 형식 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.

즉 컴파일러는 람다 표현식의 파라미터 형식에 접근 할 수 있으므로 람다 문법에서 이를 생략 할 수 있다.

    // 형식을 추론하지않았음.
    Comparator<Apple> c1 = (Apple a1, Apple a2) -> {
        Integer overrideA1 = a1.getWeight();
        Integer overrideA2 = a2.getWeight();

       return overrideA1.compareTo(overrideA2);
    };

    //형식을 추론
    Comparator<Apple> c2 = (a1, a2) -> {
        Integer overrideA1 = a1.getWeight();
        Integer overrideA2 = a2.getWeight();

        return overrideA1.compareTo(overrideA2);
    };

상황에 따라 명시적으로 형식을 알맞게 포함, 생략하는 것이 중요한듯하다.

3.5.4 지역 변수 사용

지금까지 모든 람다 표현식은 인수를 자신의 바디 안에서 사용했지만, 자유변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 

활용 할 수 있다. 이와 같은 동작을 람다 캡처링이라한다.

   int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);

하지만 자유변수에도 제약이 있다. 

지역변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야한다.

    public static void main(String[] args) {

        int portNumber = 1337;
        Runnable r = () -> System.out.println(portNumber); // 에러발생
        portNumber = 1111;
        r.run();
    }

3.6 메서드 참조

메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달 할 수 있다.

        // 기존 코드
        List<Apple> inventory = new ArrayList<>();

        inventory.sort((Apple a1, Apple a2) ->{
            Integer overrideA1 = a1.getWeight();
            Integer overrideA2 = a2.getWeight();

            return overrideA1.compareTo(overrideA2);
        });

        // 메서드 참조
        inventory.sort(comparing(Apple::getWeight));

3.6.1 요약

메서드 참조를 이용하면, 기존 메서드 구현으로 람다 표현식을 만들 수 있다. 이때 명시적으로 메서드명을 참조함으로서 가독성 을 높일 수 있다. 

메서드 명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용 할 수 있다. 

Ex) Apple::getWeight는 Apple 클래스에 정의된 getWeight의 메서드 참조이다.

메서드 참조를 만드는 방법

1. 정적 메서드 참조

 - Ex) Integer의 paseInt 메서드는 Integer::parseInt로 표현

2. 다양한 형식의 인스턴스 메서드 참조

 - Ex) String의 length 메서드는 String::length로 표현

3. 기존 객체의 인스턴스 메서드 참조

 - Ex) Transction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면,  

            이를, expensiveTransaction::getValue라고 표현

컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다.

즉, 메서드 참조는 콘텍스트의 형식과 일치해야한다.

3.6.2 생성자 참조

ClassName::new 처럼 클래스 명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다.

Supplier, Function 예제

        /**
         * 3.6.2 생성자 참조
         */
        Supplier<Apple> c1 = Apple::new;
        Apple a1 = c1.get();

        Supplier<Apple> c2 = () -> new Apple();
        Apple a2 = c2.get();

        Function<Integer, Apple> c3 = Apple::new;
        Apple a3 = c3.apply(100);

        Function<Integer, Apple> c4 = (Integer) -> new Apple(Integer);
        Apple a4 = c4.apply(120);

Integer를 포함하는 List 예제

    List<Integer> weights = Arrays.asList(7, 3, 4, 10);
    List<Apple> apples = map(weights, Apple::new);

    public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
        List<Apple> result = new ArrayList<>();
        for (Integer i : list) {
            result.add(f.apply(i)); // Function을 이용 Integer -> Apple 변형 후 add
        }
        return result;
    }

BiFunction 인터페이스 예제

    BiFunction<Color, Integer, Apple> c3 = Apple::new;
    Apple a3 = c3.apply(Color.GREEN, 110);

    BiFunction<Color, Integer, Apple> c4 = (color, weigth) -> new Apple(color, weigth);
    Apple a4 = c4.apply(Color.GREEN, 100);

인스턴스화하지 않고도 새 ㅇ성자에 접근 할 수 있는 기능을 다양한 상황에 응용 할 수 있다. 

예를들어 Map으로 생성자와 문자열값을 관련 시킬 수 있다. 그리고 String과 Integer가 주어졌을때 다양한 무게를 갖는 여러 종류의 과일을 만드는 giveMeFruit라는 메서드를 만들수 있다.

    static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
    static {
        map.put("apple", Apple::new);
        map.put("orange", Orange::new);
    }
    public static Fruit giveMeFruit(String fruit, Integer weight) {
        return map.get(fruit.toLowerCase()).apply(weight);
    }

3.7 람다, 메서드 참조 활용하기

3.7.1 1단계 : 코드 전달

/**
 * 람다, 메서드 참조 활용하기
 */
public class AppleComparator implements Comparator<Apple>{
    /**
     * 3.7.1 1단계 : 코드 전달
     */
    @Override
    public int compare(Apple o1, Apple o2) {
        return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
    }

    public static void main(String[] args) {
        List<Apple> inventory = new ArrayList<>();
        inventory.sort(new AppleComparator());
    }
    
}

3.7.2 2단계 : 익명 클래스 사용

한번만 사용할 Comparator를 위코드 처럼 구현하는 것보다는 익명 클래스를 이용하는것이 좋다.

        /**
         * 3.7.2 2단계 : 익명 클래스 사용
         */
		inventory.sort(new Comparator<Apple>() {
                @Override
                public int compare(Apple o1, Apple o2) {
                    return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
            }
        });

3.7.3 3단계 : 람다 표현식 사용

        /**
         * 3.7.3 3단계 : 함다 표현식 사용
         */
        inventory.sort((Apple o1, Apple o2) -> {
            return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
        });
        /**
         * 파라미터 추론방식
         */
        inventory.sort((o1, o2) -> {
            return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
        });

 

3.7.4 4단계 : 메서드 참조 사용

        /**
         * 3.7.4 4단계 : 메서드 참조 사용
         */
        inventory.sort(comparing(Apple::getWeight));

Java 8 이전에 비해 코드만 짧아진 것이 아니라 코드의 의미도 명확해졌다.

3.8 람다 표현식을 조합할 수 있는 유용한 메서드

3.8.1 Comparator 조합

정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환 할 수 있다.

        /**
         * 3.8.1 Comparator 조합
         */
        Comparator<Apple> c = comparing(Apple::getWeight);

역정렬

사과의 무게를 내림차순으로 정렬하고 싶다면 어떻게 해야 할까?

인터페이스 자 체에서 주어진 비교자의순서를 뒤바꾸는 reverse라는 디폴트 메서드를 사용하면 된다!

        /**
         * 역정렬
         */
        inventory.sort(comparing(Apple::getWeight).reversed());

Comparator 연결

여기서 무게가 같은 사과가 있다면 어떻게 해야할까?

이럴때는 비교결과를 더 다듬는 두번째 Comparator를 만들 수 있다.

        /**
         * Comparator 연결
         */
        inventory.sort(comparing(Apple::getWeight)
                .reversed()
                .thenComparing(Apple::getColor)); // 색깔별로 정렬

3.8.2 Predicate 조합

Predicate 인터페이스는 negate, and, or 세가지 메서드를 제공한다.

        /**
         * 3.8.2 Predicate 조합
         */
        Predicate<Apple> redApples = (Apple apple) -> Color.RED.equals(apple.getColor());
        // redApples의 결과를 반전시킨다.
        Predicate<Apple> notRedApple = redApples.negate();
        // 두 Predicate를 연결하여 새로운 Predicate로 만든다.
        Predicate<Apple> redAndHeavyApples = redApples.and((apple -> apple.getWeight() > 150));
        // Predicate 를 연결하여 복잡한 Predicate를 만든다.
        Predicate<Apple> redAndHeavyAppleOrGreen = redApples.and(apple -> apple.getWeight() > 150)
                                                .or(apple -> Color.GREEN.equals(apple.getColor()));

3.8.3 Funciton 조합

Function 인터페이스는 Function 인터페이스를 반환하는 andThen, compose 두가지 메서드를 제공한다.

andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 ㅇ비력으로 전달하는 함수를 반환한다.

compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.

        // andThen
        Function<Integer, Integer> f1 = x -> x + 1;
        Function<Integer, Integer> g1 = x -> x * 2;
        Function<Integer, Integer> h1 = f1.andThen(g1);
        int result1 = h1.apply(1); // 4을 반환

        // compose
        Function<Integer, Integer> f2 = x -> x + 1;
        Function<Integer, Integer> g2 = x -> x * 2;
        Function<Integer, Integer> h2 = f2.compose(g2);
        int result2 = h2.apply(1); // 3을 반환

예제 코드 : https://github.com/seungsoos/ModernJavaInAction

동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응 할 수 있다.

동작 파라미터화란 아직은 어떻게 실행 할 것인지 결정하지 않은 코드 블록을 의미한다. 즉, 코드블록에 따라 메서드의 동작이 파라미터화된다.

2.1 변화하는 요구사항에 대응하기

2.1.1 첫번째 시도 : 녹색 사과를 필터링

public enum Color {
    RED, GREEN
}
   public static List<Apple> filterGreenApples(List<Apple> inventory) {

        List<Apple> result = new ArrayList<>(); // 사과 누적 리스트
        for (Apple apple : inventory) {
            if (Color.GREEN.equals(apple.getColor())) { // 녹색사과 만 선택
                result.add(apple);
            }
        }
        return result;
    }

녹색사과를 선택하는데 필요한 조건에서 녹색사과 말고 빨간 사과도 필터링을 하게 변화한다. 크게 고민하지않는다면 , 해당 메서드를 복사하여 빨간사과를 필터링 할 수 있지만, 나중에 다양한 색을 필터링 시에는 적절히 대응 할 수 없다.

 

2.1.2 두번째 시도 : 색을 파라미터화

 public static List<Apple> filterGreenApples(List<Apple> inventory, Color color) {

        List<Apple> result = new ArrayList<>(); 
        for (Apple apple : inventory) {
            if (apple.getColor().equals(color)) { 
                result.add(apple);
            }
        }
        return result;
    }
        List<Apple> greenApples = filterApplesByColor(inventory, Color.GREEN);
        List<Apple> redApples = filterApplesByColor(inventory, Color.RED);

해당 형식으로 색을 파라미터화 하였다. 하지만 색 이외에도 가벼운 사과와 무거운 사과로 구분하는 조건이 추가되었다.

보통 무게가 150g 이상인 사과가 무거운 사과이다.

앞선 요구사항을 듣다보면 색과 마찬가지로 앞으로 무게의 기준도 얼마든지 바뀔 수 있다.

 

그러면 어떠한 기준으로 사과를 필터링 할 것인가? 색이나 무게중 어떤것을 기준으로 필터링할지 가리키는 플래그를 추가 할 수 있다.

2.1.3 세번째 시도 : 가능한 모든 속성으로 필터링

    public static void main(String[] args) {
        List<Apple> inventory = null;
        List<Apple> greenApples = filterApple(inventory, Color.GREEN, 0, true);
        List<Apple> redApples = filterApple(inventory, null, 150, false);
    }
    public static List<Apple> filterApple(List<Apple> inventory, Color color, int weight, boolean flag) {

        List<Apple> result = new ArrayList<>();
        for (Apple apple : inventory) {
            if ((flag && apple.getColor().equals(color)) ||
                    (!flag && apple.getWeight() > weight)) {
                result.add(apple);
            }
        }
        return result;
    }

true, false는 무엇을 의미하는걸까,,

가독성이 좋지않다. 이해하기 어렵다. 요구사항이 변경시 어떻게 바꿔야할까? 라는 생각이 든다.

2.2 동작 파라미터화

앞선 예제를 좀더 유연하게 대응 하는 방법으로, 참 또는 거짓을 반환하는 함수 프레디케이트 인터페이스를 정의하자.

※ 프레디케이트 : 논리학에서 프레디케이트는 일반적으로 특정 주어에 대해 참(true)이나 거짓(false)인지를 나타내는 함수이다.

public interface ApplePredicate {
    boolean test(Apple apple);
}

public class AppleHeavyWeightPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150; // 무거운 사과인지 판별
    }
}


public class AppleGreenColorPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
        return Color.GREEN.equals(apple.getColor()); // 녹색 사과인지 판별
    }
}

사과를 선택하는 다양한 전략 이를, 전략 디자인 패턴 이라고한다.

전략 디자인 패턴은 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.

알고리즘 패밀리 : 인터페이스 ApplePredicate / 구현체는 전략

2.2.1 네번째 시도 : 추상적 조건으로 필터링

    public static List<Apple> filterApple(List<Apple> inventory, ApplePredicate  applePredicate) {

        List<Apple> result = new ArrayList<>();
        for (Apple apple : inventory) {
            if (applePredicate.test(apple)) { // ApplePredicate 객체를 이용한 사과 검사 조건
                result.add(apple);
            }
        }
        return result;

 앞전의 코드에 비해 유연한 코드와 가독성이 좋아졌다.

Ex) 150g이 넘는 빨간사과를 검색하는 요구사항시,

public class AppleRedAndHeavyPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return Color.RED.equals(apple.getColor()) && apple.getWeight() > 150;
    }
}

구현이 가능하다.

해당 메서드는 test메서드이다. 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야한다.

2.3 복잡한 과정 간소화

public class FilteringApples {

    public static void main(String[] args) {
        List<Apple> inventory = Arrays.asList(new Apple(80, Color.GREEN),
                new Apple(155, Color.GREEN),
                new Apple(120, Color.RED));
        List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
        List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());

        System.out.println("heavyApples value = " + heavyApples);
        System.out.println("greenApples value = " + greenApples);
    }

    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate applePredicate) {
        List<Apple> result = new ArrayList<>();
        for (Apple apple : inventory) {
            if (applePredicate.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }
}

새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의해야한다. 상당히 번거로우며, 로직과 관련 없는 코드가 많이 추가된다. 자바는 클래스 선언과 인스턴스화를 동시에 수행 할 수 있는 익명 클래스가 있다.

2.3.1 익명 클래스

익명클래스는 말 그대로 이름이 없는 클래스이다. 익명클래스를 이용시 필요한 구현을 만들어 사용이 가능하다.

2.3.2 다섯번째 시도 : 익명 클래스 사용

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return Color.RED.equals(apple.getColor());
            }
        });

익명클래스를 사용하여도 첫째, 엄청난 변화가 있지는 않다, 여전히 많은 공간을 차지한다.

둘째, 많은 사람들이 익명 클래스의 사용에 익숙하지 않다.

장황한 코드는 구현하고, 유지보수하는데 시간이 오래 걸리며, 가독성이 좋지않다. 

2.3.3 여섯번째 시도 : 람다 표현식 사용

람다 표현식을 사용한 재구현이다.

List<Apple> result = 
		filterApples(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));

이전코드보다 훨씬 간단하고, 가독성이 좋다👍🏻

2.3.4 일곱번째 시도 : 리스트 형식으로 추상화

public class TestClass {

    /**
     *  제네릭 사용
     *
     *  데이터 타입에 의존적이지 않고,
     *  외부에 의해 타입을 지정
     */
    public static <T> List<T> filter(List<T> list, Predicate<T> p){
        List<T> result = new ArrayList<>();

        for (T e : list) {
            if (p.test(e)) {
                result.add(e);
            }
        }
        return result;
    }
}

이제 다양한 타입을 필터 메서드로 사용 할 수 있다.

Ex) 람다표현식

List<Apple> redApples = 
		filter(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));

 

참고 : Comparator, Comparable

예제 코드 : https://github.com/seungsoos/ModernJavaInAction

1. DB 접근 기술

JDBC(Java Database Connectivity)

  • JDBC API를 이용하여 JDBC Driver의 변경에따른 DB 접근
  • 동작원리

  • 예제)
package sec01.ex01.dao;

import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

import sec01.ex01.dto.MemberDTO;

public class MemberDAO {
   
   String driver = "oracle.jdbc.OracleDriver";
   String url = "jdbc:oracle:thin:@localhost:1521:xe";
   String user = "user1";
   String pwd = "1234";
   
   private Connection conn = null;
   private Statement stmt = null;
   private PreparedStatement pstmt = null;
   private ResultSet rs = null;
   
   private DataSource ds;

   public List<MemberDTO> listMember(){
      
      List<MemberDTO> list = new ArrayList<>();
      
      try {
         Class.forName(driver);
         System.out.println("Oracle 드라이버 로딩 성공");
         
         conn = DriverManager.getConnection(url, user, pwd);
         System.out.println("Connection 성공");
         
         stmt = conn.createStatement();
         System.out.println("Statment 생성 성공");
         
         String sql = "SELECT *FROM MEMBERTEST";
         
         rs =  stmt.executeQuery(sql);
         
         while(rs.next()) {
            String id = rs.getString("id");
            String pwd = rs.getString("pwd");
            String name = rs.getString("name");
            String email = rs.getString("email");
            Date joinDate = rs.getDate("joinDate");
            
            MemberDTO dto = new MemberDTO();
            dto.setId(id);
            dto.setPwd(pwd);
            dto.setName(name);
            dto.setEmail(email);
            dto.setJoinDate(joinDate);
            
            list.add(dto);
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {if(rs != null)  rs.close();} catch (Exception e2) {}
         try {if(stmt != null)stmt.close();} catch (Exception e2) {}
         try {if(conn != null)conn.close();} catch (Exception e2) {}
      }
      return list;
   }
}
  •  단점
    1. 쿼리를 실행하기 전, 후 많은 코드를 작성해야한다.
      • DB와의 연결 설정 및 객체 자원을 사용후 종료(close)해야한다.
    2.  DB 관련 로직에서 예외처리코드를 작성해야한다.
      • DB연결 설정에서 예외처리가 필수가 된다.
    3. 중복코드가 많다.
    4. 가독성이 좋지않다.
    5. 유지,보수가 어렵다.

SQL Mapper

  • 객체와 테이블 간의 관계를 직접 매핑하는 것이 아닌, SQL문을 실행해 쿼리 수행 결과를 어떤 객체에 바인딩 하는 방법이다.
  • 따라서 DBMS에 종속적인 방법이라고 할 수 있으며, 대표적으로 MyBatis가 있다

ORM

  • ORM 기술은 객체(Object)와 DB테이블을 매핑하여 데이터를 객체화하는 기술이다.
  • 개발자가 직접 SQL을 작성하지 않아도 자동으로 SQL문을 만들어내기 때문에 DBMS에 종속적이지 않다. 대표적으로 JPA가 있다.

2. MyBatis

  • Concept :  SQL과 Java 코드를 분리

MyBatis는 개발자에게 유연하고 효율적인 데이터베이스 액세스를 제공한다. SQL 중심적인 접근 방식과 매핑 기능을 통해 개발자는 데이터베이스 조작을 세밀하게 제어할 수 있으며, 성능 최적화와 단순성을 통해 개발 생산성과 코드의 가독성을 높인다.

 

3. JPA

    • Concept : DB 테이블을 매핑해서 데이터를 하나의 객체로 간주하고자 하며, 이러한
                         사상을
      통해서  DB와 객체지향의 패러다임 불일치를 해결하고자 한다.

JPA는 객체 지향적인 프로그래밍 모델을 사용하여 생산성을 향상시키고 유지보수를 용이하게 한다.

또한, 데이터 베이스에 대한 종속성을 줄이고 이식성을 높여서 유연성을 제공한다.

 

 

4. MyBatis 동작과정 및 장단점

동작과정

MyBatis데이터베이스 액세스 흐름

MyBatis를 사용하기 위한 기본적인 자바 인터페이스는 SqlSession이다. 이 인터페이스를 통해 명령어를 실행하고, 메퍼를 얻으며, 트랜잭션을 관리 할 수 있다. SqlSession은 인터페이스로 구현체인 SqlSessionTemplate이 있다. SqlSessionTemplate은 필요한 시점에 세션을 닫고, 커밋하거나 롤백하는 것을 포함한 세션의 생명주기를 관리한다.

 MyBatis공식문서

장단점

예시) 간략한 Team, Member라는 테이블이 있다.

DB테이블 구성도

Java Entity

두 테이블을 Join하여 모든 컬럼을 가져올때 객체지향적이지 않은, DB에 종속적인 방법을 사용하게된다.

이때 조회 결과값을 담을 Entity가없기때문에, 하나의 Entity를 만들어 사용하게된다.

※ 물론 Join 결과값을 Result <resultMap> 이라는 방법으로 1:1, 1:N. N:N  사용이 가능하나,

   실무 프로젝트 기준으로 생각하였을때 매우 복잡하다. 그렇기에 대다수의 사람들이 MyBatis를 이러한 방식으로 사용한다.

또한 insert시 Team에 대한 insert 후 useGeneratedKey방식을 이용하여 PK값을 얻어온이후 다시 Member에 대한 insert를 사용해야한다.

  • 장점
    1. SQL 리를 직접 작성하므로, 최적화된 쿼리를 구현 할 수 있다.
    2. 복잡한 쿼리도 구현 가능하다.
  • 단점
    1. 컴파일 시 오류를 확인 할 수없다.
    2. 쿼리를 직접 작성하기 때문에 JPA에 비해 생산성이 떨어진다.
    3. DB, Entity 변경 시 소스 및 쿼리 수정이 필요하다.
    4. 중복 쿼리가 발생하기 쉽다.

★ MyBatis 사용시 객체 지향적인 Java와 데이터 지향적인 DB의 DB에 종속적인 패러다임의 불일치가 발생한다.

 

 

5. JPA 동작과정 및 장단점

EntityManger

EntityManager는 데이터베이스와 상호 작용하는 주요한 인터페이스로, 엔티티의 영속성과 일관성을 유지하며, 데이터베이스 작업을 단순화하고 효율적으로 처리하는 데 도움을 준다.

  1. Entity 관리
    • EntityManager를 사용하여 엔티티를 영속성 컨텍스트에 등록하고, 상태 변경을 추적한다.
    •  엔티티를 조회, 저장, 수정, 삭제할 수 있는 기능을 제공한다.
  2. Transaction 관리
    • 트랜잭션의 시작과 종료를 관리하고, 트랜잭션 내에서 엔티티 작업을 수행한다.
    •  트랜잭션을 커밋 또는 롤백하여 데이터베이스의 일관성을 보장한다.
  3. 영속성 컨텍스트 관리

영속성 컨텍스트

영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

영속성 컨텍스트는 내부적으로 1차 캐시영역과 쓰기지연 SQL 저장소 영역이 있다.

영속성 컨텍스트 조회

  • 1차 캐시를 조회하여, 1차 캐시에 저장된 데이터가 있을경우 그대로 조회
  • 1차 캐시를 조회하여, 1차 캐시에 저장된 데이터가 없을경우 DB 조회 후, 1차 캐시에 저장한다.

엔티티 등록 - 쓰기 지연

엔티티 매니저는 데이터 변경 시 반드시 트랜잭션을 시작해야 한다.

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

// 트랜잭션 시작
tx.begin();

// 비영속
Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 영속
em.persist(member);

// 엔티티 등록
tx.commit();
  • em.persist(member); : member 엔티티를 영속 컨텍스트에 저장하지만, 데이터베이스에는 반영되지 않는다.
  • tx.commit(); : 트랜잭션을 커밋하는 순간 데이터베이스에 INSERT SQL을 보내 저장하게 된다.
  • em.persist()를 실행할 때, 영속 컨텍스트의 1차 캐시에는 member 엔티티가 저장되고, 쓰기 지연 SQL 저장소에는 member 엔티티의 INSERT SQL 쿼리문이 저장된다.
  • tx.commit()을 실행하는 순간 쓰기 지연 SQL 저장소에 저장된 INSERT SQL 쿼리를 보내 데이터베이스에 저장하는 것이다.

엔티티 수정 - 변경 감지

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

// 트랜잭션 시작
tx.begin();

// member 조회
Member member = em.find(Member.class, "member");
member.setUsername("hello");
member.setAge("20");

// 엔티티 등록
tx.commit();
  • 영속 컨텍스트의 1차 캐시에는 member의 초기 데이터가 저장되어 있을 것이다.
  • 이후 set 메서드를 통해 데이터를 변경한다.
  • 트랜잭션 커밋 시 flush()가 발생하면서 1차 캐시에서 엔티티와 스냅샷을 비교하여 변경에 대한 감지를 한다.
  • 이후 SQL UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에서 쿼리를 보낸다.

엔티티 삭제

Member member = em.find(Member.class, "member");

// 엔티티 삭제
em.remove(member);
  • 엔티티 삭제는 remove() 메서드를 통해 데이터를 삭제할 수 있다.
  • 영속성 컨텍스트와 데이터베이스에서 모두 제거된다.

엔티티 생명주기

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed): 영속성 컨텍스트에 저장된 상태
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed): 삭제된 상태

장단점

  • 장점
    1. 객체 지향적 프로그래밍 설계가 가능하다.
    2. 기본적인 SQL문법이 지원되기 때문에 생산성이 향상된다.
    3. 데이터베이스 방언이 지원되기 때문에 특정 데이터베이스에 종속되지 않는다.
  • 단점
    • QueryDsl을 사용하여도 MyBatis에 비해 복잡한 쿼리문이 구현하기 힘들다.
    • 러닝커브가 높다.

 

JPA의 경우 개발 생산성이 향상되는 장점은 있으나, 복잡한 쿼리문(통계)의 경우 myBatis 가 직관적이고 효율적이라고 생각하여

프로젝트의 특성에 맞게 사용하는것을 추천한다.

 

 

 

 

JPA

JPA는 Java Persistence API의 약자로, '자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스'이다.

자바 ORM(Object Relational Mapping) 기술에 대한 API 표준 명세이다. 나는 지금껏 '라이브러리' 정도로 생각하며, 사용하였다.

JPA는 단순한 인터페이스 이기 때문에 구현체가 없다. 이런 JPA의 구현체는 아래그림과 같다.

 

JPA를 사용하기 위해서는 JPA를 구현한 Hibernate, DataNucleus, EclipseLink 같은 ORM 프레임워크를 사용해야한다.

그중 Hibernate가 가장 범용적으로 다양한 기능을 제공한다고 한다.

3가지 ORM 프레임워크는 다음에 찾아보자.

 

Hibernate

Hibernate는 JPA 구현체 중 하나이며, 다음은 JPA와 Hibernate의 상속 및 구현 관계이다. 

JPA의 핵심인 EntityManagerFactory, EntityManager, EntityTransaction 인터페이스를 Hibernate는 각각 SessionFactory, Session, Transaction으로 상속받고 Impl로 구현하였다.

Hibernate는 JPA의 구현체이기때문에 다른 ORM프레임워크를 사용해도 되고, JPA는 인터페이스이기 때문에 본인이 구현하여 사용 할 수도 있다.

Hibernate 장점

  1. ORM 방식 사용
    • Hibernate은 자바 객체와 관계형 데이터베이스 간의 매핑을 간단하게 처리한다.
    • 개발자는 SQL 쿼리를 직접 작성하지않고도 객체를 데이터베이스에 저장 및 검색 등이 가능하다.
  2. 성능 최적화
    • Hibernate은 지연 로딩(Lazy loading) 및 캐시 기능을 지원하여 애플리케이션의 성능을 향상시킬 수 있다.
    • 지연 로딩은 필요한 시점에만 데이터를 로드하여 데이터베이스 액세스를 줄여준다.
  3. 트랜잭션 관리
    • Hibernate은 ACID (원자성, 일관성, 고립성, 지속성) 트랜잭션을 지원한다.
    • 트랜잭션 관리를 쉽게 할 수 있으며, 롤백과 커밋 기능을 제공한다.

Hibernate 단점

  1. 성능
    • 물론 SQL을 직접 작성하는 것보다 메서드 호출만으로 쿼리를 수행한다는 것은 성능이 떨어질 수 있다.
    • 실제로 초기의 ORM은 쿼리가 제대로 수행되지 않았고, 성능도 좋지 못했다한다.
    • 그러나 지금은 많이 발전하여, 좋은 성능을 보여주고 있고 계속 발전하고 있다.
  2. ​세밀함
    • 메서드 호출로 DB 데이터를 조작 하기 때문에 세밀함이 떨어진다.
    • 복잡한 통계 분석 쿼리를 메서드만으로 해결하는 것은 힘든 일이다. 이것을 보완하기 위해 SQL과 유사한 기술인 JPQL을 지원한다.
  3. ​러닝커브
    • JPA를 잘 사용하기 위해서는 알아야 할 것이 많습니다. 즉 러닝커브가 높습니다.

Spring Data JPA

Spring Data JPA는 Spring에서 제공하는 모듈 중 하나로 JPA를 쉽고 편하게 사용하도록 도와준다.

기존 JPA의 경우 EntityManager를 주입 받아 사용해야하지만, Spring Data JPA는 JPA를 한단계 더 추상화하킨 Repository 인터페이스를 제공한다.

 

Repository 구현 규칙

Method

 method  기능
 save()  레코드 저장 (insert, update)
 findOne()  primary key로 레코드 한건 찾기
 findAll()  전체 레코드 불러오기. 정렬(sort), 페이징(pageable) 가능
 count()  레코드 갯수
 delete()  레코드 삭제

Keyword

메서드 이름 키워드  샘플  설명
 And  findByEmailAndUserId(String email, String userId)  여러필드를 and 로 검색
 Or  findByEmailOrUserId(String email, String userId)  여러필드를 or 로 검색
 Between  findByCreatedAtBetween(Date fromDate, Date toDate)  필드의 두 값 사이에 있는 항목 검색
 LessThan  findByAgeGraterThanEqual(int age)  작은 항목 검색
 GreaterThanEqual  findByAgeGraterThanEqual(int age)  크거나 같은 항목 검색
 Like  findByNameLike(String name)  like 검색
 IsNull  findByJobIsNull()  null 인 항목 검색
 In  findByJob(String … jobs)  여러 값중에 하나인 항목 검색
 OrderBy  findByEmailOrderByNameAsc(String email)  검색 결과를 정렬하여 전달

 

 

Hibernate와 Spring Data JPA 차이점

Spring Data JPA의 경우 DB에 접근하는 상황에서는  Repository를 정의하여 사용한다. 개발자가 Repository 인터페이스에 정해진 규칙대로 메소드를 입력하면 Spring이 메소드 이름에 적합한 쿼리를 만든다. 또한 Hibernate의 경우 EntityManager를 통해 관리한다. 하지만 Spring Data JPA의 경우 EntityManger를 직접 다루지않고, SimpleJpaRepository의 내부적으로 EntityManager를 사용하고 있다. 

 

Spring Data JPA 공식문서

 

  • Given-When-Then으로 테스트 구조화
    1. 테스트 코드작성시 새줄, 주석을 통한 구분하여 코드의 가독성을 높이자.
    2. Given(~주어졌을때)
    3. When(~경우)
    4. Then(~결과)
  • 의미 있는 어서션 사용하기
    1. assert 불표현식을 사용시 동등연산자(==) 등을 사용시 AssertionError 에러가 발생한다.
    2. 이때 어떤 어서션이 실패했는지만 알뿐 왜 실패 했는지는 모른다.
    3. 더 나은 메시지를 얻으려면 검증하려는 테스트에 가장 적합한 어션을 선택 하자.
  • 실제 값보다 기대 값을 먼저 보이기
    1. assertEquals(기대값, 실제값)사용하기
  • 합당한 허용값 사용하기
  • 예외 처리는 JUnit에 맡기기
    1. assertThrows 어서션을 사용하여 예상되는 예외를 발생시키자.
  • 테스트 설명하기
    • 메서드명 불필요한 용어 삭제
    • @DisplayName을 사용하여 테스트 설명 추가
  • 독립형 테스트 사용하기
    1. @BeforeEach, @BeforeAll 사용시 클래스의 맨위부터 읽어야 하며, 매 단일 테스트 메서드마다 설정 메서드의 역할을 다시 떠올려야한다.
    2. static 메서드로 정의하여 각 단일 테스트마다 사용시 테스트를 독립적으로 사용가능하다.
    3. 어떤 테스트를 한 테스트 클래스에서 다른 테스트 클래스로 옮기기도 쉽다.
  • 테스트 매개변수화
    • 하나의 테스트 내에서 매개변수별로 테스트를 사용시, 첫번째 매게변수의 실패로, 뒤의 매개변수의 결과값은 표시되지 않는다.
    • @ParameterizedTest와 @ValueSource를 사용하여 테스트를 매개변수화가능하다.
    • 실행시 JUnit은 각매개변수마다 별개의 테스트를 실행한다.
  • 경계 케이스 다루기

 

테스트 코드의 중요성은 잘 알고 있지만, 어떻게 해야하는지를 잘 몰랐다. 더 깊은공부의 필요성을 느낌.

  • 빠른 실패
    1. if문 사용시 빠른실패를 통한 코드의 가독성을 높이자.
    2. Depth를 줄일 수 있다.
    3. 모든 검증 조건을 다시 상기 할 필요가 없어진다.
  • 항상 가장 구체적인 예외 잡기
    1. 예외처리시 일반적인 타입을 잡으면 잡아선 안될 오류까지 잡힐 위험이 있다.
    2. try-catch 사용
      • ex) Exception 대신 가장 구체적인 예외를 잡는다.
    3. 가장 구체적인 예외를 잡기 위해 여러 예외를 잡아야 할 수도 있다.
    4. catch 블록을 하나가 아닌 여러개 작성하여 코드는 길지만, 버그가 많은 짧은 코드보다 좋다.
  • 메시지로 원인 설명
    • 메시지 작성시 자세한 정보로 예외의 근본적인 원인을 빠르게 추적 가능하다.
  • 원인 사슬 깨지 않기
  • 변수로 원인 노출
    1. 예외는 단순한 클래스가 아니라 필드, 메서드, 생성자를 가질 수 있느 클래스이다.
    2. 예외의 중복시 맞춤형 예외클래스(Util)를 만들어 사용하자.
  • 타입 변환 전에 항상 타입 검증하기
    • 런타임에 동적 객체 타입을 처리해야 할 경우 타입변경(class) 시 다른 객체가 삽입될 경우 ClassCastException을 일으킨다.
    • 변환 전 타입 검증을 하자 
      1. 결과를 지역 변수(example)에 저장한다.
        -> Object  example =  메서드 결과 저장
      2. instanceof 연산자로 타입을 검증한다.
        -> if(example instanceof 클래스타입)
      3. 참일 경우만  if문이 실행되기에 ClassCastException이 발생하지 않는다.
  • 항상 자원 닫기
    1. 프로그램이 종료될때까지 해제되지 않는 현상을 '자원 누출' 이라 한다.
    2. close 사용시 예외가 발생할 상황도 예측하여 try-catch-finally 사용으로 예외 발생 후 에도 종료되도로 사용하자. 
  • 항상 다수 자원 닫기
    1. 자원을 직접 관리하기 보다, try-with-resources 사용하자.
  • 빈 catch블록 설명하기
    1. 예외처리의 여러가지 방식중 빈 catch 블록을 보면, 누구나 잘못되었다 생각할 것이다.
    2. 해결방법으로는 변수명을 구체적으로 명시해주고, 주석을 통한 설명을 작성하자.
  • 자바 명명 규칙 사용하기
    1. 자바의 패키지, 클래스, 인터페이스, 열거형, 상수, 메서드, 변수, 필드, 매개변수 등 각각의 요소에 맞게 명명하기
    2. 영어 공부의 필요성을 느낌
  • 프레임워크에는 Getter/Setter 규칙 적용
    1. 필드는 private 
    2. Getter / Setter는 public 
    3. 기본생성자를 추가하여 빈 인스턴스 생성 후 사용
    4. Boolean 타입 사용시 'is메서드명' 라 명명
  • 한 글자로 명명하지 않기
    1. 한글자 이름에는 이유가 있을 수 없다. 명확히 사용하자.
  • 축약 쓰지 않기
    1. 축약은 자신만 안다. 코드의 가독성을 높이기위해 사용하지 않기.
    2. 가급적 풀어쓰기를 사용하고, 목적을 잘 보여주는 단어로 사용가능하다. 
  • 무의미한 용어 쓰지 않기
  • 도메인 용어 사용하기
  • 지나치게 많은 주석 없애기
    1. 주석은 중요한 정보를 설명할 때 사용
    2. 너무 많은 주석은 코드사용하지 않기
    3. 프로젝트시 팀원들의 이해를 도울정도만!
  • 주석 처리된 코드 제거
    1. 주석처리 코드는 이해도를 떨어트린다.
    2. 잃어버리고 싶지않은 코드라면 Git을 통해관리하자.
  • 주석을 상수로 대체
    1. 주석은 코드를 설명하는데 필요하지만, 코드로 직접설명하는것이 효율적이다.
    2. 매직넘버의 설명 주석보다는 상수로 대체 사용하자.
  • 주석을 유틸리티 메서드로 대체
    1. 주석을 상수로 사용 할 수 없을 경우 메서드로 정의하자.
    2. 메서드 이름을 통해 무엇을 return할 지 파악이 가능하다.
    3. 메서드가 늘어나지만, 코드가 짧아져 이해하기 쉽다.
    4. 메서드를 재사용 할 수 있다. 
  • 구현 결정 설명하기
    1. 중요한 결정이나 코드에서 까다로운 부분을 설명할 때는 팀내의 규칙 템플릿을 사용
  • 예제로 설명하기
    1. 복잡한 정규식과 같은 코드는 주석을 통한 설명을 반영하자.
    2. 코드가 길고 구조적이지만 정보가 더 많이 제공된다.
  • 패키지를 JavaDoc으로 구조화하기
  • 메서드를 JavaDoc으로 구조화하기
  • 생성자를 JavaDoc으로 구조화하기
  • 매직 넘버를 상수로 대체
    1. 매직넘버는 의미를 파악하기 어려우며, 코드의 가독성을 떨어트림.
    2. 매직 넘버가 있으면 코드를 이해 하기 어려워지고 오류가 발생하기 쉽다.
    3. 상수로 대체시 가독성이 좋아지고 상수명을 통해 뜻이 명확해진다.
  • 정수 상수 대신 열거형
    1. 정수, 상수로 사용 할 경우 버그가 발생할 수 있다.
    2. 열거형으로 사용시 자바 컴파일 단계에서 버그를 예방할 수 있다.
  • For 루프 대신 For-each
    1. for 사용시 인덱스 변수에의한 실수여지 및 IndexOutBoundExceptions 발생가능
    2. for-each 사용시 반복 인덱스를 다루지 않아도 된다. 
    3. index i값의 에러발생 조작이 없다.
  • 순회하며 컬렉션 수정하지 않기
    1. for-each 순회시 ConcurrentModificationException 발생 우려 
    2. ConcurrentModificationException은 List의 요소를 변경, 삭제시 발생 할 수 있다.
    3. Iterator는 ConcurrentModificationException가 발생하지 않는다.
  • 순회하며 계산 집약적 연산하지 않기
    1. 한번에 쉽게 컴파일 하기
      ex) for문 사용마다 객체생성 x -> 객체 생성후 for문 사용o
    2. 잠재적 성능 저하를 막기
  • 새 줄로 그루핑
    1. 코드의 가독성을 높이기 위해 여백을 사용하자.
  • 이어붙이기 대신 서식화
    1. 어떻게 출력할지와 무엇을 출력할지 분리
      ex) System.out.printf(%)사용
    2. 코드의 가독성이 좋음
  • 직접 만들지 말고 자바 API 사용하기

시작하며

1. 코딩 실력  Level Up

2. 클린코드

 

책을 볼때는 이해가 되지만, 많은 내용이 오래되지 않아 간략 요약 정리하며, 내생각을 추가 작성하였다. 

 

  • 쓸모없는 비교 피하기
    • 최초의 나의 코드는 if, else if, else 를 많이 사용하는 좋지않은 코드였다. 내가봐도 이해가 힘들다.
  • 부정 피하기
    • if 조건문 사용시 '!' 및 '부정 Boolean 메서드' 를 사용하다보면 코드를 읽기 어렵다, 부정이 아닌 긍정 true를 사용하자.
  • 불 표현식을 직접 반환
    • 지저분한 if를 사용하기보다는 Boolean 메서드를 사용하자. Depth를 줄일 수 있다.
  • 불 표현식 간소화
    • 메서드 추출을 통한 코드의 간소화하자.
  • 조건문에서 NullPointException 피하기 
    • NullPointException을 피하기 위해 코드에서 Null Check를 가장먼저하기.
  • 스위치 실패 피하기
    • switch문을 사용시 break 사용을 활성화 하자.
  • 항상 괄호 사용하기
    • 스위치 실패 피하기와 마찬가지로 if문 사용시 {} 사용을 활성화 하자.
  • 코드 대칭 이루기
    • 코드의 대칭성을 이루어 가독성을 높이자. 

+ Recent posts