반응형

반응형
반응형

[자바의 정석 - 기초편] ch14-15,16 스트림, 스트림의 특징

https://youtu.be/7Kyf4mMjbTQ

스트림

다양한 데이터 소스(컬렉션, 배열 등) 를 표준화된 방법으로 다루기 위한 것

컬렉션 프레임워크: 컬렉션(List, Set, Map, ...)들을 표준화된 방법으로 다루기 위해 정리했지만 실패함. List, Set, Map의 성격이 달라서 사용방법도 다름

JDK 1.8부터 스트림이 등장하면서 통일됨

 

중간 연산: 연산 결과가 스트림. 반복적으로 적용 가능

최종 연산: 연산 결과가 스트림이 아님. 단 한 번만 적용 가능 (스트림의 요소를 소모)

 

스트림으로 변환(생성)

1
2
3
4
5
6
7
8
9
10
List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();
 
Stream<String> strStream = Stream.of(new String[]{"a""b""c"});
 
Stream<Integer> evenStream = Stream.iterator(0, n -> n+2);
 
Stream<Double> randomStream = Stream.generate(Math::random);
 
IntStream intStream = new Random().ints(5);
cs

 

예제

1
2
3
4
5
6
stream
   .distinct()                      // 중간 연산
    .limit(5)                        // 중간 연산
    .sorted()                        // 중간 연산
    .forEach(System.out::println)    // 최종 연산
 
cs

 

끊어서 사용 가능

1
2
3
4
5
6
7
8
9
10
String[] strArr = {"dd""aaa""CC""cc""b"};
 
Stream<String> stream = Stream.of(strArr);                // stream 생성
 
Stream<String> filteredStream     = stream.filter();        // 중간 연산
Stream<String> distinctedStream = stream.distinct();
Stream<String> sortedStream     = stream.sort();
Stream<String> limitedStream     = stream.limit(5);
 
int total = stream.count();                                 // 최종 연산
cs

 

스트림의 특징

1. 데이터 소스를 읽기만 할 뿐, 변경하지 않는다.

1
2
3
4
5
6
7
8
List<Integer> list = Arrays.asList(31542);
 
List<Integer> sortedList = list.stream()
                                .sorted()
                                .collect(Collectors.toList());
 
System.out.println(list);            // 3, 1, 5, 4, 2
System.out.println(sortedList);        // 1, 2, 3, 4, 5
cs

2. Iterator처럼 일회용 (필요하면 다시 스트림 생성해야 함)

1
2
strStream.forEach(System.out::println);
int numOfStr = strStream.count();        // 에러 -> 스트림이 이미 
cs

3. 지연 연산: 최종 연산 전가지 중산 연산이 수행되지 않음.

1
2
3
IntStream intStream = new Random().ints(146);
intStream.distinct().limit(5).sorted()              // 무한 스트림이지만 distinct() 
    .forEach(System.out::println)                    // 최종 연산
cs

4. 작업을 내부 반복으로 처리

1
2
3
4
5
for(String str : strList)
    System.out.println(str);
 
// 위와 동일
stream.forEach(System.out::println);
cs

 

5. 병렬 스트림: 스트림의 작업을 병렬로 처리 (멀티 쓰레드)

1
2
3
4
5
Stream<String> strStream = Stream.of("dd""aaa""CC""cc""b");
int sum = strStream
            .parallel()                    // 병렬 스트림으로 전환
            .mapToInt(s -> s.length())    // 문자열 길이 계산
            .sum();                        // 모든 문자열 길이의 합

cs

6. 기본형 스트림: IntStream, LongStream, DoubleStream

- 오토박싱, 언박싱의 비효율 제거 (Stream<Integer> 대신 IntStream 사용)

- 숫자와 관련된 유용한 메서드를 Stream<T>보다 더 많이 제공 (sum(), average() 등)

 

오토박싱: 1 (기본형) -> new Integer(1) (참조형)

언박싱   : new Integer(1) (참조형) -> 1 (기본형)

[자바의 정석 - 기초편] ch14-17~22 스트림만들기

https://youtu.be/AOw4cCVUJC4

컬렉션으로 스트림 생성

Collection 인터페이스의 stream()으로 컬렉션을 스트림으로 변환

 

Stream<E> stream()

예제

1
2
3
4
5
List<Integer>    list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();
 
intStream.forEach(System.out::print);    // 정상 출력
intStream.forEach(System.out::print);    // 에러, 스트림이 이미 닫힘
cs

배열로 스트림 생성

1
2
3
4
5
Stream<String> strStream = Stream.of("a""b""c");
Stream<String> strStream = Stream.of(new String[]{"a""b""c"});
 
Stream<String> strStream = Arrays.stream(new String[]{"a""b""c"});
Stream<String> strStream = Arrays.stream(new String[]{"a""b""c"}, 03);
cs

난수를 요소로 갖는 스트림 생성

1
2
3
4
5
6
7
8
IntStream intStream = new Random().ints();
intStream
    .limit(5)
    .forEach(System.out::println);
 
// 위와 동일
IntStream intStream = new Random().ints(5);
                        .forEach(System.out::println);
cs

특정 범위의 정수를 요소로 갖는 스트림 생성(IntStream, LongStream)

1
2
IntStream intStream = IntStream.range(15);
IntStream intStream = IntStream.rangeClosed(15);
cs

 

람다식을 소스로 하는 스트림 생성

1
2
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f);    // 이전 요소에 종속적
static <T> Stream<T> generate(Supplier<T> s);                // 이전 소에 독립적
cs

예제

1
2
3
4
Stream<Integer> evenStream = Stream.iterate(0, n -> n+2);    // 0, 2, 4, 6, ...
 
Stream<Double>    randomStream    = Stream.generate(Math::random);
Stream<Integer>    oneStream        = Stream.generate( () -> 1);
cs

파일을 소스로 하는 스트림 생성

1
2
3
4
5
Stream<Path> Files.list(Path dir);    // dir에 있는 파일 또는 디렉토리
 
Stream<String> Files.lines(Path path);    // 파일 내용을 라인 단위로 읽어서 String으로 변환
Stream<String> Files.lines(Path path, Charset cs);
Stream<String> lines();
cs

빈 스트림 생성

1
Stream emptyStream = Stream.empty();
cs

[자바의 정석 - 기초편] ch14-23~25 스트림의 연산

https://youtu.be/iY8ta9upajE

중간 연산

최종 연산

[자바의 정석 - 기초편] ch14-26~29 스트림의 중간연산(1)

https://youtu.be/G2lPQB42GL8

 

filter()

1
2
3
4
5
6
7
8
9
10
11
IntStream intStream = IntStream.rangeClosed(110);    // 1 ~ 10
 
intStream
    .filter(i -> i%2 != 0 && i%3 != 0)
    .forEach(System.out::print);
 
// 위와 동일
intStream
    .filter(i -> i%2 != 0)
    .filter(i -> i%3 != 0)
    .forEach(System.out::print);
cs

문자열 스트림 정렬 방법

Comparator의 comparing()으로 정렬 기준 제공

1
2
3
4
5
studentStream
    .sorted(Comparator.comparing(Student::getBan))            // 반 별로 정렬
                    .thenComparing(Student::getTotalScore)    // 총점 별로 정렬
                    .thenComparing(Student::getName))        // 이름 별로 정렬
    .forEach(System.out::println);
cs

[자바의 정석 - 기초편] ch14-30~34 스트림의 중간연산(2)

https://youtu.be/sEa4RQGG0HU

map(): 스트림의 요소 변환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Stream<File> fileStream = Stream.of(
                            new File("Ex1.java"),
                            new File("Ex1"),
                            new File("Ex1.bak"),
                            new File("Ex2.java"),
                            new File("Ex1.txt"));
 
fileStream
   .map(File::getName)                           // Stream<File> -> Stream<String>
    .filter(s -> s.indexOf('.'!= -1)            // 확장자 없는 것 제외
    .map(s -> s.substring(s.indexOf('.'+ 1)    // 확장자(. 뒤) 추출
    .map(String::toUpperCase)                    // 대문자로 변환
   .distinct()                                   // 중복 제거
    .forEach(System.out::print);
        
cs

peek(): 스트림의 요소를 소비하지 않고 읽기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Stream<File> fileStream = Stream.of(
                            new File("Ex1.java"),
                            new File("Ex1"),
                            new File("Ex1.bak"),
                            new File("Ex2.java"),
                            new File("Ex1.txt"));
 
fileStream
    .map(File::getName)                                        // 파일명 반환
    .filter(s -> s.indexOf('.'!= -1)                        // 확장자 없는 것 제외
    .peek(s -> System.out.printf("filename = %s%n", s))        // 파일명 출력
    .map(s -> s.substring(s.indexOf('.'+ 1)                // 확장자(. 뒤) 추출
    .peek(s -> System.out.printf("extension = %s%n", s))    // 확장자출력
    .map(String::toUpperCase)                                // 대문자로 변환
    .distinct()                                                // 중복 제거
    .forEach(System.out::print);                            // 최종 연산 스트림 소비
        
cs

flatMap(): 스트림의 스트림(Stream<Stream<T> >)을 스트림(Stream<T>)로 변환

1
2
3
4
5
6
7
8
9
10
Stream<String[]> strArrStrm = Stream.of(new String[]{"abc""def""ghi"},
                                         new String[]("ABC""GHI""JKLMN"});
 
// 1. map()
Stream<Stream<String> > strStrStrm = strArrStrm.map(Arrays::stream);
 
// 2. flatMap()
Stream<String> strStrStrm = strArrStrm.flatMap(Arrays::stream);
 
 
cs

예제 1

1
2
3
4
5
6
7
8
9
Stream<String[]> strArrStrm = Stream.of(new String[]{"abc""def""ghi"},
                                         new String[]("ABC""GHI""JKLMN"});
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
 
strStrm
    .map(String::toLowerCase)
    .distinct()
    .sorted()
    .forEach(System.out::println);
cs

예제 2

1
2
3
4
5
6
7
8
9
10
11
String[] lineArr = {
    "Believe or not It is true",
    "Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
lineStream
    .flatMap(line -> Stream.of(line.split(" +")))    // 정규표현식, 공백 1개 이상
    .map(String::toLowerCase)
    .distinct()
    .sorted()
    .forEach(System.out::println);
cs

[자바의 정석 - 기초편] ch14-35~39 Optional에 대한 강의입니다.

https://youtu.be/W_kPjiTF9RI

Optional<T>

T 타입 객체의 래퍼 클래스

1
2
3
4
public final clas Optional<T> {
    private final T value;    // T타입의 참조변수
    ...
}
cs

null을 Optional<>에 담아서 반환

return null;

=> return Optional<null>;

 

Optional<T> 객체 생성 방법

1
2
3
4
5
String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(null);            // NPE 발생
Optional<String> optVal = Optional.ofNullable(null);    // OK
cs

null 대신 빈 Optional<T> 객체 사용

1
2
3
Optional<String> optVal = null;                // 바람직하지 않음
Optional<String> optVal = Optional.empty();    // 빈 객체로 초기화
 
cs

Optional 객체의 값 가져오기: get(), orElse(), orElseGet(), orElseThrow()

1
2
3
4
5
6
Optional<String> optVal = Optional.of("abc");
 
String str1 = optVal.get();                                        // null이면 예외 발생
String str2 = optVal.orElse("");                                // null이면 "" 반환
String str3 = optVal.orElseGet(String::new);                    // 람다식 사용 가능 () -> new String()
String str4 = optVal.orElseThrow(NullPointerException::new)        // null이면 예외 발생 (예외 지정 가능)
cs

 

isPresent(): Optional 객체의 값이 null이면 false, 아니면 true 반환

1
2
3
if(Optional.ofNullable(str).isPresent()) {    // if (str != null) 
    System.out.println(str);
}
cs

기본형을 감싸는 래퍼 클래스: OptionalInt, OptionalLong, OptionalDouble

일반 Optional<T>보다 성능 좋음

 

OptionalInt의 값 가져오기

빈 Optional 객체와의 비교

1
2
3
4
5
6
OptionalInt opt = OptionalInt.of(0);    // 0 저장
OptionalInt op2 = OptionalInt.empty();    // 초기값 0이지만 실제로는 값이 없음
 
opt.isPresent()  -> true
opt2.isPresent() -> false
opt.equals(opt2) -> false
cs

예제 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Optional<String> optStr = Optional.of("abcde");
Optional<Integer> optInt = optStr.map(String::length); // .map(s -> s.length());
 
int result1 = Optional.of("123")
                    .filter(x -> x.length() > 0)
                    .map(Integer::parseInt)
                    .get();
 
int result2 = Optional.of("")
                    .filter(x -> x.length() > 0)
                    .map(Integer::parseInt)
                    .orElse(-1);
 
Optional.of("456")
        .map(Integer::parseInt)
        .ifPresent(x -> System.out.printf("result3 = %d%n", x);
cs

예제 2

1
2
3
4
5
OptionalInt optInt1 = OptionalInt.of(0);
OptionalInt optInt2 = OptionalInt.empty();
 
System.out.println(optInt1.getAsInt());    // 0
System.out.println(optInt2.getAsInt());    // NoSuchElementException
cs

 

[자바의 정석 - 기초편] ch14-40~44 스트림의 최종연산에 대한 강의입니다.

https://youtu.be/M_4a4tUCSPU

forEach(), forEachOrdered(): 스트림의 모든 요소에 지정된 작업 수행

1
2
3
4
5
6
7
8
void    forEach(Comsumer<super T> action)            // 병렬 스트림인 경우 순서 보장 X
void    forEachOrdered(Consumer<super T> action)     // 병렬 스트림인 경우 순서 보장 O
 
IntStream.range(110).sequential().forEach(System.out::print);
IntStream.range(110).sequential().forEachOrdered(System.out::print);
 
IntStream.range(110).parallel().forEach(System.out::print); // 순서 보장 X
IntStream.range(110).parallel().forEachOrdered(System.out::print); // 순서 보장 O
cs
 
 
 

allMatch(), anyMatch(), noneMatch(): 조건 검사

1
2
3
4
5
6
boolean allMatch  (Predicate<super T> predicate)
boolean anyMatch  (Predicate<super T> predicate)
boolean noneMatch (Predicate<super T> predicate)
 
boolean hasFailedStu = stuStream
                            .anyMatch(s -> s.getTotalScore() <= 100);
cs

findFirst(), findAny(): 조건에 일치하는 요소 찾기

1
2
3
4
5
6
7
8
9
10
Optional<T> findFirst();
Optional<T> findAny();
 
Optional<Student> result = stuStream
                                .filter(s -> s.getTotalScore() <= 100)
                                .findFirst();
 
Optional<Student> result = parallelStream
                                .filter(s -> s.getTotalScore() <= 100)
                               .findAny();
cs

reduce(): 스트림의 요소를 하나씩 줄여가며 누적연산 수행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Optional<T> reduce (BinaryOperator<T> accumulator);
T            reduce (T identity, BinaryOperator<T> accumulator);
U            reduce (T identity, BiFunction<U, T, U> accumulator, BinaryOperator<T> combiner);
 
* identity        : 초기값
* accumulator     : 이전 연산결과와 스트림의 요소에 수행할 연산 (누적해서 수행할 작업)
* combiner        : 병렬처리된 결과를 합치는데 사용할 연산 (병렬 스트림)
 
int count     = intStream.reduce(0, (a, b) -> a + 1);
/*
    int a = identity;
    for(int b : stream)
        a += 1;
*/
 
int sum        = intStream.reduce(0, (a, b) -> a + b);
/*
    int a = identity;
    for(int b : stream)
        a += b;
*/
 
int max        = intStream.reduce(Integer.MIN_VALUE, (a, b) -> a > b ? a : b);
int min        = intStream.reduce(Integer.MAX_VALUE), (a, b) -> a < b ? a : b);
cs

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
String[] strArr = {
    "Inheritance""Java""Lambda""stream""OptionalDouble""IntStream""count""sum"
};
 
Stream
    .of(strArr)
    .forEach(System.out::println);
 
boolean noEmptyStr = Stream
                        .of(strArr)
                        .noneMatch(s -> s.length() == 0);
 
Optional<String> sWord = Stream
                            .of(strArr)
                            .filter(s -> s.charAt(0== 's')
                            .findFirst();
 
System.out.println("noEmptyStr = " + noEmptyStr);
System.out.println("sWord= " + sWord.get());
 
// Stream<String[]>을 IntStream으로 변환
IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);
 
int count = intStream1.reduce(0, (a,b) -> a + 1);
int sum   = intStream2.reduce(0, (a,b) -> a + b);
 
OptionalInt max = intStream3.reduce(Integer::max);
OptionalInt min = intStream4.reduce(Integer::min);
 
System.out.println("count = " + count);
System.out.println("sum = " + sum);
System.out.println("max = " + max.getAsInt());
System.out.println("min = " + min.getAsInt());
cs

[자바의 정석 - 기초편] ch14-45~49 collect()와 Collectors에 대한 강의입니다.

https://youtu.be/u9KOajCP3D8

collect(): Collector를 매개변수로 하는 스트림의 최종 연산

            그룹별 리듀싱

1
2
Object collect(Collector collector);
Object collect(Supplier supplier, Biconsumer accumulator, Biconsumer combiner);    // 잘 안씀
cs

Collector: 수집(collect)에 필요한 메서즈를 정의해 놓은 인터페이스

1
2
3
4
5
6
7
8
public interface Collector<T, A, R> {    // T(요소)를 A에 누적한 다음, 결과를 R로 변환해서 반환
    Supplier<A>             supplier();        // StringBuilder::new               누적할 곳
    BiConsumer<A, T>        accumulator();        // (sb, s) -> sb.append(s)          누적해서 수행할 작업
    BinaryOperator<A>       combiner();          // (sb1, sb2) -> sb1.append(sb2)    결합 방법 (병렬)
    Function<A, R>          finisher();         // ab -> ab.toString()              최종 변환
    Set<Characteristics>    characteristics();    // 컬렉터의 특성이 담긴 Set을 반환
    ...
}
cs

Collectors 클래스는 다양한 기능의 컬렉터(Collector를 구현한 클래스)를 제공

* 변환: mapping(), toList(), toSet(), toMap(), toCollection(), ...

* 통계: counting(), summingInt(), averageInt(), maxBy(), minBy(), summarizingInt(), ...

* 문자열 결합: joining()

* 리듀싱: reducing()

* 그룹화와 분할: groupingBy(), partitioningBy(), collectingAndThen()

 

collect(): 최종 연산

Collector: 인터페이스

Collectors: Collector를 구현한 클래스

 

스트림을 컬렉션으로 변환: toList(), toSet(), toMap(), toCollection()

1
2
3
4
5
6
7
8
9
10
List<String> names = stuStream                                //    Stream<Student>
                            .map(Student::getName)            // -> Stream<String>
                            .collect(Collectors.toList());    // -> List<String>
 
ArrayList<String> list = names                                                    //   List<String>
                            .stream()                                            //   Stream<String>
                            .collect(Collectors.toCollection(ArrayList::new));    //-> ArrayList<String>
 
Map<String, Person> map = personStream                                                //    Stream<Person>.
                            .collect(Collectors.toMap(p -> p.getRegId(), p -> p));  // -> Map<String, Person>
cs

스트림을 배열로 변환: toArray()

1
2
3
Student[] stuNames = studentStream.toAray(Student[]::new);
Object[]  stuNames = studentStream.toAray();    // 매개변수 없으면 Object[]
Student[] stuNames = studentStream.toAray();    // ERROR
cs

스트림의 통계정보 제공: counting(), summingInt(), maxBy(), minBy()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
long count = stuStream.count();
 
// 위와 동일
long count = stuStream.collect(counting());
 
 
long totalScore = stuStream
                        .mapToInt(Student::getTotalScore)
                        .sum();
// 위와 동일
long totalScore = stuStream
                        .collect(summingInt(Student::getTotalScore));
 
 
OptionalInt topScore = studentStream
                                .mapToInt(Student::getTotalScore)
                                .max();
 
Optional<Student> topStudent = stuStream
                                    .max(Comparator.comparingInt(Student::getTotalScore));
// 위와 동일
Optional<Student> topStudent = stuStream
                                    .collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));
cs

스트림을 리듀싱: reducing()

1
2
3
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T, U> mapper, BinaryOperator<T> op)
cs

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IntStream intStream = new Random()
                            .ints(146)
                            .distinct()
                            .limit(6);
 
OptionalInt             max = intStream.reduce(Integer::max);
OptionalInt<Integer> max = intStream.boxed()
                                    .collect(reducing(Integer::max));
 
long sum = intStream.reduce(0, (a,b) -> a+b);
long sum = intStream.boxed()
                    .collect(reducing(0, (a,b) -> a+b));
 
int grandTotal = stuStream.map(Student::getTotalScore)
                          .reduce(0, Integer::sum);
int grandTotal = stuStream.collect(reducing(0, Student::getTotalScore, Integer::sum));
cs

문자열 스트림의 요소를 모두 연결: joining()

1
2
3
4
String studentNames = stuStream.map(Student::getName).collect(joining());                // ABC
String studentNames = stuStream.map(Student::getName).collect(joining(", "));            // A, B, C
String studentNames = stuStream.map(Student::getName).collect(joining(", ""[""]"));  // [A, B, C]
String studentInfo = stuStream.collect(joining(", "));    // Student의 toString()으로 결합
cs

[자바의 정석 - 기초편] ch14-50~55 스트림의 그룹화와 분할에 대한 강의입니다.

https://youtu.be/VUh_t_j9qjE

partitioningBy(): 스트림을 2분할 (예제: https://github.com/castello/javajungsuk3/blob/master/source/ch14/StreamEx7.java)

 

groupingBy(): 스트림을 n분할 (예제: https://github.com/castello/javajungsuk3/blob/master/source/ch14/StreamEx8.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Map<Boolean, List<Student> > stuBySex = stuStream
                                            .collect(partitioningBy(Student::isMale)); // 학생들을 성별로 분할
List<Student> maleStudent   = stuBySex.get(true);    // 남자: true
List<Student> femaleStudent = stuBySex.get(false);    // 여자: false
 
 
 
Map<Boolean, Long> stuNumBySex = stuStream
                                       .collect(partitioningBy(Student::isMale,
                                                               counting())); // 분할 + 통계
int maleStudentNum   = stuNumBySex.get(true);    // 남자: true
int femaleStudentNum = stuNumBySex.get(false);    // 여자: false
 
 
 
Map<Boolean, Optional<Student> > topScoreBySex = stuStream
                                                    .collect(partitioningBy(Student::isMale, 
                                                                                 maxBy(comparingInt(Student::getScore))); // 분할 + 통계
int maleTopScoreBySex   = topScoreBySex.get(true);    // 남자: true
int femaleTopScoreBySex = topScoreBySex.get(false);    // 여자: false
 
 
 
Map<Boolean, Map<Boolean, List<Student> > > failedStuBySex = stuStream
                                                    .collect(partitioningBy(Student::isMale,
                                                                                 partitioningBy(s -> s.getScore() < 150))); // 다중 분할
List<Student> failedMaleStudent   = failedStuBySex.get(true);    // 남자: true
List<Student> failedFemaleStudent = failedStuBySex.get(false);    // 여자: false
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Map<Integer, List<Student> > stuByBan = stuStream
                                            .collect(groupingBy(Student::getBan, toList()));
 
 
 
Map<Integer, Map<Integer, List<Student> > > stuByHakAndBan = stuStream
                                                                .collect(groupingBy(Student::getHak, // 1. 학년 별 그룹화
                                                                         groupingBy(Student::getBan) // 2. 반 별 그룹화
                                                                ));
 
 
Map<Integer, Map<Integer, Set<Student.Level> > > stuByHakAndBan = stuStream
                                                                        .collect(
                                                                                groupingBy(Student::getHak,
                                                                                groupingBy(Student::getBan,
                                                                                mapping(s -> {
                                                                                                if        (s.getScore() >= 200return Student.Level.HIGH;
                                                                                                else if    (s.getScore() >= 100return Student.Level.MID;
                                                                                                else                          return Student.Level.LOW;
                                                                                }, toSet())))
                                                                        );
                                                                                
cs
반응형
반응형

람다

장점

한 번 밖에 쓰지 않는 함수를 왜 굳이 함수로 만드느냐? 함수 재활용 X

 

단점

람다 디버깅 시 이름 없음, 주소 0

람다를 재사용해야 할 때 재사용 불가

 

웬만하면 쓰지 않도록. 차라리 private 함수로 만들기

쓰는 경우

1. 정말 한 번만 쓴다는 보장이 있는 경우

2. 코드가 정말 단순한 경우

 

[자바의 정석 - 기초편] ch14-1~4 람다식이란? 람다식 작성하기

https://youtu.be/7Kyf4mMjbTQ

람다:

함수(메서드)를 간단한 식(expression)으로 표현하는 방법

익명 함수(이름이 없는 함수)

1
2
3
int max(int a, int b){
    return a> b ? a : b;
}
cs

=> (a, b) -> a > b ? a : b

작성법

1. 메서드의 이름, 반환타입 제거하고 ->를 {} 앞에 추가

2. return 생략 가능

3. 매개변수의 타입이 추론 가능하다면 생략 가능

 

※ 람다 식은 익명 함수가 아니라, 익명 객체!

(a, b) -> a > b ? a : b

1
2
3
4
5
6
new Object() {
    int max(int a, int b){
        return a> b ? a : b;
    }
}
 
cs

 

[자바의 정석 - 기초편] ch14-5,6 함수형인터페이스

https://youtu.be/0Sp9eFRV8gE

함수형 인터페이스

단 하나의 추상 메서드만 선언된 인터페이스

1
2
3
4
interface MyFunction {
    public abstract int max(int a, int b);
}
 
cs

 

예시

1
2
3
4
5
6
7
List<String> list = Arrays.asList("abc""aaa""bbb""ddd""aaa");
 
Collections.sort(list, new Comparator<String>() {
                            public int compare(String s1, String s2) {
                                return s2.compareTo(s1);
                            }
                        });
cs

=> 아래와 동일 

1
2
3
List<String> list = Arrays.asList("abc""aaa""bbb""ddd""aaa");
 
Collections.sort(list, (s1, s2) -> s2.compareTo(s1));
cs

[자바의 정석 - 기초편] ch14-7,8 java.util.function패키지

https://youtu.be/HPxnDM_FkGc

자주 사용되는 다양한 함수형 인터페이스 제공

1) 매개변수가 1개인 함수형 인터페이스

Predicate<T> 예시

1
2
3
4
5
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";
 
if(isEmptyStr.test(s))
    System.out.println("This is an empty String.");
cs

2) 매개변수가 2개인 함수형 인터페이스

3) 매개변수 타입과 return 타입이 일치

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.function.*;
import java.util.*;
 
public class LambdaEx5 {
    public static void main(String[] args){
        Supplier<Integer> s = () -> (int)(Math.random()*100)+1;
        Consumer<Integer> c = i -> System.out.print(i + ", ");
        Predicate<Integer> p = i -> i%2==0;
        Function<Integer, Integer> f = i -> i/10*10;
        
        List<Integer> list = new ArrayList<>();
        makeRandomList(s, list);
        System.out.println(list);
        printEvenNum(p, c, list);
        List<Integer> newList = makeNewList(f, list);
        System.out.println(newList);
    }
    
    static <T> List<T> makeNewList(Function<T, T> f, List<T> list){
        List<T> newList = new ArrayList<T>(list.size());
        
        for(T i : list){
            newList.add(f.apply(i));
        }
        return newList;
    }
    
    static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list){
        System.out.print("[");
        for(T i : list){
            if(p.test(i)){
                c.accept(i);
            }
        }
        System.out.println("]");
    }
    
    static <T> void makeRandomList(Supplier<T> s, List<T> list){
        for(int i = 0; i<10; i++){
            list.add(s.get());
        }
    }
}
 
cs

[자바의 정석 - 기초편] ch14-9~12 Predicate의 결합. CF와 함수형 인터페이스

https://youtu.be/Kk1ZIrVZnqw

and(), or(), negate()로 두 Predicate를 하나로 결합(default 메서드)

1
2
3
4
5
6
7
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
 
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(q).or(r);
Predicate<Integer> all2 = notP.and(q.or(r));
cs

 

등가비교를 위한 Predicate의 작성에는 isEqual()를 사용 (static 메서드)

1
2
3
4
Predicate<Integer> p = Predicate.isEqual(str1);
 
boolean result = p.test(str2);
boolean result = Predicate.isEqual(str1).test(str2);
cs

 

예제

1
2
3
4
5
6
7
8
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
 
Function<StringString> h = f.andThen(g);
Function<Integer, Integer> h2 = h.compose(g);    // g.andThen(f)와 동일
 
System.out.println(h.apply("FF"));    // "FF" -> 255 -> 11111111
System.out.println(h2.apply(2));    // 2 -> "10" -> 16
cs

 

함수형 인터페이스를 사용하는 컬렉션 프레임워크의 메서드 (와일드카드 생략)

예제 (컬렉션 프레임워크들의 코드들이 굉장히 짧아짐)

1
2
3
4
5
6
7
8
9
10
11
list.forEach(i -> System.out.print(i + ", ");    // list의 모든 요소 출력
list.removeIf(x -> x % 2 == 0 || x % 3 == 0);    // 2 또는 3의 배수 제거
list.replaceAll(i -> i * 10);                    // 모든 요소에 10을 곱함
 
// map의 모든 요소를 {k, v} 형식으로 출력
map.forEach((k, v) -> System.out.print("{" + v + ", " + v + "}, "));
 
// Iterator it = map.entrySet().iterator();
// while(it.hasNext()) {
//        System.out.println(it,next()); 
// }
cs

 

[자바의 정석 - 기초편] ch14-13,14 메서드 참조, 생성자의 메서드 참조

https://youtu.be/I55ALQndw50

메서드 참조(Method Reference)

하나의 메서드만 호출하는 람다식은 '메서드 참조'로 간단히 할 수 있음

예제

1
2
3
4
Function<String, Integer> f = (s) -> Integer.parseInt(s);
 
// 위와 동일
Function<String, Integer> f = Integer::parseInt;
cs

메서드 참조를 람다식으로 바꾸는 연습 필요

 

생성자와 메서드 참조

1
2
3
4
Supplier<MyClass> s = () -> new MyClass();
 
// 위와 동일
Supplier<MyClass> s = MyClass::new;
cs
1
2
3
4
Function<Integer, MyClass> s = (i) -> new MyClass(i);
 
// 위와 동일
Function<Integer, MyClass> s = MyClass::new;
cs

배열과 메서드 참조

1
2
3
4
Function<Integer, int[]> f = x -> new int[x];
 
// 위와 동일
Function<Integer, int[]> f2 = int[]::new;
cs
반응형
반응형

Docker


environment disparity 해결

 

https://youtu.be/chnCcGCTyBg

1. 도커를 컴퓨터, 서버에 둘 다 설치

2. docker 파일 생성 -> 구현하고 싶은 환경 설정 (ex. 우분투, 파이썬, 깃)

3. 생성한 docker 파일을 컴퓨터와 서버에게 둘 다 전송

4. docker는 파일을 읽고, 필요한 것을 다운로드 받음

5. docker는 설정한 환경과 같은 버츄얼 컨테이너를 컴퓨터와 서버에 만듦

 

docker 컨테이너들은 독립적

한 개의 서버에서 각각  다른 환경의 컨테이너를 가질 수 있음

매번 새로운 서비스를 만들 때마다 새로운 서버를 사고, 설정할 필요 없음 (파이썬 서버, 자바 서버, 데이터베이스 서버, ...)

컨테이너를 생성하고 복제하면 됨

 

Kubernetes


컨테이너를 관리하기 위한 툴

https://youtu.be/S3FVcdZcZnA

도커를 이용해서 웹 사이트 배포 시 (ex. Node js, Django) 

도커 컨테이너 안에 넣어서 AWS로 보내야 할 때

-> 쿠버네티스 필요 없음

 

1. 컨테이너들의 모니터링 실시: 컨테이너 자동 재시작

특정 컨테이너가 죽으면, 해당 컨테이너를 빨리 재시작 해야함(서비스의 핵심 파트일 수 있음)

쿠버네티스는 컨테이너들을 바라보면서 문제가 있는지 없는지 체크하는 것이 아님

예를 들어, 최소 5개의 컨테이너들이 작동하게끔 할 수 있음

그 중 하나라도 죽으면 쿠버네티스가 해당 컨테이너를 재시작시킴

 

2.

예를 들어 1만 명의 유저가 접속했지만 웹/앱이 준비되지 않았을 경우

쿠버네티스는 자동으로 새 컨테이너들을 생성

쿠버네티스가 알아서 해당 웹 니즈에 맞춰 컨테이너들을 준비

사람들이 떠나고, 니즈가 줄어들면 컨테이너를 지정해 둔 최소 숫자로 자동으로 조절함

 

3.

예를 들어, 클라우드에 5개의 컨테이너가 있음

코드 버그를 고치거나 버전을 업데이트 하는 경우

쿠버네티스가 없다면 컨테이너들을 끄고, 새로운 버전을 올리고, 다시 컨테이너들을 켜야 함

-> 웹사이트가 잠시 다운됨

쿠버네티스를 사용하면 컨테이너들의 신규 버전을 차례로 업데이트 함

-> 웹사이트가 다운되는 경우 없음

 

새로운 버전 배포, 컨테이너 사이즈 조정, 컨테이너 종료 시 재시작 등 컨테이너 관리를 도와줌

 

DevOps

 


소프트웨어의 개발(Development)과 운영(Operations)의 합성어

https://youtu.be/QAj3fsttKM4

 

5가지 철학

1. 문화(Culture): DevOps를 통해 하나의 문화를 만들어 나감

2. 자동화(Automation): 자동화를 통해 효율성과 빠른 속도 지향

3. 측정(Measurement): 지표를 측정하여 지속적으로 개선

4. 공유(Sharing): 공유를 통해 함께 발전

5. 축적(File up & Pile up): 기록을 축적하여 자신을 만들어 나감

 

어떤 요구사항을 효율적으로 만족시키기 위해서

일을 자동화 하며,

변경 사항 지표들을 측정하고, 공유하고,

모든 성공과 실패의 결과물들을 지속적으로 축적해 나아가는 문화를 만들어 나가는

철학, 방법론 내지는 기술

 

https://youtu.be/OL_DqAiC57I

DevOps 엔지니어

올바른 DevOps 문화를 위해 서비스 혹은 S/W Life Cycle에서 반복적인 일들을 자동화하고,

기술적 문제 혹은 팀의 차이를 기술적으로 예방하고, 해소시키는 사람 

 

Soft Skill

1. 문제 인식: 문제가 무엇이 있는지, 정확한 원인이 무엇인지 파악해야 함

2. 선택과 집중: 문제를 적합한 방법을 통해 해결하고, 해결의 우선순위를 올바르게 설정

3. 결정: 수 많은 선택지에 대해서, 추측이 아닌 확신을 가지고 빠르게 결정

4. 업의 속성: 제공하는 서비스의 본질과 가치를 이해해야 함

5. 사용자: 사용자를 이해하고, 요구사항에 대해서 빠르게 피드백 해야 함

 

Technical Skill

1. 프로그래밍: 능숙하게 다룰 수 있는 언어 (ex. Go, Python 등)

2. 운영체제: Linux와 같은 운영체제를 능숙하게 다루는 것과 개념에 대해서 알아야 함 (Linux, Shell, OS metrics, File system, 7 layers 등)

3. 서버관리: 서버를 관리하는 기술과 운영지식을 통해 신뢰할 수 있는 서비스를 구축해야 함. (ex. IaC, CI/CD, API, 가용성, 성능 등) 

4. 오픈소스: 인프라를 이루는 S/W 들을 이해하고, 자동화 도구들을 다룰 수 있어야 함. (ex. nginx, Tomcat, MySQL, Redis, Ansible, Terraform 등)

5. 클라우드: 퍼블릭 클라우드를 능숙하게 다루고, 직접 구축 및 설계를 할 수 있어야 함. (ex. AWS, Azure, GCP, Alibaba 등)

 

Infrastructur as Code: 코드로써의 인프라

인프라를 이루는 서버, 미들웨어, 서비스 등 인프라 구성요소들을 코드를 통해 구축하는 것.

IaC는 코드로써의 장점, 즉, 작성 용이성, 재사용성, 유지보수 등의 장점을 가짐

-> Terraform: 가장 많이 쓰이는 IaC 도구

반응형
반응형

스프링 핵심 원리 - 기본편 - 인프런 | 강의 (inflearn.com)

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

EJB 어렵고 복잡하고 느림

  -> POJO, 옛날로 돌아감

  -> 스프링 (EJB 문제점 지적),

  -> 하이버네이트 (EJB 엔티티 빈 기술 대체) -> JPA (자바 표준)

  => 스프링: 전통적인 J2EE (EJB)라는 겨울을 넘어 새로운 시작

 

스프링부트

tomcat 내장 -> 별도의 웹 서버 설치 안해도 됨

라이브러리 버전 맞춰줌

 

스프링 -> 자바 기반 -> "객체 지향"

객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크

좋은 객체 지향 앱을 개발할 수 있게 도와주는 프레임워크

 

 

좋은 객체 지향 프로그래밍

  • 추상화, 캡슐화, 상속, 다형성
  • 여러 개의 독립된 단위, "객체들의 모임". 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.
  • 프로그램을 유연하고 변경 용이하도록
  • "갈아 끼우기" => "다형성"
  • 자동차가 바껴도 운전자에게 영향을 주지 않음 (자동차 역할 / 자동차 구현 / 운전자 역할 구분)
  • 클라이언트에 영향을 끼치지 않고 새로운 기능 제공

 

역할 / 구현 나눠서 구현

  - 로미오 역할: 장동건, 원빈

  - 줄리엣 역할: 김태희, 송혜교

다른 대상 (배우)으로 변경 가능 -> 유연하고 변경 용이

 

클라이언트

  - 대상의 역할(인터페이스)만 알면 됨

  - 구현 대상의 내부 구조 몰라도 됨

  - 구현 대상의 내부 구조가 변경되어도 영향을 받지 않음.

  - 구현 대상 자체를 변경해도 영향을 받지 않음

 

자바

  - 역할: 인터페이스

  - 구현: 인터페이스를 구현한 클래스, 구현 객체

 

객체 설계 시 역할(인터페이스) 먼저 부여하고, 그것을 수행하는 구현 객체 만들기

 

다형성

인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 가능

클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

 

MemberRepository 인터페이스의 구현체를 MemoryMemberRepository, JdbcMemberRepository로 갈아끼울 수 있음

 

역할(인터페이스) 자체가 변하면 클라이언트, 서버 모두 변경

인터페이스를 안정적으로 잘 설계하는 것이 중요

 

스프링=>다형성

IoC (제어 역전), DI (의존관계 주입)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원

반응형
반응형

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 대시보드 - 인프런 | 강의 (inflearn.com)

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

포맷터 - Formatter

컨버터의 특별한 버전

Converter: 범용 (객체 -> 객체)

Formatter: 문자에 특화 (객체 <-> 문자)

 

1000 -> 1,000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
 
    @Override
    public Number parse(String text, Locale locale) throws ParseException {
        log.info("text={}, locale={}", text, locale);
        return NumberFormat.getInstance(locale).parse(text);
    }
 
    @Override
        public String print(Number object, Locale locale) {
        log.info("object={}, locale={}", object, locale);
        return NumberFormat.getInstance(locale).format(object);
    }
}
cs

포맷터 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyNumberFormatterTest {
    MyNumberFormatter formatter = new MyNumberFormatter();
 
    @Test
    void parse() throws ParseException {
        Number result = formatter.parse("1,000", Locale.KOREA);
        assertThat(result).isEqualTo(1000L);
    }
 
    @Test
    void print() {
        String result = formatter.print(1000, Locale.KOREA);
        assertThat(result).isEqualTo("1,000");
    }
}
cs

 

포맷터를 지원하는 컨버전 서비스

DefaultFormattingConversionService

FormattingConversionService에 기본적인 통화, 숫자 관련 몇 가지 기본 포맷터 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @Test
    void formattingConversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
 
        // 컨버터 등록
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());
 
        // 포맷터 등록
        conversionService.addFormatter(new MyNumberFormatter());
 
        // 컨버터 사용
        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1"8080));
 
        // 포맷터 사용
        assertThat(conversionService.convert("1,000", Long.class)).isEqualTo(1000L);
        assertThat(conversionService.convert(1000String.class)).isEqualTo("1,000");
    }
cs

 

포맷터 적용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 컨버터 추가
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());
 
        // 포맷터 추가
        registry.addFormatter(new MyNumberFormatter());
    }
}
 
cs

결과

  • ${number}: 10000
  • ${{number}}: 10,000
  • ${ipPort}: hello.typeconverter.type.IpPort@59cb0946
  • ${{ipPort}}: 127.0.0.1:8080

 

스프링이 제공하는 기본 포맷터

애토네이션 기반으로 원하는 형식 지정

@NumberFormat: 숫자 관련 형식 지정 포맷터

@DateTimeFormat: 날짜 관련 형식 지정 포맷터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Controller
public class FormatterController {
    @GetMapping("/formatter/edit")
    public String formatterForm(Model model) {
        Form form = new Form();
        form.setNumber(10000);
        form.setLocalDateTime(LocalDateTime.now());
        model.addAttribute("form", form);
        return "formatter-form";
    }
 
    @PostMapping("/formatter/edit")
    public String farmatterEdit(@ModelAttribute Form form) {
        return "formatter-view";
    }
 
    @Data
    static class Form{
        @NumberFormat(pattern = "###,###")
        private Integer number;
 
        @DateTimeFormat(pattern = "yyyy-mm-dd HH:mm:ss")
        private LocalDateTime localDateTime;
    }
}
cs

 

결과

• ${form.number}: 10000

• ${{form.number}}: 10,000

• ${form.localDateTime}: 2021-01-01T00:00:00

• ${{form.localDateTime}}: 2021-01-01 00:00:00

 

정리

컨버터, 포맷터 등록 방법 다르지만, 

사용할 때는 conversionService를 통해서 일관성 있게 사용 가능

 

주의!

메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다.

특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데,

HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지

바디에 입력하는 것이다. 예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은

라이브러리를 사용한다. 객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다. 따라서

JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서

포맷을 지정해야 한다. 결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다.

컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수

있다.

잭슨 라이브러리 자체에서 제공하는 데이터 포맷터 사용해야 함

 

 

파일 업로드

문자, 바이너리 동시 전송

-> enctype="multipart/form-data"

 

boundary

 

업로드 파일 제한

파일 하나: spring.servlet.multipart.max-file-size=1MB 

파일 전체 합: spring.servlet.multipart.max-request-size=10MB

 

멀티파트 리졸버는 멀티파트 요청인 경우 서블릿 컨테이너가 전달하는 일반적인 HttpServletRequest 를

MultipartHttpServletRequest 로 변환해서 반환한다.

> MultipartHttpServletRequest 는 HttpServletRequest 의 자식 인터페이스이고, 멀티파트와 관련된

추가 기능을 제공한다.

 

서블릿과 파일 업로드2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
    @Value("${file.dir}")
    public String fileDir;
 
    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
    }
 
    @PostMapping("/upload")
    public String saveFileV1(HttpServletRequest request) throws IOException, ServletException {
        log.info("request={}", request);
 
        String itemName = request.getParameter("itemName");
        log.info("itemName={}", itemName);
 
        Collection<Part> parts = request.getParts();
        log.info("parts={}", parts);
 
        for (Part part : parts) {
            log.info("==== PART ====");
            log.info("name={}", part.getName());
            Collection<String> headerNames = part.getHeaderNames();
                for (String headerName : headerNames) {
                    log.info("header {}: {}", headerName, part.getHeader(headerName));
                }
 
            // 편의 메서드
            log.info("submittedFilename={}", part.getSubmittedFileName());
            log.info("size={}", part.getSize());
 
            // 데이터 읽기
            InputStream inputStream = part.getInputStream();
            String body = StreamUtils.copyToString(inputStream, UTF_8);
            log.info("body={}", body);
 
            // 파일 저장
            if (StringUtils.hasText(part.getSubmittedFileName())) {
                String fullPath = fileDir + part.getSubmittedFileName();
                log.info("파일 저장 fullPath={}", fullPath);
                part.write(fullPath);
            }
        }
        return "upload-form";
    }
}
cs

 

스프링과 파일 업로드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {
    @Value("${file.dir}")
    public String fileDir;
 
    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
    }
 
    @PostMapping("/upload")
    public String saveFile(@RequestParam String itemName,
                            @RequestParam MultipartFile file,
                            HttpServletRequest request) throws IOException {
        log.info("request={}", request);
        log.info("itemName={}", itemName);
        log.info("multipartFile={}", file);
 
        if (!file.isEmpty()) {
            String fullPath = fileDir + file.getOriginalFilename();
            log.info("파일 저장 fullPath={}", fullPath);
            file.transferTo(new File(fullPath));
        }
        return "upload-form";
    }
}
 
 
cs

 

@RequestParam MultipartFile file

업로드하는 HTML Form의 name에 맞추어 @RequestParam 을 적용하면 된다. 추가로

@ModelAttribute 에서도 MultipartFile 을 동일하게 사용할 수 있다.

MultipartFile 주요 메서드

file.getOriginalFilename() : 업로드 파일 명

file.transferTo(...) : 파일 저장

반응형
반응형

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 대시보드 - 인프런 | 강의 (inflearn.com)

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

API 예외 처리 복잡한 이유

1. 예외에 따라서 각각 다른 데이터 추력

2. 같은 예외라도 컨트롤러에 따라서 응답 다를 수 있음

 

@ExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("BAD", e.getMessage());
}
 
// 예외 타입 생략 -> 파라미터로 받은 예외를 처리
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
    log.error("[exceptionHandler] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
 
    return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
 
// 위에서 걸러지지 않은 예외는 여기서 처리
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e) {
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("EX""내부 오류");
}
cs

@ResponseStatus 안하면 정상 처리로 간주 (Status 200 OK)

 

@ControllerAdvice

예외 처리를 모아서 한 군데서 처리

기능과 예외처리를 분리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
 
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
        public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }
 
    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(UserException e){
        log.error("[exceptionHandler] ex", e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
 
        return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
    }
 
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("EX""내부 오류");
    }
}
cs

 

대상 컨트롤러 지정 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {
 
}
 
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {
 
}
 
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {
 
}
cs

 

스프링 타입 컨버터

IntegerToStringConverter

1
2
3
4
5
6
7
public class IntegerToStringConverter implements Converter<Integer, String> {
    @Override
    public String convert(Integer source) {
        log.info("convert source={}", source);
        return String.valueOf(source);
    }
}
cs

StringToIntegerConverter

1
2
3
4
5
6
7
public class StringToIntegerConverter implements Converter<String, Integer> {
    @Override
    public Integer convert(String source) {
        log.info("convert source={}", source);
        return Integer.valueOf(source);
    }
}
cs

StringToIpPortConverter

1
2
3
4
5
6
7
8
9
10
11
12
public class StringToIpPortConverter implements Converter<String, IpPort> {
    @Override
    public IpPort convert(String source) {
        log.info("convert source={}", source);
 
        // "127.0.0.1:8080"
        String[] split = source.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);
        return new IpPort(ip, port);
    }
}
cs

IpPortToStringConverter

1
2
3
4
5
6
7
public class IpPortToStringConverter implements Converter<IpPort, String> {
    @Override
    public String convert(IpPort source) {
        log.info("convert source={}", source);
        return source.getIp() + ":" + String.valueOf(source.getPort());
    }
}
cs

타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅 하는 것과 큰 차이가 없음

 

컨버전 서비스 - ConversionService

개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @Test
    void conversionService() {
        // 등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());
 
        // 사용
        assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
        assertThat(conversionService.convert(10String.class)).isEqualTo("10");
 
        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1"8080));
 
        String ipPortString = conversionService.convert(new IpPort("127.0.0.1"8080), String.class);
        assertThat(ipPortString).isEqualTo("127.0.0.1:8080");
    }
cs

 

 

인터페이스 분리 원칙 - ISP(Interface Segregation Principal)

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

DefaultConversionService 는 다음 두 인터페이스를 구현했다.

ConversionService : 컨버터 사용에 초점

ConverterRegistry : 컨버터 등록에 초점

이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의

관심사를 명확하게 분리할 수 있다. 특히 컨버터를 사용하는 클라이언트는 ConversionService 만

의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다. 결과적으로 컨버터를

사용하는 클라이언트는 꼭 필요한 메서드만 알게된다. 이렇게 인터페이스를 분리하는 것을 ISP 라 한다.

ISP 참고: https://ko.wikipedia.org/wiki/

%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4_%EB%B6%84%EB%

A6%AC_%EC%9B%90%EC%B9%99

 

스프링에 Converter 적용하기

1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());
        registry.addConverter(new StringToIntegerConverter());
        registry.addConverter(new IntegerToStringConverter());
    }
}
cs

 

스프링 기본 컨버터 제공

사용자 지정 컨버터의 우선순위가 더 높음

 

localhost:8080/ip-port?ipPort=127.0.0.1:8080

1
2
3
4
5
6
@GetMapping("/ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
    System.out.println("ipPort.getIp() = " + ipPort.getIp());
    System.out.println("ipPort.getPort() = " + ipPort.getPort());
    return "ok";
}
cs

 

뷰 템플릿에 컨버터 적용하기

스프링: 문자를 객체로

뷰 템플릿: 객체를 문자로  

 

${...}: 컨버터 적용 안함

${{...}}: 컨버터 적용

1
2
3
4
5
6
@GetMapping("/converter-view")
public String converterView(Model model) {
    model.addAttribute("number"10000);
    model.addAttribute("ipPort"new IpPort("127.0.0.1"8080));
    return "converter-view";
}
cs

결과

• ${number}: 10000

• ${{number}}: 10000

• ${ipPort}: hello.typeconverter.type.IpPort@59cb0946

• ${{ipPort}}: 127.0.0.1:8080

  -> IpPort 객체를 String으로 변환한 뒤 출력

 

th:object="${form}"에서 

th:field="*{ipPort}"

  -> 컨버터 자동 적용

th:value="*{ipPort}"

  -> 컨버터 적용 안됨

 

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/converter/edit")
public String converterForm(Model model){
    IpPort ipPort = new IpPort("127.0.0.1"8080);
    Form form = new Form(ipPort);
    model.addAttribute("form", form);
    return "converter-form";
}
 
@PostMapping("/converter/edit")
public String converterEdit(@ModelAttribute Form form, Model model) {
    IpPort ipPort = form.getIpPort();
    model.addAttribute("ipPort", ipPort);
    return "converter-view";
}
 
@Data
static class Form{
    private IpPort ipPort;
 
    public Form(IpPort ipPort) {
        this.ipPort = ipPort;
    }
}
cs
반응형
반응형

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 대시보드 - 인프런 | 강의 (inflearn.com)

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

API 예외 처리

HTML 페이지: 오류 페이지만 있으면 됨

API: 각 오류 상황에 맞는 응답 스펙을 정하고 JSON으로 전송

(웹 브라우저가 아니면 HTML을 직접 받아서 할 수 있는 것 거의 없음)

 

에러 직접 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response){
    log.info("API errorPage 500");
    // 에러 받아옴
    Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
 
    // 응답 객체 생성
    Map<String, Object> result = new HashMap<>();
    result.put("status", request.getAttribute(ERROR_STATUS_CODE));
    result.put("message", ex.getMessage());
 
    // 응답 코드
    Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
cs

Header의 Accept가 */*이면 그냥 HTML

Header의 Accept가 application/json이면 JSON

 

 

스프링 부트 기본 오류 처리

WebServerCustomizer가 없어도 Header의 Accept가 application/json이면 스프링 부트에서 json 형식으로 전송

(기본 경로: /error)

{

    "timestamp": "2021-07-23T07:22:22.285+00:00",

    "status": 500,

    "error": "Internal Server Error",

    "path": "/api/members/ex"

}

 

HTML 화면 처리: BasicErrorController

API 오류 처리: @ExceptionHandler

 

HandlerExceptionResolver

컨트롤러(핸들러) 밖으로 던져진 예외를 해결 및 동작 방식 변경

 

적용 전: preHandle -> 컨트롤러 -> afterCompletion -> WAS

적용 후: preHandle -> 컨트롤러 -> ExceptionResolver -> afterCompletion -> WAS

※ ExceptionResolver로 예외를 해결해도 postHandle()은 호출되지 않음

 

MyHandlerExceptionResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    try {
        if(ex instanceof IllegalArgumentException){
            log.info("IllegalArgumenException resolver to 400");
            // IllegalArgumentException을 받으면 400에러를 전송
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
 
            // return을 정상적으로 해서 예외를 삼킴
            return new ModelAndView();
        }
    } catch (IOException e) {
        log.error("resolver ex", e);
    }
 
    return null;
    }
}
cs

 

MyHandlerExceptionResolver 핸들러 등록

1
2
3
4
5
6
7
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new MyHandlerExceptionResolver());
    }
}
cs

ExceptionResolver가 ModelAndView 반환 

: try, catch 하듯이 Exception을 처리해서 정상 흐름처럼 변경 (Exception을 Resolve)

 

반환 값에 따른 동작 방식

① 빈 ModelAndView:  뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿 리턴

② ModelAndView 지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링

③ null: 다음 ExceptionResolver 를 찾아서 실행. 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던짐

 

UserExceptionHandlerResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Slf4j
public class UserExceptionHandlerResolver implements HandlerExceptionResolver {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try{
            if (ex instanceof UserException) {
                log.info("UserException resolver to 400");
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                // 헤더에 따라서 json, html 선택
                String acceptHeader = request.getHeader("accept");
                if (acceptHeader.equals("application/json")) {
                Map<String, Object> errorResult = new HashMap<>();
                errorResult.put("ex", ex.getClass());
                errorResult.put("message", ex.getMessage());
                // 객체를 String으로 변경
                String result = objectMapper.writeValueAsString(errorResult);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().write(result);
                return new ModelAndView();
            }
            else {
                return new ModelAndView("error/500");
            }
        } catch(IOException e){
            log.error("resolver ex", e);
        }
        return null;
    }
}
cs

UserExceptionHandlerResolver 추가

1
2
3
4
5
6
7
8
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new MyHandlerExceptionResolver());
        resolvers.add(new UserExceptionHandlerResolver());
    }
}
cs

 

예외를 여기서 마무리하기

예외 발생: 컨트롤러 -> WAS -> 오류 페이지 찾음 -> 다시 /error 호출

ExceptionResolver 활용: 컨트롤러에서 예외 발생해도 ExceptionResolver에서 예외 처리. 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리 끝남. (정상 처리)

 

스프링이 제공하는 ExceptionResolver

스프링 부트 제공

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver

 

① ResponseStatusExceptionResolver

예외에 따라서 HTTP 상태 코드 지정

  1. @ResponseStatus가 달려있는 예외
  2. ResponseStatusException 예외

예외 발생 컨트롤러

1
2
3
4
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
    throw new BadRequestException();
}
cs

 

여기서 처리 (상태 코드 변경 가능)

1
2
3
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
cs

 

메시지 기능

reason을 MessageSource에서 찾는 기능

1
2
3
4
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}
 
cs

messages.properties

error.bad=잘못된 요청 오류입니다.

 

@ResponseStatus: 는 라이브러리의 예외 코드와 같이 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.

애노테이션을 사용하기 때문에 동적으로 변경하는 것이 어려움.

 

=> ResponseStatusException으로 처리

1
2
3
4
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad"new IllegalArgumentException());
}
cs

 

② DefaultHandlerExceptionResolver

스프링 내부에서 발생하는 스프링 예외를 처리

ex. 파라미터 바인딩 시점에 타입이 맞지 않음 -> TypeMismatchException

 

정리

1. ExceptionHandlerExceptionResolver 다음 시간에

2. ResponseStatusExceptionResolver HTTP 응답 코드 변경

3. DefaultHandlerExceptionResolver 스프링 내부 예외 처리

 

 

반응형

+ Recent posts