silver 5. 요세푸스 문제 (1158)

|

출처 : https://www.acmicpc.net/problem/1158

문제

N명의 사람이 원을 이루며 앉아있고, 양의 정수 K가 주어질 때, 순서대로 K 번째 사람을 제거. 한 사람이 제거되면 남은 사람들로 이루어진 원을 따라 이 과정을 계속해 나가고, N명의 사람이 모두 제거될 때까지 반복. 원에서 사람들이 제거되는 순서를 (N, K)-요세푸스 순열이라고 함

N과 K가 주어지면 (N, K)-요세푸스 순열을 구하는 프로그램을 작성

입력: 첫 줄에는 N과 K가 빈 칸을 사이에 두고 순서대로 주어짐 ($1 <= K <= N <= 5,000$)

출력: 요세푸스 순열 출력

풀이

  • 아이디어: 연결 리스트(Linked List) 활용. K번째인지 확인해주는 변수를 만들어

    • K번째일 경우, 연결 리스트의 첫 번째 값을 꺼내옴
    • K번째가 아닐 경우, 연결 리스트의 첫 번째 값을 꺼내와 마지막 원소로 다시 넣어줌
      import java.util.*;
      import java.io.*;
    	
      public class Main {
        public static void main(String[] args) throws IOException {
          BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
          StringBuilder sb = new StringBuilder();	// 요세푸스 순열을 저장할 변수
    	
          StringTokenizer st = new StringTokenizer(br.readLine());
          Integer N = Integer.parseInt(st.nextToken());
          Integer K = Integer.parseInt(st.nextToken());
          Integer count = 1;	// K번째인지 확인해줄 변수
    	    
          // 1 ~ N을 가지는 리스트 생성 
          ArrayList<Integer> nums = new ArrayList<Integer>();
          for (Integer i = 0; i < N; i++){
            nums.add(i+1);
          }
    	
          LinkedList<Integer> link = new LinkedList<Integer>(nums);	// 1~N의 값을 가지는 연결 리스트 생성
          while(link.size() > 1) {	// 연결 리스트의 길이가 1보다 클 때
            if(count % K == 0) {	// K번째 수인 경우
              sb.append(link.poll() + ", ");	// 첫 번째 원소의 값을 빼와 요세푸스 순열에 넣어줌
            }
            else {	// K번째 수가 아닌 경우
              link.add(link.poll());	// 첫 번째 원소의 값을 빼와 연결 리스트의 마지막 원소로 넣어줌
            }
            count += 1;	// count 값 1 증가
          }
          sb.append(link.poll());	// 마지막 남은 값을 요세푸스 순열에 넣어줌
          System.out.println("<" + sb + ">");	// 결과 출력
        }
      }
    
    • 마지막 남은 값을 따로 넣어주는 이유는 마지막 값을 넣을 때 ‘, ‘를 요세푸스 순열에 포함시키지 않기 위함

Chapter 3. 람다 표현식

|

1. 람다란 무엇인가?

  • 람다 표현식: 메서드로 전달할 수 있는 익명 함수를 단순화한 것
    • 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있음
  • 람다의 특징
    • 익명: 이름이 없음
    • 함수: 특정 클래스에 종속되지 않음
    • 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있음
    • 간결성: 많은 코드를 구현할 필요가 없음
  • 람다 표현식 구성

    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    
    • 파라미터 리스트
    • 화살표: 람다의 파라미터 리스트와 바디를 구분
    • 람다 바디: 람다의 반환값에 해당하는 표현식

람다 표현식 예제

(String s) -> s.length()
  • String 형식의 파라미터 하나를 가지며 int를 반환
  • 람다 표현식에는 return이 함축되어 있으므로 return 문을 명식적으로 사용하지 않아도 됨
(Apple a) -> a.getWeight() > 150
  • Apple 형식의 파라미터 하나를 가지며 boolean을 반환
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}
  • int 형식의 파라미터 두 개를 가지며 리턴값이 없음 (void 리턴)
  • 람다 표현식은 여러 행의 문장을 포함할 수 있음
() -> 42
  • 파라미터가 없으며 int 42를 반환
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
  • Apple 형식의 파라미터 두 개를 가지며 int를 반환

람다의 기본 문법

  • 표현식 스타일(expression style)

    (parameters) -> expression
    
  • 블록 스타일(block-style)

    (parameters) -> {statements;}
    

람다 예제와 사용 사례

사용 사례 람다 예제
불리언 표현식 (List<String> list) -> list.isEmpty()
객체 생성 () -> new Apple(10)
객체에서 소비 (Apple a) -> {System.out.println(a.getWeight());}
객체에서 선택/추출 (String s) -> s.length()
두 값을 조합 (int a, int b) -> a * b
두 객체 비교 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

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

함수형 인터페이스

  • 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스를 의미

  • 자바 API의 함수형 인터페이스로 Comparator, Runnable 등이 있음

    public interface Comparator<T> {
        int compae(T o1, T o2);
    }
    public interface Runnable {
        void run();
    }
      
    public interface ActionListener extends EventListener {
        void actionPerformed(ActionEvnet e);
    }
      
    pblic interface Callable<V> {
        V call() throws Exception;
    }
      
    public interface PrivilegendActio<T> {
        T run();
    }
    
    • 인터페이스는 디폴트 메서드를 포함할 수 있음. 많은 디폴트 메서드가 있더라도 추상 메서드가 하나이면 함수형 인터페이스
    • 디폴트 메서드: 인터페이스의 메서드를 구현하지 않은 클래스를 고려해 기본 구현을 제공하는 바디를 포함하는 메서드
  • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있음

    Runnable r1 = () -> System.out.println("Hello World 1");	// 람다 사용
      
    Runnable r2 = new Runnable(){	// 익명 클래스 사용
        	public void run(){
                System.out.println("Hello World 2");
            }
    };
      
    public static void process(Runnable r){
        r.run();
    }
    process(r1);
    process(r2);
    process(() -> System.out.println("Hello World 3"));	// 직접 전달된 람다 표현식
    

함수 디스크립터

  • 함수형 인터페이스의 추상 메서드 시그니처(signature)는 람다 표현식의 시그니처를 가리킴

  • 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터(function descriptor)라고 부름

    • Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로 Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있음

람다와 함수형 인터페이스를 가리키는 표기법

  • () -> void: 파라미터 리스트가 없으며 void를 반환하는 함수 (ex. Runnable)
  • (Apple, Apple) -> int: 두 개의 Apple을 인수로 받아 int를 반환하는 함수
  • 람다 표현식은 변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며, 함수형 인터페이스의 추상 메서드와 같은 시그니처를 가짐

@FunctionalInterface

  • 함수형 인터페이스에 @FunctionalInterface 어노테이션이 추가됨
  • 함수형 인터페이스임을 가리키는 어노테이션
  • @FunctionalInterface로 인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킴

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

  • 자원 처리에 사용하는 순환 패턴(recurrent pattern)은 자원을 열고, 처리한 다음, 자원을 닫는 순서로 이루어짐
    • 설정(setup)과 정리(cleanup) 과정은 대부분 비슷
    • 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 가짐
  • 실행 어라운드 패턴(excute around pattern): 중복되는 준비 코드와 정리코드가 작업들을 감싸고 있는 패턴

  • 파일에서 한 행을 읽는 코드(try-with-resources 구문 사용)

    public String processFile() throws IOException {
        try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return br.readLine();	// 실제 필요한 작업을 하는 행
        }
    }
    

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

  • 현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있음. 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면?

    • 기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령
    • processFile의 동작을 파라미터화하면 됨
    • processFile 메서드가 BufferedReader를 이용해 다른 동작을 수행할 수 있도록 processFile 메서드로 동작을 전달해야 함
  • processFile 메서드가 한 번에 두 행을 읽어오도록 코드 수정

    String result = processFile((BufferedReader br) -> br.readLibe() + br.readLine());
    
    • BufferedReader를 인수로 받아 String을 반환하는 람다를 사용해 수정

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

  • 함수형 인터페이스 자리에 람다를 사용할 수 있음

    • BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 함
    @FunctionalInterface
    public interface BufferedReaderProcessor {
        String process(BufferedReader b) throws IOException;
    }
    
  • 정의한 인터페이스를 processFile 메서드의 인수로 전달

    public String processFile(BufferedReaderProcessor p) throws IOException {
        ...
    }
    

3단계 : 동작 실행

  • BufferedReaderProccessor에 정의된 process 메서드의 시그니처와 일치하는 람다를 전달할 수 있음

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

  • 따라서 processFile 바디 네에서 BufferedReaderProcessor 객체의 process를 호출할 수 있음

    public String processFile(BufferedReaderProcessor p) throws IOException {
        try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return p.process(br);	// BufferedReader 객체 처리
        }
    }
    

4단계 : 람다 전달

  • 람다를 이용해 다양한 동작을 processFile 메서드로 전달할 수 있음

  • 한 행을 처리하는 코드

    String onLine = processFile((BufferedReader br) -> br.readLine());
    
  • 두 행을 처리하는 코드

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

4. 함수형 인터페이스 사용

  • 함수형 인터페이스는 오직 하나의 추상 메서드를 지정
    • 함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 묘사
    • 함수 디스크립터(function descriptor): 함수형 인터페이스의 추상 메서드 시그니처
  • 다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요
  • 자바 8에서는 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공

Predicate

  • java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환
  • T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있음

Consumer

  • java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의
  • T 형식의 객체를 인수로 받아 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있음

Function

  • java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의
  • 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있음

람다와 함수형 인터페이스 예제

사용 사례 람다 예제 대응하는 함수형 인터페이스
불리언 표현 (List<String> list) -> list.isEmpty() Predicate<List<String>>
객체 생성 () -> new Apple(10) Supplier<Apple>
객체에서 소비 (Apple a) -> System.out.println(a.getWeight()) Consumer<Apple>
객체에서 선택/추출 (String s) -> s.length() Function<String, Integer>,
ToIntFunction<String>
두 값 조합 (int a, int b) -> a * b IntBinaryOperator
두 객체 비교 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) Comparator<Apple>,
BiFunction<Apple, Apple, Integer>,
ToIntBiFunction<Apple, Apple>

예외, 람다, 함수형 인터페이스의 관계

  • 함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않음

    • 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try-catch문으로 감싸야 함
  • Function<T, R> 형식의 함수형 인터페이스를 사용하고 있으며 직접 함수형 인터페이스를 만들기 어려운 상황이라면 try-catch문을 사용해 명시적으로 확인된 예외를 잡을 수 있음

    Function<BufferedReader, String> f = (BufferedReader b) -> {
        try{
            return b.readLine();
        }
        catch(IOException){
            throw new RuntimeException(e);
        }
    };
    

5. 형식 검사, 형식 추론, 제약

형식 검사

  • 람다가 사용되는 콘텍스트(context)를 이용해 람다의 형식(type)을 추론할 수 있음

  • 대상 형식(target type): 어떤 콘텍스트에서 기대되는 람다 표현식의 형식

  • 형식 확인 과정

    List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
    
    1. filter 메서드의 선언 확인 (filter(List<Apple> inventory, Predicate<Apple> p))
    2. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식 (대상 형식)을 기대
    3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
    4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사
    5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 함

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

  • 대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있음
  • 즉, 하나의 함다 표현식을 다양한 함수형 인터페이스에 사용할 수 있음

형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 이용해 람다 표현식과 관련된 함수형 인터페이스를 추론. 즉, 대상 형식을 이용해 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있음

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

    // 파라미터 apple에는 형식을 명시적으로 지정하지 않음
    List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor()));
    
  • 여러 파라미터를 포함하는 람다 표현식에서는 코드 가독성이 향상될 수 있음

    // 형식을 추론하지 않음
    Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    // 형식을 추론함
    Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
    
  • 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있음

지역 변수 허용

  • 지금까지 람다 표현식은 인수를 자신의 바디 안에서만 사용함
  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(free variable, 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있음
  • 람다 캡쳐링(capturing lambda): 람다 표현식 안에서 자유 변수를 활용하는 것
    • 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록)할 수 있음
    • 하지만 캡처하기 위해서는 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 함
    • 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있음
  • 이 예제는 컴파일 할 수 없음

    int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);
    portNumber = 31337;
    
    • portNumber에 값을 두 번 할당하므로 컴파일 할 수 없는 코드

지역 변수의 제약

  • 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 저장됨
  • 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음
  • 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공
  • 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것

클로저(closure)

  • 원칙적으로 클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킴
    • 클로저를 다른 함수의 인수로 전다랄 수 있음
    • 클로저는 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있음
  • 자바 8의 람다와 익병 클래스는 클로저와 비슷한 동작을 수행
    • 람다와 익명 클래스 모두 메서드의 인수로 전달될 수 있으며 자신의 외부 영역의 변수에 접근할 수 있음
    • 하지만 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없음
    • 람다가 정의된 메서드의 지역 변수값은 final 변수여야 하기 때문에 람다는 변수가 아닌 값에 국한되어 동작을 수행
  • 지역 변수값은 스택에 존재하므로 자신을 정의한 스레드와 생존을 같이 해야 하므로 지역 변수는 final이어야 함
    • 가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면 안전하지 않은 동작을 수행할 가능성이 생김
    • 인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 특별한 제약이 없음

6. 메서드 참조

요약

  • 메서드 참조: 특정 메서드만을 호출하는 람다의 축약형
    • 명시적 메서드명을 참조함으로써 가독성을 높일 수 있음
  • 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용
    • Apple::getWeight: Apple 클래스에 정의된 getWeight의 메서드 참조
    • 실제로 메서드를 호출하는 것은 아니므로 괄호는 필요 없음

메서드 참조를 만드는 방법

  • 메서드 참조의 세 가지 유형
    1. 정적 메서드 참조
      • Integer의 parseInt 메서드는 Integer::parseInt로 표현할 수 있음
    2. 다양한 형식의 인스턴스 메서드 참조
      • String의 length 메서드는 String::length로 표현할 수 있음
      • 람다 표현식의 파라미터로 전달할 수 있음 ((String s) -> s.toUpperCase() -> String::toUpperCase())
    3. 기존 객체의 인스턴스 메서드 참조
      • Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면, expensiveTransaction::getValue로 표현할 수 있음
      • 람다 표현식에서 현존하는 외부 객체의 메서드를 호출할 때 사용됨 (() -> expensiveTransaction.getValue() -> expensiveTransaction::getValue)
      • 비공개 헬퍼 메서드를 정의한 상황에서 유용하게 활용
  • 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인
    • 메서드 참조는 콘텍스트의 형식과 일치해야 함

생성자 참조

  • 클래스명과 new 키워드를 이용해 기존 생성자의 참조를 만들 수 있음

    • 정적 메서드의 참조를 만드는 방법과 비슷
  • Supplir의 () -> Apple과 같은 시그니처를 갖는 생성자가 있다고 가정

    Supplier<Apple> c1 = () -> new Apple();	// 람다 표현식은 디폴트 생성자를 가진 Apple을 만듦
    Apple a1 = c1.get();	// Supplier의 get 메서드를 호출해 새로운 Apple 객체를 만듦
      
    // 위와 같은 코드
    Supplier c1 = Apple::new;
    Apple a1 = c1.get();
    
  • Apple(Integer weight)라는 시그니치러를 갖는 생성자는 Function 인터페이스의 시그니처와 같음

    Function<Integer, Apple> c2 = Apple::new;
    Apple a2 = c2.apply(110);	// apply 메서드에 무게를 인수로 호출해 새로운 Apple 객체를 만듦
    
  • Integer를 포함하는 리스트의 각 요소를 map 같은 메서드를 이용해 Apple 생성자로 전달

    List<Integer> weights = Arrays.asList(7, 3, 4, 10);
    List<Apple> apples = map(weights, Apple::new);	// map 메서드로 생성자 참조 전달
      
    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));
        }
        return result;
    }
    
    • 다양한 무게를 포함하는 사과 리스트가 만들어짐
  • Apple(String color, Integer weight)처럼 두 인수를 갖는 생성자는 BiFunction 인터페이스와 같은 시그니처를 가짐

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

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

1단계 : 코드 전달

public class AppleComparator implements Comparator<Apple> {
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
}

inventory.sort(new AppleComparator)
  • 자바 8의 List API에서 sort 메서드를 제공하므로 정렬 메서드를 직접 구현할 필요 없음
    • sort 메서드의 시그니처: void sort(Comparator<? super E> c)
    • Comparator 객체를 인수로 받아 두 사과를 비교
  • 객체 안에 동작을 포함시키는 방식으로 다양한 전략 전달 가능 => sort의 동작은 파라미터화 됨

2단계 : 익명 클래스 사용

inventory.sort(new Comparator<Apple>(){
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});
  • 한 번만 사용할 Comparator를 익명 클래스로 작성함

3단계 : 람다 표현식 사용

  • 함수형 인터페이스를 기대하는 곳 어디든 람다 표현식 사용 가능
    • 함수형 인터페이스: 오직 하나의 추상 메서드를 정의하는 인터페이스
    • 함수 디스크립터(추상 메서드의 시그니처)는 람다 표현식의 시그니처를 정의
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
  • 자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용해 람다의 파라미터 형식을 추론함

    inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
    
  • Comparator는 Comparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메서드 comparing을 포함

    Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
      
    import static java.util.Comparator.comparing;
    inventory.sort(comparing(apple -> apple.getWeight()));
    

4단계 : 메서드 참조 사용

import static java.util.Comparator.comparing;
inventory.sort(comparing(Apple::getWeight));
  • Apple을 weight별로 비교해 inventory를 sort해라‘는 의미

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

  • 간단한 여러 개의 람다 표현식을 조합해 복잡한 람다 표현식을 만들 수 있음
    • 두 프레디케이트를 조합해 두 프레디케이트의 or 연산을 수행하는 커다란 프레디케이트를 만들 수 있음
    • 한 함수의 결과가 다른 함수의 입력이 되도록 두 함수를 조합할 수 있음

Comparator 조합

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

    Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
    

역정렬

  • 사과의 무게를 내림차순으로 정렬하고 싶다면?

    • 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공
  • 처음 비교자 구현을 그대로 재사용해 사과의 무게를 기준으로 역정렬할 수 있음

    inventory.sort(comparing(Apple::getWeight).reversed());
    

Comperator 연결

  • 무게가 같은 두 사과가 존재한다면 어떤 사과를 먼저 나열해야 할까?

    • 비교 결과를 다듬을 수 있는 두 번째 Comparator를 만들 수 있음
  • 무게로 두 사과를 비교한 다음 무게가 같다면 원산지 국가별로 사과를 정렬할 수 있음

    inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry))
    
    • thenComparing 메서드로 두 번째 비교 연산자를 만듦
    • thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달

Predicate 조합

  • 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공

  • ‘빨간색이 아닌 사과’처럼 특정 프레디케이트를 반전시킬 때 negate 메서드를 사용

    Predicate<Apple> notRedApple = redApple.negate();
    
  • and 메서드를 이용해 빨간색이면 무거운 사과를 선택하도록 두 람다를 조합

    Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
    
  • or을 이용해 ‘빨간색이면서 무거운 사과 또는 그냥 녹색 사과’ 등 다양한 조건을 만들 수 있음

    Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight > 150)
        											   .or(apple -> GREEN.equals(apple.getColor()))
    

Function 조합

  • Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공

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

    • 숫자를 증가시키는 f라는 함수와 숫자에 2를 곱하는 g하는 함수가 있다고 가정

    • f와 g를 조립해 숫자를 증가시킨 뒤 결과에 2를 곱하는 h라는 함수를 만들 수 있음

      Function<Integer, Integer> f = x -> x + 1;
      Function<Integer, Integer> g = x -> x * 2;
      Function<Integer, Integer> h = f.andThen(g);
      int result = h.apply(1);	// 4를 반환
      
  • compose 메서드: 인수로 주어진 함수를 먼저 실행한 다음 그 결과를 외부 함수의 인수로 제공

    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.compose(g);
    int result = h.apply(1);	// 3를 반환
    

10. 마치며

  • 람다 표현식: 익명 함수의 일종. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있음
    • 람다 표현식으로 간결한 코드를 구현할 수 있음
  • 함수형 인터페이스: 하나의 추상 메서드만을 정의하는 인터페이스

    • 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있음

    • 람다 표현식을 이용해 함수형 인터페이스의 추상 메서드를 즉석으로 제공할 수 있으며 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급됨

  • java.util.function 패키지는 Predicate<T>, Function<T, R>, Supplier<T>, Consumer<T>, BinaryOperator<T> 등을 포함해 자주 사용하는 다양한 함수형 인터페이스를 제공

  • 자바 8은 Predicate<T>Function<T, R> 같은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있는 IntPredicate, IntToLongFunction 등과 같은 기본형 특화 인터페이스도 제공
  • 실행 어라운드 패턴(코드 중간에 실행해야 하는 메서드에 꼭 필요한 코드)을 람다를 활용하면 유연성과 재사용성을 추가로 얻을 수 있음
  • 람다 표현식의 기대 형식(type expected)을 대상 형식(target type)이라고 함
  • 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있음
  • Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공

2022-02-17 TIL (자바, 알고리즘)

|

1. 자바

Chapter 3. 람다 표현식 부분을 공부했다.

  • 람다란? - 람다의 특징(익명, 함수, 전달, 간결성), 람다 표현식 구성(파라미터 리스트, 화살표, 람다 바디)
  • 어디에, 어떻게 람다를 사용할까? - 함수형 인터페이스, 함수 디스크립터
  • 함수형 인터페이스 사용: Predicate, Consumer, Function
  • 형식 검사, 형식 추론, 제약 - 대상 형식(target type), 람다 캡쳐링(capturing lambda)
  • 메서드 참조, 생성자 참조
  • 람다 표현식을 조합할 수 있는 유용한 메서드 - Comparator 조합(revers, thenComparing), Predicate 조합(negate, and, or), Function 조합(andThen, compose)

스트림과 람다 표현식은 앞으로 실무에서도 많이 사용할 기능이므로 활용 방법과 코드에 적용하는 방법을 잘 알고 넘어가야겠다.

2. 알고리즘

자료구조와 알고리즘의 개념이 부족한 상태에서 의욕만 앞서 문제를 푸려다 보니 문제에 어느 알고리즘을 적용해야하는지, 그 알고리즘을 어떻게 구현해야하는지를 파악하지 못해 한 문제를 푸는데 시간이 오래 걸린다. 이 점을 보완하기 위해 패스트캠퍼스의 ‘한 번에 끝내는 코딩테스트 369 Java편 초격차 패키지 Online’ 강의를 들으며 자료구조와 알고리즘부터 다시 공부하기로 결정했다.

오늘은 OT와 자료구조와 알고리즘의 이해 , 강의 환경 준비 부분까지 수강했다.

  • 자료구조란? - 대량의 데이터를 효율적으로 관리할 수 있는 데이터의 구조 (ex. 배열, 스택, 큐, 링크드 리스트, 해쉬 테이블, 힙)

  • 알고리즘이란? - 어떤 문제에 대해, 특정한 ‘입력’을 넣으면 원하는 ‘출력’을 얻을 수 있도록 만드는 프로그래밍

  • 어떤 자료구조와 알고리즘을 쓰느냐에 따라 성능의 차이가 많이 남

3

2월 9일 ~ 2월 16일까지는 개인 사정으로 공부를 진행할 수 없는 상황이었다…

오늘도 짐정리를 하느라 공부를 많이 하지 못했다.. 내일도 약속이 있어 많이 공부하지 못할 것 같은데… :sob:

일주일치 공부량이 밀린만큼 이번 주말에는 더 집중해서 공부해야겠다! :fire::fire::fire:

그래도 16일에 ‘프로그래머스 클라우드 기반 백엔드 프로그래밍 데브코스’ 서류를 제출했고, 오늘 19일에 있을 코딩 테스트 관련 메일을 받았다. 코딩 테스트 문제 난이도가 높지 않다고 하니 내일 저녁과 토요일 오전에 벼락치기해서 코딩 테스트도 통과해야지!

Chapter 2. 웹 애플리케이션 개발하기

|

1. 정보 보여주기

  • 타코 클라우드는 온라인으로 타코를 주문할 수 있는 애플리케이션
  • 타코 클라우드에 풍부한 식자재(ingredient)를 보여주는 팔레트를 사용해 고객이 창의적으로 커스텀 타코를 디자인할 수 있게 하고자 함
    • 고객 자신이 원하는 타코를 디자인할 때 식자재를 보여주고 선택할 수 있는 페이지가 타코 클라우드 웹 애플리케이션에 있어야 함
    • 선택할 수 있는 식자재의 내역은 수시로 변경될 수 있으므로, HTML 페이지에 하드코딩되면 안되며, 사용 가능한 식자재의 내역을 데이터베이스로부터 가져와 고객이 볼 수 있도록 해당 페이지에 전달되어야 함
  • 스프링 웹 애플리케이션에서는 데이터를 가져오고 처리하는 것이 컨트롤러의 일이고, 브라우저에 보여주는 데이터를 HTML로 나타내는 것은 뷰가 하는 일
  • 타코 디자인 페이지를 지원하기 위해 다음 컴포넌트를 생성
    • 타코 식자재의 속성을 정의하는 도메인(domain) 클래스
    • 식자재 정보를 가져와 뷰에 전달하는 스프링 MVC 컨트롤러 클래스
    • 식자재의 내역을 사용자의 브라우저에 보여주는 뷰 템플릿
  • 데이터베이스 관련 내용은 3장으로 미루고, 2장에서는 식자재 내역을 뷰에 제공하는 일만 컨트롤러가 할 것
    • 데이터베이스로부터 식자재를 가져오는 작업은 3장에서 컨트롤러에 추가할 것

도메인 설정하기

  • 애플리케이션의 도메인은 해당 애플리케이션의 이해에 필요한 개념을 다루는 영역
  • 타코 클라우드 애플리케이션의 도메인에는 다음과 같은 객체가 포함됨
    • 고객이 선택한 타코 디자인, 디자인을 구성하는 식자재, 고객, 고객의 타고 주문
    • 우선 타코 식자재에 초점을 둘 것
  • 타코 식자재는 매우 간단한 객체
    • 각 식자재는 타입(고기류, 치즈류, 소스류 등)과 이름을 가짐
    • 각 식자재는 쉽고 분명하게 참조할 수 있는 ID를 가짐
  • 필요한 도메인 객체인 타코 식자재를 정의하는 Ingredient 클래스

    package tacos;
      
    import lombok.Data;
    import lombok.RequiredArgsConstructor;
      
    @Data
    @RequiredArgsConstructor
    public class Ingredient {
      	
    	private final String id;
    	private final String name;
    	private final Type type;
      	
    	public static enum Type {
    		WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
    	}
    }
    
    • 식자재를 나타내는데 필요한 3개의 속성을 정의
    • final 속성들을 초기화하는 생성자와 속성들의 게터(getter)와 세터(setter) 메서드가 없고, equals(), hashCode(), toString() 등의 유용한 메서드도 정의하지 않음
      • 이유: Lombok이라는 라이브러리를 사용해 위의 메서드들을 런타임 시에 자동으로 생성하기 때문
    • 클래스에 @Data 애노테이션을 지정하면 소스 코드에 누락된 final 속성들을 초기화하는 생성자와, 속성들의 게터와 세터 등을 생성하라고 Lombok에 알려줌. 따라서 Lombok을 사용하면 Ingredient 클래스의 소스 코드 분량을 줄일 수 있음
  • Lombok은 스프링 라이브러리가 아니지만, 이 책의 코드 예제를 간략하게 만들 수 있게 해줌
    • Lombok을 사용하려면 프로젝트에 의존성으로 추가해야 함
    • 의존성을 추가한 후 @Data와 같은 Lombok 애노테이션을 코드에 추가하면 컴파일 시에 빌드 명세에 정의한 Lombok이 실행됨
    • 클래스의 속성들을 초기화하는 생성자와, 속성들의 게터와 세터 등을 Lombok이 자동 생성해주므로 에러가 생기지 않음 (개발 시점에는 해당 메서드들이 없다고 STS가 에러로 보여줌)
    • STS의 확장(extension)으로 Lombok을 추가하면 코드 작성 시점에서도 속성 관련 메서드들이 자동 생성되므로 에러가 나타나지 않게 할 수 있음
  • Taco 클래스 추가

    package tacos;
      
    import java.util.List;
    import lombok.Data;
      
    @Data
    public class Taco {
    	private String name;
    	private List<String> ingredients;
    }
    
    • 자바 도메인 객체를 나타내며, 두 개의 속성을 가짐

컨트롤러 클래스 생성하기

  • 컨트롤러는 스프링 MVC 프레임워크의 중심적인 역할을 수행
    • HTTP 요청을 처리하고, 브라우저에 보여줄 HTML을 뷰에 요청하거나, 또는 REST 형태의 응답 몸체에 직접 데이터를 추가
    • 이 장에서는 웹 브라우저의 콘텐츠를 생성하기 위해 뷰를 사용하는 컨트롤러에 초점을 둘 것 (6장에서 REST API를 처리하는 컨트롤러의 작성 방법을 알아볼 것)
  • 타코 클라우드 애플리케이션의 컨트롤러가 수행하는 일
    • 요청 경로가 /design인 HTTP GET 요청을 처리
    • 식자재 내역을 생성
    • 식자재 데이터의 HTML 작성을 뷰 템플릿에 요청하고, 작성된 HTML을 웹 브라우저에 전송
  • DesignTacoController 클래스

    @Slf4j
    @Controller
    @RequestMapping("/design")
    public class DesignTacoController {
      	
    	@GetMapping
    	public String showDesignForm(Model model) {
    		List<Ingredient> ingredients = Arrays.asList(
    				new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
    				new Ingredient("COTO", "Corn Tortilla", Type.WRAP ),
    				new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
    				new Ingredient("CARN", "Carnitas", Type.PROTEIN),
    				new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
    				new Ingredient("LETC", "Lettuce", Type.VEGGIES),
    				new Ingredient("CHED", "Cheddar", Type.CHEESE),
    				new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
    				new Ingredient("SLSA", "Salsa", Type.SAUCE),
    				new Ingredient("SRCR", "Sour Cream", Type.SAUCE));
      		
    		Type[] types = Ingredient.Type.values();
    		for (Type type : types) {
    			model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
    		}
      		
    		model.addAttribute("taco", new Taco());
      		
    		return "design";
    	}
      	
    	private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type){
    		return ingredients.stream().filter(x -> x.getType().equals(type)).collect(Collectors.toList());
    	}
    }
    
    • 적용된 애노테이션
      • @Slf4j: 컴파일 시에 Lombok에 제공되며, 이 클래스에 자동으로 SLF4K(Simple Logging Facade) Logger를 생성
      • @Controller: DesignTacoController 클래스가 컨트롤러로 식별되게 하며, 컴포넌트 검색을 해야 한다는 것을 나타냄. 스프링이 DesignTacoController 클래스를 찾은 후 스프링 애플리케이션 컨텍스트의 빈으로 이 클래스의 인스턴스를 자동 생성함
      • @RequestMapping: 이 애노테이션이 클래스 수준으로 적용될 때는 해당 컨트롤러가 처리하는 요청의 종류를 나타냄. 여기서는 DesignTacoController에서 /design으로 시작하는 경로의 요청을 처리함을 나타냄

GET 요청 처리하기

  • 클래스 수준의 @RequestMapping과 함께 사용된 @GetMapping 애노테이션은 /design의 HTTP GET 요청이 수신될 때 그 요청을 처리하기 위해 showDesingForm() 메서드가 호출됨을 나타냄

  • @GetMapping은 스프링 4.3에서 소개된 새로운 애놑이션. HTTP GET 요청에 특화되어 있음

  • 스프링 MVC에서 사용할 수 있는 요청-대응 애노테이션

    애노테이션 설명
    @RequestMapping 다목적 요청을 처리
    @GetMapping HTTP GET 요청을 처리
    @PostMapping HTTP POST 요청을 처리
    @PutMapping HTTP PUT 요청을 처리
    @DeleteMapping HTTP DELETE 요청을 처리
    @PatchMapping HTTP PATCH 요청을 처리

showDesignForm() 메서드

  • 식자재를 나타내는 Ingredient 객체를 저장하는 List를 생성
    • Ingredient 객체들을 직접 ㅗ드에 추가. 3장에서는 데이터베이스로부터 가져와 저장할 것
  • 식자재의 유형(고기, 치즈, 소스 등)을 List에서 필터링(filterByType 메서드)한 후 showDesignForm()의 인자로 전달되는 Model 객체의 속성으로 추가
    • Model은 컨트롤러와 데이터를 보여주는 뷰 사이에서 데이터를 운반하는 객체
    • Model 객체의 속성에 있는 데이터는 뷰가 알 수 있는 서블릿(servlet) 요청 속성들로 복사됨
  • 제일 마지막에 “design”을 반환
    • 모델 데이터를 브라우저에 나타내는 데 사용될 뷰의 논리적인 이름
  • 지금 애플리케이션을 실행하고 브라우저에서 /design 경로에 접속한다면 DesignTacoController의 showDesignFrom() 메서드가 실행된 후, 뷰에 요청이 전달되기 전에 List에 저장된 식자재 데이터를 모델 객체에 넣을 것
    • 아직 뷰를 정의하지 않았으므로 HTTP 404 (Not Found) 에러 발생
    • 데이터가 HTML로 작성되어 사용자 웹 브라우저에 나타나게 하는 것이 뷰의 역할

뷰 디자인하기

  • JSP(JavaServer Pages), Thymeleaf, FreeMarker, Mustache, 그루비 기반의 템플릿 등으로 뷰를 정의할 수 있음
    • 여기서는 Thymeleaf 사용
  • Thymeleaf를 사용하려면 프로젝트의 빌드 구성 파일에 또 다른 의존성을 추가해야 함
  • Thymeleaf와 같은 뷰 라이브러리들은 어떤 웹 프레임워크와도 사용 가능하도록 설계됨. 따라서 스프링의 추상화 모델을 알지 못하며, 컨트롤러가 데이터를 넣는 Model 대신 서블릿 요청 속성들을 사용
    • 뷰에게 요청을 전달하기 앞서 스프링은 Thymeleaf와 이외의 다른 뷰 템플릿이 사용하는 요청 속성에 모델 데이터를 복사
  • Thymeleaf 템플릿은 요청 데이터를 나타내는 요소 속성을 추가로 갖는 HTML

    • ex) key가 “message”인 요청 속성이 있고, Thymeleaf를 사용해 HTML <p> 태그로 나타내고자 했다면

      <p th:text="${message}"> placeholder message </p>
      
      • <p>의 요소의 몸체는 키가 “message”인 서블릿 요청 속성의 값으로 교체됨
      • th:text는 교체를 수행하는 Thymeleaf 네임스페이스 속성
      • ${} 연산자는 요청 속성의 값을 사용하라는 것을 알려줌
    • 또 다른 속성으로 th:each 를 제공. 컬렉션을 반복 처리하며, 해당 컬렉션의 요소를 하나씩 HTML로 나타냄. 따라서 List에 저장된 타코 식자재(Ingredient 객체)를 모델 데이터로부터 뷰에 보여줄 때 편리

  • Thymeleaf 템플릿 코드
    • 각 유형의 식자재마다 <div> 코드가 반복되고 사용자 자신이 생성한 타코의 이름을 지정할 수 있는 필드와 Submit 버튼을 포함
    • Submit yout taco 버튼을 클릭하게 되면 에러(HTTP 405) 발생
      • 이유: DesignTacoController에서 타코를 생성할 준비가 아직 안 되었음

2. 폼 제출 처리하기

  • 뷰(design.html)의 <form> 태그를 보면 method 속성을 보면 POST로 설정되어 있는데 <form>에는 action 속성이 선언되지 않은 것을 알 수 있음
    • 폼이 제출되면 브라우저가 폼의 모든 데이터를 모아 폼에 나타난 GET 요청과 같은 경로(/design)로 서버에 HTTP POST 요청을 전송
    • 따라서 이 요청을 처리하는 컨트롤러의 메서드가 있어야 함
  • showDesignForm() 메서드가 /design 경로의 HTTP GET 요청을 처리하도록 지정하기 위해 @GetMapping 애노테이션을 사용
    • POST 요청 처리에는 @PostMapping 애노테이션 사용 가능
  • 타코 디자인 폼의 제출을 처리하기 위해 processDesign() 메서드를 DesignTacoController에 추가

    @PostMapping
    public String processDesign(Taco design) {
    	// 이 지점에서 타코 디자인(선택된 식자재 내역)을 저장
    	// 이 작업은 3장에서 할 것
    	log.info("Processing design: " + design);
      		
    	return "redirect:/orders/current";
    }
    
    • 클래스 수준의 @PostMappingprocessDesgin()이 /design 경로의 POST 요청을 처리함을 나타내므로 타코를 디자인하는 사용자가 제출한 것을 여기서 처리
    • 타코 디자인 폼이 제출될 때 이 폼의 필드는 processDesign()의 인자로 전달되는 Taco 객체의 속성과 바인딩되므로 processDesign() 메서드에서는 Taco 객체를 사용해 어떤 것이든 원하는 처리를 할 수 있음
    • 현재 processDesign() 메서드에서 Taco 객체 관련 처리를 아무 것도 하지 않으나, 3장에서 폼으로 제출된 Taco 객체를 데이터베이스에 저장하는 퍼시스턴스 로직을 추가할 것
  • checkbox 요소들이 여러 개 있는데, 모두 ingredients라는 이름을 가지며, 텍스트 입력 요소의 이름은 name인 것을 알 수 있음
    • 이 필드들은 Taco 클래스의 ingredients 및 name 속성 값과 바인딩됨
  • 폼의 Name 필드는 간단한 텍스트 값을 가질 때만 필요하므로 Taco의 name 속성은 String 타입
  • 식자재를 나타내는 checkbox들도 텍스트 값을 갖지만, 0 또는 여러 개가 선택될 수 있으므로, 바인딩되는 Taco 클래스의 ingredients 속성은 선택된 식자재들의 id를 저장하기 위해 List<String> 타입이어야 함

  • showDesignFrom() 메서드와 processDesign() 메서드의 차이점
    • processDesign()에서 반환되는 값은 리디렉션(redirection, 변경된 경로로 재접속) 뷰를 나타내는 “redirect:”가 제일 앞에 붙음
    • 즉, processDesign()의 실행이 끝난 후 사용자의 브라우저가 /orders/current 상대 경로로 재접속되어야 한다는 것을 나타냄

타코 주문 폼을 나타내는 컨트롤러

package tacos.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
import tacos.Order;

@Slf4j
@Controller
@RequestMapping("/order")
public class OrderController {
	
	@GetMapping("/current")
	public String orderForm(Model model) {
		model.addAttribute("order", new Order());
		return "orderForm";
	}
}
  • orders/current 경로의 요청을 처리할 컨트롤러

  • @Slf4j: 컴파일 시에 SLF4J Logger 객체 생성
    • 생성한 Logger은 제출한 주문의 상세 내역을 로그로 기록하기 위해 사용할 예정
  • orderForm() 메서드는 orderForm이라는 이름의 뷰를 반환하는 역할을 수행
    • 3장에서 모델 데이터를 데이터베이스에 저장할 때 주문된 Taco 객체들로 모델을 채우도록 이 메서드를 변경할 것
  • orderForm 뷰는 orderForm.html이라는 이름의 Thymeleaf 템플릿에 의해 제공됨

    • form 태그에 폼 액션도 지정. 액션이 지정되지 않을 경우에는 폼에 나타났던 것과 같은 URL로 폼의 HTTP POST 요청이 제출되지만, /orders 경로로 제출되도록 지정하고 있음
  • /orders 경로의 POST 요청을 처리하는 또 다른 메서드를 OrderController 클래스에 추가해야 함

    @PostMapping
    	public String processOrder(Order order) {
    		log.info("Order submitted: " + order);
    		return "redirect:/";
    	}
    
    • 주문 데이터의 퍼시스턴스를 처리하는 것은 3장에서 할 것이므로 지금은 간단히 작성
    • 제출된 주문을 처리하기 위해 processOrder() 메서드가 호출될 때는 제출된 폼 필드와 바인딩 속성을 갖는 Order 객체가 인자로 전달됨

타코 주문 정보를 갖는 도메인 객체

  • Order는 주문 정보를 갖는 간단한 클래스

  • Order 클래스

    package tacos;
      
    import lombok.Data;
      
    @Data
    public class Order {
      	
    	private String deliveryName;
    	private String deliveryStreet;
    	private String deliveryCity;
    	private String deliveryState;
    	private String deliveryZip;
    	private String ccNumber;
    	private String ccExpiration;
    	private String ccCVV;
    }
    

3. 폼 입력 유효성 검사하기

  • 자바의 빈 유효성 검사(Bean Validation) API를 지원
    • 애플리케이션에 추가 코드를 작성하지 않고 유효성 검사 규칙을 쉽게 선언할 수 있음
  • 스프링 부트를 사용하면 유효성 검사 라이브러리를 프로젝트에 쉽게 추가할 수 있음
    • 유효성 검사 API와 이 API를 구현한 Hibernate(하이버네이트) 컴포넌트는 스프링 부트의 웹 스타터 의존성으로 자동 추가되기 때문

스프링 MVC에 유효성 검사 적용하기

  • 유효성을 검사할 클래스(여기서는 Taco와 Order)에 검사 규칙을 선언
  • 유효성 검사를 해야 하는 컨트롤러 메서드에 검사를 수행한다는 것을 지정
    • 여기서는 DesignTacoController의 processDesign() 메서드와 OrderControllerprocessOrder() 메서드가 해당됨
  • 검사 에러를 보여주도록 폼 뷰를 수정
  • 유효성 검사 API는 몇 가지 애노테이션을 제공하며, 이 애노테이션들은 검사 규칙을 선언하기 위해 도메인 객체의 속성에 지정할 수 있음
    • 유효성 검사 API를 구현한 Hibernate 컴포넌트에는 더 많은 유효성 검사 애노테이션이 추가됨

유효성 검사 규칙 선언하기

  • Taco 클래스의 경우는 name 속성의 값이 없거나 null인지 확인하며, 최소한 하나 이상의 식자재 항목을 선택했는지 확인해야 함

    package tacos;
      
    import java.util.List;
      
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
      
    import lombok.Data;
      
    @Data
    public class Taco {
      	
    	@NotNull
    	@Size(min=5, message="Name must be at least 5 characters long")
    	private String name;
      	
    	@Size(min=1, message="You must choose at least 1 ingredient")
    	private List<String> ingredients;
    }
    
    • @NotNull@Size를 사용해 유효성 검사 규칙을 선언
  • 제출된 타코 주문의 유효성 검사를 하기 위해서는 Order 클래스에 관련 애노테이션을 적용해야 함

    package tacos;
      
    import javax.validation.constraints.Digits;
    import javax.validation.constraints.Pattern;
    import javax.validation.constraints.NotBlank;
    import org.hibernate.validator.constraints.CreditCardNumber;
      
    import lombok.Data;
      
    @Data
    public class Order {
      	
    	@NotBlank(message="Name is required")
    	private String deliveryName;
      	
    	@NotBlank(message="Street is required")
    	private String deliveryStreet;
      	
    	@NotBlank(message="City is required")
    	private String deliveryCity;
      	
    	@NotBlank(message="State is required")
    	private String deliveryState;
      	
    	@NotBlank(message="Zip code is required")
    	private String deliveryZip;
      	
    	@CreditCardNumber(message = "Not a valid credit card number")
    	private String ccNumber;
      	
    	@Pattern(regexp="^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message="Must be formatted MM/YY")
    	private String ccExpiration;
      	
    	@Digits(integer=3, fraction=0, message="Invalid CVV")
    	private String ccCVV;
    }
    
    • 배달 주소에 관한 속성들의 경우 사용자가 입력을 하지 않은 필드가 있는지 확인만 하면 되므로 자바 빈 유효성 검사 API의 @NotBlank 애노테이션을 사용할 것
    • 대금 지불에 관한 필드에서 ccNumber 속성의 경우는 값이 있는지와 입력 값이 유효한 신용 카드 번호인지도 확인해야 함. ccExpiration 속성의 경우는 MM/YY 형식의 값이어야 함. ccCVV 속성의 값은 세 자리 수가 되어야 함. 이런 종류의 유효성 검사를 하기 위해서는 다른 자바 빈 유효성 검사 API 애노테이션과 Hibernate Validator의 또 다른 애노테이션을 사용해야 함
    • 모든 우효성 검사 애노테이션은 message 속성을 가짐. 사용자가 입력한 정보가 애노테이션으로 선언된 유효성 규칙을 충족하지 못할 때 보여줄 메시지를 message 속성에 저장

폼과 바인딩될 때 유효성 검사 수행하기

  • 각 폼의 POST 요청이 관련 메서드에서 처리될 때 유효성 검사가 수행되도록 컨트롤러를 수정해야 함

  • 제출된 Taco의 유효성 검사

    @PostMapping
    public String processDesign(@Valid Taco design, Errors errors) {
    	if (errors.hasErrors()) {
    		return "design";
    	}
      		
    	// 이 지점에서 타코 디자인(선택된 식자재 내역)을 저장
    	// 이 작업은 3장에서 할 것
    	log.info("Processing design: " + design);
      		
    	return "redirect:/orders/current";
    }
    
    • DesignTacoController의 processDesign() 메서드 인자로 전달되는 Taco에 자바 빈 유효성 검사 API의 @Valid 애노테이션 추가
      • 제출된 Taco 객체의 유효성 검사를 수행하라고 스프링 MVC에 알려줌
      • 어떤 검사 에러라도 있으면 에러의 상세 내역이 Errors 객체에 저장되어 processDesign()으로 전달됨
  • 제출된 Order의 유효성 검사

    @PostMapping
    public String processOrder(@Valid Order order, Errors errors) {
    	if(errors.hasErrors()) {
    		return "orderForm";
    	}
      		
    	log.info("Order submitted: " + order);
    	return "redirect:/";
    }
    
    • OrderController의 processOrder() 메서드 변경

유효성 검사 에러 보여주기

  • Thymeleaf는 fieldsth:errors 속성을 통해 Errors 객체의 편리한 사용 방법을 제공

  • ex) 신용 카드 번호 필드의 유효성 검사 에러를 보여줄 때 에러 참조를 사용하는 <span> 요소를 주문 폼 템플릿(orderForm.html)에 추가할 수 있음

    <label for="ccNumber">Credit Card #: </label> 
    <input type="text" th:field="*{ccNumber}" /> 
    <span class="validationError"
    	th:if="${#fields.hasErrors('ccNumber')}"
    	th:errors="*{ccNumber}">CC Num Error</span>
    
    • <span> 요소의 class 속성은 자용자의 주의를 끌기 위한 에러의 명칭을 지정하는데 사용되고, th:if 속성에서는 이 <span>을 보여줄지 말지를 결정하며, fields 속성의 hasErrors() 메서드를 사용해 ccNumber 필드에 에러가 있는지 검사한 후, 있다면 <span>이 나타남
    • th:errors 속성은 ccNumber 필드를 참조. 필드에 에러가 있다고 가정하고 <span>에 사전 지정된 메시지를 검사 에러 메시지로 교체

4. 뷰 컨트롤러로 작업하기

  • 타코 클라우드 애플리케이션의 세 가지 컨트롤러는 애플리케이션의 서로 다른 기능을 제공하지만 프로그래밍 패턴은 동일
    • 스프링 컴포넌트 검색에서 자동으로 찾은 후 스프링 애플리케이션 컨텍스트의 빈으로 생성되는 컨트롤러 클래스임을 나타내기 위해 모두 @Controller 애노테이션을 사용
    • HomeController 외의 다른 컨트롤러에서는 자신이 처리하는 요청 패턴을 정의하기 위해 클래스 수준의 @RequestMapping 애노테이션을 사용
    • 메서드에서 어떤 종류의 요청을 처리해야 하는지 나타내기 위해 @GetMapping 또는 @PostMapping 애노테이션이 지정된 하나 이상의 메서드를 가짐
  • HomeController와 같이 모델 데이터나 사용자 입력을 처리하지 않는 단단한 컨트롤러의 경우 다른 방법으로 컨트롤러를 정의할 수 있음

  • 뷰에 요청을 전달하는 일만 하는 컨트롤러(뷰 컨트롤러)를 선언

    package tacos.web;
      
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
      	
    	@Override
    	public void addViewControllers(ViewControllerRegistry registry) {
    		registry.addViewController("/").setViewName("home");
    	}
    }
    
    • WebConfig는 뷰 컨트롤러의 역할을 수행하는 구성 클래스. WebMvcConfigurer 인터페이스를 구현
    • WebMvcConfigurer 인터페이스는 스프링 MVC를 구성하는 메서드를 정의. 인터페이스임에도 불구하고, 정의된 모든 메서드의 기본적인 구현을 제공하므로 필요한 메서드만 선택해 오버라이딩하면 됨
      • addViewControllers(): 하나 이상의 뷰 컨트롤러를 등록하기 위해 사용할 수 있는 ViewControllerRegistry를 인자로 받음
      • 뷰 컨트롤러가 GET 요청을 처리하는 경로인 “/”를 인자로 전달하여 호출
      • ViewControllerRegistration 객체를 반환하고 “/” 경로의 요청이 전달되어야 하는 뷿 home을 지정해 연달아 ViewControllerRegistration 객체의 setViewName()을 호출

5. 뷰 템플릿 라이브러리 선택하기

  • 스프링 부트 자동-구성에서 지원되는 템플릿

    템플릿 스프링 부트 스타터 의존성
    FreeMarker spring-boot-starter-freemarker
    Groovy 템플릿 spring-boot-starter-groovy-templates
    JavaServer Pages (JSP) 없음(톰캣이나 제티 서블릿 컨테이너 자체에서 제공됨)
    Mustache spring-boot-starter-mustache
    Thymeleaf spring-boot-starter-thymeleaf
  • 원하는 뷰 템플릿을 선택하고 의존성으로 추가한 후 /templates 디렉터리에 템플릿을 작성하면, 스프링 부트는 선택한 템플릿 라이브러리를 찾아 스프링 MVC 컨트롤러의 뷰로 사용할 컴포넌트를 자동으로 구성
  • JSP를 선택한다면, 내장된 톰캣과 제티 컨테이너를 포함해 자바 서블릿 컨테이너는 /WEB-INF 밑에서 JSP 코드를 찾음. 애플리케이션을 실행 가능한 JAR 파일로 생성한다면 요구사항을 충족시킬 방법이 없으므로 애플리케이션을 WAR 파일로 생성하고 종전의 서블릿 컨테이너에 설치하는 경우에는 JSP를 선택
    • 실행 가능한 JAR 파일로 생성한다면 JSP를 제외한 나머지 중 하나를 선택

템플릿 캐싱

  • 템플릿은 최초 사용될 때 한 번만 파싱(코드 분석)됨. 파싱된 결과는 향후 사용을 위해 캐시에 저장됨
    • 프로덕션에서 애플리케이션을 실행할 때 매번 요청을 처리할 때마다 불필요하게 템플릿 파싱을 하지 않으므로 성능을 향상시킬 수 있음
    • 개발 시에는 템플릿을 변경한 경우 변경된 웹 브라우저를 보기 위해서는 애플리케이션을 다시 시작해야 함
  • 템플릿 캐싱을 비활성화하는 방법이 있음. 각 템플릿의 캐싱 속성만 false로 설정하면 됨
    • 모든 속성은 캐싱을 활성화하는 true가 기본값
    • 만약 false로 변경한 경우, 프로덕션에서 애플리케이션을 배포할 때는 true로 변경해주어야 함
  • 스프링 부트의 DevTools를 사용하면 모든 템플릿 라이브러리의 캐싱을 비활성화함
    • 애플리케이션이 실무 운영을 위해 배포될 때는 DevTools 자신이 비활성화되므로 템플릿 캐싱이 활성화될 수 있음

요약

  1. 스프링은 스프링 MVC라는 웹 프레임워크를 제공하는데, 스프링 MVC는 스프링 애플리케이션의 웹 프론트엔드 개발에 사용
  2. 스프링 MVC는 애노테이션을 기반으로 하며, @RequestMapping, @GetMapping, @PostMapping과 같은 애노테이션을 사용해 요청 처리 메서드를 선언할 수 있음
  3. 대부분의 요청 처리 메서드들은 마지막에 Thymeleaf 템플릿과 같은 논리 뷰 이름을 반환. 모델 데이터와 함께 해당 요청을 전달하기 위해
  4. 스프링 MVC는 자바 빈 유효성 검사 API와 Hibernate Validator 드으이 유효성 검사 API 구현 컴포넌트를 통해 유효성 검사를 지원
  5. 모델 데이터가 없거나 처리할 필요가 없는 HTTP GET 요청을 처리할 때는 뷰 컨트롤러를 사용할 수 있음
  6. Thymeleaf에 추가하여 스프링은 다양한 뷰 템플릿을 지원

Chapter 2 끝!!!

Chapter 2. 동작 파라미터화 코드 전달하기

|

동작 파라미터화

  • 동작 파라미터화(behavior parameteriztion)를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응 가능
  • 동작 파라미터화: 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미
    • 이 코드 블록은 나중에 프로그램에서 호출. 즉, 코드 블록의 실행은 나중으로 미뤄짐

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

  • 농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터맇아는 기능을 추가한다고 가정

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

  • 사과 색을 정의하는 Color num이 존재한다고 가정

    enum Color { RED, GREEN }
    
  • 첫 번째 시도 결과 코드

    public static List<Apple> filterGreenApples(List<Apple> inventory) {
        List<Apple> result = new ArrayList<>();	// 사과 누적 리스트
        for(Apple apple : inventory){
            if(GREEN.equals(apple.getColor())) {
                result.add(apple);
            }
        }
        return result;
    }
    
  • 만약 빨간 사과도 필터링하고 싶어진 경우 어떻게 할까?

    • 메서드를 복사해 새로운 메서드를 만든 후 조건만 변경해도 되지만, 거의 비슷한 코드가 반복 존재하면 그 코드를 추상화시키는 것이 좋음

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

  • 색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있음

    public static List<Apple> filterApplesByColor(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, GREEN);
    List<Apple> redApples = filterApplesByColor(inventory, RED);
    
  • 만약 무게를 기준으로 사과를 구분하고 싶다면 위의 메서드에서 color 파라미터를 무게를 의미하는 파라미터로 변경해주는 것도 하나의 방법

    • 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 중복 됨. 소프트웨어 공학의 DRY(don’t repeat yourself) 원칙을 어기는 것
    • 색과 무게를 filter라는 메서드로 합치는 방법도 있음. 어떤 기준으로 사과를 필터링할지 구분하는 또 다른 방법이 필요. 따라서 색이나 무게 중 어떤 것을 기준으로 필터링할지 가리키는 플래그를 추가할 수 있음

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

  • 모든 속성을 메서드 파라미터로 추가

    public static List<Apple> filterApples(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;
    }
    
  • 위 메서드 사용

    List<Apple> greenApples = filterApplesByColor(inventory, GREEN, 0, true);
    List<Apple> redApples = filterApplesByColor(inventory, null, 150, false);
    
    • true, false는 무엇을 의미? 앞으로 요구사항이 바뀌었을 때도 유연하게 대응 불가
  • filterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것

2. 동작 파라미터화

  • 선택 조건을 사과의 어떤 속성에 기초해 불리언 값을 반환하는 방법으로 결정할 수 있음

    • 프레디케이트: 참 또는 거짓을 반환하는 함수
  • 선택 조건을 결정하는 인터페이스를 정의

    public interface ApplePredicate {
        boolean test (Apple apple);
    }
    
    • ApplePredicate는 사과 선택 전략을 캡슐화함
  • 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate 정의 가능

    public class AppleHeavyWeightPredicate implements ApplePredicate {
        public boolean test(Apple apple){
            return apple.getWeight() > 150;
        }
    }
      
    public class AppleGreenColorPredicate implements ApplePredicate {
        public boolean test(Apple apple){
            return GREEN.equals(apple.getColor());
        }
    }
    
  • 위 조건에 따라 filter 메서드가 다르게 동작할 것
    • 전략 디자인 패턴(strategy design pattern): 전략이라 불리는 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음 런타임에 알고리즘을 선택하는 기법 (자세한 내용)
    • ApplePredicate가 알고리즘 패밀리이고 이를 구현한 두 메서드가 전략
  • ApplePredicate가 다양한 동작을 수행할 수 있는 방법
    • filterApples에서 ApplePredicate 객체를 받아 애플의 조건을 검사하도록 메서드를 고쳐야 함
    • 동작 파라미터화, 즉 메서드가 다양한 동작을 받아 내부적으로 다양한 동작을 수행할 수 있음

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

  • ApplePredicate를 이용한 필터 메서드

    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
        List<Apple> result = new ArrayList<>();	// 사과 누적 리스트
        for(Apple apple : inventory){
            if(p.test(apple)) {		// 프레디케이트 객체로 사과 검사 조건을 캡슐화함
                result.add(apple);
            }
        }
        return result;
    }
    
  • 위 예제에서 가장 중요한 구현은 test 메서드. filterApples 메서드의 새로운 동작을 정의하는 것이 test 메서드

    • 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 함
    • test 메서드를 구현하는 객체를 이용해 불리언 표현식 등을 전달할 수 있으므로 이는 코드를 전달하는 것이나 마찬가지
    • 3번에서 람다를 이용해 여러 개의 ApplePredicate 클래스를 정의하지 않고도 여러 조건의 표현식을 filterApples 메서드로 전달하는 방법을 살펴볼 것

3. 복잡한 과정 간소화

  • 현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하여 여러 클래스를 정의한 다음에 인스턴스화해야 함. 상당히 번거로운 작업이며 시간 낭비
  • 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스(anoymous class)라는 기법을 제공
    • 익명 클래스를 이용하면 코드의 양을 줄일 수 있음

익명 클래스

  • 익명 클래스는 자바의 지역 클래스(블록 내부에 선언된 클래스)와 비슷한 개념
  • 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있어, 즉석에서 필요한 구현을 만들어서 사용할 수 있음

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

  • 익명 클래스를 이용해 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 코드를 다시 구현한 코드
  List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
      public boolean test(Apple apple){
          return RED.equals(apple.getColor());
      }
  });
  • 익명 클래스는 여전히 많은 공간을 차지하고 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않음

  • 익명 클래스로 인터페이스를 구현하는 여러 클래스를 선언하는 과정을 조금 줄일 수 는 있지만 여전히 불만족

    • 코드 조각을 전달하는 과정에서 결국 객체를 만들고 명시적으로 새로운 동작을 정의하는 메서드를 구현해야 한다는 점은 변하지 않음

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

  • 자바 8의 람다 표현식을 이용해 위 예제 코드를 간단하게 재구현할 수 있음

    List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
    
    • 코드가 간결해지면서 문제를 더 잘 설명하는 코드가 됨 (복잡성 문제 해결 가능)

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

public interface Predicat<T> {
    booelan test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p){	// 형식 파라미터 T 등장
    List<T> result = new ArrayList<>();
    for(T e : list){
        if(p.test(e)){
            result.add(e);
        }
    }
    return result;
}
  • 다른 과일과 정수, 문자열 등의 리스트에 필터 메세드를 사용할 수 있음

5. 마치며

  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있음
  • 코드 전달 기법을 이용하면 동작을 메서드 인수로 전달할 수 있음
  • 자바 API의 많은 메서드는 정렬, 스레드 ,GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있음

Chapter 2 끝!!!