728x90
반응형
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
반응형

프록시 패턴

  • 인터페이스를 구현을 통해 진짜 구현체를 대신하여 부가기능을 제공하거나 리턴 값을 변경할 수 있도록 대리자(surrogate)나 자리표시자(placeholder)의 역할을 하는 객체를 제공하는 패턴

각 클래스는 자신이 해야 할 일(SRP: Single Responsibility Principle)만 해야하는데 부가적인 기증을 제공할 때 이런 패턴을 주로 사용함

하지만 부가적인 기능을 추가할 때마다 위임하는 코드가 중복해서 발생할 수 있음

코딩 예시: https://bamdule.tistory.com/154

 

[디자인 패턴] 프록시 패턴 (Proxy Pattern)

1. 프록시 패턴이란? 실제 객체를 바로 이용하는 것 대신 가상 객체에 실제 객체를 선언하여 실제 객체의 기능과 추가적인 기능을 사용함으로 써 기능의 흐름을 제어하는 디자인 패턴입니다. 2.

bamdule.tistory.com

 


매번 클래스를 만드는게 아니라 동적으로 생성하는 방식

-> java reflextion을 활용한 다이내믹 프록시 사용

  • (컴파일 타임이 아닌) 런타임에 인터페이스를 구현하는 클래스/프록시 인스턴스를 만들어 사용하는 프로그래밍 기법

프록시 인스턴스 만들기

  • Object Proxy.newProxyInstance(ClassLoader, Interfaces, InvocationHandler)
    • object로 리턴되기 때문에 type casting 필요
//BookService는 반드시 인터페이스여야 함
BookService bookService = (BookService) Proxy.newProxyInstance(BookService.class.getClassLoader(), new Class[]{BookService.class},
        new InvocationHandler() {
            BookService bookService = new DefaultBookService();
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("rent")) { //rent 라는 method에만 적용하고 싶다면
                    System.out.println("aaaa");
                    Object invoke = method.invoke(bookService, args);
                    System.out.println("bbbb");
                    return invoke;
                }
             //나머지 method는 그대로 리턴
                return method.invoke(bookService, args);
            }
        });

근데 매번 이렇게 구현하는 게 더 복잡, 유연한 구조가 아님.

그리고 클래스 기반의 (자바가 제공하는) proxy를 만들 수 없음.

-> 그래서 Spring AOP(프록시 기반의 AOP)가 등장

 


클래스의 프록시가 필요하다면?

-> subclass를 만들어서 프록시를 만드는 라이브러리를 사용

 

CGlib lib 사용

  • https://github.com/cglib/cglib/wiki
  • 스프링, 하이버네이트가 사용하는 라이브러리
  • 버전 호환성이 좋지 않아서 서로 다른 라이브러리 내부에 내장된 형태로 제공되기도 한다.
MethodInterceptor handler = new MethodInterceptor() {
    BookService bookService = new BookService(); //class
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        if(method.getName().equals("rent")){ //method 이름이 rent 일 때만 작업
            log.info("aaa");
            Objet invoke = method.invoke(bookService, args);
            log.info("bb");
            return invoke;
        }
        //나머지 method는 그대로 리턴
        return method.invoke(bookService, args);
    }
};

BookService bookService = (BookService) Enhancer.create(BookService.class, handler);

 

ByteBuddy lib 사용

  • https://bytebuddy.net/#
  • 스프링에서 버전 관리해줌
  • 바이트 코드 조작뿐 아니라 런타임(다이내믹) 프록시를 만들 때도 사용할 수 있다.
Class<? extends BookService> proxyClass = new ByteBuddy().subclass(BookService.class)
        .method(named("rent")) //method 이름이 rent면 적용
        .intercept(InvocationHanderAdaptor.of(new InvocationHander(){
            BookService bookService = new BookService();
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
                log.info("aaa");
                Object invoke = method.invoke(bookService, args);
                log.info("Bbb");
                return invoke;
            }
        }))
        .make().load(BookService.class.getClassLoader()).getLoaded();
BookService bookService = proxyClass.getConstructor(null).newInstance();

 

서브 클래스를 만드는 방법의 단점

  • 상속(extends)을 사용하지 못하는 경우 프록시를 만들 수 없음
    • private 생성자(하위 클래스에서 항상 부모의 생성자를 호출한다)
    • final 클래스
  • 인터페이스가 있을 때는 인터페이스의 프록시를 만들어 사용할 것.

 

다이내믹 프록시 기법이 사용되는 곳

  • 스프링 데이터 JPA
  • 스프링 AOP
  • Mockito
  • 하이버네이트 lazy initialization(@OneToMany 등 entity 들고 있을 때 프록시 객체로 처음에 리턴)

 

728x90
반응형

'architecture > sw architecture' 카테고리의 다른 글

람다 아키텍처 vs 카파 아키텍처  (0) 2023.03.22
반응형

갑자기 local로 띄운 서버가 mac -> paralles -> window -> ie에서 안된다면..?

호스트 파일에도 설정이 잘 있는데도 안된다면..?

나 같은 경우 아래와 같은 에러가 브라우저에 찍혔다..

  •  INET_E_RESOURCE_NOT_FOUND
  • CONNECTION_REFUSED

 

hosts 파일의 주소를 아래와 같이 외부 ip로 변경하면 우선 해결된다.

근데 외부 ip가 바뀔 경우 다시 손대야하는 불편함이 있다..

728x90
반응형

'서버 세팅 & tool > vm on mac' 카테고리의 다른 글

[redis] 설치 / redis-cli  (0) 2025.01.09
[terminal] alias in mac terminal  (0) 2022.06.08
[vm] nginx 설치  (0) 2022.02.24
[parallels] nox...... 99%.....  (0) 2022.02.23
[vm] axon server 설치  (0) 2022.01.12
반응형
  • 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
반응형

쿠버네티스에 nginx를 깔고 시작한다.

WAF = web application firewall = L7 방화벽(http 트래픽를 filter, monitor, block하는 web service)

nginx 방화벽은 코드레벨에 가까운 방화벽

MSA기반의 아키텍쳐에서는 개발자가 모든 코드를 다 알 수 없음(어디에 위험요소가 있는지 알 수 없음) 그렇기 때문에 더욱 코드 레발단의 방화벽이 필요함.

 

설정: yaml 파일

설정 명령어:

kubectl apply -f aaa.yaml

특징: 

  • 헤더/바디에 어떤 값이 오면 막는 기능 가능
  • data-guard: 데이터 마스킹이나 데이터 중 민감 데이터같은게 있으면 탐지하거나 할 수 있음
  • 로그 설정 가능
  • waf 설정파일에서 정책/로그정책 등 지정가능
  • 지정 시 파일 이름에는 ap- 로 시작하고 yaml 파일 안에서는 ap- 다음 부분부터 작성하면 되는 듯

 

강의에서는 도커에서 시연을 하는데 도커를 잘 몰라 바로 따라가기가 어려웠다.

728x90
반응형

'서버 세팅 & tool > nginx' 카테고리의 다른 글

라이브환경 인증서 교체  (0) 2024.01.08
[이슈해결][apache] 304 NOT_MODIFIED  (0) 2023.10.12
[nginx] API gateway  (0) 2022.03.14
[nginx] 실전 cors 해결하기  (0) 2022.03.14
[nginx] load balancing  (0) 2022.03.11

+ Recent posts