방어적 복사(defensive copying) : 가변 객체를 다룰 때, 해당 객체를 외부로부터 보호하기 위해 사용되는 기법이다. 이 기법은 객체의 내부 상태를 변경할 수 없도록 하기 위해 객체를 복사하여 사용하는 것을 의미한다.

 

악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려는 시도가 늘고있는만큼, 클라이언트가 내가 작성한 코드의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야한다.

 

생성자에서 받은 가변 매개변수 각각을 방어적 복사하고, 이 복사본으로 유효성을 검사해야한다. 그리고 인스턴스 안에서는 원본이 아닌 복사본을 사용한다. 그리고 이때 매개변수가 제3자에 의해 확장할 수 있는 타입이라면 방어적 복사본을 만들 때  clone을 사용해서는 안 된다.

  • clone() 메서드는 객체의 얕은 복사(shallow copy)를 생성하므로, 내부에 포함된 가변 객체들은 동일한 참조를 유지하게 됩니다. 이는 복사본을 사용하여도 원본 객체의 상태 변경이 복사본에 영향을 미칠 수 있음을 의미합니다.

 

핵심 정리 : 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.

매개변수 유효성 검사는 메서드나 함수가 실행되기 전에 입력 값이 기대한 조건을 충족하는지 확인하는 과정이다. 이를 통해 잘못된 입력이 메서드나 함수에 전달되는 것을 방지할 수 있다.

 

아래 코드처럼 문서화하고 명시적으로 검사할 수 있다.

/**
 * 두 개의 양수를 더하는 메서드
 * @param a 첫 번째 양수
 * @param b 두 번째 양수
 * @return 두 양수의 합
 * @throws IllegalArgumentException 매개변수가 음수인 경우 발생
 */
public int add(int a, int b) {
    if (a < 0 || b < 0) {
        throw new IllegalArgumentException("매개변수는 양수이어야 합니다.");
    }
    return a + b;
}


 

핵심 정리 : 메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다. 이런 습관을 반드시 기르도록 하자. 그 노력은 유효성 검사가 실제 오류를 처음 걸러낼 때 충분히 보상받을 것이다.

병렬화를 사용하면 여러 프로세스 또는 스레드를 동시에 실행하여 작업을 처리한다. 이렇게 함으로써 여러 프로세서 또는 코어를 활용하여 작업을 분산시키고 병렬적으로 실행함으로써 전체 작업을 효율적으로 처리할 수 있다. 하지만 스트림 병렬화는 절대 시도조차 하지 말자.

 

데이터 소스가 Stream.iterate 거나 중간 연산으로 limit을 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다. Stream.iterate 메서드는 무한한 요소 시퀀스를 생성하므로 병렬 처리가 어렵다. 또한 limit 메서드는 요소의 수를 제한하기 때문에 요소를 나누어 병렬 처리하는 것이 어려워진다. 이러한 상황에서는 파이프라인 병렬화로 성능 향상을 기대하기 어렵다.

 

스트림을 잘못 병렬화하면 (응답 불가를 포함해) 성능이 나빠질 뿐만 아니라  결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다.

 

조건일 잘 갖춰지면 병렬화해 프로세스 코어 수에 비례하는 성능 향상을 만들 수 있지만, 그런 조건은 없다고 봐도 될 것같다.(개인적인생각)

 

 

핵심 정리 : 계산도 올바로 수행하고 성능도 빨라질 거라는 확신 없이는 스트림 파이프라인 병렬화는 시도조차 하지 말라. 스트림을 잘못 병렬화하면 프로그램을 오동작하게 하거나 성능을 급격히 떨어뜨린다. 병렬화하는 편이 낫다고 믿더라도, 수정 후의 코드가 여전히 정확한지 확인하고 운영 환경과 유사한 조건에서 수행해보며 성능지표를 유심히 관찰하라. 그래서 계산도 정확하고 성능도 좋아졌음이 확실해졌을 때, 오직 그럴 때만 병렬화 버전 코드를 운영 코드에 반영하라.

 

그냥 스트림 병렬화는 하지말자.

컬렉션(Collection):

  • 일련의 객체를 저장하고 관리하는 자료구조.
  • 주요 컬렉션 인터페이스에는 List, Set, Queue 등이 있으며, 각각 ArrayList, LinkedList, HashSet, TreeSet, PriorityQueue 등의 구현체가 있다.
  • 컬렉션은 데이터를 저장하고 필요할 때 검색, 추가, 삭제 등의 작업을 수행할 수 있다.
  • 컬렉션은 데이터의 저장 순서가 보장될 수 있고, 중복된 요소를 허용할 수도 있다.

스트림(Stream):

  • 데이터를 처리하는 파이프라인 형태의 API.
  • 자료 처리 연산을 지원하며, 이를 이용하여 데이터를 필터링, 매핑, 정렬, 그룹화하는 등의 작업을 수행할 수 있다.
  • 스트림은 컬렉션, 배열, 파일 등의 데이터 소스에서 데이터를 가져와서 연속된 연산을 수행한다.
  • 스트림은 데이터 소스를 변경하지 않고, 중간 연산과 최종 연산으로 구성되어 있다.
  • 스트림은 데이터를 한 번만 처리할 수 있으며, 처리된 결과는 다시 스트림으로 반환되거나 최종 결과로 수집된다.

원소 시퀀스 : 데이터의 순서가 중요한 시퀀스 형태를 말한다. 이 시퀀스는 각각의 요소가 순서대로 나열되어 있으며, 각 요소는 고유한 위치(인덱스)를 가지고 있다. 대부분의 컬렉션과 배열은 원소 시퀀스로 간주될 수 있다.

 

Iterable : Java에서 컬렉션을 표현하기 위한 인터페이스다. 이 인터페이스는 컬렉션의 요소를 반복하는 데 사용된다.

 

핵심 정리 : 원소 시퀀스를 반환하는 메서드를 작성할 때는, 이를 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있을 수 있음을 떠올리고, 양쪽을 다 만족시키려 노력하자. 컬렉션을 반환할 수 있다면 그렇게 하라. 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList 같은 표준 컬렉션 담아 반환하라. 그렇지 않으면 (반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면) 전용 컬렉션을 구현할지 고민하라. 컬렉션을 반환하는 게 불가능하면 스트림과 Iterable 중 더 자연스러운 것을 반환하라. 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면, 그때는 안심하고 스트림을 반환하면 될 것이다(스트림 처리와 반복 모두에 사용할 수 있으니).

수집기(collector) : 스트림의 최종 연산(terminal operation) 중 하나로, 스트림의 요소를 수집하여 다양한 형태로 결과를 생성하거나 반환하는 역할을 한다.  

  1. toList(): 스트림의 요소를 리스트로 수집한다.
  2. toSet(): 스트림의 요소를 집합(Set)으로 수집한다.
  3. toMap(): 스트림의 요소를 맵(Map)으로 수집한다.
  4. joining(): 문자열 요소를 하나의 문자열로 연결한다.
  5. groupingBy(): 지정된 기준에 따라 스트림의 요소를 그룹화한다.

스트림과 수집기를 사용한 예시

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("apple", "banana", "grape", "watermelon", "orange");

        // 길이가 5 이상인 단어들을 대문자로 변환하여 리스트로 수집
        List<String> result = words.stream()
                                   .filter(word -> word.length() >= 5) // 길이가 5 이상인 단어 필터링
                                   .map(String::toUpperCase)           // 대문자로 변환
                                   .collect(Collectors.toList());      // 리스트로 수집

        // 결과 출력
        System.out.println("Result: " + result);
    }
}

 

핵심 정리 : 스트림 파이프라인 프래그래밍의 핵심은 부작용 없는 함수 객체에 있다. 스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다. 종단 연산 중 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 한다. 계산 자체에는 이용하지 말자. 스트림을 올바로 사용하려면 수집기를 잘 알아둬야 한다. 가장 중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining이다.

스트림 : 데이터 요소들의 연속된 시퀀스를 처리하는 데 사용된다. 스트림은 컬렉션, 배열 등의 데이터 소스로부터 데이터를 읽어들이고, 그 데이터를 변환하거나 조작할 수 있는 기능을 제공한다.

 

스트림을 사용한다고 무조건 더 코드가 좋아지지는 않는다. 스트림을 과도하게 사용하면 프로그램이 읽거나 유지보수하기 더 어려워진다. 특히 스트림에 익숙하지 않은 프로그래머라면 더욱 그렇다. 이를 절충하기 위해 스트림을 적당히 사용하면 명확하게 이해하기 좋아진다.

 

핵심 정리 : 스트림을 사용해야 멋지게 처리할 수 있는 일이 있고, 반복 방식이 더 알맞은 일도 있다. 그리고 수많은 작업이 이 둘을 조합했을 때 가장 멋지게 해결된다. 어느 쪽을 선택하는 확고부동한 규칙은 없지만 참고할 만한 지침 정도는 있다. 어느 쪽이 나은지가 확연히 드러나는 경우가 많겠지만, 아니더라도 방법은 있다. 스트림과 반복 중 어느쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하라.

표준 함수형 인터페이스(Standard Functional Interfaces) : 자바에서 함수형 프로그래밍을 지원하기 위해 미리 정의된 인터페이스들을 말한다. 이러한 인터페이스들은 자주 사용되는 함수형 작업을 수행하기 위해 특별히 설계되었으며, 자바에서 람다 표현식과 함께 주로 사용된다.

 

자바 8에서는 java.util.function 패키지에 표준 함수형 인터페이스들이 정의되어 있다. 이러한 인터페이스들은 주로 함수형 인터페이스를 사용하는 메서드의 매개변수나 반환값으로 사용된다. 주요 표준 함수형 인터페이스들은 다음과 같다:

  • Consumer<T>: 단일 입력 매개변수를 받고, 반환값이 없는 동작을 수행한다. (void accept(T t))
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        // 문자열을 출력하는 Consumer
        Consumer<String> printer = str -> System.out.println(str);
        
        // 사용 예시
        printer.accept("Hello, world!");
    }
}

 

  • Function<T, R>: 입력 값을 받아 다른 타입의 결과 값을 반환합니다. (R apply(T t))
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        // 문자열의 길이를 반환하는 Function
        Function<String, Integer> stringLength = str -> str.length();
        
        // 사용 예시
        int length = stringLength.apply("Hello, world!");
        System.out.println("Length: " + length);
    }
}
  • Predicate<T>: 입력 값을 받아 조건을 검사하고 boolean 값을 반환합니다. (boolean test(T t))
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        // 짝수 여부를 판별하는 Predicate
        Predicate<Integer> isEven = num -> num % 2 == 0;
        
        // 사용 예시
        boolean result = isEven.test(4);
        System.out.println("Is even? " + result); // true
    }
}
  • Supplier<T>: 매개변수가 없고, 반환값을 생성하는 동작을 수행합니다. (T get())
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        // 현재 시간을 제공하는 Supplier
        Supplier<Long> currentTime = () -> System.currentTimeMillis();
        
        // 사용 예시
        long time = currentTime.get();
        System.out.println("Current time: " + time);
    }
}
  • UnaryOperator<T>: 단일 입력 값을 받아 동일한 타입의 결과 값을 반환합니다. (T apply(T t))
  • BinaryOperator<T>: 두 개의 입력 값을 받아 동일한 타입의 결과 값을 반환합니다. (T apply(T t1, T t2))

 

핵심정리 : 이제 자바도 람다를 지원한다. 여러분도 지금부터는 API를 설계할 때 람다도 염두에 두어야 한다는 뜻이다. 입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다. 단, 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있음을 잊지 말자.

 

 

메서드 참조(Method Reference) : 자바에서 메서드를 참조하는 방법을 말한다. 람다 표현식이나 익명 클래스에서 메서드를 호출하는 것 대신에, 이미 존재하는 메서드의 이름을 사용하여 해당 메서드를 참조하는 방식이다.

 

  1. 정적 메서드 참조(Static Method Reference): 클래스이름::정적메서드이름 형태로 사용된다.
  2. 인스턴스 메서드 참조(Instance Method Reference): 참조변수::인스턴스메서드이름 형태로 사용된다.
  3. 클래스 생성자 참조(Constructor Method Reference): 클래스이름::new 형태로 사용되며, 인스턴스를 생성하는 데 사용된다.

 

 

// 람다 표현식
Function<String, Integer> toInteger = s -> Integer.parseInt(s);

// 메서드 참조로 변경
Function<String, Integer> toInteger = Integer::parseInt;

핵심 정리 : 메서드 참조는 람다의 간단명료한 대안이 될 수 있다. 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.

+ Recent posts