728x90
반응형
728x90
반응형
반응형
Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.

위와 같은 에러가 날 경우 프로퍼티로 설정한 driverClassName을 살펴보자. com.mysql.jdbc.driver는 deprecated 된 이름이다.

"driverClassName": "com.mysql.cj.jdbc.Driver"

위와 같이 driverClassName을 수정해야한다.

728x90
반응형
반응형

조건: 두 list object가 같은 결과로 리턴, 후에 합쳐서 데이터 추가 세팅 후, 날짜를 기준으로 정렬한 값을 반환해야 한다.

환경: springboot2.6.2

 

1. nativeQuery=true, projection interface 사용

@Query(value = "select hur.gid, hur.ci, hur.nick, hur.last_logout as lastLogout, " +
        "huld.begin_chip as beginChip, huld.last_chip as lastChip, huld.last_chip - huld.begin_chip as changeChip, huld.win_chip as winChip, timestamp(huld.base_date) as baseDateTime, hll.expired_Date as expiredDate, " +
        "case when hll.expired_date > now() and huld.base_date = DATE_FORMAT(now(), '%Y-%m-%d') then 'true' else 'false' end as canRelease " +
        "from hd_user_loss_daily huld " +
        "join hd_user_rat hur on hur.gid = huld.gid " +
        "join hd_loss_limit hll on hll.ci = hur.ci " +
        "where hur.ci = :#{#req.ci} " +
        "and huld.base_date between :#{#req.startDate} and :#{#req.endDate} ; "
        , nativeQuery = true)
<S extends LossChipDailyProjection> List<S> getListUserLossDailyWithUserRat(@Param("req") LossChipDailyReq req);



@Query(value = "select log.ci, " +
        "case when log.gid = 'ADMIN' then log.sval1 else log.gid end as releaser, " +
        "case when log.gid = 'ADMIN' then log.log_date else null end as adminReleaseDate, " +
        "case when log.gid <> 'ADMIN' then log.log_date else null end as elseReleaseDate, " +
//            "log.ival2 as lastChip, " +
        "log.log_date as baseDateTime " +
        "from hd_loss_limit_log log " +
        "where log.log_type = 'RELEASED' " +
        "and log.ci = :#{#req.ci} " +
        "and log.log_date between :#{#req.startDateTime} and :#{#req.endDateTime} ; "
        , nativeQuery = true)
<S extends LossChipDailyProjection> List<S> getListReleaseInfoByCi(@Param("req") LossChipDailyReq req);

request param을 object로 넘기고 싶으면, SpEL을 사용하면 된다.

public interface LossChipDailyProjection {
    String getCi();

    String getGid();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getAdminReleaseDate();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getElseReleaseDate();

    String getReleaser();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getExpiredDate();

    Boolean getCanRelease();

    String getNick();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getLastLogout();

    Long getBeginChip();

    Long getLastChip();

    Long getChangeChip();

    Long getWinChip();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getBaseDateTime();
}

서비스 로직은 아래와 같다.

//public class LossChipDailyRes implements LossChipDailyProjection
//LossChipDailyRes -> LossChipDailyProjection로 바꿔도 동일

public List<LossChipDailyRes> getList(LossChipDailyReq req){
	//같은 response list로 가져와서
    List<LossChipDailyRes> listUser = lossDailyRepository.getListUserLossDailyWithUserRat(req);
    List<LossChipDailyRes> listRelease = logRepository.getListReleaseInfoByCi(req);

	//검사 후 추가 값 세팅
    for(LossChipDailyRes eachR : listRelease){
        LocalDateTime dailyBaseDateTime = eachR.getBaseDateTime().withHour(0).withMinute(0).withSecond(0);
        for(LossChipDailyRes eachD : listUser){
            if(eachD.getBaseDateTime().equals(dailyBaseDateTime)){
                eachR.setBeginChip(eachD.getBeginChip());  //에러발생
            }
        }
    }
	
    //두 리스트 합치고 정렬해서 반환
    List<LossChipDailyRes> unionContent = Stream.concat(listUser.stream(), listRelease.stream())
                                        .sorted(Comparator.comparing(LossChipDailyProjection::getBaseDateTime).reversed())
                                        .collect(Collectors.toList());

    return unionContent;
}

repository에서 반환되는 값을 class로 받아도 실제로 그리 안 들어가고 interface에 머무는 듯하다. 

왜냐면

  1. @JsonFormat부분을 class에 넣으면 날짜 변환이 안된다.
  2. setter 쪽 로직에서 아래와 같은 에러가 난다.
java.lang.UnsupportedOperationException: A TupleBackedMap cannot be modified.

인터페이스 -> 클래스로 매핑하는 로직 따위를 넣으면서까지 이 방식을 고수하고 싶지 않아서 다른 방법을 찾아본다.

(조금만 더 찾아보면 될 것 같기도 한데.... 잘 모르겠다) 


2.  @NamedNativeQuery를 사용

위와 같이 native query를 사용하되 return 값을 class로 받을 수 있는 방법을 고민하다가 @NatedNativeQuery를 사용하면 result mapping class와 매핑 룰을 지정하여 class로 리턴할 수 있을 것 같아서 시도해보았다. entity클래스 상단에 아래 부분을 추가해보았다.

@NamedNativeQuery(name = "UserLossDaily.getListUserLossDailyWithUserRat2",
query = "select hur.gid, hur.ci, hur.nick, hur.last_logout as lastLogout, " +
        "huld.begin_chip as beginChip, huld.last_chip as lastChip, huld.last_chip - huld.begin_chip as changeChip, huld.win_chip as winChip, timestamp(huld.base_date) as baseDateTime, hll.expired_Date as expiredDate, " +
        "case when hll.expired_date > now() and huld.base_date = DATE_FORMAT(now(), '%Y-%m-%d') then 'true' else 'false' end as canRelease " +
        "from hd_user_loss_daily huld " +
        "join hd_user_rat hur on hur.gid = huld.gid " +
        "join hd_loss_limit hll on hll.ci = hur.ci " +
        "where hur.ci = :#{#req.ci} " +  //에러발생
        "and huld.base_date between :#{#req.startDate} and :#{#req.endDate} ;"
        , resultSetMapping = "Mapping.LossChipDailyRes")
@SqlResultSetMapping(name =  "Mapping.LossChipDailyRes",
classes = @ConstructorResult(targetClass = LossChipDailyRes.class, columns = {
        @ColumnResult(name = "gid"),
        @ColumnResult(name = "ci"),
        @ColumnResult(name = "nick"),
        @ColumnResult(name = "lastLogout"),
        @ColumnResult(name = "beginChip"),
        @ColumnResult(name = "lastChip"),
        @ColumnResult(name = "changeChip"),
        @ColumnResult(name = "winChip"),
        @ColumnResult(name = "baseDateTime"),
        @ColumnResult(name = "expiredDate"),
        @ColumnResult(name = "canRelease"),
}))
@Entity
...

그런데 신박한 에러가 난다. 바로 SpEl을 사용할 수 없는 것이었다.

Space is not allowed after parameter prefix ':'

구글링 해보니 : 앞에 \\를 줘서 escape 하라는 글이 있었는데, 그건 select절 내에서 named parameter를 가져올 때나 가능하고 저렇게 SpEl문법을 escape 할 때 사용할 수 있는 것 같아 보이지는 않았다.

개인적으로

  1. request parameter를 method req변수로 쫙 늘어뜨리고 싶지 않아서(object로 전달하고 그 안에서 꺼내서 매핑하고 싶어서)
  2. entitiy 위에 매핑 룰/쿼리를 쫙 쓰면서 관리하고 싶지 않아서(쿼리는 repository에서 관리하는 게 보기에도 찾기에도 좋다고 생각) 빠르게 포기했다.

3. 결국 돌고 돌아 JQPL로..

사실 jpql로 쓰면 바로 될 것이라는 것을 알고 있었지만

  1. entity에 (단순히 select를 위한) relation표기를 별로 안 좋아해서
  2. response dto에 변수가 많은, 순서에 예민한 constructor생성을 피해보려고

최대한 다른 방식으로 해보고 싶었는데.. 짧은 시간 안에 해답을 찾기가 어려웠다.

@Query(value = "select new com.model.restriction.LossChipDailyRes( " +
        "hur.gid, hur.ci, hur.nick, hur.lastLogout, " +
        "huld.beginChip, huld.lastChip, huld.winChip, huld.pk.baseDate, hll.expiredDate, " +
        "case when hll.expiredDate > current_timestamp and huld.pk.baseDate = function('date_format', current_date, '%Y-%m-%d') then true else false end ) " +
        "from UserLossDaily huld " +
        "join huld.userRat hur " +
        "join hur.restrictionLawCi hll " +
        "where hur.ci = :#{#req.ci} " +
        "and huld.pk.baseDate between :#{#req.startDate} and :#{#req.endDate} "
        )
List<LossChipDailyRes> getListUserLossDailyWithUserRat3(@Param("req") LossChipDailyReq req);


 @Query(value = "select new com.model.restriction.LossChipDailyRes(" +
        "log.ci, " +
        "case when log.gid = 'ADMIN' then log.sval1 else log.gid end, " +
        "case when log.gid = 'ADMIN' then log.pk.logDate else null end, " +
        "case when log.gid <> 'ADMIN' then log.pk.logDate else null end, " +
//            "log.ival2, " +
        "log.pk.logDate) " +
        "from UserRestrictionLawCiLog log " +
        "where log.logType = 'RELEASED' " +
        "and log.ci = :#{#req.ci} " +
        "and log.pk.logDate between :#{#req.startDateTime} and :#{#req.endDateTime} "
        )
List<LossChipDailyRes> getListReleaseInfoByCi3(@Param("req") LossChipDailyReq req);
//조인이 필요한 엔티티마다 릴레이션 추가
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "gid", updatable = false, insertable = false)
UserRat userRat;
//변수 순서가 개복치..
public LossChipDailyRes(String gid, String ci, String nick, LocalDateTime lastLogout, Long beginChip, Long lastChip, Long winChip, LocalDate baseDate, LocalDateTime expiredDate, Boolean canRelease) {
    this.ci = ci;
    this.gid = gid;
    this.expiredDate = expiredDate;
    this.canRelease = canRelease;
    this.nick = nick;
    this.lastLogout = lastLogout;
    this.beginChip = beginChip;
    this.lastChip = lastChip;
    this.changeChip = lastChip - beginChip;
    this.winChip = winChip;
    this.baseDate = baseDate;
    this.baseDateTime = baseDate.atTime(0, 0, 0);
}

native query -> JPQL로 변환 시 주의사항

  • native query에서는 dto이름 그대로 매핑하기 때문에 as로 이름을 다시 지정해줬어야 핬지만, jpql query로 바꿀 때는 as를 쓰면 안된다(쿼리 파싱이 안되서 서버가 안 뜸). 순서만 dto constructor랑 맞추줘야 함.
  • sql function을 사용할 때 function으로 다 바꿔야하고 now()(mysql전용)도 current_date(jpa용) 등으로 바꿔야한다.
  • true/false도 quotation 처리 확인해야한다. boolean으로 못 가져오고 string으로 인식할 수 있음..
//native query
case when hll.expired_date > now() and huld.base_date = DATE_FORMAT(now(), '%Y-%m-%d') then 'true' else 'false' end as canRelease 

//jpql
case when hll.expiredDate > current_timestamp and huld.pk.baseDate = function('date_format', current_date, '%Y-%m-%d') then true else false end

 

위와 같이 jpql로 바꾸니.. 다행히 서비스 로직을 그대로 사용할 수 있었다.

native query를 자주 사용하지 않아 + 복합적인 조건으로 인해 시간이 걸린 삽질이었지만, native query의 특징을 배울 수 있었던 시간이었다..

 


SpEl

https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions

 

SpEL support in Spring Data JPA @Query definitions

<p>Spring Data JPA allows manually defining the query to be executed by a repository method using the <code>@Query</code> annotation. Unfortunately parameter binding in JPQL is quite limited only allowing you to set a value and providing some type conversi

spring.io

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.named-parameters

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

@NamedNativeQuery

https://thorben-janssen.com/spring-data-jpa-dto-native-queries/

 

Spring Data JPA - How to Return DTOs from Native Queries

Returning DTOs from a native query with Spring Data JPA is a little harder than you might expect. But there are 2 good solutions for it.

thorben-janssen.com

 

jpa projection

https://www.baeldung.com/spring-data-jpa-projections

 

Spring Data JPA Projections | Baeldung

A quick and practical overview of Spring Data JPA Projections.

www.baeldung.com

https://moonsiri.tistory.com/95

 

[SpringBoot] JPA Projection

Projection은 Entity의 속성이 많을 때 일부 데이터만 조회하는 방법입니다. 아래 UserEntity를 참고하여 설명하겠습니다. @Entity @Table(name = "user") @NoArgsConstructor(access = AccessLevel.PROTECTED) pu..

moonsiri.tistory.com

 

728x90
반응형
반응형

CASE, IF Function(IF 함수), IF Statement(IF 문)의 차이

  • case는 스위치문 같고
  • if는 조건이 두개인 case와 같다.
  • if statement는 프로시져에나 사용되는 완전 다른 이야기.

https://stackoverflow.com/questions/30047983/mysql-case-vs-if-statement-vs-if-function

 

MySQL - CASE vs IF Statement vs IF function

Who can please explain the difference between CASE-statement, IF-statement and IF-function? What is the difference in terms of usage and "how it works"?

stackoverflow.com

 

728x90
반응형
반응형

aggregate function 이라고 불리는 것들이 있다. 

흔히 group by와 함께 쓰이는 것들로 min/max/avg/sum 등 과 같은 것이다.

특징은 다음과 같다

  • null 값은 무시
  • 주로 여러줄 -> 한줄로 만듦
  • 숫자(sum/avg 등 함수의 결과)는 decimal(정수)/double(소수)로 리턴함
  • 자동 캐스팅 함수가 있음(sum/avg는 number로)

기타 함수 별 특징은 아래 사이트를 확인하자.

https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html

 

MySQL :: MySQL 8.0 Reference Manual :: 12.20.1 Aggregate Function Descriptions

MySQL 8.0 Reference Manual  /  ...  /  Functions and Operators  /  Aggregate Functions  /  Aggregate Function Descriptions 12.20.1 Aggregate Function Descriptions This section describes aggregate functions that operate on sets of values. They are

dev.mysql.com


그에 비해 window function은 

  • 각 줄별로 계산 결과를 포함한다.(축약된 데이터가 아니라 원래 row에 추가해서 보여줄 수 있음)
  • 대부분의 aggregate function을 그대로 사용 가능.
  • 그 외 순위, 순서, 비율 등과 관련된 함수도 있음.
  • group by 사용 불가

역시나 자세한 설명은 공식 문서에서.

https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html

 

MySQL :: MySQL 8.0 Reference Manual :: 12.21.1 Window Function Descriptions

12.21.1 Window Function Descriptions This section describes nonaggregate window functions that, for each row from a query, perform a calculation using rows related to that row. Most aggregate functions also can be used as window functions; see Section 12

dev.mysql.com

기본 모양새

함수(컬럼) over (partition by 컬럼 order by 컬럼)

 

데이터 위치 바꾸기

  • lag 데이터를 밀고
  • lead는 데이터를 당김

참고

좋은 설명.. https://moonpiechoi.tistory.com/128

 

[SQL] 윈도우 함수 (WINDOW FUNCTION)

WINDOW FUNCTION 개요 행과 행 간의 관계를 쉽게 정의하기 위해 만든 함수가 윈도우 함수다. 윈도우 함수는 분석 함수나 순위 함수로도 알려져 있다. 윈도우 함수는 기존에 사용하던 집계 함수도 있

moonpiechoi.tistory.com

 

728x90
반응형

'개발 > sql' 카테고리의 다른 글

[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
[DB] 분산환경에서 데이터 저장소 선택과 활용  (0) 2023.07.24
[형상관리] flyway vs liquibase  (0) 2022.07.08
[mysql] jsonpath  (0) 2022.05.27
[sql] case vs if  (0) 2022.05.02
반응형

이벤트 종료 처리를 위해 날짜 비교 로직을 스크립트에 넣었는데, IE에서만 제대로 실행되지 않는 문제가 있었다.

콘솔에 딱히 에러가 없었는데, 찾아보니 IE에서는 안된다고 잘 알려진 이슈였다.

var nowTime = new Date();
var endTime = new Date("2022-04-26 10:00:00"); //invalid date in IE
if (nowTime > endTime){
    //alert("이벤트가 종료되었습니다. 감사합니다.");
    location.href = 'naver.com'
}

 

그러면 어떻게 해야 IE가 알아볼까 싶어서 찾아봤는데, 제일 쉽게 수정할 수 있는 방법은 Date constructor를 사용하는 방법이었다.

var nowTime = new Date();
//var endTime = new Date("2022-04-26 10:00:00"); //invalid date in IE
//var endTime = new Date(2022, 4, 26, 10, 00, 00); //May로 표시됨 0이 1월...
var endTime = new Date(2022, 3, 26, 10, 00, 00); 
if (nowTime > endTime){
    //alert("이벤트가 종료되었습니다. 감사합니다.");
    location.href = 'naver.com'
}

다만 조심해야하는 것은 monthIndex가 0부터 시작하기 때문에 4월은 3으로, 12월은 11로 표시해야 한다. 그리고 시간은 24시간 체계이다.

new Date(year, monthIndex, day, hours, minutes, seconds)

 

자세한 설명은 아래 링크를 참조하자!

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date

 

Date() constructor - JavaScript | MDN

Creates a JavaScript Date instance that represents a single moment in time in a platform-independent format.Date objects contain a Number that represents milliseconds since 1 January 1970 UTC.

developer.mozilla.org

 

728x90
반응형

'개발 > javascript' 카테고리의 다른 글

[js][IE] inline script [object]  (0) 2023.01.11
[js] 0.07 * 100?  (0) 2023.01.03
[ts] typescript utility type  (0) 2022.07.05
[js] 자바스크립트 기초  (0) 2022.01.25
반응형
  • retrieve vs exchange(exchangeToMono)

retrieve: http status code가 200일 경우 response body처리
http status code가 다른 경우(400, 500, etc.) WebClientResponseException 떨어짐
에러 처리 커스텀하게 하고 싶으면 onStatus 사용

exchange: any response에서도 사용 가능하나 꼭 response 내용을 사용해야 함(성공이건 실패건 상관없이) 아니면 memory leak이 있을 수 있다고..

응답이 200이고 응답 body에 대한 처리만 하고 싶은 경우 retrieve.
이 외에 응답 코드가 4xx, 5xx 등 특정 코드에 따라 다른 결과를 주고 싶거나 응답 헤더를 사용하고 싶은 경우는 exchange를 사용


  • 에러 처리할 때 doOnNext vs flatMap?

둘 다 작동하긴 하지만 함수의 사상 상 doOnNext가 더 적합한 것 같다. 둘 다 두면 위에서 걸려서 아래로 안 흐른다.

public Mono<BaseResponse<String>> getPopo(){
    System.out.println(">> popo " + Thread.currentThread().getName());
    return webClient.get()
            .uri(uriBuilder -> uriBuilder.path("/api/hello/popo").build())
            .retrieve()
            .onStatus(HttpStatus::isError, resp -> Mono.error(new Exception()))
            .subscribeOn(Schedulers.boundedElastic())
           .bodyToMono(new ParameterizedTypeReference<BaseResponse<String>>(){})
//                .doOnNext(res -> {
//                    if(res.getHeader().getStatus() == 200){
//                        throw new Exception("error");
//                    }
//                })
         
            .flatMap(res -> {
                if(res.getHeader().getStatus() == 300){
                    return Mono.error(new Exception("검증실패"));
                    //return Mono.empty();
                }else{
                    System.out.println(">> popo2 " + Thread.currentThread().getName());
                    return Mono.just(res);
                }
            })
            .delayElement(Duration.ofSeconds(10))
            ;

}

 


참고: https://binux.tistory.com/56

 

[Spring Reactive] WebClient

Web Reactive Stack 공식문서의 WebClient 부분을 읽고 해석하며 작성했습니다. Web on Reactive Stack The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for th..

binux.tistory.com

 

728x90
반응형

'개발 > reactive' 카테고리의 다른 글

[reactor] Sinks  (0) 2022.06.13
[webflux] block vs toFuture  (0) 2022.03.31
[webflux] 실무투입고민  (0) 2022.03.30
[spring] spring-web and spring-webflux  (0) 2022.03.25
[reactive] 10. Flux  (0) 2022.03.25
반응형

이전 글: 2022.03.30 - [개발/reactive] - [webflux] 실무투입고민

 

[webflux] 실무투입고민

이전 글: 2022.03.25 - [개발/reactive] - [spring] spring-web and spring-webflux [spring] spring-web and spring-webflux spring mvc 기반 spring-web의 restTemplate으로 api를 요청하고 있던 와중, restTemp..

bangpurin.tistory.com

 

현재 실무에서 spring-web + spring-webflux(webClient)인 1번 프로젝트spring-webflux only인 2번 프로젝트를 동시에 작업하고 있다.

비동기를 사용하는 부분은 주로 외부 api call 인 부분이며, 최종 res는 동기로 내려주게 사용 중이다. 

비슷한 로직이어도 두 프로젝트에서 다르게 작동하는 부분이 있어 정리해본다.


1. 비동기로 함수를 실행시키기 위해 block()을 사용

block()/blockFirst()/blockLast() are blocking, which is not supported in thread

1번 프로젝트에서는 spring-mvc 기반이기 때문에 Mono.zip.block()이 가능했지만, 2번 프로젝트에서는 위와 같은 에러가 뜨면서 작동하지 않는다.

2번 프로젝트는 thread blocking이 지원되지 않기 때문에 에러가 난다. 해결방안을 찾아봐도 spring-web을 추가하거나 다른 걸 쓰라고 한다.

대안으로는 Mono.zip.toFuture.get()으로 미래에 완료가 되면 꺼내오는 방식을 사용하면 된다.

 

반대로 1번 프로젝트의 block()을. toFuture(). get()으로 수정한다면?

-> 정상 작동된다.

 

그렇다면 toFuture.get 은 blocking 일까? 왜냐면 두 함수의 동작이 똑같아 보이기 때문이다.

둘 다 subscribes immediately 하기 때문에 그래 보인다!

block()은 스레드를 진짜 블로킹하지만 toFuture은 completableFuture을 리턴하여 작업이 끝날 때까지 기다렸다가 get으로 바디를 꺼내 주는데 즉시 실행하기에 블로킹으로 보인다는.. 것..이라는 것

즉 toFuture가 스레드 관리에 더 flexible 하다.

https://stackoverflow.com/questions/58504527/is-mono-tofuture-blocking

 

Is Mono.toFuture() blocking?

From the Official Documentation of Mono#block() it is said that: Subscribe to this Mono and block indefinitely until a next signal is received. Returns that value, or null if the Mono completes ...

stackoverflow.com


Nothing happens until you subscribe… until something does

2. webflux기반인 2번 프로젝트에서는 controller단의 Mono return만으로도 subscribe가 작동한다.

spring-web기반, 혹은 webflux의 서비스나 내부의 함수에서 작동하는 게 아니었다

https://stackoverflow.com/questions/56487429/who-calls-subscribe-on-flux-or-mono-in-reactive-webapplication

 

who calls subscribe on Flux or Mono in reactive webapplication

I am looking at some examples of reactive web applications and i am seeing them like this @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody public Mono<Person>

stackoverflow.com

https://github.com/reactor/reactor-netty/blob/db27625064fc78f8374c1ef0af3160ec3ae979f4/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java#L962

 

GitHub - reactor/reactor-netty: TCP/HTTP/UDP/QUIC client/server with Reactor over Netty

TCP/HTTP/UDP/QUIC client/server with Reactor over Netty - GitHub - reactor/reactor-netty: TCP/HTTP/UDP/QUIC client/server with Reactor over Netty

github.com


Nothing happens until you subscribe… until something does

 

3. webflux 기반인 프로젝트에서 애매하게. toFuture(). get()을 결과에 써놓고 아무 데서도 subscribe를 하지 않는다면.. (메인 스레드는 subscribe가 올 때까지 무한) pending이 걸린다..

//컨트롤러
public BaseResponse<UserDetailRes> getUserDetailInfo(@RequestParam String memberValue) {
    return getUserDetailInfoSS(memberValue).toFuture().get();
}

//서비스
public Mono<BaseResponse<UserDetailRes>> getUserDetailInfoSS(String memberValue){
	Mono<String> a = callA(); //webclient 호출
	Mono<Object> b = ...; //webclient 호출
	Mono<Object> c = ...; //webclient 호출

	return Mono.zip(a, b, c).flatMap();
}

여기서 엄청난 고민을 했었는데 두 가지 방법으로 해결할 수 있다.

첫 번째. Mono 함수에서 subscribeOn으로 다른 스레드에게 할당을 해서 해소한다. 근데 이게 온전한 해결방법인지는 모르겠다. 뭔가 얻어걸린듯한..

처음에는 subscribeOn의 추가만으로 정상 작동한다는 게 이해가 안 갔는데,, 다른 스레드가 구독을 한다는 의미로 받아들여지는 것 같다.

여기서 또 특이한 점은 모든 Mono 함수에 subscribeOn을 넣을 필요도 없고 맨 처음 함수에만 넣어도 된다. 심지어 zip에 넣어도 작동한다.

즉 위의 예시에서는 callA()의 함수만 넣어도 b, c도 동일한 스레드로 작동하기 때문에 두 번 넣을 필요가 없다.

단 a는 넣지 않고 b나 c에 넣으면 다시 작동이 안 된다..

형태가 맘에 안 든다면 zip에 넣어도 된다. 이는 해당 job 전체를 별도 스레드 할당하는 것이고 위는 그 api를 별도 스레드로 할당한다는 차이가 있다.

Schedulers.boundedElastic()옵션은 다른 논블로킹 작업들에게 영향 없이 하나의 싱글 스레드에서 블로킹 작업한다는 의미이다. 다른 옵션들도 많이 있다.

//다른 서비스
public Mono<String> callA() {
	return webclient.get()
            .uri()
            .retrieve()
            .bodyToMono(생략)
            .subscribeOn(Schedulers.boundedElastic()) //스레드 할당
	;
}

//서비스
//subscribeOn을 어디서 호출하는게 좋을지 고민이 필요하다
public Mono<BaseResponse<UserDetailRes>> getUserDetailInfoSS(String memberValue){
	Mono<String> a = callA(); //webclient 호출
	Mono<Object> b = callB().subscribeOn(Schedulers.boundedElastic()) //webclient 호출
	Mono<Object> c = ...; //webclient 호출

	return Mono.zip(a, b, c)
        .flatMap()
        .subscribeOn(Schedulers.boundedElastic())
    ;
}

 

두 번째. 아까 위에서 언급한, 'webflux의 경우 controller return에서 subscribe를 해준다'를 응용한다!

subscribeOn은 지우고(물론 별도 스레드를 할당해서 할 거면 둬야지) mono로 리턴하게 바꾼다.

//컨트롤러
public Mono<BaseResponse<UserDetailRes>> getUserDetailInfo(@RequestParam String memberValue) {
    return getUserDetailInfoSS(memberValue);
}

//서비스
public Mono<BaseResponse<UserDetailRes>> getUserDetailInfoSS(String memberValue){
	Mono<String> a = callA(); //webclient 호출
	Mono<Object> b = ...; //webclient 호출
	Mono<Object> c = ...; //webclient 호출

	return Mono.zip(a, b, c).flatMap();
}

이렇게 명료한 것을.. spring-web의 block의 늪에 빠져 큰 그림을 못 본 점 + 서비스에서 모노 리턴도 subscribe가 되는 거 아닌가 생각했던 안일한 생각으로 인해 돌고 돌아서 도착한 것 같다.

 


참고

https://projectreactor.io/docs/core/release/reference/#getting-started

 

Reactor 3 Reference Guide

10:45:20.200 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) (1) 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | request(unbounded) (2) 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | onNext(1) (3) 1

projectreactor.io

 

728x90
반응형

'개발 > reactive' 카테고리의 다른 글

[reactor] Sinks  (0) 2022.06.13
[webclient] 비슷한데 뭘 써야할지 모르겠는 것들  (0) 2022.04.01
[webflux] 실무투입고민  (0) 2022.03.30
[spring] spring-web and spring-webflux  (0) 2022.03.25
[reactive] 10. Flux  (0) 2022.03.25
반응형

이전 글: 2022.03.25 - [개발/reactive] - [spring] spring-web and spring-webflux

 

[spring] spring-web and spring-webflux

spring mvc 기반 spring-web의 restTemplate으로 api를 요청하고 있던 와중, restTemplate가 deprecated 된다는 말을 듣고, 대체제인 webClient를 사용해보려고 공부를 하다 보니 webflux의 영역에 들어와 버렸다...

bangpurin.tistory.com

springboot2

springboot2를 사용하게 되면 서블렛 기반으로 갈지 리엑티브 기반으로 갈지 고민하게 된다. 예전 강의나 자료에서는 둘 중 하나만 골라서 얹어야 한다고 그러는데, 요즘에는 둘 다 얹어서 사용 가능하다(정확히 말하자면 둘 다 있으면 서블랫 기반으로 돌고 webflux의 몇몇 라이브러리를 사용할 수 있다)

새로운 프로젝트를 하기로 했고, 역시나 새로운 기술의 도입의 유혹에서 벗어나지 못하고 있다. spring-web기반으로만 작업해보아서 webflux를 사용해보려고 하는데,, 하면서 이게 맞는지 확신이 안서는데, 빠르게 결정을 해야 하는 상황을 맞닥뜨렸다. 다음은 고민의 일지이다.

 

1. web/webflux를 동시 사용해보자. 딱히 소스가 달라지는건 없지만 restTemplate가 없어진다는데 그거라도 보완하는 거야..

즉 spring-mvc(톰캣)를 사용하면서 api 요청 부분은 webClient로 비동기 처리, 디비는 동기

-> 어드민 백엔드와 같이 단순 CRUD 일 경우 활용하기로 하였다.

implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-webflux"

implementation "org.springframework.boot:spring-boot-starter-data-jpa"

implementation "org.springdoc:springdoc-openapi-ui:${swaggerVersion}"
implementation "org.springdoc:springdoc-openapi-webflux-core:${swaggerVersion}"

 

2. 너무 달라지는 게 없는 것 같으니까 spring-webflux로만 개발해보자, 근데 아직 R2DBC는 개발 장벽이 너무 커, 그리고 아직 DB까지 비동기로 진행할 일도 별로 없어..

즉 spring-webflux(netty)를 메인으로 사용하되 디비는 동기방식 사용

-> 테스트해보니 몇몇 필터만 수정해주면 spring-mvc방식으로 개발했던 코드도 동일하게 작동하였다. 게다가 reactive 공부하면서 바로바로 적용 가능하니 api서버 개발 시 시도해볼 만한 스택이다.

implementation "org.springframework.boot:spring-boot-starter-webflux"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springdoc:springdoc-openapi-webflux-ui:${swaggerVersion}"

spring-webflux가 controller -> service -> repository 구조도 지원하지만(annotation 기반)

router -> handler/service -> repository 구조가 진짜 모습이다.(함수형 프로그래밍 모델 기반)

하지만 진짜 webflux의 기능을 쓰지 못하기 때문에 성능상 이점이 없을 듯하여 굳이 이렇게 사용하지 않아도 될 것 같다.

게다가 webflux 디펜덴시만 있으면 스웨거나 filter 설정 등 기존 로직에 영향이 있을 수 있다.

고생에 비해 성과가 미미할 수 있으니 고민이 필요하다.

 

3. webflux with r2dbc.. 디비까지 모든 걸 다 리액티브로 바꾸자니 소스에 하나라도 블로킹 걸리면 의미가 없으니 조심해야 하고, 개발 장벽도 있고 무엇보다 혼자 하는 게 아니라 팀으로 개발하니 팀원들의 동의도 구해야 하고.. 개발 속도도 안 날 것 같으니 우선 보류!

디비마저도 비동기로 처리하는 설정, 궁극적으로 지향해야 하는 부분이지만 일반적인 api에는 이렇게까지 사용할 필요는 없을 것 같다. 진짜 콜이 많거나 백그라운드에서 동작하는 것들이 많을 때(배치성 업무) 효력이 좋을 것 같다.

implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
//implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
//implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
//implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
//implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug")
implementation("org.springdoc:springdoc-openapi-webflux-ui:$openApiVersion")
implementation("dev.miku:r2dbc-mysql")

router, handler, r2dbc 관련 처리에 익숙하면 해볼 만한데 아직 미숙하여 100프로 전환은 아직 무리가 아닐까 싶다.

 

위 설정에서 조심해야 하는 게 webflux의 유무에 따른 swagger dependency도 달라져야 한다는 건데, 까딱하다가는 스웨거가 안 나올 수 있으니 꼭 같이 확인해야 한다.

 

우선 시험 삼아 어드민 백엔드에는 spring-web, spring-webflux /springfox3.0 swagger의 형태로 유지,

api 서버에는 spring-web을 드러내고 spring-webflux / springdoc3 swagger만 둔 형태로 유지해 볼 생각이다. 약간 모험이긴 한데 공부를 하고 알아갈 때마다 바로바로 개선할 수 있다는 이점이 있달까..


참고

https://www.baeldung.com/spring-mvc-async-vs-webflux

https://marrrang.tistory.com/4

 

Spring Webflux에 대하여

Spring webflux에 대해 찾아보고 정리한 글입니다. 일부만 정리되어 있어요, 참고 부탁드려요 ㅎㅎ Spring webflux Spring MVC vs Spring webflux? spring MVC는 Servlet spec에 기반하여 만들어져 있고 Servlet A..

marrrang.tistory.com

https://dreamchaser3.tistory.com/13

 

Spring Webflux - Functional Endpoints

개요 Spring WebFlux는 WebFlux.fn이라는 요청을 라우팅하고 처리하는 경량의 함수형 프로그래밍 모델을 포함하고 있다. 물론 Webflux는 기존 MVC처럼 어노테이션 기반의 프로그래밍 모델도 지원하기 때

dreamchaser3.tistory.com

 

728x90
반응형

'개발 > reactive' 카테고리의 다른 글

[webclient] 비슷한데 뭘 써야할지 모르겠는 것들  (0) 2022.04.01
[webflux] block vs toFuture  (0) 2022.03.31
[spring] spring-web and spring-webflux  (0) 2022.03.25
[reactive] 10. Flux  (0) 2022.03.25
[reactive] 9. Mono  (0) 2022.03.23
반응형

2022.03.16 - [개발/spring] - [spring] ChainedTransactionManager deprecated from boot 2.5

 

[spring] ChainedTransactionManager deprecated from boot 2.5

한 트랜젝션에서 서로 다른 DB에 update/insert 작업을 연속적으로 하게 될 때 이를 하나의 트랜젝션으로 보존할 수 있도록 스프링에서는 ChainedTransactionManager라는 클래스를 제공해주었다. 물론 이는

bangpurin.tistory.com

다중 DB의 트랜젝션 관리를 위해서 chainedTransactionManager를 사용 중에 있다.

오늘 라이브러리 수정 작업을 하면서 혹시 영향이 있을까 관련 테스트를 했는데, 중간에 exception을 던져도 rollback 되지 않고 commit이 되길래 이게 왜 안되지 싶었다.

하여 관련하여 구글링해보니 아래와 같은 글이 있었다.

1 - Change your exception to an unchecked exception (extends RuntimeException)
2 - Forces Spring to rollback a checked exception.

https://stackoverflow.com/questions/68001086/rollback-is-not-working-for-springboot-jparepository

 

Rollback is not working for Springboot JpaRepository

I have Spring Boot (v2.4.1) application with Hibernate and JpaRepository like Dao. Here is code: import javax.transaction.Transactional; @Service public class SomeService { pu...

stackoverflow.com

 

즉 checked exception인 Exception.class을 던지면 롤백이 안되고, Exception으로 롤백을 하고 싶다면 롤백 옵션을 줘야 한다는 것..

이것도 모르고 throw new Exception 이렇게 테스트를 했으니.. 당연히 안되었던 것이다..ㅎ

무튼 아래와 같이 RuntimeException을 발생시켜서 롤백이 잘 되는지 확인하였다.

@Transactional(transactionManager = Constants.CHAINED_TRANSACTION_MANAGER)
public BaseResponse resetLossMoney(ResetLossMoneyRequest req) throws Exception {
    checkExpiredDateAndSave(req.getCi());
    setChangedAndSave(req.getCi());
    if(1==1) throw new RuntimeException("test");
    insertResetLossMoneyLog(req.getCi(), GID_DEFAULT);
    return new BaseResponse();
}

 

위 내용과 별도로 트랜젝션이 잘 되는지 확인하고 싶다면 로그백에 아래를 추가하면 확인 가능하다.

<logger name="org.springframework.transaction.interceptor" level="TRACE"/>

 


 

  • 스프링 프레임워크에서는 기본적으로 Unchecked Exception(런타임 예외)이 발생하면 트랜잭션을 롤백합니다.
  • 반면, Checked Exception(체크드 예외)은 기본적으로 트랜잭션을 롤백하지 않습니다. 이는 Checked Exception이 비즈니스 로직 내에서 예외적인 상황으로 처리될 수 있기 때문입니다.
  • checked exception에서도 롤백이 필요하다면 rollbackFor을 활용하면 된다.
@Transactional(rollbackFor = IOException.class)
public void performTransaction() throws IOException {
    repository.save(new Entity());

    // Checked Exception 발생
    if (true) {
        throw new IOException("Checked Exception");
    }

    repository.save(new Entity());
}

 

  •  특정 Unchecked Exception에 대해서는 롤백을 원하지 않을 때 noRollbackFor 속성을 사용합니다.

왜 기본적으로 Unchecked Exception에 대해서만 롤백할까?

  1. 비즈니스 로직과의 분리: Checked Exception은 비즈니스 로직 내에서 발생할 수 있는 예외적인 상황을 나타내며, 이를 비즈니스 로직의 일부로 처리하는 경우가 많습니다. 반면, Unchecked Exception은 예외적 상황이 아니라 코드의 오류나 논리적 문제를 나타내므로 트랜잭션의 롤백이 필요합니다.
  2. 복구 가능성: Checked Exception은 복구 가능한 예외로 간주되어, 개발자가 예외를 처리하여 정상적인 흐름으로 돌아갈 수 있다고 판단할 수 있습니다. Unchecked Exception은 복구가 불가능한 예외로 간주되어 트랜잭션을 롤백하는 것이 합리적입니다.
728x90
반응형
반응형

spring mvc 기반 spring-web의 restTemplate으로 api를 요청하고 있던 와중, restTemplate가 deprecated 된다는 말을 듣고, 대체제인 webClient를 사용해보려고 공부를 하다 보니 webflux의 영역에 들어와 버렸다. 물론 webClient도 sync call을 지원하지만 수많은 api 콜을 비동기로 하면 자연스레 내 api의 속도도 빨라질 것이 아닌가? 위기를 기회로 전환하며 새로운 아키텍처를 익히려고 spring-webflux를 추가하였다.

그런데 spring reactive강의를 듣던 도중, 두 dependency는 spring context의 혼란을 야기하므로 같이 사용하면 안 된다는 말을 들었다. 오래된 강의긴 했지만 나름 스프링 저명인사가 말한 것이기에 안되리라 생각하고 좌절하며 관련 내용을 더 찾아보기로 했다.

 

1. spring mvc vs spring webflux

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#web

spring mvc가 있으면 mvc가 우선순위가 높고 webflux만 있으면 webflux를 쓴다고 한다. 즉, 둘 다 있으면 mvc가 우선 순위다.

근데 애초에 같이 있어도 된다는 전제를 한다면 같이 써도 된다는 것 아닌가?!

 

2. spring mvc with webclient

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

스프링 공식문서에 spring mvc + webclient형식으로 같이 써도 된다는 글이 있다.. 하하하하하 살았다.

 

그리고 스프링에서 같이 쓰는 동영상을 올린 적이 있다(물론 주내용은 springboot2에 대한 데모지만)

26분부터~ restTemplate -> webClient 바꾸는 내용 하면서 언급함.

 

spring mvc에 webClient를 써야 하는가에 대한 많은 토론

https://stackoverflow.com/questions/51953826/resttemplate-vs-webclient-benefits-in-servlet-based-web-mvc-app

 

RestTemplate vs WebClient benefits in Servlet based web-mvc app

I'm looking for a clarification on the bolded text in the statement below (I've provided the full paragraph for context only): The RestTemplate is not a good fit for use in non-blocking applicat...

stackoverflow.com

 

728x90
반응형

'개발 > reactive' 카테고리의 다른 글

[webflux] block vs toFuture  (0) 2022.03.31
[webflux] 실무투입고민  (0) 2022.03.30
[reactive] 10. Flux  (0) 2022.03.25
[reactive] 9. Mono  (0) 2022.03.23
[reactive] 8. webflux  (0) 2022.03.22

+ Recent posts