본문 바로가기

Study/개발일지

[백엔드온라인TIL] java 학습 18일차

java bean 개념 

 

1. Spring Bean이란 ?

  • Spring Bean은 Spring IoC 컨테이너가 관리하는 자바 객체로서 컨테이너에 의해 생명주기가 관리되는 객체를 의미한다. 
  • IoC 컨테이너 안에 들어있는 객체로 필요할 때마다 IoC 컨테이너에서 가져와서 사용한다.
  • 어노테이션인 @Bean을 사용하거나 xml 설정을 통해 일반 객체를 Bean으로 등록이 가능하다.
  •  즉, Spring 에서는 Bean은 ApplicationContext가 알고 있는 객체이며 ApplicationContext가 생성하고 직접 관리해주는 객체를 의미한다.

 

XML 기반의 Bean 정의 방법들
<!-- 간단한 빈 정의 -->
<bean id="..." class="..."></bean>

<!-- scope와 함께 빈 정의 -->
<bean id="..." class="..." scope="singleton"></bean>

<!-- property와 함께 빈 정의 -->
<bean id="..." class="...">
	<property name="beaninit" value="Hello World!"/>
</bean>

<!-- 초기와 메소드와 함께 빈 정의 -->
<bean id="..." class="..." init-method="..."></bean>

 

2. Spring Bean의 생명 주기

  • 객체 생성 -> 의존 설정 -> 초기화 -> 사용 -> 소멸 과정의 생명 주기를 가지고 있다.
  • Bean은 스프링 컨테이너에 의해 생명주기를 관리한다. 
  •  스프링 컨테이너가 초기화될 때 먼저 빈 객체를 설정에 맞춰 생성하며 의존 관계를 설정한 뒤 해당 프로세스가 완료되면 빈 객체가 지정한 메소드를 호출해서 초기화를 진행한다.
  • 객체를 사용해 컨테이너가 종료될 때 빈이 지정한 메서드를 호출해 소멸 단계를 거친다.
  • 스프링은 InitializingBean 인터페이스와 DisposableBean을 제공하며 빈 객체의 클래스가 InitializingBean Interface 또는 DisposableBean을 구현하고 있으며 해당 인터페이스에서 정의된 메소드를 호출해 빈 객체의 초기화 또는 종료를 수행한다.
  • 또한 어노테이션을 이용한 빈 초기화 방법에는 @PostConstruct와 빈 소멸에서는 @PreDestory를 사용한다.
// 생성 관련한 인터페이스
public interface InitializingBean { 
	void afterPropertiesSet() throws Exception;
}

// 소멸 관련한 인터페이스
public interface DispoasableBean { 
	void destroy() throws Exception;
}

public class Test implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws Exception {
    	//빈 생성후 메소드 호출
        System.out.println("afterPropertiesSet() 실행");
    }
    
    @Override
    public void destroy() throws Exception {
    	// 소멸을 진행하는 메소드
        System.out.println("destroy() 실행");
    }
   
    
}

 

 

 

 java Stream 개념

 

스트림의 사전적 의미는 '흐르다' 또는 '개울'입니다. 프로그래밍에서의 스트림도 사전적 의미와 크게 다르지 않습니다. 다만, 여기서는 물이 흐르는 것은 아니고 '데이터의 흐름'을 말합니다.

 

뭔가 추상적으로는 대충 데이터가 흐르겠구나.. 싶은데 정확히 어떻게 흐르고 결과를 이용하는지 알기가 어렵습니다. 그래서 제가 이해를 돕기 위한 그림을 준비하였습니다.

 

 

 

 

위 그림은 어부가 물고기를 그물로 잡고, 여러 마리를 일정한 기준으로 모아서 상자에 넣고, 이들을 하나로 모은 뒤 트럭에 실어서 우리의 밥상(?)까지 운반하는 과정을 나타내고 있습니다.

 

stream도 이와 별반 다르지 않습니다. 물고기와 같은 어류의 이동을 stream이라고 정의할 수 있습니다.

 

먼저, 어부가 어류 중에서도 고등어를 잡고 싶어서 그물로 고등어를 잡았습니다. 이 행위를 filter라고 하고, 이 연산자를 중간 연산자라고 합니다.

 

그리고 고등어를 포장하지 않고 생으로 팔 수는 없기 때문에 상자에 담아야 합니다. 이 행위를 map이라고 하고, 이 연산자도 마찬가지로 중간 연산자라고 합니다.

 

마지막으로, 고등어가 실린 수많은 상자를 운반하여 다른 곳으로 이동하면서 끝이 납니다. 이 행위를 collect라고 하고, 이 연선자는 최종 연산자라고 합니다.

 

 

어떤가요, 이제 조금 감이 잡히지 않나요? 스트림은 수많은 데이터의 흐름 속에서 각각의 원하는 값을 가공하여 최종 소비자에게 제공하는 역할을 한다고 보면 되겠습니다.

 

 

 

Stream의 특징

위의 스트림을 이제 조금 딱딱하게 설명할 때가 왔습니다. 기능적인 측면에서 스트림은 컬렉션(배열 포함)의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다.

 

우리는 사실 반복자를 스트림이 아니더라도 계속 사용해 왔습니다. 단적인 예로 Iterator 반복자가 있죠.

 

 

  import java.util.*;
   
  public class Main {
   
  public static void main(String[] args) {
  List<Integer> list = Arrays.asList(1, 2, 3);
  Iterator<Integer> it = list.iterator();
  while (it.hasNext()) {
  int num = it.next();
  System.out.println(num);
  }
  }
   
  }
view rawIterator.java hosted with ❤ by GitHub

 

 

정수가 있는 리스트를 하나씩 순회하면서 값을 출력하는 단순한 코드입니다. 이제, 이를 스트림으로 바꿔 보겠습니다.

 

 

  import java.util.*;
  import java.util.stream.*;
   
  public class Main {
   
  public static void main(String[] args) {
  List<Integer> list = Arrays.asList(1, 2, 3);
  Stream<Integer> stream = list.stream();
  stream.forEach(System.out::println);
  }
   
  }

 

 

어떤가요? 무슨 말인지는 모르겠어도 코드가 확 간결해졌다는 것을 아실 수 있을겁니다. 사실 저기서도 굳이 Stream을 변수로 따로 빼지 않고 바로 출력을 해도 되지만, 스트림을 정의하는 방식 중 하나를 보여주려고 일부러 한 줄을 더 코딩하였습니다.

 

 

이제, 스트림의 특징이 무엇인지 알아 보겠습니다.

 

 

(1) 람다식으로 요소 처리 코드를 제공한다.

위의 코드에서 볼 수 있듯이, 스트림은 람다식 또는 메소드 참조를 이용합니다. 따라서, 코드가 간결해지는 장점이 있습니다.

 

 

(2) 내부 반복자를 사용하므로 병렬 처리가 쉽다.

외부 반복자란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말합니다. 우리가 흔히 사용하는 index를 이용한 반복문이나 Iterator를 사용한 while문은 모두 외부 반복자를 이용하는 것입니다. 반면, 내부 반복자는 컬렉션 내부에서 요소들을 반복시키고, 개발자는 요소당 처리해야 할 코드만 제공하는 코드 패턴을 말합니다.

 

 

 

 

위의 그림은 외부 반복자와 내부 반복자를 나타낸 것입니다. 내부 반복자는 요소들의 변경 순서를 변경하거나, 멀티 코어 CPU를 최대한 활용하기 위해서 요소들을 분배시켜 병렬 작업을 할 수 있도록 도와 줍니다.

 

 

 

 

스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어납니다. 따라서, 요소의 병렬 처리가 컬렉션 내부에서 처리되므로 효율적인 병렬 처리가 가능합니다.

 

 

(3) 중간 처리와 최종 처리가 존재한다.

스트림은 컬렉션의 요소에 대해 중간 처리와 최종 처리를 수행할 수 있는데, 중간 처리에서는 매핑, 필터링, 정렬을 수행하고 최종 처리에서는 반복, 카운팅, 평균, 총합 등의 집계 처리를 수행합니다.

 

만약, 학생 객체를 요소로 가지는 컬렉션이 있다고 가정하면, 중간 처리에서 학생의 수학 점수를 뽑아 내고 최종 처리에서는 수학 점수의 평균값을 산출하는 행위를 할 수 있습니다.

 

1. Stream.collect()

collect()는 Stream의 데이터를 변형 등의 처리를 하고 원하는 자료형으로 변환해 줍니다.

Collect는 다음과 같은 기능들을 제공합니다.

  • Stream의 아이템들을 List 또는 Set 자료형으로 변환
  • Stream의 아이템들을 joining
  • Stream의 아이템들을 Sorting하여 가장 큰 객체 리턴
  • Stream의 아이템들의 평균 값을 리턴

이 외에 다른 많은 기능들도 제공합니다.

Collect의 기본적인 기능들을 예제와 함께 알아보겠습니다.

2. Stream의 아이템들을 HashSet으로 리턴

Stream.collect()는 Stream의 요소들을 다른 자료형으로 변환합니다.

Stream.collect()는 아래와 같이 3개의 인자를 받고, 이 인자들을 이용하여 Stream의 요소들을 다른 자료형으로 변환합니다.

collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

아래 예제는 Stream.collect()를 이용하여 Stream의 결과를 HashSet<String>로 리턴하는 코드입니다. HashSet에 대한 Supplier, accumulator, combiner를 collect()의 인자로 전달해주었고 HashSet 객체가 리턴되었습니다.

Stream<String> fruits = Stream.of("banana", "apple", "mango", "kiwi", "peach", "cherry", "lemon");
HashSet<String> fruitHashSet = fruits.collect(HashSet::new, HashSet::add, HashSet::addAll);
for (String s : fruitHashSet) {
    System.out.println(s);
}

Output:

banana
apple
cherry
kiwi
lemon
peach
mango

2.1 Collectors를 이용하여 동일한 내용 구현

위의 코드는 collect에 3개의 params을 넣어야 해서 다소 사용하기 번거롭습니다. Collectors라는 라이브러리가 기본적인 기능들을 제공해주어 이런 불편함을 해결해줍니다.

다음은 Collectors와 함께 collect로 HashSet 객체를 만드는 예제입니다. 인자에 Collectors.toSet()을 넣어주면 Set 자료형으로 만들어 줍니다.

Stream<String> fruits = Stream.of("banana", "apple", "mango", "kiwi", "peach", "cherry", "lemon");
Set<String> fruitSet = fruits.collect(Collectors.toSet());
for (String s : fruitSet) {
    System.out.println(s);
}

Output:

banana
apple
cherry
kiwi
lemon
peach
mango

3. Stream의 요소들을 List로 변환

Collectors를 이용하여 스트림의 요소들을 List 객체로 변환할 수 있습니다.

아래와 같이 Collectors.toList()를 인자에 전달하면 List 객체로 리턴됩니다.

Stream<String> fruits = Stream.of("banana", "apple", "mango", "kiwi", "peach", "cherry", "lemon");
List<String> fruitList = fruits.collect(Collectors.toList());
for (String s : fruitList) {
    System.out.println(s);
}

Output:

banana
apple
mango
kiwi
peach
cherry
lemon

4. Stream 요소를 1개의 String 객체로 변환

Stream의 모든 요소들을 1개의 String으로 만들 수도 있습니다.

아래와 같이 Collectors.joining()을 인자로 넣어주시면 됩니다.

Stream<String> fruits = Stream.of("banana", "apple", "mango", "kiwi", "peach", "cherry", "lemon");
String result2 = fruits.collect(Collectors.joining());
System.out.println(result2);

Output:

bananaapplemangokiwipeachcherrylemon

만약 문자열을 합칠 때 구분자를 넣어주고 싶다면 Collectors.joining(", ")처럼 인자로 넣어주면 됩니다.

Stream<String> fruits = Stream.of("banana", "apple", "mango", "kiwi", "peach", "cherry", "lemon");
String result2 = fruits.map(Object::toString).collect(Collectors.joining(", "));
System.out.println(result2);

Output:

banana, apple, mango, kiwi, peach, cherry, lemon

5. 가장 큰 객체 1개만 리턴

Stream의 아이템들을 어떤 조건으로 비교하여 충족되는 1개의 아이템만 리턴받을 수 있습니다.

다음 코드는 문자열의 길이가 가장 긴 요소를 리턴하는 예제입니다. Optional<String>으로 리턴하며, 문자열 길이를 비교하는 함수(Comparator)는 직접 만들어 인자로 전달하였습니다.

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.maxBy;

Stream<String> fruits = Stream.of("banana", "apple", "mango", "kiwi", "peach", "cherry", "lemon");
Function<String, Integer> getCount = fruit-> fruit.length();
Optional<String> result = fruits.map(Object::toString).collect(maxBy(comparing(getCount)));
System.out.println("result: " + result.orElse("no item"));

Output:

result: banana

6. Collectors로 평균 값 구하기

Collectors는 스트림 아이템들의 평균 값을 구해주는 기능들도 제공합니다.

List<Integer> list = Arrays.asList(1,2,3,4);
Double result = list.stream().collect(Collectors.averagingInt(v -> v*2));
System.out.println("Average: "+result);

Output:

Average: 5.0

7. Custom 객체에 Collect 적용

Custom 객체에 대해서도 Collect를 할 수도 있습니다. Collectors.toMap()을 사용하면 됩니다.

Stream<Fruit> fruits2 = Stream.of(new Fruit("1", "banana"), new Fruit("2", "apple"),
        new Fruit("3", "mango"), new Fruit("4", "kiwi"),
        new Fruit("5", "peach"), new Fruit("6", "cherry"),
        new Fruit("7", "lemon"));
Map<String, String> map = fruits2.collect(Collectors.toMap(Fruit::getId, Fruit::getName));
for (String key : map.keySet()) {
    System.out.println("key: " + key + ", value: " + map.get(key));
}

static class Fruit {
    public String id;
    public String name;

    Fruit(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }
    public String getName() {
        return name;
    }
}

Output:

key: 1, value: banana
key: 2, value: apple
key: 3, value: mango
key: 4, value: kiwi
key: 5, value: peach
key: 6, value: cherry
key: 7, value: lemon

8. toMap() 수행시, 동일한 Key에 대한 예외처리

toMap() 수행 중, 동일한 key를 갖고 있는 데이터를 만나면 IllegalStateException을 발생시킵니다. 이런 경우 예외처리를 할 수 있습니다. 3번째 param으로, 존재하는 값과 새로운 값이 중 어떤 값을 저장할지 정의해야 합니다. 아래 코드는 기존에 등록된 값을 사용하도록 (existingValue, newValue) -> existingValue)로 정의 했습니다. (코드를 보면 key 5를 갖고 있는 데이터가 두개 있습니다.)

Stream<Fruit> fruits2 = Stream.of(new Fruit("1", "banana"), new Fruit("2", "apple"),
        new Fruit("3", "mango"), new Fruit("4", "kiwi"),
        new Fruit("5", "peach"), new Fruit("6", "cherry"),
        new Fruit("5", "lemon"));
Map<String, String> map = fruits2.collect(
    Collectors.toMap(item -> item.getId(), item -> item.getName(),
            (existingValue, newValue) -> existingValue));
for (String key : map.keySet()) {
    System.out.println("key: " + key + ", value: " + map.get(key));
}

key 5의 경우 peach와 lemon이 있는데, peach가 먼저 등록되었기 때문에 peach가 map에 저장됩니다.

key: 1, value: banana
key: 2, value: apple
key: 3, value: mango
key: 4, value: kiwi
key: 5, value: peach
key: 6, value: cherry

이번에는 동일 key를 갖고 있는 데이터가 있을 때, 두개의 값을 합하여 저장하도록 정의할 수 있습니다. 3번째 param으로, 두개의 값을 더한 값을 리턴하는 함수를 정의하면 됩니다.

Stream<Fruit> fruits2 = Stream.of(new Fruit("1", "banana"), new Fruit("2", "apple"),
        new Fruit("3", "mango"), new Fruit("4", "kiwi"),
        new Fruit("5", "peach"), new Fruit("6", "cherry"),
        new Fruit("5", "lemon"));
Map<String, String> map = fruits2.collect(
        Collectors.toMap(item -> item.getId(), item -> item.getName(),
                (existingValue, newValue) -> {
                    String concat = existingValue + ", " + newValue;
                    return concat;
                }));
for (String key : map.keySet()) {
    System.out.println("key: " + key + ", value: " + map.get(key));
}

key 5의 값을 보면, 두 개의 문자열이 합쳐진 문자열이 저장되었습니다.

key: 1, value: banana
key: 2, value: apple
key: 3, value: mango
key: 4, value: kiwi
key: 5, value: peach, lemon
key: 6, value: cherry
728x90