프로그래밍/Java

SpringBoot 으로 MSA 구현하기(1) - Java 에서 자주 쓰는 코드

seungkyua@gmail.com 2023. 7. 27. 08:24
반응형

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을 이용하면 다양한 작업을 수행할 수 있다.

  1. 필터링(Filtering): filter() 메서드를 사용하여 특정 조건을 만족하는 요소들만 걸러낼 수 있다.
  2. 변환(Mapping): map() 메서드를 사용하여 요소들을 다른 형태로 변환할 수 있다.
  3. 소팅(Sorting): sorted() 메서드를 사용하여 요소들을 정렬할 수 있다.
  4. 그룹화(Grouping): collect() 메서드를 사용하여 요소들을 그룹화할 수 있다.
  5. 집계(Reduction): reduce() 메서드를 사용하여 요소들을 축소하여 하나의 값으로 만들 수 있다.
  6. 제한(Limiting): limit() 메서드를 사용하여 스트림의 크기를 제한할 수 있다.
  7. 건너뛰기(Skipping): skip() 메서드를 사용하여 스트림의 앞부분 요소들을 건너뛸 수 있다.
  8. 병렬 처리(Parallel Processing): parallelStream() 메서드를 사용하여 스트림 요소들을 병렬로 처리할 수 있다.
  9. 중복 제거(Distinct): distinct() 메서드를 사용하여 스트림의 중복 요소들을 제거할 수 있다.
  10. 조인(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 에는 다음과 같은 종류가 있다.

  1. HashMap : key - value 로 저장되며 key 의 중복을 허용하지 않는다
  2. TreeMap : HashMap 의 기능에 key 가 정렬되어 있다.
  3. LinkedHashMap: HashMap 의 기능에 key 입력 순서대로 출력된다.
  4. 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 을 사용한다.

반응형