SpringBoot 으로 MSA 구현하기(1) - Java 에서 자주 쓰는 코드
MSA 패턴(CQRS, SAGA 등)을 코드로 구현하기 위해서 SpringBoot 으로 REST API 어플리케이션을 만드는 것을 정리하고 있는데 자료 구조외에 자주쓰게 되는 코드들이 있어서 이를 정리해 봤다.
어플리케이션 개발에 Java 를 사용하는 이유는 아직 우리나라에서는 Java 가 많이 쓰이기 때문이다. SpringBoot 으로 어느 정도 정리되면 이후에는 Gin (Go 언어 기반 웹 프레임워크)으로 정리할 예정이다.
1. null check
Primitive Type 보다는 Object 를 많이 사용하기 때문에 null check 를 항상 해야 된다.
String nullString = null;
String emptyString = "";
if (Objects.isNull(nullString))
System.out.println("nullString is null");
if (Objects.nonNull(emptyString))
System.out.println("emptyString is not null");
--------- output -----------
nullString is null
emptyString is not null
Objects 클래스를 사용하여 가독성있게 null 체크를 할 수 있다.
2. null check & default value 세팅
null check 와 기본 값 세팅을 메소드 체이닝으로 바로 할 수 있다.
BigDecimal bd = null;
BigDecimal defaultValue = Optional.ofNullable(bd).orElse(BigDecimal.ZERO);
System.out.println(defaultValue);
--------- output -----------
0
Optional.ofNullable
은 static 메소드로 null
혹은 객체
를 담고있는 Optional 을 리턴한다.
Optional.orElse
는 메소드로 객체의 값이 null
일 경우 넘겨주는 아큐먼트로 그 값을 채운다.
여기서는 BigDecimal.ZERO 로 채워진 것을 알 수 있다.
3. null check & throw Exception
null check 와 null 일 때 예외를 던질 수 있다.
BigDecimal bd = null;
Optional.ofNullable(bd).orElseThrow(() -> new IllegalArgumentException("db is null"));
System.out.println(defaultValue);
--------- output -----------
Exception in thread "main" java.lang.IllegalArgumentException: db is null
at com.ask.example.Main.lambda$setDefaultValue$0(Main.java:45)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at com.ask.example.Main.setDefaultValue(Main.java:45)
at com.ask.example.Main.main(Main.java:24)
orElseThrow
메소드는 function 을 파라미터로 받으며 여기서는 람다함수를 사용하여 간단히 IllegalArgumentException 을 생성하여 던진다.
4. Array 와 List 를 Stream 으로 만들기
Stream은 Java 8부터 추가된 기능으로, Collection(Array, List, Map 등)과 같은 데이터 요소들을 처리하는데 도움을 주는 기능이다. Stream을 이용하면 다양한 작업을 수행할 수 있다.
- 필터링(Filtering):
filter()
메서드를 사용하여 특정 조건을 만족하는 요소들만 걸러낼 수 있다. - 변환(Mapping):
map()
메서드를 사용하여 요소들을 다른 형태로 변환할 수 있다. - 소팅(Sorting):
sorted()
메서드를 사용하여 요소들을 정렬할 수 있다. - 그룹화(Grouping):
collect()
메서드를 사용하여 요소들을 그룹화할 수 있다. - 집계(Reduction):
reduce()
메서드를 사용하여 요소들을 축소하여 하나의 값으로 만들 수 있다. - 제한(Limiting):
limit()
메서드를 사용하여 스트림의 크기를 제한할 수 있다. - 건너뛰기(Skipping):
skip()
메서드를 사용하여 스트림의 앞부분 요소들을 건너뛸 수 있다. - 병렬 처리(Parallel Processing):
parallelStream()
메서드를 사용하여 스트림 요소들을 병렬로 처리할 수 있다. - 중복 제거(Distinct):
distinct()
메서드를 사용하여 스트림의 중복 요소들을 제거할 수 있다. - 조인(Joining):
joining()
메서드를 사용하여 문자열 요소들을 합쳐서 하나의 문자열로 만들 수 있다.
먼저 Stream 을 만들어 보자.
배열을 Stream 으로 만들기
String[] fruits = { "apple", "banana", "orange", "grape", "banana" };
Stream stream = Arrays.stream(fruits);
System.out.println(stream);
--------- output -----------
java.util.stream.ReferencePipeline$Head@36baf30c
List 를 Stream 으로 만들기
List<String> fruits = new ArrayList<String>(
Arrays.asList("apple", "banana", "orange", "grape", "banana"));
Stream stream = fruits.stream();
System.out.println(stream);
--------- output -----------
java.util.stream.ReferencePipeline$Head@36baf30c
5. Stream 을 다른 Collection 으로 바꾸기
Stream 은 Collectors 클래스를 사용하여 다른 Collection 으로 쉽게 변경이 가능하다.
Stream → Set 으로 변경 (중복 제거)
참고로 distinct 로도 중복 제거가 가능하다.
List<String> fruits = new ArrayList<String>(
Arrays.asList("apple", "banana", "orange", "grape", "banana"));
Set<String> newSet = fruits.stream()
.collect(Collectors.toSet());
newSet.forEach(item -> System.out.println(item));
--------- output -----------
banana
orange
apple
grape
중복된 banana 는 하나만 출력된다.
Stream → LinkedList 로 변경
List<String> fruits = new ArrayList<String>(
Arrays.asList("apple", "banana", "orange", "grape", "banana"));
List<String> newList = fruits.stream()
.collect(Collectors.toCollection(
LinkedList::new
));
newList.forEach(item -> System.out.println(item));
--------- output -----------
apple
banana
orange
grape
banana
Stream → Map 으로 변경
List<String> fruits = new ArrayList<String>(
Arrays.asList("apple", "banana", "orange", "grape", "banana"));
Map<String, Integer> newMap = fruits.stream()
.collect(Collectors.toSet()).stream()
.collect(Collectors.toMap(
Function.identity(),
String::length
));
newMap.forEach((key, value) -> System.out.println(key + " " + value));
--------- output -----------
banana 6
orange 6
apple 5
grape 5
Map 은 중복 키를 가질 수 없기 때문에 Set 으로 변환한 다음에 Map 으로 변환하였다.
여기서 Function.identity()
는 item 그 자체의 값으로 apple
과 같은 리스트의 요소를 가리키며, Map의 키로 사용되었다. String::Length
는 스트링 객체의 길이를 구하는 메소드를 호출한 리턴 값으로 Map 의 값으로 사용되었다.
6. Stream 과 데이터 변경 메소드들
Stream.map()
map()
은 Item 각각에 대해서 다른 형태로 변경할 수 있다.
List<String> fruits = new ArrayList<String>(
Arrays.asList("apple", "banana", "orange", "grape", "banana"));
List<String> newList = fruits.stream()
.map(item -> item.toUpperCase())
.collect(Collectors.toList());
newList.forEach(item -> System.out.println(item));
--------- output -----------
APPLE
BANANA
ORANGE
GRAPE
BANANA
map()
은 아규먼트로 function
을 받으며 결과로 Stream 을 반환한다. 아규먼트로 들어가는 함수는 각 요소를 변경하여 반환하는 함수이어야 한다.
Stream.flatMap()
flatMap()
은 중첩된 Collection 을 한꺼풀 벗긴다고 생각하면 된다.
List<List<String>> listInList = Arrays.asList(
Arrays.asList("apple", "banana"),
Arrays.asList("orange", "grape", "banana"));
List<String> newList = listInList.stream()
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
newList.forEach(item -> System.out.println(item));
--------- output -----------
apple
banana
orange
grape
flatMap()
도 function
를 아규먼트로 받고 결과를 Stream 으로 변환한다. 단 아규먼트로 들어가는 함수는 각 요소에 영향을 주면서 Stream
을 반환하는 함수이어야 한다.
6. enum 활용
enum 은 enum 타입이 반환된다.
enum Gender {
MALE("male"),
FEMALE("female");
private final String param;
Gender(String param) {
this.param = param;
}
String getParam() {
return this.param;
}
}
System.out.println(Gender.MALE.getParam());
--------- output -----------
male
Gender.MALE
값은 enum Gender
타입이다.
하지만 아래와 같이 문자열을 enum 값으로 변환할 수 있다. 이 방법은 Json
을 marshal , unmarshal 할 때 많이 사용된다.
enum Gender {
MALE("male"),
FEMALE("female");
private final String param;
private static final Map<String, Gender> paramMap =
Arrays.stream(Gender.values())
.collect(Collectors.toMap(
Gender::getParam,
Function.identity()
));
Gender(String param) {
this.param = param;
}
static Gender fromParam(String param) {
return Optional.ofNullable(param)
.map(paramMap::get)
.orElseThrow(() -> new IllegalArgumentException("param is not valid"));
}
String getParam() {
return this.param;
}
}
System.out.println(Gender.fromParam("male"));
--------- output -----------
MALE
static 으로 Map<String, Gender>
변수를 선언하여 거기에 값을 채워넣고, static fromParam
메소드를 사용하여 String 값으로 Gender
값을 가져온다.
7. Map 의 종류
Map 에는 다음과 같은 종류가 있다.
- HashMap : key - value 로 저장되며 key 의 중복을 허용하지 않는다
- TreeMap : HashMap 의 기능에 key 가 정렬되어 있다.
- LinkedHashMap: HashMap 의 기능에 key 입력 순서대로 출력된다.
- MultiValueMap: HashMap 가 다른 점이 key 의 중복을 허용한다.
다만, MultiValueMap 을 사용하기 위해서는 spring-core
패키지를 import 해야 한다. (apache commons 의 commons-collections4 에도 MultiValueMap 은 있지만 spring-core 가 더 사용하기 편하다)
pom.xml 에 아래와 같은 dependcy 를 추가한다.
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.0.9</version>
</dependency>
</dependencies>
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("Accept-Encoding", "compress;q=0.5");
map.add("Accept-Encoding", "gzip;q=1.0");
map.forEach((key, value) -> System.out.println(key + " " + value));
--------- output -----------
Accept-Encoding [compress;q=0.5, gzip;q=1.0]
REST API 를 만들 때 Header 에 값을 넣기 위해서 위와 같이 MultiValueMap 을 사용한다.