728x90
반응형
728x90
반응형
반응형
개요: 앱에서 동영상 시청 시 리워드를 주는 플랫폼으로 google admob 사용 중. 보상 지급 수치에 대한 유효성 적립이 필요하여 서버 콜백 부분 추가 개발 진행.
 
서버 측 인증(SSV) 콜백 확인이란:
  • 서버 측 확인은 앱에서 보상형 광고 조회수를 확인하는 추가 단계로, 표준 클라이언트 측 콜백 외에 추가로 실시됨
  • 서버 측 확인을 사용하여 완료된 보상형 동영상 광고 조회수를 확인하여 앱에서 실제로 동영상 광고 시청을 완료한 사용자에게만 보상을 제공할 수 있음
  • 사용자가 보상형 동영상 광고 조회를 완료할 때마다 Google에 설정한 콜백 URL을 사용하여 조회를 확인
 
적용으로 얻는 효과:
  1. 사용자가 동영상 광고를 다 볼 때 에드몹에서 설정한 콜백 url 호출
  2. 응답으로 성공(200 ok)이 떨어지면 시청을 완료했다고 간주
  3. 보상 수치의 유효성 확인 가능

특이사항: '광고형식' 이 총 6종류가 있는데, 그 중 rewarded(리워드) 에 해당하는 것에 한함
 
  • admob 관리 페이지에 들어가서 해당 앱을 찾는다

  • 콜백을 설정하고자 하는 광고 단위를 클릭

  • 고급 설정 > 서버 측 확인 이 부분이 있는지 확인 

  • 서버 측 확인 수정 클릭, 콜백 URL 입력 후 URL 확인 버튼 클릭

  • 4번처럼 확인이 되었으면 확인된 URL 사용 버튼을 눌러 저장


 
 
개발 내용
1. 관련 dependency 추가

환경: java8, springboot1.5.9, maven

 <!-- 애드몹 인증 -->
 <dependency>
  <groupId>com.google.crypto.tink</groupId>
  <artifactId>apps-rewardedads</artifactId>
  <version>1.7.0</version>
 </dependency>
 <dependency>
  <groupId>joda-time</groupId>
  <artifactId>joda-time</artifactId>
  <version>2.10.10</version>
 </dependency>
 <dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.8.9</version>
 </dependency>

 

 
2. 개발 가이드 문서
이 중 Tink의 RewardedAdsVerifier 사용 방식으로 구현
 
3. 특이사항
  1. https 통신 필수
    1. 로컬에서 테스트를 하려면 로컬 인증서를 설치, 세팅해야 함
    2. 최종적으로 사용할 컴포넌트에 https 설정이 되어 있어야 함
  2. 구글과의 테스트 통신을 위해서 외부에서 접속되는 url이어야 함
  3. 실패 시 상태코드 200이 아닌 다른 코드로 반환해야 함(exception handler 유의)
    1. 서버에 연결할 수 없거나 서버에서 기대하는 응답을 제공하지 않으면 Google은 1초 간격으로 최대 5회 SSV 콜백을 전송하려고 재시도함
 
728x90
반응형
반응형

환경: springboot 2.6.2, java11 기반의 spring-jpa project

 

1. chained transaction

@Transactional(transactionManager = Constants.CHAINED_TRANSACTION_MANAGER)
public void depositWithCredential(MoneyExchangeCredentialRequest credentialRequest) {
    log.warn("> outside tx name: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    ...
2023-01-05 09:39:09 DEBUG [s.t.s.AbstractPlatformTransactionManager.getTransaction      : 370][http-nio-8600-exec-8] Creating new transaction with name [com.MoneyExchangeService.depositWithCredential]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'chainedTransactionManager'
2023-01-05 09:39:09 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-8] Opened new EntityManager [SessionImpl(1728738865<open>)] for JPA transaction
2023-01-05 09:39:09 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-8] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@6335fe08]
2023-01-05 09:39:09 DEBUG [s.t.s.AbstractPlatformTransactionManager.getTransaction      : 370][http-nio-8600-exec-8] Creating new transaction with name [com.MoneyExchangeService.depositWithCredential]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'chainedTransactionManager'
2023-01-05 09:39:09 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-8] Opened new EntityManager [SessionImpl(641170442<open>)] for JPA transaction
2023-01-05 09:39:09 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-8] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@23d44be1]
2023-01-05 09:39:09 DEBUG [s.t.s.AbstractPlatformTransactionManager.getTransaction      : 370][http-nio-8600-exec-8] Creating new transaction with name [com.MoneyExchangeService.depositWithCredential]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'chainedTransactionManager'
2023-01-05 09:39:09 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-8] Opened new EntityManager [SessionImpl(391846484<open>)] for JPA transaction
2023-01-05 09:39:09 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-8] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@5b527309]

3개의 DB가 물려있기 때문에 총 3번의 transaction이 열림

 

2. 엄마 transaction 확인, 편의상 앞으로 dWC라고 부를 예정

2023-01-05 09:39:09 WARN > outside tx name: com.MoneyExchangeService.depositWithCredential

 

3. propagation = REQUIRES_NEW

createDailyMoneyExchangeIfAbsentAndGetWithLock 함수 안을 살펴본다.

@Transactional(transactionManager = Constants.CHAINED_TRANSACTION_MANAGER)
public void depositWithCredential(MoneyExchangeCredentialRequest credentialRequest) {
    log.warn("> outside tx name: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    ...
    MoneyExchange dailyExchange = createDailyMoneyExchangeIfAbsentAndGetWithLock(request);
    ...
    
}

private MoneyExchange createDailyMoneyExchangeIfAbsentAndGetWithLock(MoneyExchangeRequest request) {
    MoneyExchange.PK pk = createDailyMoneyExchangePK(request);
    if(moneyExchangeRepository.findById(pk).isEmpty()) { //조회 1
        moneyExchangeEntityService.createMoneyExchangeFromNewTransaction(new MoneyExchange(pk)); //새로운 트랜, 조회 2 + save
    }
    return moneyExchangeRepository.findOneWithLock(pk); //select for update; start lock
}

//다른 서비스

@Transactional(transactionManager = Constants.USER_TRANSACTION_MANAGER, propagation = Propagation.REQUIRES_NEW)
public void createMoneyExchangeFromNewTransaction(MoneyExchange moneyExchange) {
    log.warn("> createMoneyExchangeFromNewTransaction requires new tx name: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    moneyExchangeRepository.save(moneyExchange);
}

//레파지토리

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM MoneyExchange c WHERE c.pk = :pk")
MoneyExchange findOneWithLock(@Param("pk") MoneyExchange.PK pk);
2023-01-05 09:45:58 WARN  [c.n.g.s.m.MoneyExchangeService          .depositWithCredentia: 242][http-nio-8600-exec-1] > outside tx name: com.MoneyExchangeService.depositWithCredential

...

2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-1] Found thread-bound EntityManager [SessionImpl(1294697638<open>)] for JPA transaction
2023-01-05 09:45:58 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-1] Participating in existing transaction
Hibernate: 
    select

    from
        hd_moneyexchange moneyexcha0_ 
    where

2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-1] Found thread-bound EntityManager [SessionImpl(1294697638<open>)] for JPA transaction
2023-01-05 09:45:58 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 429][http-nio-8600-exec-1] Suspending current transaction, creating new transaction with name [com.MoneyExchangeEntityService.createMoneyExchangeFromNewTransaction]

2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-1] Opened new EntityManager [SessionImpl(866488609<open>)] for JPA transaction
2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-1] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@57ad1a62]
2023-01-05 09:45:58 WARN  [c.n.g.s.MoneyExchangeEntityService      .createMoneyExchangeF:  28][http-nio-8600-exec-1] > createMoneyExchangeFromNewTransaction requires new tx name: com.MoneyExchangeEntityService.createMoneyExchangeFromNewTransaction
2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-1] Found thread-bound EntityManager [SessionImpl(866488609<open>)] for JPA transaction
2023-01-05 09:45:58 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-1] Participating in existing transaction
Hibernate: 
    select
       
    from
        hd_moneyexchange moneyexcha0_ 
    where
       
2023-01-05 09:45:58 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-1] Initiating transaction commit
2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-1] Committing JPA transaction on EntityManager [SessionImpl(866488609<open>)]
Hibernate: 
    insert 
    into
        hd_moneyexchange
       
    values

2023-01-05 09:45:58 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-1] Closing JPA EntityManager [SessionImpl(866488609<open>)] after transaction
2023-01-05 09:45:58 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-1] Resuming suspended transaction after completion of inner transaction
Hibernate: 
    select
       
    from
        hd_moneyexchange moneyexcha0_ 
    where
    
    for update

로그를 보면 같은 select가 총 3번 실행하는 것을 알 수 있는데,

  • 하나는 if절 안 isEmpty를 위해
  • 두 번째는 새로운 서비스 안에서 새로운 transaction(자식 transaction, cMEFNT)을 시작하고 save 전에 select
  • 그리고 다시 부모 transaction으로 돌아가 row lock을 위해 조회

사실 이 부분에는 불필요한 로직이 있어서 수정할 예정이다.

다만 자식 transaction과 propagation_new의 관계를 보기 좋은 예시라 들고 왔는데, 로그를 보면 자식 transaction가 끝나고 insert commit 후 다시 엄마 transaction으로 돌아가는 것을 볼 수 있다.

 

4. 마지막의 row lock을 위한 테스트

@Lock(LockModeType.PESSIMISTIC_WRITE)

수동 db조작으로 테스트

select for update가 실행되고 다음 부분에 디버그를 걸어 임시 정지, db를 조작하며 테스트

  1. 해당 row select 가능(readable)
  2. 해당 row update 불가능(pending)
  3. 다른 row update 가능
  4. select for update가 끝나고(transaction 종료) 해당 row update 가능

 

program으로 테스트

  1. 유저 a에 대한 row 삽입
  2. 유저 b에 대한 row 삽입
  3. 유저 a에 대한 api 호출 -> 해당 row lock -> sleep
  4. 유저 b에 대한 api호출 -> 역시 해당 row lock (조회됨) -> sleep

-> row 단위 lock 확인

  1. 유저 a에 대한 row 삽입
  2. 유저 a에 대한 api 호출 -> 해당 row lock -> sleep
  3. 유저 a에 대한 api 호출 -> sleep도 못가고 select for update에서 pending -> timeout으로 에러처리

-> 같은 row 일 때 lock 되는 것 확인

 

  • default timeout 1분(1분 뒤 에러처리)

기본 값은 DB 설정 값을 따른다.

mysql 기준

mysql version

확인 쿼리는 아래..

select @@innodb_lock_wait_timeout

//lock 이후에 sleep
>>>>>>>>sleeping

//신규 요청
2023-01-05 11:36:10 INFO  [c.n.g.common.filter.RequestLogFilter    .doFilter            :  34][http-nio-8600-exec-2] [REQUEST] [POST] /api/external/money
...
Hibernate: 
    select
      
    from
        hd_moneyexchange moneyexcha0_ 
    where
     
    for update
            
2023-01-05 11:37:10 WARN  [o.h.engine.jdbc.spi.SqlExceptionHelper  .logExceptions       : 137][http-nio-8600-exec-2] SQL Error: 1205, SQLState: 40001
2023-01-05 11:37:10 ERROR [o.h.engine.jdbc.spi.SqlExceptionHelper  .logExceptions       : 142][http-nio-8600-exec-2] Lock wait timeout exceeded; try restarting transaction
2023-01-05 11:37:10 DEBUG [s.t.s.AbstractPlatformTransactionManager.processRollback     : 833][http-nio-8600-exec-2] Initiating transaction rollback
..
Response body: {"header":{"status":500,"message":"could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.PessimisticLockException: could not extract ResultSet","isSuccessful":false}}

timeout 설정 방법

## 전역 설정
spring.jpa.properties.javax.persistence.query.timeout=5000

//

## DB config 별 설정이 나눠져 있다면
a.jpa.properties.javax.persistence.query.timeout=4000

@ConfigurationProperties(prefix = "a.jpa")
public static class AJpaProperties {
    private Map<String, String> properties = new HashMap<>();

    public Map<String, String> getProperties() {
        return properties;
    }

    public void setProperties(Map<String, String> properties) {
        this.properties = properties;
    }
}

// or //

Map<String,Object> properties = new HashMap();
properties.put("javax.persistence.query.timeout", 5000);
EntityManager entityManager = entityManagerFactory.createEntityManager(properties);
//쿼리 별 설정
//전역 설정이 있어도 override됨
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM MoneyExchange c WHERE c.pk = :pk")
@QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "6000")})
   MoneyExchange findOneWithLock(@Param("pk") MoneyExchange.PK pk);

설정 후 로그, local timeout(6초)를 따른다.

2023-01-05 13:22:07 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-2] Participating in existing transaction
    select
       
    from
        hd_moneyexchange moneyexcha0_ 
    where
       for update
            
2023-01-05 13:22:12 WARN  [com.zaxxer.hikari.pool.ProxyConnection  .checkException      : 182][http-nio-8600-exec-2] HikariPool-1 - Connection com.mysql.cj.jdbc.ConnectionImpl@23ae77d9 marked as broken because of SQLSTATE(null), ErrorCode(0)
com.mysql.cj.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request

참고) queryHint에 다른 옵션들도 있을까 싶어서 아래 옵션을 주었으나 무시되었다. 아직까지도 지원을 하지 않는 모양

@QueryHints({@QueryHint(name = "javax.persistence.lock.scope", value = "EXTENDED")})

..
[http-nio-8600-exec-1] HHH000121: Ignoring unrecognized query hint [javax.persistence.lock.scope]

참고) spring-jpa lock option종류

  • LockModeType.PESSIMISTIC_WRITE
    일반적인 옵션. 데이터베이스에 쓰기 락
    다른 트랜잭션에서 읽기도 쓰기도 못함. (배타적 잠금)
  • LockModeType.PESSIMISTIC_READ
    반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용
    다른 트랜잭션에서 읽기는 가능함. (공유 잠금)
  • LockModeType.PESSINISTIC_FORCE_INCREMENT
    Version 정보를 사용하는 비관적 락

참고) mysql read-lock

https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html

참고) 테이블이 lock 걸린 경우 확인 쿼리; row lock은 안 잡힘

SHOW OPEN TABLES 
where `database` = ''
and `table` = ''
;

row lock은 아래 쿼리로 조회 시 row lock이 있다고는 알 수 있는데, 정작 누가 걸린 건지는 안 알려줌..

SHOW ENGINE INNODB STATUS;

 

5. 나머지 엄마 transaction 로직 확인

@Transactional(transactionManager = Constants.CHAINED_TRANSACTION_MANAGER)
public void depositWithCredential(MoneyExchangeCredentialRequest credentialRequest) {
    log.warn("> outside tx name: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    ...
    MoneyExchange dailyExchange = createDailyMoneyExchangeIfAbsentAndGetWithLock(request);
    ...
    
    runAndSaveErrorLogIfFail(request, money, () -> {
        saves(...); //엄마 트랜; 바로 커밋 안함
        System.out.println(">> before withdraw");
        log.warn("> before throw tx name: {}", TransactionSynchronizationManager.getCurrentTransactionName());
        throw new Exception();
    });
}

private void runAndSaveErrorLogIfFail(MoneyExchangeRequest request, long availableMoney, Runnable runnable) {
    try {
        log.warn("> runAndSaveErrorLogIfFail Thread Name before: " + Thread.currentThread().getName());
        runnable.run();
    } catch (AException e) {
        log.error("MoneyExchangeService.runAndSaveErrorLogIfFail() AException, message: {}, request: {}", e.getExceptionMessage(), request);
        saveErrorLog(request, e.getExceptionMessage(), availableMoney);
        throw e;
    } catch (Exception e) {
        log.error("MoneyExchangeService.runAndSaveErrorLogIfFail() Exception, message: {}, request: {}", e.getMessage(), request);
        saveErrorLog(request, e.getMessage(), availableMoney);
        throw e;
    }
}

private void saves(...) {
    // 저장1
    long realDepositMoney = sendGameMail(request, user, configurations.getExchangeGameConfiguration());
    // 저장2
    saveDailyMoneyExchange(dailyExchange, changedMoney); //엄마 트랜
    // 저장3
    saveDailyMoneyExchangeLog(request, user, changedMoney, originAvailableMoney); //엄마 트랜
}
Hibernate: //저장1; 
    select

    from
        hd_user_notice usernotice0_ 
    where
  
...
2023-01-05 10:15:04 WARN  [c.n.g.s.m.MoneyExchangeService          .saveDailyMoneyExchan: 394][http-nio-8600-exec-2] > saveDailyMoneyExchangeLog tx name: com.MoneyExchangeService.depositWithCredential
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-2] Found thread-bound EntityManager [SessionImpl(1213852261<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-2] Participating in existing transaction
Hibernate:  //저장3; 부모 transaction
    insert 
    into
        hd_moneyexchange_log
        
    values
        
2023-01-05 10:15:04 WARN  [c.n.g.s.m.MoneyExchangeService          .saveDailyMoneyExchan: 397][http-nio-8600-exec-2] > saveDailyMoneyExchangeLog tx name: com.MoneyExchangeService.depositWithCredential
>> before withdraw
2023-01-05 10:15:04 WARN  [c.n.g.s.m.MoneyExchangeService          .lambda$depositWithCr: 253][http-nio-8600-exec-2] > before throw tx name: com.MoneyExchangeService.depositWithCredential
...
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-2] Found thread-bound EntityManager [SessionImpl(157605982<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 429][http-nio-8600-exec-2] Suspending current transaction, creating new transaction with name [com.MoneyExchangeLogEntityService.logFromNewTransaction]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-2] Opened new EntityManager [SessionImpl(251535742<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-2] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1bd03ae5]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-2] Found thread-bound EntityManager [SessionImpl(1213852261<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 429][http-nio-8600-exec-2] Suspending current transaction, creating new transaction with name [com.MoneyExchangeLogEntityService.logFromNewTransaction]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-2] Opened new EntityManager [SessionImpl(830560897<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-2] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1afe4eee]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-2] Found thread-bound EntityManager [SessionImpl(2044396758<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 429][http-nio-8600-exec-2] Suspending current transaction, creating new transaction with name [com.MoneyExchangeLogEntityService.logFromNewTransaction]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 412][http-nio-8600-exec-2] Opened new EntityManager [SessionImpl(1710520606<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doBegin             : 440][http-nio-8600-exec-2] Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7f696057]
2023-01-05 10:15:04 WARN  [c.n.g.s.MoneyExchangeLogEntityService   .logFromNewTransactio:  22][http-nio-8600-exec-2] > error logFromNewTransaction tx name: com.MoneyExchangeLogEntityService.logFromNewTransaction
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-2] Found thread-bound EntityManager [SessionImpl(830560897<open>)] for JPA transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-2] Participating in existing transaction
Hibernate: //에러 로그 삽입
    insert 
    into
        hd_moneyexchange_log
      
    values
      
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-2] Initiating transaction commit
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-2] Committing JPA transaction on EntityManager [SessionImpl(1710520606<open>)]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-2] Closing JPA EntityManager [SessionImpl(1710520606<open>)] after transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-2] Resuming suspended transaction after completion of inner transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-2] Initiating transaction commit
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-2] Committing JPA transaction on EntityManager [SessionImpl(830560897<open>)]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-2] Closing JPA EntityManager [SessionImpl(830560897<open>)] after transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-2] Resuming suspended transaction after completion of inner transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-2] Initiating transaction commit
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-2] Committing JPA transaction on EntityManager [SessionImpl(251535742<open>)]
2023-01-05 10:15:04 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-2] Closing JPA EntityManager [SessionImpl(251535742<open>)] after transaction
2023-01-05 10:15:04 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-2] Resuming suspended transaction after completion of inner transaction
2023-01-05 10:15:06 DEBUG [s.t.s.AbstractPlatformTransactionManager.processRollback     : 833][http-nio-8600-exec-2] Initiating transaction rollback
2023-01-05 10:15:06 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doRollback          : 583][http-nio-8600-exec-2] Rolling back JPA transaction on EntityManager [SessionImpl(2044396758<open>)]
2023-01-05 10:15:06 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-2] Closing JPA EntityManager [SessionImpl(2044396758<open>)] after transaction
2023-01-05 10:15:06 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-2] Resuming suspended transaction after completion of inner transaction
2023-01-05 10:15:06 DEBUG [s.t.s.AbstractPlatformTransactionManager.processRollback     : 833][http-nio-8600-exec-2] Initiating transaction rollback
2023-01-05 10:15:06 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doRollback          : 583][http-nio-8600-exec-2] Rolling back JPA transaction on EntityManager [SessionImpl(1213852261<open>)]
2023-01-05 10:15:06 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-2] Closing JPA EntityManager [SessionImpl(1213852261<open>)] after transaction
2023-01-05 10:15:06 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-2] Resuming suspended transaction after completion of inner transaction
2023-01-05 10:15:06 DEBUG [s.t.s.AbstractPlatformTransactionManager.processRollback     : 833][http-nio-8600-exec-2] Initiating transaction rollback
2023-01-05 10:15:06 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doRollback          : 583][http-nio-8600-exec-2] Rolling back JPA transaction on EntityManager [SessionImpl(157605982<open>)]
2023-01-05 10:15:06 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-2] Closing JPA EntityManager [SessionImpl(157605982<open>)] after transaction
2023-01-05 10:15:06 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-2] Resuming suspended transaction after completion of inner transaction
2023-01-05 10:15:06 DEBUG [.m.m.a.ExceptionHandlerExceptionResolver.doResolveHandlerMeth: 416][http-nio-8600-exec-2] Using @ExceptionHandler

 

private void runAndSaveErrorLogIfFail(MoneyExchangeRequest request, long availableMoney, Runnable runnable) {

runnable을 받는 함수는 처음 봐서 특이점이 있나 싶었는데, 원작자에게 물어보니 try-catch 에러로그 부분을 다양한 곳에서 공통으로 쓰려고 하셨다고 한다. 즉, logic 함수만 갈아 끼고 error발생 시 처리하는 부분을 공통화하기 위한 로직이었다. 예전 pr때 catch부분이 너무 똑같고 많이 반복된다는 지적이 있었는데 그걸 보완하기 위한 코드인 듯. 멀티스레드나 그런 걸 위한 건 아니다.

 

로그를 보면서 코드를 분석해 본다.

  • 저장 1에 대한 select 실행
  • 저장 2에 hibernate로그는 없음
  • saves 함수의 저장 3번(saveDailyMoneyExchangeLog) 로직이 실행되고(엄마 transaction) 
    • 이 부분이 왜 실행되는지 모르겠다. saves 함수를 나온 것도 아니고, throw 전의 로그를 찍기도 전에 insert 로그가 찍힌다. 물론 나중에 롤백되긴 하는데.. 다른 insert문은 하나도 안 찍히고 마지막 거만 찍히는 게 이해가 잘 안 가는 부분.
  • saves를 나오고 throw를 만나서 새로운 transaction이 열림(자식 transaction시작; creating new transaction with name logFromNewTransaction)
  • transatcion manager가 chained 여서 transaction이 세 번 열림..
  • 에러 로그를 insert 하고 commit 하고 엄마 transaction에게 돌아가고
  • 엄마 transaction은 exception을 전파받아 doRollback을 세 번(엄마 transaction자체가 chained transaction) 실행
  • 그리고 마지막으로 exception은 @ExceptionHandler를 타고 최종 전파된다.

 

throw를 지우고 성공 시 로그

Hibernate: //저장1; 
    select

    from
        hd_user_notice usernotice0_ 
    where
  
...
2023-01-05 14:07:36 WARN  [c.n.g.s.m.MoneyExchangeService          .saveDailyMoneyExchan: 396][http-nio-8600-exec-1] > saveDailyMoneyExchangeLog tx name: com.MoneyExchangeService.depositWithCredential
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doGetTransaction    : 375][http-nio-8600-exec-1] Found thread-bound EntityManager [SessionImpl(425779152<open>)] for JPA transaction
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-1] Participating in existing transaction
Hibernate: //저장3 쿼리 실행
    insert 
    into
        hd_moneyexchange_log
      
    values
#### saves 함수 밖...      
2023-01-05 14:07:36 WARN  [c.n.g.s.m.MoneyExchangeService          .lambda$depositWithCr: 256][http-nio-8600-exec-1] > before throw tx name: com.MoneyExchangeService.depositWithCredential
##static close
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-1] Initiating transaction commit
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-1] Committing JPA transaction on EntityManager [SessionImpl(1732642577<open>)]
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-1] Closing JPA EntityManager [SessionImpl(1732642577<open>)] after transaction
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-1] Resuming suspended transaction after completion of inner transaction
##log close
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-1] Initiating transaction commit
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-1] Committing JPA transaction on EntityManager [SessionImpl(425779152<open>)]
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-1] Closing JPA EntityManager [SessionImpl(425779152<open>)] after transaction
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-1] Resuming suspended transaction after completion of inner transaction
## user close
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.processCommit       : 740][http-nio-8600-exec-1] Initiating transaction commit
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCommit            : 557][http-nio-8600-exec-1] Committing JPA transaction on EntityManager [SessionImpl(1465294398<open>)]
Hibernate: //저장1 쿼리 실행
    insert 
    into
        hd_user_notice
     
    values
     
Hibernate: 
    update //저장2 쿼리 실행
        hd_moneyexchange 
    set
       
    where
       
2023-01-05 14:07:36 DEBUG [o.s.orm.jpa.JpaTransactionManager       .doCleanupAfterComple: 648][http-nio-8600-exec-1] Closing JPA EntityManager [SessionImpl(1465294398<open>)] after transaction
2023-01-05 14:07:36 DEBUG [s.t.s.AbstractPlatformTransactionManager.cleanupAfterCompleti: 996][http-nio-8600-exec-1] Resuming suspended transaction after completion of inner transaction

 

디버그를 걸고 보니 위 원인을 알 수 있었다.

save 함수 구현체에 있는 isNew부분이 포인트다.

isNew() 메서드는 새로운 Entity를 판단하기 위해 ID값을 확인하는데 기본 전략은 아래와 같다.

  • ID의 타입이 객체 타입(reference; string) 일 때: null
  • ID의 타입이 기본 타입(primitive) 일 때: 0

 

merge vs persist?

 merge는 Detached 상태의 Entity를 다시 영속화 하는데 사용되고 persist는 최초 생성된 Entity를 영속화 하는데 사용된다.

  • Merge : Detached Entity ⇒ Managed Entity
  • Persist : New Entity ⇒ Managed Entity
em.merge() is trying to retrieve an existing entity from DB with a SELECT query and update it. So if the entity was never persisted, this will waste some CPU cycles. On the other side em.persist() will only produce one INSERT query.

참고로 entity가 persisted, 즉 영속된 entity인지는 EntityManager안의 contains함수를 통해 알 수 있다.

em.contains(entity)

 

저장 1

  • pk(객체 타입) is not null -> merge -> 영속성 없음, select 조회, 영속성 부여 -> 조회 결과가 없으니 후에 insert 예약 -> seq가 null로 인서트 예정이었으나 DB에서 넣어줌 -> 트랜젝션 종료쯤 commit시 날아감

저장 2

  • 자식 transaction으로 save한 결과의 entity를 넘겨 받음 -> pk(객체 타입) is not null -> merge -> em.contains(entity) == true -> 영속성 있음, 추가 select 없음 -> dirty checking, 수정된거 확인 후에 update 예약 -> 트랜젝션 종료쯤 commit시 날아감

저장 3

  • key가 auto increment로 생성되는 seq(Long) 값이고 최초에는 null. -> persist -> id(seq)값을 가져오기 위해 insert를 쏴서 얻음

>> 그래서 중간에 insert 쿼리가 뜬금없이 날아감

그리고 저장 3의 persist 된 엔티티는 후에 수정된 사항이 없어서 추가 insert문이 안 날아간다.

 

save 호출 후 엔티티를 사용할 일이 있다면(추가 수정건 등) 영속상태의 엔티티를 반환받아서 사용하는 게 성능에 좋다(추가 select문이 안 날아간다).


참고) chained transaction manager

현재 chained transaction manager 설정이 아래와 같고 저장 1은 USER, 저장 2도 USER, 저장 3은 LOG transaction manager에 속한다.

@Primary
@Bean(Constants.CHAINED_TRANSACTION_MANAGER)
public PlatformTransactionManager transactionManager(
        @Qualifier(Constants.USER_TRANSACTION_MANAGER)
                PlatformTransactionManager userPlatformTransactionManager,
        @Qualifier(Constants.LOG_TRANSACTION_MANAGER)
                PlatformTransactionManager logPlatformTransactionManager,
        @Qualifier(Constants.STATIC_TRANSACTION_MANAGER)
                PlatformTransactionManager staticPlatformTransactionManager) {
    return new ChainedTransactionManager(userPlatformTransactionManager,
            logPlatformTransactionManager,
            staticPlatformTransactionManager);
}
transaction USER begin
  transaction LOG begin
    transaction STATIC begin
    transaction STATIC commit
  transaction LOG commit -> error rollbacks LOG, USER
transaction USER commit -> error, only rollbacks USER
지정된 순서대로 트랜잭션이 실행되고, 지정된 역순으로 트랜잭션이 종료된다.
다시 말해, 에러를 내기 쉬운 트랜잭션을 마지막에 지정해서 트랜잭션 종료 작업이 최초로 호출되도록 해야 한다
왜냐면 에러를 낼 가능성이 높은 트랜잭션이 최초로 롤백이 돼야 그 뒤의 다른 트랜잭션도 따라서 롤백되기 때문이다. ChainedTransactionManager는 단지 트랜잭션 시작과 종료를 동시에 해줄 뿐이지 Two Phase Commit을 지원하는 게 아니라서 이미 다른 트랜잭션이 커밋된 상황에서 하나의 트랜잭션이 롤백 됐다고 해서 이미 커밋된 것들이 다시 롤백되지는 않는다.
따라서 가장 위험한 요소를 최초로 커밋/롤백 시도 하도록 해야 한다.

 


참고 jpa save

https://leegicheol.github.io/jpa/jpa-is-new/

 

Spring Data JPA isNew 메서드

김영한 : 실전! 스프링 데이터 JPA 강의를 공부한 내용입니다.

leegicheol.github.io

https://taesan94.tistory.com/266

 

Spring Data Jpa Insert 할 때 Select가 나가네..

문제 상황 설계한 Entity의 id가 Auto Increament값이 아니다. 생성자가 호출되는 시점에 fk의 조합으로 생성된다. makeReservedSeatId 함수에서 만들어진다. @Entity @Table(name = "reserved_seat") public class ReservedSeat

taesan94.tistory.com

 

728x90
반응형
반응형

수수료 설정하는 화면을 만들고 있는데, 

0.07을 서버에서 받아오면 화면에서 7%로 100을 곱하여 보여줘야하는 부분이다.

 

신기한 점은 다른 숫자들은 별 문제 없는데

이상하게 7만 넣으면(서버에서 0.07로 받아와 100을 곱하면) 아래와 같이 나타나는게 아닌가?!

 

찾아보니 precision number로 정상적인 작동이었다.

https://stackoverflow.com/questions/6486234/mysterious-calculation-error-when-multiply-by-100

 

Mysterious calculation error when multiply by 100

The following script contains a very strange error. I want to check if a value is a positive integer. To do this, I multiply by 100 to enclose the value to decimal. If I test 0.07, the script does ...

stackoverflow.com

 

하지만 화면에서는.. 아무래도 기획자가 싫어할 터..

적당히 라운드를 줘서 표기하는거로 처리하였다.

<span>{{ (settingValue.commissionRate * 100).toFixed(2) }}%</span>
728x90
반응형

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

[js][IE] inline script [object]  (0) 2023.01.11
[ts] typescript utility type  (0) 2022.07.05
[js][IE] Invalid Date in IE  (0) 2022.04.26
[js] 자바스크립트 기초  (0) 2022.01.25
반응형

GET api를 브라우저나 포스트맨으로 api를 쐈을 때는 잘 나오는데, webClient나 curl로 쏘면 아래와 같이 안 나오는 경우가 있다.

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>

 

테스트해 보니 url에 https:// 를 안 붙이면 나는 듯하다.

 

보통은 이렇게 해결하면 되지만 애초에 ssl 설정이 안 되어 있는 곳이 있다.

추가적인 옵션를 주는 방법이 있지 않을까 해서 살펴본다.

curl은 -L 옵션을 주면 아래와 같이 자동 redirect를 해서 결과가 나오는 것을 볼 수 있다. 

-L, --location
(HTTP/HTTPS) If the server reports that the requested page has moved to a different location (indicated with a Location: header and a 3XX response code), this option will make curl redo the request on the new place.
If used together with -i, --include or -I, --head, headers from all requested pages will be shown. When authentication is used, curl only sends its credentials to the initial host. If a redirect takes curl to a different host, it won't be able to intercept the user+password. See also --location-trusted on how to change this. You can limit the amount of redirects to follow by using the --max-redirs option.

When curl follows a redirect and the request is not a plain GET (for example POST or PUT), it will do the following request with a GET if the HTTP response was 301, 302, or 303. If the response code was any other 3xx code, curl will re-send the following request using the same unmodified method.
 curl -L -v 'dev.com/api/external/money-exchange/configuration?exchangeGameType=&sync=false'


*  Trying 10....
* TCP_NODELAY set
* Connected to dev.com (10..) port 80 (#0)
> GET /api/external/money-exchange/configuration?exchangeGameType=&sync=false HTTP/1.1
> Host: dev.com
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Server: nginx/1.18.0
< Date: Tue, 03 Jan 2023 01:05:17 GMT
< Content-Type: text/html
< Content-Length: 169
< Connection: keep-alive
< Location: https://dev.com/api/external/money-exchange/configuration?exchangeGameType=&sync=false
< 
* Ignoring the response-body
* Connection #0 to host dev.com left intact
* Issue another request to this URL: 'https://dev.com/api/external/money-exchange/configuration?exchangeGameType=&sync=false'
*   Trying 10...
* TCP_NODELAY set
* Connected to dev.com (10.) port 443 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=KR; ST=Gyeonggi-do; O=N Corporation; CN=*..com
*  start date: Dec 21 00:00:00 2022 GMT
*  expire date: Jan 21 23:59:59 2024 GMT
*  subjectAltName: host "dev.com" matched cert's "*..com"
*  issuer: C=GB; ST=Greater Manchester; L=Salford; O=Sectigo Limited; CN=Sectigo RSA Organization Validation Secure Server CA
*  SSL certificate verify ok.
> GET /api/external/money-exchange/configuration?exchangeGameType=&sync=false HTTP/1.1
> Host: dev.com
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 200 
< Server: nginx/1.18.0
< Date: Tue, 03 Jan 2023 01:05:17 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< 
* Connection #1 to host dev.com left intact
{"header":{"status":200,"message":"OK","isSuccessful":true},"result":{}}}
* Closing connection 0
* Closing connection 1

 

curl도 -L 옵션을 주면 되니, webclient도 관련 옵션이 있을 것 같아 찾아보았는데..

this.webClient =  webClientBuilder.clone()
    .baseUrl(url)
    .defaultHeaders(httpHeaders -> httpHeaders.set(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
    .clientConnector(new ReactorClientHttpConnector( //
        HttpClient.create().followRedirect(true) ////
    )) //
    .build();

위와 같이 redirect true 옵션을 주면 된다.

 

https://stackoverflow.com/questions/47655789/how-to-make-reactive-webclient-follow-3xx-redirects

728x90
반응형
반응형

목표: 로컬호스트에 https 적용하기

 

1. 소스가 있는 파일까지 가서 cli를 연다.

2. 터미널로 아래 명령어를 입력한다.

 keytool -genkey -alias test-gia-ssl -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650

위 명령어에서 조심해야 할 부분

-alias test-gia-ssl

  • key alias 이름을 원하는 대로 지정

-keystore keystore.p12

  • key store 이름을 원하는대로 지정

 

3. 소스 root에 키파일이 생성되었는지 확인한다.

 

4. 프로퍼티 파일을 열고 아까 키파일 생성 시 입력한 정보를 적어준다.

server.ssl.enabled=true
server.ssl.key-store=keystore.p12
server.ssl.key-store-password=123456
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=test-gia-ssl

 

5. 서버를 띄운다.

6. swagger나 actuator로 테스트를 하면 되는데 http로는 안되고 https를 붙여야 되는 것을 확인한다.

 

728x90
반응형
반응형

목표: Junit 코드를 분석, 리팩토링하는 과정을 공유

  • 보이스카우트 규칙: 소프트웨어 개발에서는 모듈을 체크인할 때, 반드시 체크아웃할 때 보다 아름답게(깨끗하게) 한다는 규칙
The Boy Scout Rule : "Always leave the campground cleaner than you found it."
보이 스카웃 규칙 : 언제나 처음 왔을 때보다 깨끗하게 해놓고 캠프장을 떠날 것.

 

[15-2] 코드 수정

1. prefix 제거

f: 범위 정보

public class ComparisonCompactor {

    private static final String ELLIPSIS = "...";
    private static final String DELTA_END = "]";
    private static final String DELTA_START = "[";

    private int fContextLength;
    private String fExpected;
    private String fActual;
    private int fPrefix;
    private int fSuffix;
    ...
}

 

2. 캡슐화되지 않은 조건문 수정

3. 똑같은 이름의 변수를 여기저기서 사용하지 말자

public String compact(String message) {
    if (expected == null || actual == null || areStringsEqual()) { //
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
    String expected = compactString(this.expected);  //
    String actual = compactString(this.actual); //
    return Assert.format(message, expected, actual);
}
-----------------------------------------------
public String compact(String message) {
    if (shouldNotCompact()) {
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
	String compactExpected = compactString(expected);
	String compactActual = compactString(actual);

    return Assert.format(message, expected, actual);
}

private boolean shouldNotCompact() {
    return expected == null || actual == null || areStringsEqual();
}

 

4. 긍정의 조건문을 만들자

public String compact(String message) {
    if (canBeCompacted()) {
        findCommonPrefix();
        findCommonSuffix();
        String compactExpected = compactString(expected);
        String compactActual = compactString(actual);
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

private boolean canBeCompacted() { //
    return expected != null && actual != null && !areStringsEqual();
}

 

5. 정확한 이름을 부여하라

위 compact 함수는 항상 압축(compact)을 실행하는 게 아니라 조건에 따라 하고 있으며, formatted된 string을 반환한다.

public String formatCompactedComparison(String message) { //
...
}

 

6. 함수는 한가지 일만 해야한다 / 함수, 변수 분리

...

private String compactExpected;
private String compactActual;

...

public String formatCompactedComparison(String message) {
    if (canBeCompacted()) {
        compactExpectedAndActual(); //
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

private void compactExpectedAndActual() { 
    findCommonPrefix();
    findCommonSuffix();
    compactExpected = compactString(expected);
    compactActual = compactString(actual);
}

 

7. 일관성 부족

위에서 3, 4번째 줄이 클래스 변수 세팅하는거고 1, 2번째 줄에서는 함수 안에서 해버림.. 모두 빼버리자..

8. 변수 이름 정확하게(fPrefix -> prefix -> prefixIndex)

private void compactExpectedAndActual() {
    prefixIndex = findCommonPrefix(); // 클래스 변수 세팅; 이름 바꿈
    suffixIndex = findCommonSuffix(); // "
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private int findCommonPrefix() {
    int prefixIndex = 0;
    int end = Math.min(expected.length(), actual.length());
    for (; prefixIndex < end; prefixIndex++) {
        if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) {
            break;
        }
    }
    return prefixIndex;
}

private int findCommonSuffix() { // prefixIndex 사용
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) { 
            break;
        }
    }
    return expected.length() - expectedSuffix;
}

 

9. 숨겨진 시간적인 결합(hidden temporal coupling)

변수 세팅의 순서가 중요하면, 명시적으로 보여줘야한다.

private compactExpectedAndActual() {
    prefixIndex = findCommonPrefix();
    suffixIndex = findCommonSuffix(prefixIndex); //
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private int findCommonSuffix(int prefixIndex) {
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    return expected.length() - expectedSuffix;
}

 

10. 근데 9번이 좀 억지스러워보이고 왜 순서가 필요한지 이유를 설명하지 못한다. 그래서 하나의 함수로 묶으면 좀 더 안전!

private compactExpectedAndActual() {
    findCommonPrefixAndSuffix(); //하나로 합치고
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private void findCommonPrefixAndSuffix() {
    findCommonPrefix(); //맨 첨에 실행해버림
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    suffixIndex = expected.length() - expectedSuffix;
}

private void findCommonPrefix() { //롤백
    int prefixIndex = 0;
    int end = Math.min(expected.length(), actual.length());
    for (; prefixIndex < end; prefixIndex++) {
        if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) {
            break;
        }
    }
}

이제 저 큰 아이를 리팩토링해아겠다는 생각이 든다.

 

10. 캡슐화(for loop 조건, if문 조건)

11. 올바른 변수명 사용(index -> length)

계속 함수를 고치고보니 index가 사실 진짜 index라기보다 길이를 의미하는 것을 알게되었다.

private void findCommonPrefixAndSuffix() {
    findCommonPrefix();
    int suffixLength = 1; //
    for (; suffixOverlapsPrefix(suffixLength); suffixLength++) { //
        if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
            break;
        }
    }
    suffixIndex = suffixLength;
}

private char charFromEnd(String s, int i) {
    return s.charAt(s.length() - i);
}

private boolean suffixOverlapsPrefix(int suffixLength) {
    return actual.length() = suffixLength < prefixLength || expected.length() - suffixLength < prefixLength;
}

length는 1에서 시작하지 않는다.. 관련해서 쫙 바꿔보자

public class ComparisonCompactor {
    ...
    private int suffixLength;
    ...

    private void findCommonPrefixAndSuffix() {
        findCommonPrefix();
        suffixLength = 0;
        for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
            if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
                break;
            }
        }
    }

    private char charFromEnd(String s, int i) {
        return s.charAt(s.length() - i - 1);
    }

    private boolean suffixOverlapsPrefix(int suffixLength) {
        return actual.length() = suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength;
    }

    ...
    private String compactString(String source) {
        String result = DELTA_START + source.substring(prefixLength, source.length() - suffixLength) + DELTA_END;
        if (prefixLength > 0) {
            result = computeCommonPrefix() + result;
        }
        if (suffixLength > 0) {
            result = result + computeCommonSuffix();
        }
        return result;
    }

    ...
    private String computeCommonSuffix() {
        int end = Math.min(expected.length() - suffixLength + contextLength, expected.length());
        return expected.substring(expected.length() - suffixLength, end) + (expected.length() - suffixLength < expected.length() - contextLength ? ELLIPSIS : "");
    }
}
  • computeCommonSuffix에서 +1을 없애고
  • charFromEnd에 -1을 추가하고
  • suffixOverlapsPrefix에 <=를 사용
  • suffixIndex를 suffixLength로

 

12. 죽은 코드는 삭제

있으나마나 한 조건문(항상 참)은 과감히 지워야 한다.

 

[15-5] 최종코드...생략

 

결론

  • 코드를 리팩터링 하다보면 원래 했던 변경을 되돌리는 경우가 흔하다. 리팩터링은 코드가 어느 수준에 이를 때까지 수많은 시행착오를 반복하는 작업이기 때문이다.
  • 리팩토링에는 정답이 있는 것이 아니라 그 당시의 플랫폼이나 언어, 개발환경에 따라 달라지며, 스스로 만족하는 수준에 이를 때까지 반복되는 작업이다.

 

728x90
반응형
반응형

꺼낸 데이터를 dto로 매핑할 때 보통 jpql의 constructor 방식을 사용한다.

constructor에 보낸 named parameter를 결과로 다시 받아오고 싶어서 아래와 같이 select 절에 logType을 다시 받아오도록 했다.

물론 constructor에 파라미터는 잘 추가되어 있다.

@Query(value = "select new com.event.moneyexchange.MoneyExchangeAllResponse$Data( "
    + "count(DISTINCT l.hno), count(l), sum(l.ival1 + l.ival2), sum(l.ival3 - (l.ival1 + l.ival2)), :logType "
    + ") " +
    "from CommonMoneyExchangeLog l " +
    ...
    )
public Data(Long user, Long count, Long money, Long fee, MoneyExchangeLogType type) {
  this.user = user;
  this.count = count;
  this.money = money;
  this.fee = fee;
  this.type = type;
}

그런데 logType을 추가하기 전에는 잘 되던 게, 이거 하나 추가했다고 서버가 안 뜨는 것이다.. (띠용)

 Unable to locate appropriate constructor on class [com.event.moneyexchange.MoneyExchangeAllResponse$Data]. Expected arguments are: long, long, long, long
[cause=org.hibernate.PropertyNotFoundException: no appropriate constructor in class: com.event.moneyexchange.MoneyExchangeAllResponse$Data]

 

아무리 구글링해도 정확히 해결한 사람을 보기가 힘들어

constructor로 받아올 때는 결과 값이 아닌 값들은 자동 생략(omit)되는 가보다 하고 넘어가긴 했다.

혹시 답을 아는 사람이 있다면 꼭 알려주길..ㅠ

 

+ 나랑 동일한 경험을 한 사람..

https://stackoverflow.com/questions/73691528/spring-boot-add-query-parameter-into-the-select-statement

 

Spring Boot Add Query Parameter Into the Select Statement

I have a query like so (which i put one of the parameters of the method into the DTO projection): @Query("SELECT new com....dto.SomeDTO(x.id, x.name, :abc) FROM SomeEntity x WHERE x.property =

stackoverflow.com

 

728x90
반응형

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

[webClient] 301 move permanently  (0) 2023.01.03
[ssl-https] in localhost  (0) 2023.01.02
[spring-jpa] inner class constructor  (0) 2022.12.09
[ExceptionHandler] 컨트롤러 에러처리  (0) 2022.11.23
[web] file download inside jar  (0) 2022.11.02
반응형

jpql 에서 inner class로 데이터를 받고 싶은 경우가 있다.

@Query에서 dto로 바로 받을 수 있는데, 이 때 .으로 inner class를 표시하면 아래와 같이 에러가 난다.

java.lang.IllegalArgumentException: org.hibernate.QueryException: could not instantiate class

static inner class를 지칭할 때는 . 이 아닌 $로 해야한다.

public class MoneyExchangeAllResponse {
  private Data total;
  
  @Getter
  @Setter
  public static class Data{
    private Long user;
    private Long count;
    private Long money;
    private Long fee;

    public Data(Long user, Long count, Long money, Long fee) {
      this.user = user;
      this.count = count;
      this.money = money;
      this.fee = fee;
    }
  }
}


@Query(value = "select new com.event.moneyexchange.MoneyExchangeAllResponse$Data( "
")
...

 

그리고 jqpl의 count는 long을 반환한다..ㅋ

728x90
반응형

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

[ssl-https] in localhost  (0) 2023.01.02
[spring-jpa] constructor에는 쿼리 결과만  (0) 2022.12.13
[ExceptionHandler] 컨트롤러 에러처리  (0) 2022.11.23
[web] file download inside jar  (0) 2022.11.02
[request part] file upload  (0) 2022.10.31
반응형

2022 nhn forward track2 2시

로그인에 사용하는 OAuth : 과거, 현재 그리고 미래(안하운님)

 

현재의 인증/인가에 널리 사용되는 OAuth와 OpenID에 대해 알아보고, IDP들이 제공해 주는 최신 스펙에 대해 알아봅니다.

OAuth가 발전하고 바뀌게 된 배경과 동기를 알아보고, 미래를 위해 개발이 진행되고 있는 프로토콜인 GNAP에 대해서도 알아봅니다.

목차'

1. 인증과 인가

2. OAuth의 시초, 1.0a

3. 표준화의 시작, OAuth 2.0

4. 발전하는 웹에 적응해 나가는 OpenID Connect, Oauth 2.1

5. 미래의 프로토콜, GNAP

 

728x90
반응형
반응형

2022 nhn forward track7 5시

Notification 서비스 자동화 Test 이야기(김태주님)

 

 

 

728x90
반응형

+ Recent posts