📌 TIL
1) Stream API
Stream API는 Data를 파이프 라인 형식으로 처리하기 위한 API 입니다. Collection, 배열, 파일 등 데이터의 집합체(Data Source)에서 각각의 요소를 꺼내서 그것을 처리의 흐름(Stream)에 전달하기 위한 구성을 제공합니다.
Stream은 함수 조작을 한 결과를 Stream으로 반환하는 "중간 조작"과 처리 결과를 Data로 반환하는 "종단 조작"이 있다고 합니다. 중간 조작이나 종단 조작 모두 메소드 인수로 함수형 인터페이스를 받는 경우가 많기 때문에 람다식을 이용하면 보다 가독성이 좋은 코드를 작성할 수 있어요. Stream은 for문과 유사한데, 왜 Stream을 쓰는걸까요 ?
그 이유는, 가독성 측면에서 있습니다. for문에 비해 Stream을 사용하면 훨씬 가독성이 좋은 코드를 작성할 수 있어요.
[ Stream 사용 방법 ]
Stream은 Collection 타입에서 사용할 수 있습니다. 대표적으로 List, Set에서 사용할 수 있고 Array나 Map 같은 경우는 Stream을 사용할 수 있는 형태로 변환해주는 과정이 필요해요.
💡 Array
String[] numebr = {"One", "Two", "Three"};
Stream<String> strStream = Arrays.stream(strArray);
💡 Map
HashMap<Integer, String> map = new HashMap<>();
Set<Map.Entry<Integer, String>> entries = map.entrySet();
entries.stream().forEach(entry -> {
String key = entry.getKey();
Integer value = entry.getValue();
});
[ Stream API의 처리 ]
- Data Source를 준비
- Stream을 생성
- 추출, 가공 등의 중간 처리
- 출력, 집계 등의 종단 처리
Stream API에는 다양한 메서드들이 있지만, 그 중 대표적으로 forEach , filter , map에 대해 살펴보도록 할게요.
🔍 forEach
Stream.of("Mon","Tue","Wed").forEach(day -> {
if(day.equals("Tue"))
return;
System.out.println(day);
});
forEach는 요소를 돌면서 실행되는 stream 연산의 최종 작업입니다. (forEach는 중간에 return 문이 있더라도 끝까지 순환합니다. - 주의) 위 예제에서 출력값은 "Mon", "Wed" 입니다.
🔍 filter
List<String> dayWithE = Stream.of("Mon", "Tue", "Wed")
.filter(day -> day.contains("e"))
.collect(Collectors.toList());
filter는 지정된 조건에 따라 값이 추출됩니다. 기존에 작성하던 if문과 유사하다고 생각하면 될 것 같습니다. 조건에 따라 종료하기 위해서는 예외를 던져야 합니다.
🔍 map
List<String> upperDay = Stream.of("Mon","Tue","Wed")
.map(String::toUpperCase)
.collect(Collectors.toList());
map은 전달된 값을 가공합니다.
여기까지, Stream API의 주요 메서드 3가지를 살펴보았는데요 forEach같은 경우 for-loop에 비해 가독성이 좋지만 반면 단점들이 존재합니다. (break 문 사용이 불가능, 성능 저하 등) 따라서 forEach 같은 경우 데이터를 출력하는 용도로만 사용하는 것이 좋다고 해요.
지난 Day2에서 배웠던 Optional 과도 연관지어 Stream API를 사용할 수 있는데요 !
List<String> days = Arrays.asList("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun");
days.stream()
.filter(day -> {
if(day.contains("u"))
return true;
return false;
}).findAny().orElseThrow();
다음과 같이 findAny() 메서드는 Optional을 반환하기 때문에 orElseThrow()를 통해 값이 존재하지 않을 때 처리를 해줄 수 있어요 !
2) 의존 관계
자바에서 의존관계는 B에 변경되었을 때 그 영향이 A에 미치는 관계를 A는 B에 의존하고 있다고 합니다. (A와 B는 의존관계 !)
조금 더 풀어서 설명하자면 ,
클래스 A가 다른 클래스(혹은 인터페이스) B를 사용할 때 A는 B에 의존한다고 이야기 합니다. 즉, 한 객체의 코드에서 다른 객체를 생성하거나 다른 객체의 메서드를 호출할 때, 그리고 파라미터로 객체를 전달받아 사용할 때 의존성이 발생한다고 할 수 있습니다. 지난 Day2에서도 이야기 했지만 의존성의 정도를 결합도라고 합니다. 결합도를 낮추는 것이 좋은 객체지향 설계 방법이라고 했었죠?
[ 코드에서 의존 관계 ]
- 다른 클래스의 레퍼런스 변수를 사용하는 경우
- 다른 클래스의 인스턴스를 생성하는 경우
- 다른 클래스를 상속받는 경우
// (1)
public class Car {
private Engine engine;
}
// (2)
public class Car {
private Engine engine = new Engine();
}
// (3)
public class Bus extends Car {
}
[ 의존성이 위험한 이유 ]
(1). A가 B에 의존중일 때, B의 변경은 A에게 영향을 끼칩니다. 즉, B의 변경이 A의 변경을 초래할 가능성이 존재하는데 이런 의존의 영향은 만약 C가 A를 의존하고 있다면 C에게 까지 전파되는 상황이 발생하게 됩니다.
(2). 테스트가 어렵습니다. 특정 모듈의 작동을 테스트 한다고 할 때 다른 모듈이 필요하다면, 즉 의존관계가 있다면 ? 특정 모듈만을 독립적으로 떼어내어 테스트하기가 어렵습니다.
이렇게 의존 관계의 문제점을 해결하기 위해 착안된 개념이 "의존성 주입(Dependency Injection)"이라는 개념입니다.
3) 의존성 주입
의존성 주입(DI, Dependency Injection)이란, 위에서 이야기한 의존성의 위험성을 보완하고자 사용되는 패턴입니다. 의존성 주입은 불필요한 객체를 직접 생성하거나 참조하는 것이 아니라, 외부에서 넣어주는 방식입니다. 즉, 객체의 의존관계를 내부에서 결정하는 것이 아닌 객체의 외부에서, 런타임 시점에 결정하는 방식인거죠.
예를 들어,
public class Car {
private final SomeEngine someEngine = new SomeEngine();
public Car () {
...
}
}
고객이 원하는대로 엔진을 교체할 수 있는 자동차를 판매하려고 합니다. 하지만 위와 같은 방식이라면 SomeEngine()이라는 엔진밖에 사용할 수 없겠죠. 교체가 불가능하며, 만약 엔진의 스펙이 바뀌게 되면 자동차 역시 그에 맞춰 설계를 변경해야 할겁니다.
이를 의존성 주입을 통해 해결해보면,
public class Car {
private final Engine engine
public Car(Engine engine){
this.engine = engine;
}
}
다양한 방식이 있지만 대표적으로 생성자를 통해서 의존성을 주입할 수 있습니다. 이렇게 Engine 이라는 인터페이스를 필드로 갖고, 이를 구현하는 구현체를 주입하도록 작성하게 되면 외부에서 Car라는 클래스를 생성할 때 생성자의 인자로 원하는 엔진을 주입해주기만 하면, 우리가 의도했던 대로 프로그램을 구성할 수 있게 됩니다.
[ 의존성 주입을 사용하는 이유 ]
1. 모듈간 의존성이 줄어든다.
2. 재사용성이 높아진다.
3. 유닛 테스트를 쉽게 만들어준다.
4. 가독성이 증가한다.
결국 인터페이스로 의존 관계를 맺어, 결합도를 낮추고 의존 관계를 약하게 만드는 것이 객체지향적인 코드를 작성했다고 볼 수 있으며, 캡슐화를 보다 잘 지킬 수 있습니다.
이렇게, 의존성 주입을 통해 결국 우리는 의존(관계)의 역전(Dependency Inversion)이라는 또 하나의 개념을 살펴볼 수 있습니다.
4) 의존(관계)의 역전
의존(관계)의 역전(Dependency Inversion)이란 위의 의존성 주입의 예시에서 살펴보았던 것처럼 객체에서 어떤 클래스를 참조하여 사용해야 하는 상황이 생길 때, 그 클래스를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하는 것을 말합니다.
위에서 우리는 SomeEngine()이라는 구현체를 직접적으로 참조했다가, Engine이라는 인터페이스를 참조하도록 수정하여 의존성을 주입시키는 방식으로 코드를 수정했죠? 이런 방식을 의존 관계의 역전이라고 표현합니다.
아직까지는 사실 잘 이해가 되지 않는데요, 의존관계 역전은 다음과 같이 표현하기도 합니다.
"고수준 컴포넌트가 저수준 컴포넌트에 의존하지 않도록 의존 관계를 역전시키는 것"
그렇다면, 고수준 컴포넌트와 저수준 컴포넌트가 무엇인지 간단하게 알아볼게요.
현재는 휘발유 엔진을 사용하는 자동차를 설계했어요. 즉 , 고수준 모듈인 자동차가 저수준 모듈인 휘발유 엔진에 의존하는 상태입니다. 그러나 추후 요구사항에 의해 엔진을 전기 엔진으로 변경하고 싶다는 요구를 받게 되었어요. 단순히 휘발유 엔진을 전기 엔진으로 바꾸면 끝일까요? 이것은 엔진에 의존하던 자동차에게 까지 전파되어 영향을 미치게 됩니다.
따라서, 우리는 추상화나 다형성을 이용해 의존 관계를 역전시켜야 합니다. "휘발유 엔진"을 "엔진" 자체로 추상화하는 것을 뜻합니다.
이렇게 고수준 모듈이 저수준 모듈에 의존했던 상황이 역전되어 저수준 모듈이 고수준 모듈에 의존하게 된다는 것을 의미하게 됩니다.
고수준 컴포넌트 | 저수준 컴포넌트 |
구체적인 기술을 사용하지 않는 클래스 (정책, 비즈니스 로직) | 구체적인 기술을 사용하는 클래스 |
ex) Service 계층, Controller 계층 | ex) Repository 계층 |
컴포넌트 ❓ : 컴포넌트는 클래스와 같이 특정 기능을 수행하는 기능적 단위입니다.
5) 스프링을 통한 의존성 주입 살펴보기
스프링 프레임워크는 스프링 컨테이너에 의해 관리되는 객체인 빈(Bean)을 가지고 있습니다. 또한 빈의 생성과 같은 제어를 담당하는 스프링 컨테이너는 IoC 컨테이너인 빈 팩토리(Bean Factory)가 존재합니다. 그러나 실제로는 빈의 생성 및 관계설정 외 추가적인 기능이 필요한데 이를 위해, 빈 팩토리를 확장한 애플리케이션 컨텍스트(Application Context)를 주로 사용합니다.
- 모든 클래스가 빈이 되는 것은 아니고, @Bean이 붙은 메소드의 이름으로 빈 목록을 생성하며, 싱글톤으로 생성됩니다.
- 스프링은 애플리케이션 컨텍스트 등록된 빈 목록을 통해 의존성 주입을 해줄 수 있습니다. (없으면 설정정보를 통해 생성을 요청)
(1) @Profile
@Component
@Profile("prod")
public class MysqlRepository implements Repository
@Component
@Profile("test")
public class H2Repository implements Repository
- MysqlRepository와 H2Repository 클래스가 있을 때, 둘다 @Component 가 붙었기 때문에 스프링 빈으로 등록된다.
- @Profile 어노테이션을 통해 프로그램이 시작하는 시점에 어떤 Repository를 사용할 지 결정할 수 있다.
- 실행 방법은 먼저 jar 파일을 생성해줘야 합니다. ( clean -> jar 실행 후 새로 생긴 build 폴더 내의 libs 파일 )
- java -jar 실행하려는 jar파일.jar --spring.profiles.active=prod(test) 명령어를 수행하면 Profile을 통해 사용하려는 Repository를 선택할 수 있습니다.
(2) @Order
스프링의 @Order 어노테이션은 컴포넌트나 빈(Bean)의 로드 순서를 정의할 수 있어요. 따라서 다음과 같이 작성하면,
@Order(0)
@Component
public class MysqlRepository implements Repository
@Order(1)
@Component
public class H2Repository implements Repository
--------------------------------------------------------
List<Repository> repositories;
repositories.get(0) // MysqlRepository
repositories.get(1) // H2Repository
순서대로 빈이 로드 된 것을 확인할 수 있습니다.
정리
- Stream은 Java 8에 추가되었고, 컬렉션이나 배열 등에 저장된 요소를 하나씩 참조하며 코드를 실행할 수 있는 기능입니다.
- 의존 관계는 B가 변경되었을 때, 그 영향이 A에게 까지 미치면 A가 B를 의존하고 있다고 하며 둘은 의존관계라고 합니다.
- 의존성 주입은 의존관계의 문제점을 해결하기 위해 외부로부터 의존성을 주입하는 방식을 말합니다.
- 의존관계 역전은 의존성 주입을 통해 저수준 모듈이 고수준 모듈에 의존하게 되는 것을 말합니다.
< 참고 자료 >