Java Enum을 활용해보기

제휴사 연동서비스를 개발하던 중 퇴사자분이 만들어놓은 코드를 만났는데 대략 비슷하게 구현을 해보면 이렇다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//...
public String getStatusType(String type, String code) {
String result = "";
switch(type){
case "Booking":
if("BK04".equals(code)){
result = "OK";
} else if(List.of("BK03","BK05").contains(code)){
result = "CANCEL";
} else if(List.of("BK01","BK02").contains(code)){
result = "HOLD";
}
break;
case "Payment":
if("BK04".equals(code)){
result = "OK";
} else if(List.of("BK03","BK05","BK06","BK07").contains(code)){
result = "CANCEL";
} else if(List.of("BK01","BK02","BK09").contains(code)){
result = "HOLD";
}
break;
//...
}
return result;
}
1
2
3
String bookingType = getStatusType("Booking", vo.getCode());
String paymentType = getStatusType("Payment", vo.getCode());
String otherType ...

이 코드는 Booking과 Payment 같은 타입과 기존 아이템의 상태코드 값을 받아서 매칭되는 상태값을 반환하는 메서드였다. 코드는 처음에 보면 별 생각이 없었지만, 타입 유형이 늘어나고 코드가 많아지면 switch문이 복잡해지고 유지보수가 어려워지는 문제가 생길 수 있다고 생각이 들었다.

검색을 하던 도중 시간이 좀 지난 글이지만 우아한형제들 기술블로그 - Java Enum 활용기 라는 글에서 3. 데이터 그룹관리 세션의 내용을 적용할 수 있겠다고 판단되었다. Enum을 활용하여 데이터를 그룹화 하고 각 타입은 본인이 수행해야할 기능만 책임지도록 하는게 중요해 보였다.

개선된 Enum 클래스는 대략 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum BookingType{
OK("완료", List.of(ItemStatus.OK),
CANCEL("취소", List.of(ItemStatus.CANCEL, ItemStatus.REFUND),
HOLD("대기", List.of(ItemStatus.PENDING, ItemStatus.UNAVAILABLE),
//...
EMPTY("없음", Collections.emptyList());
private String title;
private List<ItemStatus> codeList;

public static BookingType findByItemStatus(ItemStatus code){
return Arrays.stream(NaverBookingType.values())
.filter(itemCode -> itemCode.hasStatusCode(code))
.findAny()
.orElse(EMPTY);
}
public boolean hasStatusCode(ItemStatus code){
return typeList.stream()
.anyMatch(statusCode == code);
}
}
1
2
3
4
ItemStatus code = ItemStatus.findByCode(vo.getCode());
BookingType bookingType = BookingType.findByItemStatus(code);
PaymentType paymentType = PaymentType.findByItemStatus(code);
OtherType otherType = ...

이제는 직관적으로 각 유형에 대한 상태코드를 볼 수 있다. BookingType의 OK가 가진 상태코드를 직관적으로 볼수 있어서 가독성이 좋아졌다.

데이터들의 그룹화를 통해서 상태매핑 관계를 파악하기 더 수월해지고 입출력도 Enum을 사용해서 별도의 검증없이도 예측이 가능하고 실수로 다른 문자열이 들어올 일이 없어서 타입 안정성이 보장된다.

추후 기능 확장 시에도 Enum 클래스만 수정하면 되서 side-effect가 적을 것으로 기대된다.

[WebFlux] map과 flatMap에 대해서

spring WebFlux를 사용하다보면 체인 연산자로 FlatMap을 사용해야할지, Map을 사용해야할지 헷갈리는 경우가 있어서 정리해봤다.

map 함수

public final Flux map(Function<? superT,? extends V> mapper)

  • Transform the items emitted by this Flux by applying a synchronous function to each item.

flux.map

map 함수는 데이터 스트림의 각 요소를 변환하는데 사용되는 함수로 각 요소를 동기적으로 처리하며, 각 요소를 변환하여 새로운 데이터 스트림을 반환한다.

간단한 샘플코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void sampleMapTest() {
// Arrange
Function<String, String> mapper = (str) -> str.toUpperCase();
Flux<String> testFlux = Flux.just("apple", "banana", "carrot");

// Act
Flux<String> resultFlux = testFlux.map(mapper);

// Assert
StepVerifier.create(resultFlux)
.expectNext("APPLE", "BANANA", "CARROT")
.expectComplete()
.verify();
}

Function mapper는 스트림을 구독할 때 실행되며 각 요소의 순서를 유지하며 입력값을 변환하여 새로운 값으로 출력하게 된다.

flatMap 함수

public final Flux flatMap(Function<? superT,? extendsPublisher<? extends V>> mapper, int concurrency)

  • Transform the elements emitted by this Flux asynchronously into Publishers, then flatten these inner publishers into a single Flux through merging, which allow them to interleave.

flux.flatMap

flatMap 함수는 데이터 스트림의 각 요소별로 publisher를 생성하여 여러개의 하위 스트림이 생성되고 subscribe를 통해 단일 스트림으로 묶은 Publisher로 반환한다.

이러한 Publisher를 비동기적으로 처리하여 순서를 보장하지 않기 때문에 순서를 보장하고 싶으면 flatMapSequential 이나 concatMap 등을 사용할 수 있다.

샘플코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void sampleFlatMapTest() {
// Arrange
Function<String, Publisher<String>> mapper = (str) -> Flux.just(str.toUpperCase().split("")).subscribeOn(Schedulers.boundedElastic());
Flux<String> testFlux = Flux.just("apple", "banana", "carrot");

// Act
Flux<String> resultFlux = testFlux.flatMap(mapper);

List<String> output = new ArrayList<>();
resultFlux.subscribe(output::add);

// Assert
assertThat(output).containsExactlyInAnyOrder("A", "P", "P", "L", "E", "B", "A", "N", "A", "N", "A", "C", "A", "R", "R", "O", "T");
}

mapper에서 Publisher로 반환되는데 별도의 스레드를 할당하고 flatMap에서 비동기로 동작하게 되면서 순서를 보장하지 않는 flatMap의 응답값은 apple, banana, carrot의 글자가 섞이게 된다.

사용 시 고려사항

map은 각 요소가 1:1로 매핑되서 동기적으로 처리된다.

flatMap은 각 요소에 대해서 1:다 매핑이 되서 단일 스트림으로 병합되고 publisher의 동작에 따라서 동기적, 혹은 비동기적으로 동작할 수 있다.

단순히 DTO 객체변환이라든지, 단일요소에 대한 간단한 계산같은건 map을 사용하면 좋고 각 요소의 처리 시간이 다를 수 있는 작업이나 외부 서비스, DB 호출 등의 작업을 할때는 비동기 동작이 가능한 flatMap을 사용하는게 좋다.

map은 단일 데이터 스트림의 각 요소를 독립적으로 처리하기 때문에 요소의 크기에 따라 성능차이가 크진 않지만 flatMap은 데이터 스트림을 개별적으로 처리 후 병합하기 위해 데이터 스트림의 크기나 작업량에 따라서 성능적으로 영향이 map에 비해서 상대적으로 클 수 있다.

Hexo 버전 업그레이드, 경고 지우기

버전 업그레이드

오랜만에 블로그를 정비하다가 hexo 버전을 업그레이드 하고 싶었다.
node가 그렇게 익숙하진 않아서 구글링으로 찾아보니 그냥 원하는 버전을 npm으로 설치하면 된다고 한다.
How can I upgrade hexo? #4572

7.0.0버전이 최신이긴 한데 이슈 생기면 대응하기 귀찮으니까 그냥 바로 아래의 6.3.0 으로 업그레이드 후 실행했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
npm i hexo@6.3.0
hexo s

WARN YAMLException: please see https://github.com/hexojs/hexo/issues/4917
FATAL
TypeError: Cannot read property 'length' of undefined
at composeNode (D:\workspace\blog\node_modules\hexo\node_modules\js-yaml\lib\loader.js:1492:60)
at composeNode (D:\workspace\blog\node_modules\hexo\node_modules\js-yaml\lib\loader.js:1441:12)
at readDocument (D:\workspace\blog\node_modules\hexo\node_modules\js-yaml\lib\loader.js:1625:3)
at loadDocuments (D:\workspace\blog\node_modules\hexo\node_modules\js-yaml\lib\loader.js:1688:5)
at Object.load (D:\workspace\blog\node_modules\hexo\node_modules\js-yaml\lib\loader.js:1714:19)
at Hexo.yamlHelper (D:\workspace\blog\node_modules\hexo\lib\plugins\renderer\yaml.js:20:15)
at Hexo.tryCatcher (D:\workspace\blog\node_modules\bluebird\js\release\util.js:16:23)
at Hexo.<anonymous> (D:\workspace\blog\node_modules\bluebird\js\release\method.js:15:34)
at D:\workspace\blog\node_modules\hexo\lib\hexo\render.js:81:22
at tryCatcher (D:\workspace\blog\node_modules\bluebird\js\release\util.js:16:23)
at Promise._settlePromiseFromHandler (D:\workspace\blog\node_modules\bluebird\js\release\promise.js:547:31)
at Promise._settlePromise (D:\workspace\blog\node_modules\bluebird\js\release\promise.js:604:18)
at Promise._settlePromise0 (D:\workspace\blog\node_modules\bluebird\js\release\promise.js:649:10)
at Promise._settlePromises (D:\workspace\blog\node_modules\bluebird\js\release\promise.js:729:18)
at _drainQueueStep (D:\workspace\blog\node_modules\bluebird\js\release\async.js:93:12)
at _drainQueue (D:\workspace\blog\node_modules\bluebird\js\release\async.js:86:9)
at Async._drainQueues (D:\workspace\blog\node_modules\bluebird\js\release\async.js:102:5)
at Immediate.Async.drainQueues [as _onImmediate] (D:\workspace\blog\node_modules\bluebird\js\release\async.js:15:14)
at processImmediate (internal/timers.js:464:21)

??? 뭐야 갑자기 왜…

당황했지만 일단 에러 로그에서 들어가보라고 나온 github issue 를 들어가보니 비슷한 오류에 대해서 이미 많은 토론이 진행되어 있었다.

6.1.0 버전으로 올렸을때 이런 이슈가 발생했고(2,3 도 비슷한듯) 해결책은 크게 두가지 였는데

  1. 6.0.0 으로 다운그레이드
  2. js-yaml 을 4.1.0 으로 업그레이드

이와 별개로 현재 사용중인 icarus 테마도 지원하는 버전이 있을거라고 생각해서 들어가보니 5.0.1 버전에서 hexo 6에 대한 언급이 있어서 이참에 최신버전으로 올렸다.

hexo-icarus-theme 5.0.1 release note

일단 위 내용들을 고려해서 6.0.0 으로 다운그레이드를 시도 했고 다시 실행해보니 정상적으로 실행되었다.

1
2
npm i hexo@6.0.0
hexo s

아직 의문인 점은 6.3.0 버전에서 설치되는 js-yaml은 분명 4.1.0 인데 서버 실행 시 오류가 발생하고 6.0.0으로 한번 내렸다가 다시 6.3.0 으로 올리면 정상적으로 실행된다는 점이다 … ㅎㅎ;;

거슬리던 경고들

하는 김에 그동안 거슬렸던 경고들도 몇개 있었는데 대략 아래와 같은 내용이었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
INFO  Validating config
Inferno is in development mode.
INFO =======================================
██╗ ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗
██║██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝
██║██║ ███████║██████╔╝██║ ██║███████╗
██║██║ ██╔══██║██╔══██╗██║ ██║╚════██║
██║╚██████╗██║ ██║██║ ██║╚██████╔╝███████║
╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
=============================================
INFO === Checking package dependencies ===
INFO === Checking theme configurations ===
WARN Theme configurations failed one or more checks.
WARN Icarus may still run, but you will encounter unexcepted results.
WARN Here is some information for you to correct the configuration file.
WARN [
{
keyword: 'const',
dataPath: '.widgets[7].type',
schemaPath: '#/properties/type/const',
params: { allowedValue: 'profile' },
message: 'should be equal to constant'
},
...
...
]

로그로 봤을땐 테마의 위젯 설정값이 뭔가 잘못된거 같길래 한참 이것저것 검색하다가 위젯 설정값을 찬찬히 보니까 type의 값을 주석처리 해놔서 발생한 오류였다.
진짜 이거 찾느라 시간 보낸거 아까워서 ㅠㅠ 나처럼 바보짓 하는 사람 없길 바라면 적는다.

1
2
3
4
5
6
7
- # Where should the widget be placed, left sidebar or right sidebar
position: left
type: #adsense
# AdSense client ID
client_id: ""
# AdSense AD unit ID
slot_id: ""

작업들을 하면서 느끼는건데 Jekyll을 사용해서 샘플로 만들어봤을때 너무 느린 속도가 싫어서 Hexo를 선택했는데 포스팅한 글을 git으로 동시관리가 안되고 별도 repo 설정해서 이중관리 해야하는 부분이나 영어보다 중국어 레퍼런스가 더 많아서 관련 자료 찾는데 쉽지 않은 부분 때문에 요즘엔 다른 엔진으로 갈아타는건 어떨까 하는 생각도 든다.

일단 블로그 글도 별로 없는데 이런거 고민할 필요는 없을거 같기도 하고 ㅋㅋㅋ 나중에 다시 생각해보자

log4j2 설정

사내 프로젝트의 여러가지 설정, 프로퍼티들은 dev, test, master로 분리되어있는데 살펴보다보니 log4j2-test.xml이 없고 test 프로파일로 빌드 시 log4j2-dev.xml을 바라보고 있었다.

시간도 좀 여유가 있던 시점이라 그냥 내가 만들어서 분리시켰는데 하는김에 SQL 관련 로그 설정을 변경했고 공부도 할겸 내용을 정리했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- SQL Logger -->
<logger name="jdbc.connection" additivity="false">
<level value="OFF" />
<appender-ref ref="console"/>
</logger>

<logger name="jdbc.audit" additivity="false">
<level value="OFF"/>
<appender-ref ref="console"/>
</logger>

<logger name="jdbc.resultset" additivity="false">
<level value="OFF" />
<appender-ref ref="console"/>
</logger>

<logger name="jdbc.sqlonly" additivity="false">
<level value="OFF"/>
<appender-ref ref="console"/>
</logger>

<logger name="jdbc.resultsettable" additivity="false">
<level value="info"/>
<appender-ref ref="console"/>
</logger>

<logger name="jdbc.sqltiming" additivity="false">
<level value="info" />
<appender-ref ref="console"/>
</logger>
내용
jdbc.connection 열려있는 모든 번호와 연결 수립 및 해제 이벤트를 기록. 이는 연결 문제를 찾아내는데 매우 유용 (Connection Pool 설정)
jdbc.audit ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남긴다. 많은 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않는다.
jdbc.resultset ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 매우 방대한 양의 로그가 생성된다.
jdbc.sqlonly SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여진다. (원래는 ? 로 표시되는데, log4j 덕분에 완전한 쿼리로 보여지는 것임)
jdbc.resultsettable SQL 결과 조회된 데이터의 table을 로그로 남긴다.
jdbc.sqltiming SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함한다.

SQL관련

sqlonly와 sqltiming은 sql로그를 찍는 공통적인 기능을 하며 sqltiming이 sqlonly를 포함하고 있습니다.

#{id} 등 파라미터로 넣어준 값도 모두 치환하여 출력해줍니다.

둘다 찍도록 하면 sql문이 두번 찍히니 하나만 찍어줍니다.

  • jdbc.sqlonly : SQL문을 로그로 남깁니다.
  • jdbc.sqltiming : SQL문과 수행된 시간을 로그로 남깁니다.

TABLE관련

결과를 table형식으로 만들어 출력해줍니다.

  • jdbc.resultsettable : SQL 결과 조회된 데이터를 table 형식으로 만들어줍니다.

JDBC호출 관련

아래 두개의 설정은 일반적으로는 킬 필요가 없는 설정입니다.

두개가 묶음으로 JDBC 호출정보를 출력해주는 기능을 하며 ResultSet을 포함하느냐 포함하지 않느냐의 차이입니다.

  • jdbc.audit : ResultSet을 제외한 JDBC 호출 정보를 로그로 남긴다.
  • jdbc.resultset : ResultSet을 포함한JDBC 호출 정보를 로그로 남깁니다.

additivity : 중복된 로그를 남길 지 여부를 설정한다.

기본적으로 logger는 정의한 패키지의 조상으로부터 모든 appender를 상속받아서 만약 어떤 Logger가 같은 appender를 사용한다면 메시지가 2번 찍히게 된다.

additivity=”false” 설정하면 이를 방지할 수 있다.

Log Level

로그 레벨은 다음과 같다. (OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL)

레벨 범위
OFF 이벤트를 기록하지 않음
FATAL 아주 심각한 에러가 발생한 상태를 나타냄. 시스템적으로 심각한 문제가 발생해서 어플리케이션 작동이 불가능할 경우가 해당하는데, 일반적으로는 어플리케이션에서는 사용할 일이 없음.
ERROR 요청을 처리하는중 문제가 발생한 상태를 나타냄.
WARN 처리 가능한 문제이지만, 향후 시스템 에러의 원인이 될 수 있는 경고성 메시지를 나타냄.
INFO 로그인, 상태변경과 같은 정보성 메시지를 나타냄.
DEBUG 개발시 디버그 용도로 사용한 메시지를 나타냄.
TRACE 디버그 레벨이 너무 광범위한 것을 해결하기 위해서 좀더 상세한 상태를 나타냄.
ALL 모든 이벤트를 기록함

[WebFlux] Mono.defer(), Mono.fromCallable()

Mono.defer()Mono.fromCallable() 모두 지연 평가(lazy evaluation)를 제공하는데 사용된다. 지연 평가란 호출 시점을 지연시켜서 필요한 시점에서 코드를 실행하는 방식을 의미한다.

Mono.defer() 메서드는 Mono의 factory 메서드 중 하나로, 인자로 전달받은 Supplier 함수를 사용하여 Mono 객체를 생성한다. 이때 Supplier 함수가 호출되는 시점은 Mono 객체를 subscribe 할 때이다.

Mono.fromCallable() 메서드는 Mono의 factory 메서드 중 하나로, 인자로 전달받은 Callable 함수를 사용하여 Mono 객체를 생성한다. 이때 Callable함수가 호출되는 시점은 Mono객체 생성 시점이다.

따라서 Mono.defer()는 Mono 객체를 생성할 때마다 Supplier함수를 실행하여 객체를 생성하는 방식을 사용하며, Mono.fromCallable()은 Mono객체를 생성하는 시점에서 Callable함수를 실행하여 객체를 생성하는 방식을 사용한다. Mono.fromCallable()은 즉시 실행되므로, Mono.defer()보다 더 많은 자원을 소비할 수 있다.

Mono.defer()는 Mono의 객체 생성 시점을 늦추고, 객체 생성과 동시에 상태를 초기화할 수 있는 장점이 있고 반면에 Mono.fromCallable()은 Callable함수의 실행 결과를 즉시 Mono객체에 포함시키므로, 결과를 즉시 필요로 하는 경우에 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args)
{
callExternalService()
.repeat(3)
.doOnNext(System.out::println)
.subscribe();
}

private static Mono<?> callExternalService()
{
System.out.println("External service is called.");
return Mono.just("result");
}

/*
External service is called.
result
result
result
result
*/

Mono.just(“result”)의 결과를 repeat(3)으로 세 번 반복하게 되면, 해당 Mono를 세 번 실행하게 된다.

하지만, Mono.just(“result”)는 호출될 때마다 값을 생성하는 게 아니라, 최초 1회 호출시 생성한 값을 계속해서 방출하게 된다. 그래서 "External service is called."는 처음 1회만 출력되고, 세 번 반복되는 Mono.just(“result”)는 이미 생성된 값을 방출하게 되므로, 추가로 호출할 필요가 없어지게 된다.

따라서 "External service is called."는 최초 1회만 출력되고, 세 번 반복되는 Mono.just(“result”)는 이미 생성된 값을 방출하게 된다.