728x90
반응형
728x90
반응형
반응형

환경: springboot3, spring batch5, mybatis

그동안 jpa만 주구장창 사용했어서 올만에 Mybatis 설정이다!

 

1. 디비 정보 등록(application.yml)

2. 빈 등록

@Configuration
@RequiredArgsConstructor
@MapperScan(
    value = {"com.batch.ranking.mapper"},
    annotationClass = LogDataSource.class,
    sqlSessionFactoryRef = "LogDbSqlSessionFactory",
    sqlSessionTemplateRef = "LogDbSqlSessionTemplate")
public class LogDataSourceConfig {

  public static final String SOURCE_DATASOURCE_NAME = "LogDbDataSource";

  @Value("classpath:mybatisConfig.xml")
  private Resource configLocation;

  @Bean(SOURCE_DATASOURCE_NAME)
  public DataSource LogDbDataSource() {
    DataSourceProperty dataSourceProperty = //get them from property

    Properties properties = new Properties();
    properties.setProperty("url", dataSourceProperty.getJdbcUrl());
    properties.setProperty("user", dataSourceProperty.getUsername());
    properties.setProperty("password", dataSourceProperty.getPassword());

    AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
    dataSource.setUniqueResourceName(SOURCE_DATASOURCE_NAME);
    dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
    dataSource.setXaProperties(properties);
    dataSource.setMaxPoolSize(connectionPoolProperty.getMaximumPoolSize()); //from property
    dataSource.setMinPoolSize(connectionPoolProperty.getMinimumIdle());

    return dataSource;
  }

//Qualifier is mandatory otherwise it will connect to Primary bean
  @Bean
  public SqlSessionFactory LogDbSqlSessionFactory(
      @Qualifier("LogDbDataSource") DataSource LogDbDataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setConfigLocation(configLocation);
    bean.setDataSource(LogDbDataSource);
    return bean.getObject();
  }

  @Bean
  public SqlSessionTemplate LogDbSqlSessionTemplate(
      @Qualifier("LogDbSqlSessionFactory") SqlSessionFactory LogDbSqlSessionFactory) {
    return new SqlSessionTemplate(LogDbSqlSessionFactory);
  }
}

2-1. mybatis 설정은 자바로 해도 되지만 분리하는 게 가독성이 좋아서 분리하였다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0/EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <typeAlias type="com.batch.adapter.mybatis.handlers.RankingTypeHandler" alias="RankingTypeHandler" />
    </typeAliases>
</configuration>

2-2. 매퍼 마킹하는 어노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogDataSource {}

3. 사용하는 job에서 부를 경우

  private final SqlSessionFactory LogDbSqlSessionFactory;

  @Bean
  @StepScope
  public MyBatisCursorItemReader<Ranking> DailyRankingReader() {
    return new MyBatisCursorItemReaderBuilder<Ranking>()
        .sqlSessionFactory(LogDbSqlSessionFactory)
        .queryId(
            "com.batch.domain.ranking.mapper.DailyRankMapper.selectDailyTop100")
        .build();
  }

4. 매퍼에 쿼리 작성

@LogDataSource
public interface DailyRankMapper {

  List<Ranking> selectDailyTop100();
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.batch.domain.ranking.mapper.DailyRankMapper">
  <resultMap id="sinyutnoriRanking"
    type="com.batch.domain.ranking.model.Ranking">
    <constructor>
      <idArg column="memberid" javaType="java.lang.String" name="memberId"/>
      <arg column="regdate" javaType="java.time.LocalDate" name="registerDate"/>
      <arg column="kind" javaType="com.batch.domain.ranking.type.RankingType" name="RankingType" typeHandler="RankingTypeHandler"/>
      <arg column="gamemoney" javaType="java.lang.Long" name="gameMoney"/>
      <arg column="winrate" javaType="java.lang.Long" name="winRate"/>
      <arg column="matchcnt" javaType="java.lang.Long" name="matchCount"/>
      <arg column="wincnt" javaType="java.lang.Long" name="winCount"/>
      <arg column="defeatcnt" javaType="java.lang.Long" name="defeatCount"/>
      <arg column="ranking" javaType="java.lang.Integer" name="ranking"/>
    </constructor>
  </resultMap>

  <select id="selectDailyTop100" resultMap="Ranking">
    <![CDATA[
    SELECT  memberid
         , regdate
         , kind
         , gamemoney
         , winrate
         , matchcnt
         , wincnt
         , defeatcnt
         , @RNUM := @RNUM + 1 AS ranking
    FROM Table
      ORDER BY (wincnt + defeatcnt + drawcnt) DESC
      , (wincnt / (wincnt + defeatcnt + drawcnt)) * 100 DESC
      , gamemoney
      ) B, (SELECT @RNUM := 0) r
    WHERE @RNUM < 100
    ]]>
  </select>
</mapper>

5. enum으로 바로 꺼내고 싶다면 type handler 작성

public class RankingTypeHandler extends BaseTypeHandler<RankingType> {

  @Override
  public void setNonNullParameter(
      PreparedStatement ps, int i, RankingType parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter.getCode());
  }

  @Override
  public RankingType getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    return RankingType.findByCode(rs.getInt(columnName)).orElse(null);
  }

  @Override
  public RankingType getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    return RankingType.findByCode(rs.getInt(columnIndex)).orElse(null);
  }

  @Override
  public RankingType getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    return RankingType.findByCode(cs.getInt(columnIndex)).orElse(null);
  }
}

 

여기서 궁금한 사항

Mapper interface를 직접 사용하지 않은데도(자바 로직에서) 클래스가 필요한 것인가?

reader/writer를 보면 아래와 같이 직접 db factory를 연결했으니 interface는 필요 없는 게 아니냐는 문의를 주셔서 좀 더 알아본다.

  @Bean
  @StepScope
  public MyBatisCursorItemReader<SinyutnoriRanking> dailyRankingMatchCntReader(
      @Qualifier(HangameLogDataSourceConfig.SESSION_FACTORY) SqlSessionFactory logDb) {
    return new MyBatisCursorItemReaderBuilder<SinyutnoriRanking>()
        .sqlSessionFactory(logDb)
        .queryId(LOG_MAPPER + "selectDailyTop1000UsersByMatchCnt")
        .build();
  }

 

설정이 대략 이런식으로 연결되어 있다고 할 때...

@Configuration
@RequiredArgsConstructor
@MapperScan(
    value = {"com.batch.domain.mapper.gamemapper.*"},
    annotationClass = DataSource.class,         //mapper interface에 해당 어노테이션을 달아야 
    sqlSessionFactoryRef = DataSourceConfig.SESSION_FACTORY,
    sqlSessionTemplateRef = "DbSqlSessionTemplate")
public class DataSourceConfig {
@DataSource
public interface GameMapper {
<mapper namespace="com.batch.domain.mapper.gamemapper.GameMapper">

 

1. interface 삭제 가능? No

:  GameMapper삭제하고 (Config에 annotationClass 주석하니; 하건 안하건 둘 다) 에러 발생

Caused by: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.batch.domain.mapper.gamemapper.GameMapper.insertRank

xml의 namespace가 인터페이스와 연결되어 있어야 쿼리 주입이 가능

2. 직접적인 함수의 호출이 없으므로 함수는 삭제 가능? Yes

xml안에는 <select>, <insert> 등 여러 쿼리가 있지만 직접 호출하지 않으므로 interface에 연결하는 함수는 없어도 된다.

@DataSource
public interface GameMapper {

//  List<Ranking> selectDailyTop1000UsersByMatchCnt();
}
@Bean
@StepScope
public MyBatisCursorItemReader<Ranking> dailyRankingMatchCntReader(
    @Qualifier(DataSourceConfig.SESSION_FACTORY) SqlSessionFactory logDb) {
  return new MyBatisCursorItemReaderBuilder<Ranking>()
      .sqlSessionFactory(logDb)
      .queryId(LOG_MAPPER + "selectDailyTop1000UsersByMatchCnt")
      .build();
}

select 함수가 직접적으로 선언되지 않아도 작동한다.

작동은 되지만 나중에 관리차원에서 헷갈릴까 봐 지울지 말지 약간 걱정은 된다..

 

 

728x90
반응형
반응형

웹 페이지의 성능 최적화는 로딩최적화와 렌더링최적화 두 단계로 나뉜다.

로딩 최적화

브라우저 렌더링

5단계

  • 파싱
    • html을 해석해 DOM 트리 구축
    • html에 포함되어 있거나 리소스로부터 다운 받은 css를 해석해 CSSOM 트리 구축
  • 스타일 계산
    • DOM에 CSSOM 정보 매칭 -> render 트리 그림
      • script, meta, link 태그는 렌더링에 반영하지 않음
      • CSS로 감춘 노드 역시 렌더 트리에 포함하지 않음
  • 레이아웃(=리플로우)
    • 브라우저의 뷰포트 안에서 노드가 가져야 할 정확한 위치와 크기를 계산
    • 객체의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하면서 계산
    • 레이아웃 결과로 각 노드의 정확한 위치와 크기값을 픽셀값으로 렌더 트리에 반영
  • 페인트
    • 레이아웃에서 계산된 값을 이용해 렌더트리의 각 노드를 화면 상의 실제 픽셀로 변환함
    • 위치와 관계 없는 CSS 속성들이 적용됨 (색상, 투명도 등)
  • 합성 & 렌더
    • 페인트 된 레이어들을 합성하여 스크린을 업데이트
    • CSS Transform 동작

상세: https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/

 

로딩 최적화

로딩 최적화 기준: 브라우저

  • W3C Navigation Timing API - Processing Model: https://www.w3.org/TR/navigation-timing-2/
  • 전통적인 로딩 최적화의 기준이 됨.
  • DOMContentLoaded, Load 이벤트 시점이 빠르다 => 브라우저가 페이지에 포함된 리소스를 준비하는 것이 빠르다.

로딩 최적화 기준: 사용자

  • 같은 크기의 리소스, 같은 타이밍에 Load이벤트가 발생해도 얼마든지 사용자 입장에서 더 빨라 보이는 페이지가 있다.
  • 조금조금씩 보여주면 사람들은 기다릴 수 있다(https://web.dev/articles/critical-rendering-path?hl=ko)

관련 지표들

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Speed Index (SI)
  • Interaction to Next Paint (INP)
  • Toatal Blocking Time (TBT)
  • Cumulative Layout Shift (CLS)

로딩, 렌더링 지표 비유

  • 뭔가 진행되고 있구나 : FP, FCP
  • 이제 원하는 내용을 읽을 수 있다 : LCP
  • 이제 얼추 동작하는구나 : FID, INP, TTI
  • FCP, LCP, INP를 향상시키는데 주요한 리소스 기준으로 최적화
  • 메인 섹션의 컨텐츠, 현재 경로의 내용등을 먼저
  • 메뉴, 배너, 공지사항, 분석툴, 다음 경로 캐싱등을 나중에

 

CSS 최적화

CSS : render blocking resource

  • 렌더 트리를 구성하기 위해서는 DOM 트리와 CSSOM 트리가 모두 필요함
  • DOM 트리는 순차적으로 구성될 수 있지만, CSSOM 트리는 전체 CSS 를 모두 해석해야 구성 가능 (캐스캐이딩 방식)
  • CSSOM 트리가 구성되기 전까지는 렌더 트리를 만들 수 없음
  • media 속성에 따라 Blocking을 피할 수 있음

최적화 가이드

  • CSS 는 항상 최상단 (head 영역)에 배치한다.
<head>
  <link href="style.css" rel="stylesheet">
</head>
  • media 쿼리를 올바르게 사용한다.
<link href="style.css"    rel="stylesheet">
<link href="style.css"    rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css"    rel="stylesheet" media="print">
  • @import 를 사용하지 않는다.
  • 경우에 따라 CSS 를 HTML에 인라인으로 포함시킨다.(네트워크 요청 수 줄이기)

 

자바스크립트 최적화

자바스크립트 : parser blocking resource

  • 자바스크립트는 DOM 과 CSSOM 을 동적으로 변경할 수 있음
  • 자바스크립트는 자신이 실행되기 직전까지의 DOM 트리에만 접근이 가능함
  • HTML 파싱 과정에서 script 태그를 만나면 스크립트 실행이 완료될 때까지 DOM 트리 생성이 중단됨
  • 외부 자바스크립트의 경우 모든 스크립트가 다운로드 된 후 실행될 때까지 DOM 트리 생성이 중단됨
  • 자바스크립트 실행은 CSSOM 이 완성될 때까지 중단됨

최적화 가이드

  • 자바스크립트는 항상 문서의 최하단 (</body> 직전) 에 배치
<body>
  <div>...</div>
  <div>...</div>
  <script src="app.js" type="text/javascript" />
</body>
  • 초기 렌더링에 쓰이지 않는 스크립트는 defer, async 속성을 명시하여 Blocking 을 방지
    • defer IE10 >= 지원
    • async는 순서보장 없음
    • defer는 순서보장(IE9에서는 순서보장안됨)
    • async vs defer attributes
<head>
    <link rel="preload" href="style.css" as="style">
    <link rel="preload" href="main.js" as="script">

    <link rel="stylesheet" href="style.css">
</head>
<body>
    ...
    <script src="https://google.com/analatics.js" type="text/javascript" defer />
    <script src="main.js"></script>
</body>

 

리소스 최적화

  • 브라우저, 사용자기준에서 모두 효과적
  1. 요청(Request) 줄이기
  2. 실제 리소스의 용량 줄이기

 

요청을 줄이는 방법

  1. 중복되거나 불필요 파일 제거
  2. 하나의 JS, CSS 파일 사용
    • webpack을 이용한 번들링
    • 단순한 concat도 의미가 있음
  3. HTML, CSS로 대체 가능한 이미지 제거(작은 이미지인 경우 base64 이미지도 방법)
  4. 이미지 Sprites
    1. 작은 이미지가 여러개 들어간 웹앱
    2. css의 backgound-position을 설정해 사용
    3. Webpack 사용자의 경우 webpack-spritesmith를 사용하여 이미지 합치기와 css 설정 등 자동화 가능

 

불필요 데이터 제거

  • CSS
    • 간결한 css selector 사용
    • 불필요한 css rule 제거
  • JS
    • 만능 util.js 정리
    • 오버스펙 라이브러리 지양
    • 파일에 포함된 sourcemap 제거
  • HTML 마크업 최적화
    • HTML을 단순하게 구성한다 (태그의 중첩을 최소화한다)
    • 공백, 주석 등을 제거한다

Minify (Uglify)

  • HTML, Javascript, CSS 모두 Minify 해서 사용
  • 불필요한 주석이나 공백등을 제거할 수 있음

 

<요약>

네트워크 리소스 최소화

  • 네트워크 요청의 개수를 줄인다 (Javascript 와 CSS 파일을 가능한한 합친다)
  • 아이콘이 많은 경우 이미지 스프라이트를 사용한다
  • HTML, CSS, Javascript 를 Minified 해서 사용한다

크리티컬 렌더링 패스 최적화

  • HTML 과 CSS 의 구조를 최대한 단순하게 만든다
  • CSS는 HTML 문서의 최상단에 배치한다
  • CSS의 media 타입을 정확하게 지정한다
  • 크리티컬 CSS는 인라인 시킨다
  • Javascript 는 HTML 문서의 최하단에 배치한다
  • 초기 로딩과 렌더링에 꼭 필요없는 Javascript는 async나 defer 속성을 사용한다

렌더링 최적화

DOM 조작으로 인한 렌더트리 변경

JavaScript

  • 자바스크립트 코드를 통해 동적으로 Style 속성 or 클래스명을 변경
  • CSS Animation (Transition) 등으로 인한 변경도 이 과정에 속함

Style 계산

  • 어떤 CSS 룰이 어떤 DOM 요소에 적용되어야 하는지를 계산하는 과정

Layout

  • 실제 그려질 좌표정보를 픽셀단위로 계산하는 과정
  • 자식이나 형제 요소들에게 영향을 줌

Paint

  • Layout 과정에서 정해진 영역에 픽셀을 채우는 과정

Composite

  • 각각의 분리된 레이어들을 합성하는 과정

 

리플로우와 리페인트

  • 리플로우는 전체 픽셀을 다시 계산해야 하기 때문에 부하가 큼
  • 리페인트는 실제 계산된 픽셀로 화면에 그리는 과정이기 때문에 주로 부하가 적음

DOM 트리 변경

  • DOM 추가 / 삭제
  • 리플로우 발생

위치나 사이즈 변경

  • CSS 속성 중 높이, 넓이, 위치 등에 영향을 주는 속성값 변경
  • height, width, left, top, font-size, line-height 
  • 리플로우 발생

색상이나 투명도 설정

  • CSS 속성 중 높이, 넓이, 위치 등에 영향을 주지 않는 속성값 변경
  • background-color, color, visibility, text-decoration 
  • 리페인트 발생

레이아웃 최적화 방법

CSS 규칙 수 최소화

  • 사용하는 규칙이 적을 수록 계산이 빠름
  • 복잡한 selector도 지양해야 함

DOM 깊이 최소화

  • 문서가 작고 얕을 수록 계산이 빠름

가능한한 하위 노드의 스타일을 변경

  • DOM 트리 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 미치기 때문
  • 변경범위를 최소화할수록 레이아웃 범위가 줄어듦

숨겨진(display: none) 엘리먼트 수정

  • 숨겨진 엘리먼트를 변경할 경우에는 레이아웃이 동작하지 않음

영향 받는 엘리먼트 제한

  • 변경으로 인해 영향을 받는 엘리먼트를 제한 해야 함
  • ex: 콘텐츠로 인해 높이가 변경 될 경우 위 아래 위치한 엘리먼트들에 위치에도 영향을 줌
    • fixed 혹은 absolute position을 사용하여 영향을 받은 엘리먼트를 제한 해야 함

애니메이션

목표 : 60fps

  • 한 프레임에 대한 처리가 16ms 내로 완료되어야 함 (브라우저가 동작하는 시간을 고려하면 10ms 이내)

requestAnimationFrame() 사용

  • 브라우저의 프레임 속도 (60fps) 에 맞추어 애니메이션을 실행할 수 있도록 해줌(혹은 모니터 주사율에 맞추어준다)
  • 정확한 시간에 호출됨(프레임 시작 시 호출), setTimeout, setInterval은 프레임 종료 시 호출되므로 일정하지 않음
  • 현재 페이지가 보이지 않는 상태인 경우에는 렌더링이 발생하지 않도록 해 줌

position: absolute 처리

  • position을 absolute나 fixed로 설정하면 주변 레이아웃에 영향을 주지 않음
  • 애니메이션 영역이 주변 영역에 영향을 주지 않도록 할 것

transform 사용

  • position, width, height 등은 Layout 을 발생시킴
  • transform 은 Composite 만 발생시키기 때문에 훨씬 빠름 (GPU 사용)

구형 브라우저의 일괄처리

동일한 요소의 스타일을 여러 번 변경

const myelement = document.getElementById('myelement');

myelement.style.width = '100px';
myelement.style.height = '200px';
myelement.style.margin = '10px';
  • 3번의 리플로우가 예상됨

스타일 변경은 한 번에 모아서 처리

<style>
  .newstyles {
      width: 100px;
      height: 200px;
      margin: 10px;
  }
</style>

<script>
  const myelement = document.getElementById('myelement');
  
  myelement.classList.add('newstyles');
</script>
  • 클래스 사용으로 한 번으로 줄일 수 있음

그 외

  • DOM 요소 추가 시에도 appendChild 를 여러 번 하면 리플로우 여러 번 발생 -> innerHTML 사용
  • 최신 브라우저에서는 최적화 필요 없음

강제 동기 레이아웃

강제 동기 레이아웃 피하기

  • 스타일 변경 후 offsetHeight, offsetTop과 같은 계산된 값을 요청할 경우 강제로 레이아웃을 수행함
const tabBtn = document.getElementById('tab_btn');
const menuBtn = document.getElementById('menu_btn');

tabBtn.style.fontSize = '24px';

console.log(testBlock.offsetTop); // offsetTop 호출 직전 레이아웃

tabBtn.style.margin = '10px';
// 레이아웃
  • 레이아웃 이유
    • 계산된 값을 반환하기 전에 변경된 스타일이 계산 결과에 적용되어 있지 않으면 변경 이전 값을 반환하기 때문
  • 최신 브라우저에도 영향 받는 부분이므로 강제 동기 레이아웃을 하지 않도록 주의해야 함

레이아웃 스래싱(thrashing) 피하기

  • 많은 레이아웃을 연속적으로 빠르게 실행하는 경우 강제 동기 레이아웃에서 더 좋지 않은 결과를 나타냄
function resizeAllParagraphs(paragraphs) {
  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}
  • 이 코드는 단락 그룹을 반복 실행하고 각 단락의 너비를 상자의 너비와 일치하도록 설정함
  • 반복문 안에서 style.width를 설정하고 box.offsetWidth를 호출함으로 인해 매 반복 시 마다 레이아웃 발생
  • 레이아웃이 대량 발생함

개선 방법

  • 박스 너비를 읽어오는 부분을 분기분 바깥 쪽에서 수행하면 레이아웃 스래싱을 막을 수 있음
function resizeAllParagraphs(paragraphs) {
  const width = box.offsetWidth;

  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = width + 'px';
  }
}

강제 동기 레이아웃을 일으키는 동작 정리

 

메모리 관리

  • 의도하지 않은 전역 변수 사용
  • 잊혀진 타이머
  • 참조된 DOM Node 삭제
  • 클로저 사용에 따른 누수

메모리 누수 사례 및 해결방법

의도하지 않은 전역 변수 사용

메모리 누수

  • 전역 함수 내부에서 선언문(ex: const) 없이 변수를 사용하는 경우 해당 변수는 전역에 선언됨
function foo(arg) {
    bar = "의도하지 않은 전역 변수";
}
  • 전역 변수에 값을 할당하면 해당 함수가 종료되더라도 전역에 값에 대한 참조가 남게 되어 메모리 누수 발생
function foo(arg) {
    this.bar = "이 경우도 전역에 선언됨";
}
foo();
  • 전역 함수 선언문 안에서 this를 사용하면 전역에 선언됨

해결 방법

  • use strict 사용하여 의도 되지 않은 전역 변수를 미연에 방지

잊혀진 타이머

메모리 누수

  • Javascript에서 타이머로 사용하는 setInterval 함수는 일반적으로 아래의 코드와 같이 사용함
let someResource = getData();
const interval = setInterval(function() {
    const someNode = document.getElementById('some-node');
    if(someNode) {
        someNode.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
  • someNode 엘리먼트(id : 'some-node')가 유효할 경우에는 문제가 없음
  • 그러나 someNode 엘리먼트가 삭제될 경우 Interval 함수에서 사용하는 someResource는 불필요 함에도 불구하고 메모리 상에 남아있음

해결 방법

  • someNode가 삭제 될 경우 타이머 자체를 중단하여 불필요 메모리를 해제함
someResource = null; // IE7 이하 브라우저
clearInterval(interval);

참조된 DOM Node 삭제

메모리 누수

const elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    elements.image.src = 'http://some.url/image';
    elements.button.click();
    console.log(elements.text.innerHTML);
    // ...
}

function removeButton() {
    document.body.removeChild(document.getElementById('button'));
}
  • removeButton()을 수행할 경우 '#button'은 삭제되지만 '#button'에 대한 참조(elements.button)는 남아 있게 됨
  • 이 때문에 '#button'은 GC되지 않음

해결 방법

  • removeButton() 함수에서 #button 삭제 시 참조도 같이 삭제함
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    elements.button = null;
}

이중 클로저로 인한 메모리 누수

  • 이중 클로저 사용 시 메모리 누수가 발생할 수 있음
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);
  • 매번 타이머 실행시 마다 새로운 함수가 실행
  • 과정 설명
    1. setInterval 1회 실행
      • [fn1]originalThing = null;
      • [fn1]theThing = {...};
    2. setInterval 2회 실행
      • [fn2]originalThing = [fn1]theThing;
      • [fn2]theThing = {...};
    3. setInterval 3회 실행
      • [fn3]originalThing = [fn2]theThing;
      • [fn3]theThing = {...};
    4. ...
    • 전제 조건
      • 실행 함수를 횟수에 따라 다음과 같이 표기하고 변수와 값에 prefix로 붙임
        • 첫번째 실행 함수 : [fn1]
        • 첫번째 실행 함수의 변수 a : [fn1]a
  • 타이머 실행으로 새로운 함수가 실행될 때 마다 이전 함수의 변수를 참조함

이중 클로저 메모리 누수 해결 방법

  • 타이머와 같이 반복적으로 매번 실행되는 함수에서 클로저 사용 시 이전 실행 함수의 변수를 참조하지 않도록 주의해야 함

누수 확인: 크롬 개발도구 > Memory 탭

  • Heap Snapshot
    • 페이지의 자바스크립트 객체와 관련된 DOM 노드 사이의 메모리 분포를 보여줌
    • 스냅샷끼리 비교해서 메모리 누수를 찾아낼 수 있음
  • Allocation instrumentation on timeline
    • 시간의 흐름에 따라 메모리 누수를 확인할 수 있음
  • Allocation sampling
    • 메모리가 할당된 Javascript 함수를 보여줌
728x90
반응형
반응형

환경: springboot 2.7.6, mysql 8

 

@Id로 엔티티의 primitive key를 명시할 때 꼭 wrapper 클래스로 작성해야 하는지에 대한 의문이 생겼다.

wrapper클래스의 경우 null을 허용하는데, 엔티티를 저장할 때 키 값이 없으면(null) 자동 생성해 주는 기능 때문에 0이 의도치 않게 들어가면 이슈가 있을 수 있다고 생각했다.

하여 정리해본다.

 

id의 경우 아래와 같은 케이스가 있다.

  • select 시: id는 not null이기 때문에 굳이 wrapper를 쓸 일이 없음
  • insert 시:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long seq;
  • jpa의 @GeneratedValue(strategy = GenerationType.IDENTITY)가 선언된 id 콜롬 & 디비에서 AUTO_INCREMENT 설정된 경우
    • 해당 경우는 디비의 설정을 사용한다는 뜻으로 mysql에 설정된 seq bigint(20) NOT NULL AUTO_INCREMENT COMMENT처럼 AUTO_INCREMENT 옵션이 들어간 경우 1부터 저장하기 때문에 0은 무시
  @Id
  private long seq;
  • @GeneratedValue 가 선언되지 않은 id 콜롬 & 디비에서 AUTO_INCREMENT 설정된 경우
    • 0 자체가 null과 동일하게 auto increment를 생성하라는 의미이기 때문에 0이 저장되지 않고 다음 sequence가 저장됨, AUTO_INCREMENT 옵션이 들어간 경우 1부터 저장하기 때문에 0은 무시
  • @GeneratedValue 가 선언되지 않은 id 콜롬 & 디비에서 AUTO_INCREMENT 설정되지 않은 경우
    • 0이 저장됨

참고로 mysql 기본 설정은 AUTO_INCREMENT일 때 0은 제외한다.
sql_mode항목에 NO_AUTO_VALUE_ON_ZERO 값이 설정되어 있으면 auto increment 시 0을 추가한다.

각자 디비에 어떻게 설정되어 있는지는 아래 쿼리로 확인 가능하며

SHOW VARIABLES LIKE 'sql_mode';

혹시.. 0을 의미 있는 값으로 세팅하려거든 아래처럼 하면 된다.

SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';

 

즉, id값을 primitive로 사용할지 wrapper로 사용할지에 대한 판단은, db에서의 0에 대한 설정과 값의 null/0 여부를 파악 후 결정하는 게 좋겠다

728x90
반응형
반응형

collation 이란

데이터베이스의 문자열 Datatype(CHAR, VARCHAR, TEXT 등)에는 캐릭터 셋(Character set)과 콜래이션(Collation)이라는 속성이 있다.

캐릭터 셋(Character set)은 각 문자가 컴퓨터에 저장될 때 어떻게 저장될지(encoding)에 대한 규칙의 집합이고,

콜래이션(Collation)은 특정 캐릭터 셋(Character set)에 의해

  • 데이터베이스에 저장된 값들을 비교 검색(where clause)하거나
  • 문자들을 서로 정렬(order by) 등의 작업을 위해 비교할 때
  • 그리고 인덱싱을 할 때

사용하는 규칙들의 집합을 의미한다.

예를 들어 

  • int형은 123 < 345 으로 명확히 비교할 수 있고
  • date형은 2013-01-01 < 2022-01-01로 명백하나

문자열의 경우

  • '가'와 '나' 중 어느 것이 큰지
  • 'a'와 'A' 중 어느 것이 큰지 
  • '가'와 'ㄱㅏ'는 어떻게 비교해야 하는지

혼란스럽다. 이와 관련하여 정리된 방식이 collation이라고 생각하면 된다.

 

대표적인 collation 타입

  • utf8mb4_bin
    • binary 저장 값으로 정렬; 각 문자를 byte 취급하여 byte 값을 비교(언어적인 규칙이 고려되지 않음)
    • A는 41, a는 61이기 때문에 오름차순 정렬 시 A~Z 다음 a~z가 정렬된다.
      • SELECT HEX(WEIGHT_STRING('A' collate utf8mb4_bin)) as 'A',  HEX(WEIGHT_STRING('a' collate utf8mb4_bin)) as 'a'  FROM dual;
  • utf8mb4_general_ci
    • 간단하고 빠르게 사용할 수 있는 타입
    • 모든 유니코드가 고려된 건 아니지만 일반적으로 많이 사용됨
      • 유니코드 중 Basic Multilingual Plane (BMP)를 벗어나면 정렬이 틀리게 될 수 있음
      • 하지만 중국어(C), 일본어(J), 한국어(K) 통칭 CJK는 BMP에 포함되어 있어 국내에서도 잘 쓰이는 타입
    • case insensitive로 A는 41, a도 41로 같기 때문에 A와 a가 혼용되어 정렬된다.
      • SELECT HEX(WEIGHT_STRING('A' collate utf8mb4_general_ci)) as 'A',  HEX(WEIGHT_STRING('a' collate utf8mb4_general_ci)) as 'a'  FROM dual;
  • utf8mb4_unicode_ci
    •  모든 유니코드를 고려한 정렬 규칙으로 고려하는 규칙 자체가 많아 utf8mb4_general_ci 방식보다 느림
      • 한국어, 영어, 중국어, 일본어 사용 환경에서는 utf8mb4_general_ci와 동일한 결과를 냄
      • 더 특수한 문자의 정렬 순서는 달라질 수 있음
    • case insensitive로 A는 0E33, a도 0E33으로 같기 때문에 A와 a가 혼용되어 정렬된다.
      • SELECT HEX(WEIGHT_STRING('A' collate utf8mb4_unicode_ci)) as 'A', HEX(WEIGHT_STRING('a' collate utf8mb4_unicode_ci)) as 'a'  FROM dual;

 

간단하게 collation 타입 읽는 법

ex. utf8mb4_0900_ai_ci

  • utf8mb4 : 문자 하나당 1~4byte 할당(mb4 : 4byte 지원), 바로 이어서 지역 및 언어를 나타내는 단어로 세분화되기도 함
    • utf8mb3는 3byte가 할당되는 방식으로 mysql8에서 deprecated
  • 0900 : Unicode Collation Algorithm (UCA) version 9.0 표준을 따른다는 뜻
    • mysql8 추가; 더 세분화된 정렬법 적용
  • ai : accent insensitive (이전버전에서는 악센트 구분이 안되었으며 MySQL 8.0부터 추가됨)
  • ci : case insensitive (대소문자 구분하지 않음)
 

그 외 아래와 같은 표기법이 사용될 수 있음

Suffix
Meaning
비고
_ai
Accent-insensitive
mysql8 추가
_as
Accent-sensitive
mysql8 추가
_ci
Case-insensitive
 
_cs
Case-sensitive
 
_ks
Kana-sensitive
mysql8 추가
일본어 히라가나-가타카나 같게할지 여부
_bin
Binary
 
  • ​MySQL 8.0.1 버전부터 utf8mb4_0900_ai_ci이 기본값임

 

mysql8부터 추가된 collation type의 pad attribute

  • 이전 버전에서는 PAD SPACE를 사용하던 것과 다르게 NO PAD 속성이 생김
    • 기본 값은 PAD SPACE
  • NO PAD 속성은 문자열 끝에 빈 문자열이 있는 경우에 문자열 비교 시 공백까지 포함하여 비교함(공백도 의미를 가진다는 전제)
  • 따라서 정렬 시 의도한 대로 정렬이 되지 않을 수 있으니 사용하고 있는 collation type이 어떤 pad attribute를 갖는지 확인해야 함

 

그냥 접속 시 매번 collation이 다르게 접속될 수 있음

  • jdbc 커넥터 버전에 따라 커넥션 별 collation이 달라질 수 있음
  • 워크벤치에서도 지정하지 않으면 외부 영향을 받아 달라질 수 있음
  • 따라서 커넥터 레벨에서 collation을 지정하거나 접속 시 명령어에 아래 내용 추가하여 항상 고정된 collation으로 붙을 수 있도록 하는 게 좋음
-- Set the character set and collation for the session
SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;

 

collation이 다르게 접속이 될 경우 데이터 저장 시 에러 발생할 가능성 있음

문자열을 기준으로 다음 작업을 할 때 아래 에러가 발생할 수 있음:

  • 테이블을 join 하거나
  • 값을 비교, 필터링하거나
  • string 연산(concat 등)을 할 때
오류 코드: 1267Illegal mix
of collations (utf8_general_ci, IMPLICIT) and (utf8_unicode_ci, IMPLICIT) for
operation '='

이 경우 내 로컬(커넥션) 설정과 서버의 collation 설정이 다르거나 테이블, 콜롬 등에 설정된 collation이 상이하기 때문으로 collation에 대해서 확인 필요

그럴리는 없겠지만 혹시 테이블, 콜롬별 collation 설정이 다르다면 아래와 같이 join 조건에 collate 타입을 명시해야 한다.

SELECT *
FROM table1
JOIN table2 ON table1.name = table2.description COLLATE utf8_general_ci;

 

지금 내가 사용하는 디비에서 collation 값 조회하는 방법

  • mysql8
show variables where variable_name like '%collation%';

Variable_name                |Value             |
-----------------------------+------------------+
collation_connection         |utf8mb4_0900_ai_ci|
collation_database           |utf8mb4_general_ci|
collation_server             |utf8mb4_general_ci|
default_collation_for_utf8mb4|utf8mb4_0900_ai_ci|


#전체 값 확인
SHOW COLLATION WHERE Charset = 'utf8mb4';


Collation                 |Charset|Id |Default|Compiled|Sortlen|Pad_attribute|
--------------------------+-------+---+-------+--------+-------+-------------+
utf8mb4_0900_ai_ci        |utf8mb4|255|Yes    |Yes     |      0|NO PAD       |
utf8mb4_0900_as_ci        |utf8mb4|305|       |Yes     |      0|NO PAD       |
utf8mb4_0900_as_cs        |utf8mb4|278|       |Yes     |      0|NO PAD       |
utf8mb4_0900_bin          |utf8mb4|309|       |Yes     |      1|NO PAD       |
utf8mb4_bg_0900_ai_ci     |utf8mb4|318|       |Yes     |      0|NO PAD       |
...생략
  • mysql5
show variables where variable_name like '%collation%';

Variable_name       |Value             |
--------------------+------------------+
collation_connection|utf8mb4_general_ci|
collation_database  |utf8mb4_general_ci|
collation_server    |utf8mb4_general_ci|


#전체 값 확인
SHOW COLLATION WHERE Charset = 'utf8mb4';

Collation             |Charset|Id |Default|Compiled|Sortlen|
----------------------+-------+---+-------+--------+-------+
utf8mb4_general_ci    |utf8mb4| 45|Yes    |Yes     |      1|
utf8mb4_bin           |utf8mb4| 46|       |Yes     |      1|
utf8mb4_unicode_ci    |utf8mb4|224|       |Yes     |      8|
utf8mb4_icelandic_ci  |utf8mb4|225|       |Yes     |      8|

 

collation 변경 방법

1) 데이터베이스 레벨

ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

2) 테이블 레벨

ALTER TABLE my_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ;

3) 콜롬 레벨

ALTER TABLE mytable MODIFY name VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci;

4) 세션 레벨

SET collation_connection = 'utf8mb4_general_ci' ;

5) 쿼리 레벨(insert문 동일)

SELECT 
    @_now := now(),
    @ver := '1.17.0' collate utf8mb4_general_ci , 
    @domain := 'my domain' collate utf8mb4_general_ci , 
    @port_ := '1234'
;

6) 서버 레벨

[mysqld]
character-set-server=latin1
collation-server=latin1_swedish_ci

 

결론

  • 같은 문자셋이라도 콜레이션에 따라 영어의 경우 대소문자의 구분, 일본어의 경우 히라가나와 가타카나의 구분, 한글 자음과 결합문자를 구분하는 방법 등이 달라짐
  • 관련해서 정렬 시 정확도와 검색 속도에 영향이 있음
  • MySql 5-> 8로 올릴 때 collation 설정 값이 정렬 등에 영향을 줄 수 있다는 것을 인지할 필요 있음
  • MySQL 8.0의 기본 collation 인 utf8 mb4_0900_ai_ci는 utf8이며 글자당 4byte까지 저장하고, 0900 버전의 UCA 규칙을 따르며 accent, 대소문자, 히라가나와 가타카나, 한글 자음과 결합문자를 구분하지 않음

관련 상세 내용은 버전별 공식 문서를 확인하자

https://dev.mysql.com/doc/refman/8.4/en/charset.html

 

MySQL :: MySQL 8.4 Reference Manual :: 12 Character Sets, Collations, Unicode

MySQL 8.4 Reference Manual  /  Character Sets, Collations, Unicode Chapter 12 Character Sets, Collations, Unicode MySQL includes character set support that enables you to store data using a variety of character sets and perform comparisons according to a

dev.mysql.com

https://dev.mysql.com/doc/refman/5.7/en/charset.html

 

MySQL :: MySQL 5.7 Reference Manual :: 10 Character Sets, Collations, Unicode

MySQL 5.7 Reference Manual  /  Character Sets, Collations, Unicode Chapter 10 Character Sets, Collations, Unicode MySQL includes character set support that enables you to store data using a variety of character sets and perform comparisons according to a

dev.mysql.com

관련 내용 테스트 한 블로그

https://blog.naver.com/sory1008/223071678680

728x90
반응형

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

[mysql] delete, drop, truncate  (0) 2024.10.02
[mysql] basic functions  (0) 2024.09.09
DB isolation level  (0) 2024.05.22
[mysql] merge into..?  (0) 2024.05.17
[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
반응형

(DB가 아닌) 애플리케이션 단 락에 대해 알아본다.

2023.01.12 - [개발/spring] - [jpa] lock종류와 사용 시 주의사항

 

Optimistic Locking 낙관적 락

  • @Version 어노테이션 사용(엔티티 당 하나)
  • 최초 커밋만 인정하는 방법
  • entity에 바로 락을하는 것이 아니라 버전 넘버를 저장하는 방식
    • 초기 버전 값은 0
  • 저장할 때 버전 넘버가 다르거나(해당 row못찾음) 0이면 롤백하고 에러 발생
  • intIntegerlongLongshortShortjava.sql.Timestamp
    • we can also use other approaches, such as timestamps, hash value computation, or serialized checksum.
  • 버전 애트리뷰트는 엔티티를 통해 읽을 수 있지만 절대 개발자가 직접 업데이트하거나 증가시켜선 안된다
    • 벌크 연산은 버전을 무시하기 때문에 벌크 연산을 할 경우 버전 필드를 직접 증가시켜야 한다
@Version
@UpdateTimestamp
@Column(nullable = false)
private LocalDateTime updated;

 

Pessimistic Locking 비관적 락

  • DB에서 제공하는 락기능을 사용
  • entity에 접근하는 순간 락이 걸림
  • 트랜잭션끼리의 충돌이 발생한다고 가정하고 우선 락을 거는 방법
  • @Lock 어노테이션 사용
  • 락이 실행할 때 transaction이 없으면 에러 발생(transactionRequiredException)
  • 락을 바로 얻을 수 없으면 LockTImeoutException 던짐
    • timeout 설정 필요: 락을 잡고 있는 최대 시간
      • QueryHint 이용; javax.persistence.lock.timeout; 단위 ms
    • 모든 DBMS가 지원하는 건 아님
  • If the time it takes to obtain a lock exceeds the value of this property, a LockTimeoutException will be thrown, but the current transaction will not be marked for rollback.

 

@Lock(LockModeType.PESSIMISTIC_READ)
@QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")})
public Optional<Customer> findById(Long customerId);

 

 

LockModeType

public enum LockModeType {
  READ,
  WRITE,
  OPTIMISTIC,
  OPTIMISTIC_FORCE_INCREMENT,
  PESSIMISTIC_READ,
  PESSIMISTIC_WRITE,
  PESSIMISTIC_FORCE_INCREMENT,
  NONE;

 

optimistic lock 관련

  • NONE
    • 락 모드를 적용하지 않아도 엔티티에 버전 애트리뷰트가 있으면 Optimistic Locking이 적용된다
    • Second Lost Update Problem을 예방할 수 있다
    • 조회한 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다(UPDATE 쿼리 사용)
    • 이때 데이터베이스의 버전 값이 현재 버전이 아니면 예외 발생
  • OPTIMISTIC
    • NONE 을 사용하면 엔티티를 수정해야 버전을 체크하지만 OPTIMISTIC을 사용하면 엔티티를 조회만 해도 버전을 체크한다
    • 쉽게 얘기하면 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장한다
    • dirty read와 non repeatable read를 방지한다
    • 트랜잭션을 커밋할 때 버전 정보를 조회해서(SELECT 쿼리 사용) 현재 엔티티의 버전과 같은지 검증하고 같지 않으면 예외가 발생한다.
  • OPTIMISTIC_FORCE_INCREMENT
    • Optimistic Locking을 사용하면서 버전 정보를 강제로 증가한다
    • 논리적인 단위의 엔티티 묶음을 관리할 떄 사용한다
    • 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해 버전을 강제로 증가시킨다
    • 이때 데이터베이스의 버전이 엔티티 버전과 다르다면 예외가 발생한다
    • 추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다 따라서 총 2번의 버전 증가가 나타날 수 있다

persistent lock 관련

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

jpa에 위와 같이 이름으로 ForUpdate 줘도 만들어지는 쿼리는 select for update로 나가게 된다.

728x90
반응형
반응형

springframework에서 제공하는 spring retry에 대해 알아보자

maven repo: https://mvnrepository.com/artifact/org.springframework.retry/spring-retry

 

spring aspect 라이브러리를 사용할 수도 있는데, 해당 라이브러리는 springboot starter에 있을 수 있기에 한번 확인하고 넣는 게 좋다.

springboot2.7.3 기준 5.3.24 버전이 들어가 있다.

 

관련 어노테이션을 사용하려면 아래처럼 enable 시켜줘야 한다.

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class AppConfig {
}

실행 원리는 proxy!!

 

재시도를 해야 하는 함수에 @Retryable을 달아주고

재시도 시 실행해야하는 함수에 @Recover를 달아준다.

import org.springframework.retry.annotation.Recover;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Retryable(value = { SomeTransientException.class }, maxAttempts = 3)
    public void performTask() throws SomeTransientException {
        // Your logic here
        System.out.println("Trying to perform task...");
        throw new SomeTransientException("Temporary failure");
    }

    @Recover
    public void recover(SomeTransientException e) {
        // Recovery logic here
        System.out.println("Recovering from failure: " + e.getMessage());
    }
}

api에러 뿐만 아니라 아래와 같이 디비 에러에도 가능. IO에러 등등 많이 커버한다.. @Retryable에서 throw 하는 에러 타입과 @Recover에서 받는 에러타입이 반드시 같아야 한다.

@Service
public interface MyService { 

    @Retryable(retryFor = SQLException.class)
    void retryServiceWithRecovery(String sql) throws SQLException; 

    @Recover
    void recover(SQLException e, String sql); 
}

Retryable 어노테이션에는 어떤 exception이 발생할 때 재시도를 할지와 최대 몇 번 실행할지를 지정해 줄 수 있다.

  • maxAttempts: 실패가 n번 나면 recover 함수를 실행한다.
    • default: 3
  • backoff: 재실행하고 n초 쉬었다가 재실행(단위: ms)
    • default:  a fixed delay of 1000ms
  • multiplier: 재시도와 재시도 사이의 시간 간극이 n배로 점점 멀어짐
    • default: 0(무시)
  • BackOffPolicy: 아래처럼 설정 값들에 따라 재시도 간격이 달라진다.
    • With no explicit settings the default is a fixed delay of 1000ms
    • Only the delay() set: the backoff is a fixed delay with that value
    • When delay() and maxDelay() are set the backoff is uniformly distributed between the two values
    • With delay(), maxDelay() and multiplier() the backoff is exponentially growing up to the maximum value
    • If, in addition, the random() flag is set then the multiplier is chosen for each delay from a uniform distribution in [1, multiplier-1]

 

위 값들은 클래스에서 상수 값으로 관리할 수 있지만 공용사용과 환경별 관리를 위헤 프로퍼티 파일로 뺄 수도 있다.

이때 해당 변수 명이 달라지니(ex. maxAttemps -> maxAtempsExpression) 반드시 공식문서를 참고해야 한다!

https://docs.spring.io/spring-retry/docs/api/current/org/springframework/retry/annotation/Retryable.html

 

공통 설정 혹은 더 복잡한 설정을 위해 java config로 설정할 수도 있다.

@Configuration
@EnableRetry
public class RetryConfig {


    @Bean
    public RetryTemplate retryTemplate(){
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(3000l);
        
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        Map<Class<? extends Throwable>, Boolean> includeExceptions = new HashMap<>();
        includeExceptions.put(CannotAcquireLockException.class, true);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, includeExceptions);
        retryTemplate.setRetryPolicy(retryPolicy);

        return retryTemplate;
    }
    
}

 

참고로 delay는 간격은 랜덤으로도 줄 수 있다.

@Retryable(
    value = {CannotAcquireLockException.class},
    maxAttempts = 4,
    backoff = @Backoff(random = true, delay = 1000, maxDelay = 5000, multiplier = 2)

 


기본설명:

https://www.baeldung.com/spring-retry

좀더 복잡하게 사용하기:

https://medium.com/@AlexanderObregon/using-springs-retryable-annotation-for-automatic-retries-c1d197bc199f

https://medium.com/@vmoulds01/springboot-retry-random-backoff-136f41a3211a


이럴 수가.. 적으면서 이거 전에 본적 있는데 싶었는데 무려 정리를 했던 적이 있던 것ㅋㅋㅋㅋ

2022.02.04 - [개발/spring] - [retry] spring-retry for auto retry

 

[retry] spring-retry for auto retry

spring-retry? exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다. 일시적인 connection error 등에 유용하게 사용할 수 있다. 환경: springboot2.6.2 implementation 'org.springframework.retry:spri

bangpurin.tistory.com

 

728x90
반응형
반응형

2024.05.23 - [개발/spring] - [transaction] isolation level

 

[jpa] transaction isolation level

2024.05.22 - [개발/sql] - DB isolation level DB isolation levelisolation level 이란 무엇인가?디비 동시성을 관리하고 데이터 정합성을 유지하기 위해 서로 다른 트랜젝션끼리의 관계를 정의한 것 존재 이유?언

bangpurin.tistory.com

A transaction is a group of sequential operations on a database that forms a logical unit of working with data.

Transactions are used to maintain the consistency and integrity of data in a database.

 

 It’s important to know that transactions are thread-local, so they apply to the current thread only.

 

propagation: 세션의 트랜젝션을 어떻게 이용할지; 무결성과 정합성을 유지하기 위한 방법

종류 트랜젝션 존재 시 트랜젝션 미존재 시 비고
REQUIRED 기존 트랜잭션 이용 신규 트랜잭션 생성 기본값
SUPPORTS 기존 트랜잭션 이용  트랜잭션 없이 수행  
MANDATORY 기존 트랜잭션 이용 exception 발생 꼭 이전 트랜잭션이 있어야 하는  경우
NEVER exception 발생 정상적으로 트랜잭션 없이 수행 트랜잭션 없을 때만 작업이 진행되어야할 때
NOT_SUPPORTED 기존 트랜젝션은 중지하고 대기, 트랜젝션 없이 실행하다가 끝나면 기존 트랜젝션 실행 트랜잭션 없이 로직 수행 기존 트랜잭션에 영향을 주지 않아야할 때
REQUIRES_NEW 현재 트랜잭션은 중지되고 대기. 새로운 트랜잭션을 생성하고 종료되면 현재 트랜젝션이 다시 진행 신규 트랜잭션을 생성하고 로직을 실행 이전 트랜잭션과 구분하여 새로운 트랜잭션으로 처리가 필요할 때;
락과 함께 사용할 경우 데드락 조심
NESTED 현재 트랜잭션에 Save Point를 걸고 이후 트랜잭션을 수행 REQUIRED와 동일하게 작동
(신규 트랜잭션을 생성하고 로직이 수행)
DBMS특성에 따라 지원 혹은 미지원;
jpa에서 사용 불가
  • Nested is not possible in the JPA dialect because you cannot create a save point here. Nested, unlike Required New, creates a kind of save point. For example, if you are updating a huge batch of data, you won’t have to roll back everything in case of an error; you may roll back just to the save point.

 

두 함수간 트랜젝션을 전파하는 경우(출처: chat gpt..)

부모 함수 -> 자식 함수라고 가정할 때

트랜잭션 전파의 핵심은 부모 메서드에서 트랜잭션이 이미 시작되었는지 여부입니다. parentMethod()에서 트랜잭션이 시작되었다면, 그 안에서 호출되는 모든 자식 메서드(접근 제어자 public이든 private이든 상관없이)는 동일한 트랜잭션 경계 내에서 실행됩니다.

부모 자식 부모 transaction 전파 여부
public + @Transactional private in same class  O
public + @Transactional public in same class, @Transactional유무 상관없이 O
public + @Transactional public in different class, @Transactional유무 상관없이 O

1. Transaction은 public에서 시작, private은 함수의 일부라 판단하여 트랜젝션 이어짐

@Service
public class MyService {

  @Transactional
  public void parentMethod() {
    // Transaction starts here
    privateChildMethod(); // This method is part of the same transaction
  }

  private void privateChildMethod() {
    // This method participates in the transaction started by parentMethod
  }
}

2. 자식 함수가 같은 클래스에 있으면 부모 트랜젝션 전파됨

만약 반대로 부모가 @TRANSACTIONAL이 없고 같은 클래스의 자식에게 @TRANSACTIONAL이 있다면 자식의 트랜젝션은 신규로 생성되지 않음(프록시를 타지 않아서)

@Service
public class MyService {

  @Transactional
  public void parentMethod() {
    // Transaction starts here
    publicChildMethod(); // This call bypasses the proxy
  }

  public void publicChildMethod() {
    // This method does not participate in the transaction started by parentMethod
  }
}

3. 부모와 자식 클래스가 다를 경우, 자식이 기본 Transaction을 사용할 경우

클래스가 다를 경우 자식 함수가 proxy의 영향을 받기 때문에 부모의 트랜젝션이 자식에게 전파된다.

자식 함수가 @Transaction 어노테이션이 있건 없건 전파되는데, 만약 자식 함수도 Transaction 어노테이션이 있고 별다른 propagation 설정이 없다면 기본 전파 옵션이 Propagation.REQUIRED 이기 때문에 기존 트랜젝션을 탄다. 이 경우가 transaction 중첩이 가능한 부분이고, 위 옵션에 따라 달라진다.

@Service
public class ParentService {

  @Autowired
  private ChildService childService;

  @Transactional
  public void parentMethod() {
    // Transaction starts here
    childService.childMethod(); // This call goes through the proxy
  }
}

@Service
public class ChildService {

  @Transactional //있건없건 트랜젝션 영향 받음
  public void childMethod() {
    // This method participates in the transaction started by parentMethod
    // because the call goes through the Spring proxy
  }
}

https://www.baeldung.com/spring-transactional-propagation-isolation

한 번쯤 읽어볼 만한 시행착오

https://technixc.medium.com/how-to-use-transactional-annotation-like-a-pro-4129308ad069

728x90
반응형
반응형

2024.05.22 - [개발/sql] - DB isolation level

 

DB isolation level

isolation level 이란 무엇인가?디비 동시성을 관리하고 데이터 정합성을 유지하기 위해 서로 다른 트랜젝션끼리의 관계를 정의한 것 존재 이유?언제 그리고 어떤 식으로 한 트랜젝션이 만든 변화

bangpurin.tistory.com

Isolation Level은 동시 트랜잭션이 수행될때 다른 트랜잭션이 동일한 데이터에 대해서 어떻게 보일지에 대한 범위를 나타낸다.

​Isolation is the third letter in the ACID acronym. It means that concurrent transactions should not impact each other. 

 

위에서 DB단에서의 isolation level에 대해 살펴보았다. 이제 application 단에서의 isolation 설정을 알아보자.

 

isolation level을 아래와 같이 트랙잭션 별로 설정할 수 있다.

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedTransaction() {
    // Your code here
}

@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedTransaction() {
    // Your code here
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadTransaction() {
    // Your code here
}

@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableTransaction() {
    // Your code here
}
  • Isolation.DEFAULT: 디비의 default 설정에 따름
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED: 처음 업데이트 값으로 오버라이드 위험
  • Isolation.REPEATABLE_READ: 오버라이드 할 것 같으면 에러 발생
    • 에러 발생하면 재시도할 수 있음
    • @Retryable(maxAttempts = 15) // This is actually quite a lot. Ideally, 1–3 attempts should be sufficient.
    • 스래드 200개 생성됨..(과다하게 생성됨; 동시성 이슈)
    • 즉각 재시도(디비저장) 보다 다시 큐를 쌓도록하는게 동시성을 낮출 수
  • Isolation.SERIALIZABLE
    • 동시에 저장하려고하면 디비에서 에러발생
    • 그래도 100프로 보장 못함; 실패나면 롤백되는게 있음

 

주의사항

데이터 정합성(data integrity)과 성능(performance)을 고려하여 설정해야 한다.

사용하고자 하는 level 이 DB에서 지원하는 레벨인지 확인해야 한다.

 

애플리케이션 전체 기본 설정을 바꾸려면 아래와 같이 설정값을 추가한다.

(spring boot version 확인하고 넣도록 하자) 

springboot 2.7.3 ~ 3.2에는 아래와 같다.

spring.datasource.hikari.transaction-isolation

https://docs.spring.io/spring-boot/docs/2.7.3/reference/htmlsingle/

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.data.spring.datasource.hikari

 

Common Application Properties

 

docs.spring.io

 

728x90
반응형
반응형

isolation level 이란 무엇인가?

  • 디비 동시성을 관리하고 데이터 정합성을 유지하기 위해 서로 다른 트랜젝션끼리의 관계를 정의한 것
  • 트랜잭션이 동시에 수행될때 다른 트랜잭션이 동일한 데이터에 대해서 어떻게 보일지에 대한 범위를 나타낸다.

 

존재 이유?

  • 언제 그리고 어떤 식으로 한 트랜젝션이 만든 변화를 보이게 할지(visibility) 조절하면서 정합성(consistency)과 성능(performance) 사이의 균형(balance)을 맞추기 위함

 

isolation level 4가지

Read Uncommited

  • 제일 낮은 레벨
  • 언제? 이슈 위험 높음(정확도 낮음); 성능이 중요할 때
  • 정의: 커밋되지 않은, 수정된 데이터가 다른 트랜젝션에서도 보임(dirty reads)
  • 이슈: dirty read, non repeatable read, phantom read 모두 발생

 

Read Commited

  • 기본 설정인 DB: postgreSQL, oracleDB
  • 언제? 정합성이 중요하나 반복해서 읽었을 때 같을 필요 없을 경우
  • 정의: 커밋된 수정 사항만 다른 트랜젝션에서 보임
  • 이슈: 같은 데이터를 여러번 조회할 경우 다른 결과를 볼 수도 있음; 다른 트렌젝션에서 변경했음(non repeatable reads)
    • non repeatable read, phantom read 모두 발생

 

Repeatable Read

  • 기본 설정인 DB: mySQL
  • 언제? 정합성이 중요하고 한 트랜젝션 내에서 한 데이터를 여러번 조회했을 때 같은 결과가 나와야하는 경우
  • 정의: 같은 트랜젝션 내 한 데이터를 여러번 조회할 경우 항상 같은 결과를 보장함(트랜젝션 시작 전 커밋된 데이터만 보임)
  • 이슈: 다른 트랜젝션에서 삽입/삭제된 row(혹은 변화)를 볼 수 없음(phantom reads)

 

Serializable

  • 제일 높은 레벨
  • 언제? 데이터의 무결성과 정합성이 엄청 중요할 때(금융..)
  • 정의: 마치 동기로 실행하듯 하나 끝나고 다른 하나를 실행한다.(sequentially)
  • 이슈: 성능이 안좋음; 가끔 테이블 전체를 Lock 함

 

728x90
반응형

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

[mysql] basic functions  (0) 2024.09.09
[mysql] collation이란  (0) 2024.06.10
[mysql] merge into..?  (0) 2024.05.17
[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
[DB] 분산환경에서 데이터 저장소 선택과 활용  (0) 2023.07.24
반응형

환경: mysql 5.7

oracle에는 merge into 가 있어 값이 있을때는 수정하고 없을때는 추가하여 pk 없음 에러 혹은 중복키 저장 에러가 나지 않고 작업을 진행할 수 있었는데, mysql에는 같은 기능을 하는 쿼리가 있는지 알아본다.

oracle

MERGE INTO [UPDATE되거나 INSERT 될 테이블]
USING [MERGE를 진행하고 싶은 대상, 조인, 서브쿼리도 사용 가능]
ON [조건]
WHEN MATCHED THEN [조건에 맞는 데이터가 있을 시 실행할 구문, UPDATE, DELETE]
WHEN NOT MATCHED THEN [조건에 맞는 데이터가 없을 시 실행할 구문, INSERT]
;

 

mysql

INSERT INTO 테이블 (
	[콜롬들...]
)VALUES(
	[값들...]
)
ON DUPLICATE KEY UPDATE
	[PK값들..]

예시

category 테이블 pk가 service_code, category_code 인 경우, category_name을 추가하거나 수정하려고 한다면..

CREATE TABLE `category` (
  `service_code` varchar(20) NOT NULL COMMENT '서비스 코드',
  `category_code` varchar(20) NOT NULL COMMENT '카테고리 코드',
  `category_name` varchar(20) NOT NULL COMMENT '카테고리 명',
  PRIMARY KEY (`service_code`,`category_code`)

 

INSERT INTO category 
	(service_code, category_code, category_name) 
VALUES
	('admin', 'character', '캐릭터')
ON DUPLICATE KEY UPDATE 
	service_code = 'admin' , category_code = 'character'
;

위처럼 쓸 수도 있고 아래처럼 작성해도 동일하다.

INSERT INTO category 
	(service_code, category_code, category_name) 
VALUES
	('admin', 'character', '캐릭터')
ON DUPLICATE KEY UPDATE 
	service_code = VALUES(service_code), category_code = VALUES(category_code)
;

날려보면 아래처럼 성공하는 로그가 찍힌다.

하지만 실제 row는 수정되지 않았다! 그렇다고 신규 row가 생기지도 않았다.

참고로 기존 row를 지우고 날리면 1로 결과가 떨어지고 신규 row가 추가된다. 허나 이 상태에서 category name을 바꾸고 날려보면 1로 떨어져도 반응이 없다..

 

쿼리를 아래처럼 수정하면 

INSERT INTO category (service_code, category_code, category_name) 
VALUES ('admin', 'character', '캐릭터797')
ON DUPLICATE KEY UPDATE 
    service_code = VALUES(service_code),
    category_code = VALUES(category_code),
    category_name = VALUES(category_name);

기존 row가 있을 경우 결과가 2로 떨어지고 데이터도 수정된 것을 확인할 수 있다. 없을 경우 1로 떨어지고 추가된다.

왜?!???

 

With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values. If you specify the CLIENT_FOUND_ROWS flag to the mysql_real_connect() C API function when connecting to mysqld, the affected-rows value is 1 (not 0) if an existing row is set to its current values.

결과에 대해 더 찾아보니 아래와 같이 내린다는 것을 알게 되었다.

  • 1: 신규 row로 insert
  • 2: 기존 row update
  • 0: 변경 없음

 참고

mysql 5.7의 경우

https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html

 

MySQL :: MySQL 5.7 Reference Manual :: 13.2.5.2 INSERT ... ON DUPLICATE KEY UPDATE Statement

13.2.5.2 INSERT ... ON DUPLICATE KEY UPDATE Statement If you specify an ON DUPLICATE KEY UPDATE clause and a row to be inserted would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row occurs. For example, if column a is de

dev.mysql.com

 

mysql 8부터는 아래 문서를 확인해야 한다.

https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html

 

MySQL :: MySQL 8.0 Reference Manual :: 15.2.7.2 INSERT ... ON DUPLICATE KEY UPDATE Statement

15.2.7.2 INSERT ... ON DUPLICATE KEY UPDATE Statement If you specify an ON DUPLICATE KEY UPDATE clause and a row to be inserted would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row occurs. For example, if column a is de

dev.mysql.com

 

 

728x90
반응형

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

[mysql] collation이란  (0) 2024.06.10
DB isolation level  (0) 2024.05.22
[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
[DB] 분산환경에서 데이터 저장소 선택과 활용  (0) 2023.07.24
[형상관리] flyway vs liquibase  (0) 2022.07.08

+ Recent posts