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 을 사용한다.