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

멀티 스레드 환경을 지원하기 위해 사용

1. Runnable 인터페이스

특징

  • Runnable 인터페이스는 Java 1.0부터 존재하는 기본적인 인터페이스로, 단일 메서드 run()을 제공합니다.
  • 반환값이 없으며, 예외를 던질 수 없습니다.
@FunctionalInterface
public interface Runnable {
    void run();
}
public class RunnableExample {
    public static void main(String[] args) {
        // Runnable 구현체 생성
        Runnable runnableTask = () -> {
            System.out.println("Runnable Task is running...");
        };

        // 스레드에 Runnable 전달하여 실행
        Thread thread = new Thread(runnableTask);
        thread.start();
    }
}

장점

  • 간단한 구조로, 스레드에서 수행할 작업을 정의하기 쉽습니다.
  • 예외를 명시적으로 처리할 필요 없이 간단하게 작업을 정의할 수 있습니다.

단점

  • 반환값을 제공하지 않으므로, 작업 수행 결과를 받을 수 없습니다.
  • run() 메서드는 체크된 예외(Checked Exception)를 던질 수 없으므로, 예외 처리가 필요한 경우 내부적으로 처리해야 합니다.

2. Callable 인터페이스

특징

  • Callable 인터페이스는 Java 5에서 java.util.concurrent 패키지와 함께 도입된 인터페이스로, 단일 메서드 call()을 제공합니다.
  • 반환값을 가지며, 예외를 던질 수 있습니다.
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        // Callable 구현체 생성
        Callable<String> callableTask = () -> {
            return "Callable Task Completed";
        };

        // ExecutorService를 사용하여 Callable 실행
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(callableTask);

        try {
            // 작업 완료 후 결과 가져오기
            String result = future.get();
            System.out.println("Callable Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

 

장점

  • call() 메서드는 작업 수행 후 결과를 반환할 수 있습니다.
  • 체크된 예외를 던질 수 있어, 예외 처리가 더 유연합니다.
  • Future와 같은 API를 통해 비동기 작업의 결과를 받을 수 있습니다.

단점

  • Runnable에 비해 구조가 약간 복잡하며, ExecutorService와 Future 같은 추가적인 클래스와 함께 사용해야 하는 경우가 많습니다.

언제 Runnable과 Callable을 사용해야 하는가?

  • Runnable을 사용할 때:
    • 작업 수행의 결과가 필요 없고, 단순히 백그라운드 작업이나 이벤트 처리를 할 때 적합합니다.
    • 예외를 명시적으로 처리할 필요가 없을 때 사용합니다.
    • 예: 이벤트 핸들링, 타이머 작업, 단순 스레드 실행.
  • Callable을 사용할 때:
    • 작업 수행의 결과를 반환해야 할 때 적합합니다.
    • 작업 중 예외 처리가 필요하고, 호출한 쪽에서 이를 확인할 필요가 있을 때 사용합니다.
    • 예: 데이터베이스 쿼리, 복잡한 계산 작업, 외부 시스템 호출.
  • Runnable과 Callable 모두 ExecutorService를 통해 실행할 수 있습니다. Runnable을 사용하면 결과가 없는 Future<?>를 반환하고, Callable을 사용하면 작업 결과를 담은 Future<V>를 반환합니다.
ExecutorService executor = Executors.newFixedThreadPool(2);

// Runnable 예제
Runnable runnableTask = () -> System.out.println("Runnable Task Running");
Future<?> runnableFuture = executor.submit(runnableTask); // 결과가 없음

// Callable 예제
Callable<Integer> callableTask = () -> {
    return 123;
};
Future<Integer> callableFuture = executor.submit(callableTask); // 결과가 있음

executor.shutdown();

 

executor service.submit는 멀티스레드 시작!!

ExecutorService 종료 관련

ExecutorService는 작업을 스레드 풀에서 관리하고 실행하는 인터페이스로, 사용이 끝나면 반드시 종료(shutdown)해줘야 합니다. 그렇지 않으면 애플리케이션이 종료되지 않고 백그라운드에서 스레드가 계속 실행될 수 있습니다.

ExecutorService의 종료 필요성

ExecutorService는 기본적으로 백그라운드 스레드 풀을 관리합니다. 따라서 다음과 같은 이유로 사용이 끝난 후 반드시 종료해야 합니다:

  1. 리소스 해제:
    • 스레드 풀에 의해 사용되는 스레드와 기타 리소스를 해제하여 메모리 누수를 방지합니다.
  2. 정상적인 애플리케이션 종료:
    • 스레드 풀이 종료되지 않으면 JVM이 종료되지 않고 계속 대기 상태에 있을 수 있습니다.
  3. 명시적 종료 호출:
    • executor.shutdown()을 호출하여 스레드 풀을 정상적으로 종료합니다. 이 메서드는 더 이상 새로운 작업을 수락하지 않고, 기존에 제출된 작업이 완료될 때까지 기다립니다.
    • executor.shutdownNow()를 호출하면 모든 작업을 중지하고, 실행 중인 작업을 즉시 종료하려고 시도합니다.
    • ExecutorService는 AutoCloseable을 구현하지 않기 때문에 try-with-resources 구문을 직접 사용할 수 없습니다.
public class ExecutorServiceExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        try {
            executor.submit(() -> System.out.println("Task 1"));
            executor.submit(() -> System.out.println("Task 2"));
        } finally {
            // ExecutorService 종료
            executor.shutdown();
            try {
                if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
}

위 코드에서는 shutdown() 메서드를 호출하여 새로운 작업을 수락하지 않도록 하고, awaitTermination()을 사용하여 스레드 풀이 완전히 종료될 때까지 대기합니다. awaitTermination()은 주어진 시간 동안 스레드 풀이 종료될 때까지 기다리며, 그 시간이 지나도 종료되지 않으면 shutdownNow()를 호출하여 강제로 종료를 시도합니다.

728x90
반응형
반응형

기본 Xms Xmx

 java -jar -Dspring.profiles.active=real xxx.jar

로 실행하고 있는 프로세스가 있다. 띄울 때 최소/최대 힙 사이즈를 안 줘서 기본값으로 어떻게 들고 있는지 궁금했다.

java -XX:+PrintFlagsFinal -version | grep -E "InitialHeapSize|MaxHeapSize"
   size_t InitialHeapSize                          = 62914560      
   size_t MaxHeapSize                              = 994050048

위 명령어를 사용하면 현재 JVM의 기본 힙 설정을 알 수 있다.

각각은 바이트 단위이다. 따라서 좀 더 이해하기 쉽게 바꿔보면

  1. 바이트(Byte) -> 킬로바이트(Kilobyte):
    • 1KB = 1024B
    • 62914560 ÷ 1024 = 61440 KB
  2. 킬로바이트(Kilobyte) -> 메가바이트(Megabyte):
    • 1MB = 1024KB
    • 61440 ÷ 1024 = 60 MB

따라서, initialHeapSize가 62914560이라는 값은 60MB를 잡고 있다는 뜻이다.

또한 최대 값을 계산해보면

  1. 바이트(Byte) -> 킬로바이트(Kilobyte):
    • 1KB = 1024B
    • 994050048 ÷ 1024 = 970752 KB
  2. 킬로바이트(Kilobyte) -> 메가바이트(Megabyte):
    • 1MB = 1024KB
    • 970752 ÷ 1024 = 948 MB

따라서, maxHeapSize가 994050048 바이트라는 값은 약 948MB를 최대 값으로 설정했다는 것이다.

 

지금 메모리 현황 보기

free -h

              total        used        free      shared  buff/cache   available
Mem:           3.7G        933M        976M        137M        1.8G        2.3G
Swap:          2.0G         13M        2.0G

 

gc 관련 모니터링(jstat) 권한없어도 가능

jstat -gc <PID> <interval> <count>
jstat -gc 12345 1000 10  # 12345 PID의 JVM에 대해 1초 간격으로 10번 GC 정보를 출력

//
S0C    S1C    S0U    S1U      EC       EU        OC        OU      MC       MU     CCSC     CCSU       YGC    FGC
1024.0 1024.0   0.0   0.0   8192.0   1024.0   20480.0    8192.0    512.0    488.0   64.0     62.0       3      1
  • S0C/S1C: Survivor space 0/1의 용량.
  • EC: Eden 영역의 용량.
  • OC: Old 영역의 용량.
  • YGC/FGC: Young/Full GC의 발생 횟수.

 

OOM이 터질 때 자동으로 덤프를 뜨게 하는 옵션을 주려면 아래와 같은 옵션을 자바에 추가한다.

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/경로/heapdump.hprof

 

이미 뭔가 데드락이나 무한루프에 빠진 것 같다면 확인하기

각 스레드의 상태현재 실행 중인 코드를 볼 수 있음(권한 필요)

jstack <PID>
"main" #1 prio=5 os_prio=0 tid=0x00000000023f6000 nid=0x2c runnable [0x0000000002a1e000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.Thread.sleep(Native Method)
        at Example.main(Example.java:5)

 

현재 돌고 있는 프로세스의 덤프 뜨는 법

jmap이나 jcmd 명령어 사용(권한 필요)

sudo jmap -dump:format=b,file=/경로/heapdump.hprof <PID>
-- or 
jcmd <PID> GC.heap_dump <경로>


// /proc/3272/root폴더에 권한이 없을 경우
Exception in thread "main" com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file /proc/3272/root/tmp/.java_pid3272: target process 3272 doesn't respond within 10500ms or HotSpot VM not loaded
        at jdk.attach/sun.tools.attach.VirtualMachineImpl.<init>(VirtualMachineImpl.java:100)
        at jdk.attach/sun.tools.attach.AttachProviderImpl.attachVirtualMachine(AttachProviderImpl.java:58)
        at jdk.attach/com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:207)
        at jdk.jcmd/sun.tools.jmap.JMap.executeCommandForPid(JMap.java:128)
        at jdk.jcmd/sun.tools.jmap.JMap.dump(JMap.java:208)
        at jdk.jcmd/sun.tools.jmap.JMap.main(JMap.java:114)

 

MAT(Eclipse Memory Analyzer Tool)

hprof파일을 얻었으면 분석 프로그램인 eclipse MAT 프로그램이 필요하다.

다운로드하고 hprof파일을 열어준다. hprof 파일의 용량이 클 수록 오래걸린다.

 

분석 보고서 이해하기

  • Overview (개요): 메모리 상태, 누수 가능성, 가장 큰 객체 등을 요약하여 보여줌
  • Dominator Tree: 힙 메모리의 최상위 점유자를 트리 구조로 보여주며 메모리 점유 비율이 큰 객체를 쉽게 파악 가능
  • Histogram: 클래스별로 객체 수와 메모리 점유량
  • Top Consumers: 메모리 사용량이 큰 객체 그룹

힙 분석 예시

  1. 메모리 누수 확인:
    • Leak Suspects Report를 사용하면 누수 가능성이 있는 객체를 분석하여 보여줌
    • "Path to GC Root" 기능을 사용해 메모리에서 해제되지 않은 객체의 참조 경로를 추적할 수 있음
  2. Dominator Tree 분석:
    • Dominator Tree를 통해 메모리를 가장 많이 차지하는 객체 파악
    • with outgoing references를 사용하여 참조 중인 객체들을 확인
  3. Histogram 분석:
    • 클래스별로 객체 수와 메모리 점유율을 확인하여 특정 클래스가 메모리를 많이 사용하는지 파악
    • 특정 클래스에서 메모리를 많이 사용하는 객체가 있다면, 이를 "List Objects -> with incoming references"로 추적 가능

 

728x90
반응형
반응형
class ImmutableCollections {
  • Java 9 and Later: Provides factory methods (List.of, Set.of, Map.of) to create immutable collections.
    • List.of(...): Creates an immutable list.
    • Set.of(...): Creates an immutable set.
    • Map.of(...): Creates an immutable map.
  • Java 16: Enhances immutable collection creation with additional methods for more control.

 

특징

  • Thread-Safety: Immutable collections are inherently thread-safe because their state cannot change after construction.
  • No Modifications: Methods that modify the collection (like add, remove, put, etc.) throw UnsupportedOperationException.
  • Efficient: Immutable collections are often more memory-efficient and can be optimized by the JVM.

 

아래 함수가 호출되면 에러 발생! 추가 삭제 정렬 불가.

// all mutating methods throw UnsupportedOperationException
@Override public void    add(int index, E element) { throw uoe(); }
@Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); }
@Override public E       remove(int index) { throw uoe(); }
@Override public void    replaceAll(UnaryOperator<E> operator) { throw uoe(); }
@Override public E       set(int index, E element) { throw uoe(); }
@Override public void    sort(Comparator<? super E> c) { throw uoe(); }

 

 

참고로 리스트 콜랙션에도 불변객체를 바로 만들 수 있는데,, ImmutableCollections 함수와는 무관하지만 스트림에 추가된 기능이다.

toList() Method in Streams (Java 16 and Later)

  • Purpose: Collects the elements of a Stream into an immutable List.
  • Usage: Provides a convenient way to create an immutable list directly from a stream.

Collectors.toList() vs. toList()

  • Collectors.toList(): from java8, Creates a mutable list by default, which can be modified after collection. ArrayList
  • Stream.toList(): Introduced in Java 16, creates an immutable list directly from a stream, offering a more streamlined approach to get immutable results.
728x90
반응형
반응형

환경: springboot3, java17

 

소스를 배포 환경(ex. dev, stage, real...)에 따라 실행할 때 보통 profile 옵션을 줘서 환경에 맞는 프로퍼티 파일을 들고 갈 수 있게 한다.

 $JAVA11_PATH/java -jar -DappName=$APP_NAME -Dspring.profiles.active=$ACTIVE_PROFILE $DEPLOY_DIR/$JAR_NAME 1>/dev/null 2>/dev/null &

 

테스트 코드를 짤 때 필요한 설정파일은 보통 test/resources패키지 안에 application.yml로 만든다.

문득 궁금한 게 어떨 때는 test/resources/application-test.yml로 만들고 어떨 때는 test/resources/application.yml로 만들었던 것 같다.

두 개가 같은 상황으로 인식되나? 궁금해서 파헤쳐 본다.

 

테스트 코드를 위한 프로퍼티 파일

1. test/resources/application.yml로 만들면

테스트가 돌아갈 때 기본적으로 들고 가며 main/resources/application.yml을 덮어쓰는 효과가 난다.

테스트 코드에 아무 설정을 하지 않아도 적용된다.

2. test/resources/application-test.yml로 만들면

테스트 폴더 안에 있지만 어쨌건 profile이 적용된 파일이라 프로파일을 적용해야만 반영된다. 

적용하는 방법은 아래처럼 소스에다 명시한다(모든 소스에다 다 해야 하면 1번 방법을 쓰는 게 낫다.)

@ActiveProfiles("test") // test profile을 적용하라는 뜻
@Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {

혹은 외부에서 주입해야 한다.(위에 jar 실행 시처럼)

 

반대로 해당 빈을 특정 프로파일(ex. test)에서만 사용해야 한다면

1. 빈으로 프로파일을 명시한다.

@Profile("test")
@Configuration

위 설정은 profile=test 일 때만 로드된다.

2. 테스트에서만 사용하는 설정의 경우

프로파일에 상관없이 테스트에서만 사용된다면 아래 어노테이션을 사용하면 된다.

@TestConfiguration

 

사용 예제

즉, 테스트 코드에서 아래 어노테이션을 사용한다면..

@ActiveProfiles("test") 

1. 테스트 코드에서 2. 테스트 프로파일을 불러오는 것이므로

@Profile("test")
@Configuration

가 달린 설정과

@TestConfiguration

가 달린 설정 모두를 불러오게 된다.


추가적으로 궁금한 사항.....

@TestConfiguration
public class TestBatchConfig {
 @Bean
  public JobTestUtils jobTestUtils(){

해당 파일은 테스트 코드가 돌 때 자동으로 빈으로 등록하는 것인데

@ActiveProfiles("test") // test profile을 적용하라는 뜻
@ContextConfiguration(classes = {TestBatchConfig.class}) //or @Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {
		
  @Autowired private JobTestUtils jobTestUtils;

정작 테스트 코드에서 두 번째 줄을 지우면 JobTestUtils 빈을 못 찾아서 테스트가 실패한다. 아니 테스트 코드 설정이라면서 왜정작 못 불러오는 거지..

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'DailyRankingJobConfigTest'
: Unsatisfied dependency expressed through field 'jobTestUtils': No qualifying bean of type 'JobTestUtils' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations:

 

관련하여 spring공식 문서를 보니 아래와 같은 부분이 있다.

정리하면, 일반 클래스(top-level)로 configuration클래스를 만들면 스캐닝되지 않고 inner class에 static으로 만들어야지만 된다는 것...?!

https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html#testing.spring-boot-applications.detecting-configuration

 

Testing Spring Boot Applications :: Spring Boot

To test whether Spring MVC controllers are working as expected, use the @WebMvcTest annotation. @WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverte

docs.spring.io

그래서 아래처럼 inner class로 만들어보니 진짜 된다..! 이럴 수가

@ActiveProfiles("test") 
@SpringBootTest
class DailyRankingJobConfigTest {
	@Autowired private JobTestUtils jobTestUtils;
    
    ...
    
    
  @TestConfiguration
  public static class TestBatchConfig {

    @Bean
    public JobTestUtils jobTestUtils() {
      return new JobTestUtils();
    }
  }
}

 

하지만 매번 inner class로 넣을 수도 없어서 그냥 Import구문을 넣어야겠다..

728x90
반응형
반응형

basics

1. String functions

  • CONCAT(str1, str2, ...): Concatenates two or more strings into one.
SELECT CONCAT('Hello', ' ', 'World');
-- Output: Hello World
  • LENGTH(str): Returns the length of a string in bytes.
SELECT LENGTH('MySQL');
-- Output: 5
  • LOWER(str) / UPPER(str): Converts the string to lowercase or uppercase.
SELECT LOWER('HELLO');
-- Output: hello
  • SUBSTRING(str, pos, len): Extracts a substring from a string starting at a specified position and for a specified length.
SELECT SUBSTRING('MySQL Functions', 1, 5); //1부터 시작
-- Output: MySQL
  • TRIM(str): Removes leading and trailing spaces from a string.
SELECT TRIM('  Hello  ');
-- Output: Hello

2. Numeric Functions

  • ABS(x): Returns the absolute value of x.
SELECT ABS(-5);
-- Output: 5
  • CEIL(x) / FLOOR(x): Rounds up or down to the nearest integer. 올림/버림
SELECT CEIL(4.3), FLOOR(4.3);
-- Output: 5, 4
  • ROUND(x, d): Rounds the number x to d decimal places. 소수점x개만 보이게
SELECT ROUND(5.567, 2);
-- Output: 5.57
  • POWER(x, y): Returns x raised to the power of y.
SELECT POWER(2, 3);
-- Output: 8
  • MOD(x, y): Returns the remainder of x divided by y.
SELECT MOD(10, 3);
-- Output: 1

3. Date and Time Functions

  • NOW(): Returns the current date and time.
SELECT NOW();
-- Output: 2024-09-09 12:34:56
  • CURDATE(): Returns the current date.
SELECT CURDATE();
-- Output: 2024-09-09
  • CURTIME(): Returns the current time in HH:MM:SS format.
SELECT CURTIME();
-- Output: 12:34:56
  • DATE_ADD(date, INTERVAL expr unit): Adds a time interval to a date.
SELECT DATE_ADD('2024-09-09', INTERVAL 5 DAY);
-- Output: 2024-09-14
  • DATE_SUB(date, INTERVAL expr unit): Subtracts a time interval from a date.
SELECT DATE_SUB('2024-09-09', INTERVAL 1 MONTH);
-- Output: 2024-08-09
  • ADDDATE(date, INTERVAL expr unit) / SUBDATE(date, INTERVAL expr unit): Synonyms for DATE_ADD and DATE_SUB, respectively.
SELECT ADDDATE('2024-09-09', INTERVAL 10 DAY);
-- Output: 2024-09-19
  • TIMESTAMPADD(unit, interval, datetime): Adds an interval to a timestamp.
SELECT TIMESTAMPADD(DAY, 5, '2024-09-09 12:00:00');
-- Output: 2024-09-14 12:00:00
  • DATEDIFF(date1, date2): Returns the difference in days between two dates. (앞 - 뒤) 일자
    • =timestampdiff(day, date2, date1) 과 같음
SELECT DATEDIFF('2024-09-15', '2024-09-09');
-- Output: 6
  •  TIMESTAMPDIFF(unit, datetime1, datetime2): Returns the difference between two dates in the specified unit (second, minute, hour, day, week, month, year). (뒤 - 앞)로 계산하며 -도 나옴
SELECT TIMESTAMPDIFF(HOUR, '2024-09-09 10:00:00', '2024-09-09 15:00:00');
-- Output: 5
  • 주의해야 할 사항!
    • timestampdiff는 시간으로 계산하고 24시간으로 날짜일수를 계산
    • datediff는 시간은 버리고 순수 날짜로만 계산 
select timestampdiff(day, last_login, last_logout) as timestampdifff
, datediff(last_logout, last_login) as datedifff
, last_login , last_logout 
from user
having timestampdifff !=  datedifff
;


timestampdifff|datedifff|last_login         |last_logout        |
--------------+---------+-------------------+-------------------+
           -10|      -11|2024-06-10 16:52:12|2024-05-30 21:04:17|
             3|        4|2024-07-25 17:49:34|2024-07-29 08:16:30|
             0|        1|2024-03-15 17:49:32|2024-03-16 02:01:10|
             0|        1|2024-07-31 08:34:44|2024-08-01 07:01:18|

 

  • YEAR(date): Extracts the year from a date.
SELECT YEAR('2024-09-09');
-- Output: 2024
  • MONTH(date): Extracts the month from a date (1-12).
SELECT MONTH('2024-09-09');
-- Output: 9
  • DAY(date): Extracts the day of the month from a date (1-31).
SELECT DAY('2024-09-09');
-- Output: 9
  • HOUR(time), MINUTE(time), SECOND(time): Extracts the hour, minute, or second from a time value.
SELECT HOUR('12:34:56'), MINUTE('12:34:56'), SECOND('12:34:56');
-- Output: 12, 34, 56
  • DAYOFWEEK(date): Returns the day of the week for a date (1 for Sunday, 7 for Saturday).
    • 일월화수목금토
    •  1 2 3 4 5 6 7
SELECT DAYOFWEEK('2024-09-09');
-- Output: 2 (Monday)
  • DAYOFYEAR(date): Returns the day of the year (1-366).
SELECT DAYOFYEAR('2024-09-09');
-- Output: 253
  • WEEK(date): Returns the week number (1-53) of the year for the date.
SELECT WEEK('2024-09-09');
-- Output: 36
  • DATE_FORMAT(date, format): Formats the date based on the specified format string.Common format specifiers:
    • %Y: Year (4 digits)
    • %m: Month (2 digits)
    • %d: Day of the month (2 digits)
    • %H: Hour (24-hour format)
      • %h :12-hour format
    • %i: Minute
    • %s: Second
SELECT DATE_FORMAT('2024-09-09', '%W, %M %d, %Y');
-- Output: Monday, September 09, 2024
  • STR_TO_DATE(str, format): Parses a string into a date based on the format specified.
SELECT STR_TO_DATE('09-09-2024', '%d-%m-%Y');
-- Output: 2024-09-09

  • LAST_DAY(date): Returns the last day of the month for the given date.
SELECT LAST_DAY('2024-09-09');
-- Output: 2024-09-30
  • EXTRACT(unit FROM date): Extracts a part of the date based on the specified unit (e.g., year, month, day).
SELECT EXTRACT(YEAR FROM '2024-09-09');
-- Output: 2024

 

4. Aggregate Functions

  • COUNT(column): Returns the number of non-NULL rows in a column.
SELECT COUNT(*) FROM employees;
  • SUM(column): Returns the sum of values in a numeric column.
SELECT SUM(salary) FROM employees;
  • AVG(column): Returns the average value of a numeric column.
SELECT AVG(salary) FROM employees;
  • MAX(column) / MIN(column): Returns the maximum or minimum value in a column.
SELECT MAX(salary) FROM employees;

5. Control Flow Functions

  • IF(expr, true_value, false_value): Returns one value if a condition is true and another if it's false.
SELECT IF(salary > 5000, 'High', 'Low') FROM employees;
  • CASE: Evaluates a list of conditions and returns one of several possible results.
SELECT 
  CASE 
    WHEN salary > 5000 THEN 'High'
    WHEN salary BETWEEN 3000 AND 5000 THEN 'Medium'
    ELSE 'Low'
  END AS salary_range
FROM employees;

 

window functions

1. ROW_NUMBER()

  • Description: Assigns a unique, sequential integer to rows within a result set, starting at 1. Rows with the same values in the ORDER BY clause receive different ranks.
SELECT name, salary,
       ROW_NUMBER() OVER (ORDER BY salary DESC) AS rank
FROM employees;
 

Example Output:

name salary rank
Alice 8000 1
Bob 7000 2
Charlie 7000 3
David 6000 4

2. RANK()

  • Description: Assigns a rank to rows within a result set based on the ORDER BY clause. Rows with equal values in the ORDER BY clause receive the same rank, and the next rank will be skipped (i.e., if two rows share rank 1, the next rank will be 3).
SELECT name, salary,
       RANK() OVER (ORDER BY salary DESC) AS rank
FROM employees;

Example Output:

name salary rank
Alice 8000 1
Bob 7000 2
Charlie 7000 2
David 6000 4

3. DENSE_RANK()

  • Description: Similar to RANK(), but it doesn’t skip any ranks. Rows with equal values in the ORDER BY clause receive the same rank, but the next rank is consecutive (i.e., no ranks are skipped).
SELECT name, salary,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS rank
FROM employees;

Example Output:

name salary rank
Alice 8000 1
Bob 7000 2
Charlie 7000 2
David 6000 3

Syntax Breakdown:

ROW_NUMBER() OVER (ORDER BY column_name)
RANK() OVER (ORDER BY column_name)
DENSE_RANK() OVER (ORDER BY column_name)
 
  • ORDER BY: Specifies the column or columns to determine the order of the ranking.
  • You can also partition the data using the PARTITION BY clause to restart the ranking for each partition.

4. Using PARTITION BY:

  • You can use the PARTITION BY clause to restart the ranking for each subset of data, such as departments.
SELECT name, department, salary,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS department_rank
FROM employees;
  • This query ranks employees by salary within each department.

Example Output:

name department salary department_rank
Alice HR 8000 1
Bob HR 7000 2
Charlie IT 9000 1
David IT 8500 2

Summary of Ranking Functions:

  • ROW_NUMBER(): Assigns a unique rank to each row.
  • RANK(): Assigns the same rank to ties, but skips the next rank(s).
  • DENSE_RANK(): Assigns the same rank to ties, but does not skip ranks.

 

ALIAS

SQL에서는 WHERE 절에서 select 절에 선언한 컬럼 별칭(alias)를 바로 사용할 수 없음. 왜냐면 SQL 쿼리에서 데이터 필터링을 위한 조건이 먼저 실행되기 때문에, 이 시점에서는 아직 SELECT 절에서 생성된 별칭이 존재하지 않기 때문

해결책: 서브쿼리 또는 HAVING 절을 사용

1. 서브쿼리를 사용한 해결 방법:

SELECT timestampdiff(day, last_login, last_logout) AS timestampdifff, 
       datediff(last_logout, last_login) AS datedifff, 
       last_login, 
       last_logout 
FROM (
    SELECT last_login, 
           last_logout, 
           timestampdiff(day, last_login, last_logout) AS timestampdifff,   ---
           datediff(last_logout, last_login) AS datedifff   ---
    FROM user
) AS subquery
WHERE timestampdifff != datedifff;

여기서 subquery는 먼저 timestampdifff와 datedifff 값을 계산한 후, 외부 쿼리에서 이 값을 필터링함

2. HAVING 절을 사용하는 방법:

만약 GROUP BY가 사용된다면 HAVING 절을 이용할 수 있으나, 이 경우에는 GROUP BY가 필요 없을 때도 활용할 수 있음. HAVING 절은 SELECT 절이 실행된 후에 필터링을 적용하기 때문에 별칭을 사용할 수 있음

 
SELECT timestampdiff(day, last_login, last_logout) AS timestampdifff, 
       datediff(last_logout, last_login) AS datedifff, 
       last_login, 
       last_logout 
FROM user
HAVING timestampdifff != datedifff;

 

union vs union all

  • union : 결과에서 중복 삭제(더 느림)
  • union all: 중복 포함

 

left join, right join, inner join

  • INNER JOIN: Returns only matching rows from both tables. 교집합
  • LEFT JOIN(left outer join): Returns all rows from the left table, and matching rows from the right table. If there’s no match, NULL is returned for columns from the right table. (from A left join B A에서 모두 보여주고 B에서 없으면 null)
  • RIGHT JOIN(right outer join): Returns all rows from the right table, along with matching rows from the left table. If there is no match, NULL is returned for the left table's columns.
  • JOIN (without a prefix) defaults to INNER JOIN.

 

728x90
반응형

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

[p6spy] 설정 방법  (0) 2024.10.21
[mysql] delete, drop, truncate  (0) 2024.10.02
[mysql] collation이란  (0) 2024.06.10
DB isolation level  (0) 2024.05.22
[mysql] merge into..?  (0) 2024.05.17
반응형

환경: springboot3.1.5, spring batch5, junit5

 

어찌어찌 배치 프로그램은 짰는데, 테스트코드는 어떻게 짜야할지 막막했다.

심지어 이 배치는 디비에서 오늘에 해당하는 데이터를 읽어 다른 디비에 적재하는 배치인데 

  1. "오늘"이라는 날짜 디펜덴시가 있는 데이터가 필요하고
  2. 이걸 타 디비에 실제로 넣어야 한다.

h2를 추가하여 로컬 배치로 돌리는 방법이 있겠지만 돌리는 날짜에 기반한 샘플 데이터를 만들어 넣는 게 좀 귀찮았고

디비 작업이야, 쿼리만 정확하면 보증되는 것이라(이미 다른 곳에서 돌고 있는 쿼리라서 실행이 보장되어 있음)

내가 검증하고 싶은 건 데이터를 정확히 꺼내오는 것이 아닌 job, step 등이 순차적으로 잘 도는지에 대해 작성하고 싶었다.

 

하여 db select, insert 부분을 mocking 할 수 있으면 좋겠다는 생각을 했다.

 

step1. get job launcher 

bean으로 등록하거나

(아래 코드 테스트 안 해봄)

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestBatchConfig {

    @Bean
    public JobLauncherTestUtils jobLauncherTestUtils() {
        return new JobLauncherTestUtils();
    }
}
@SpringBootTest
@SpringBatchTest // mandatory?
@Import({TestBatchConfig.class, YourJobConfig.class})  // Replace YourJobConfig with your actual job configuration class
public class YourJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

util로 만들어 빈으로 등록

public class JobTestUtils {

      @Autowired private ApplicationContext applicationContext;
      @Autowired private JobRepository jobRepository;
      @Autowired private JobExplorer jobExplorer;
      @Autowired private JobLauncher jobLauncher;

      public JobLauncherTestUtils getJobTester(String jobName) {
        Job bean = applicationContext.getBean(jobName, Job.class);
        JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils();
        jobLauncherTestUtils.setJobLauncher(jobLauncher);
        jobLauncherTestUtils.setJobRepository(jobRepository);
        jobLauncherTestUtils.setJob(bean);
        return jobLauncherTestUtils;
      }

      public JobParameters makeJobParameters(JobParameters parameters) {
        return new JobParametersBuilder(jobExplorer).addJobParameters(parameters).toJobParameters();
      }
      ...
  }
@TestConfiguration
public class TestBatchConfig {

  @Bean
  public JobTestUtils jobTestUtils() {
    return new JobTestUtils();
  }
}
@ActiveProfiles("test")
@Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {

  @Autowired private JobTestUtils jobTestUtils;

...
}

 

step2. mocking 하고자 하는 reader/writer가 빈으로 등록되어야 한다.

실제 job class에서 아래와 같이 item reader/writer가 주입되도록 하고..

@Configuration
@RequiredArgsConstructor
public class DailyRankingJobConfig {

  private final DailyRankingJobParameter jobParameter;

  @Qualifier("dailyRankingMatchCntReader")
  private final MyBatisCursorItemReader<Ranking> dailyRankingMatchCntReader;

  @Qualifier("dailyRankingGameMoneyReader")
  private final MyBatisCursorItemReader<Ranking> dailyRankingGameMoneyReader;

  @Qualifier("dailyRankingWriter")
  private final ItemWriter<Ranking> dailyRankingWriter;

테스트 코드에도 빈을 주입하는데.. @MockBean어노테이션을 이용한다. 여기서 주의할 건 name에 꼭 빈 이름을 넣어야 한다.. 안 그럼 못 찾는 듯.. 에러가 발생한다.

@ActiveProfiles("test")
@Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {

  @Autowired private JobTestUtils jobTestUtils;

  @MockBean(name = "dailyRankingMatchCntReader")
  private MyBatisCursorItemReader<Ranking> dailyRankingMatchCntReader;

  @MockBean(name = "dailyRankingGameMoneyReader")
  private MyBatisCursorItemReader<Ranking> dailyRankingGameMoneyReader;

  @MockBean(name = "dailyRankingWriter")
  private ItemWriter<Ranking> dailyRankingWriter;
  
  ...
  
   @Test
  @DisplayName("성공 케이스")
  void job__success() throws Exception {
    // given
    JobParameters parameters =
        new JobParametersBuilder()
            .addString(
                "date", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), true)
            .addString("test version", UUID.randomUUID().toString(), true)
            .toJobParameters();

    given(dailyRankingMatchCntReader.read()).willReturn(getRanks().get(0), getRanks().get(1), null);
    given(dailyRankingGameMoneyReader.read()).willReturn(getRanks().get(1), null);
    doNothing().when(dailyRankingWriter).write(any());

    // when
    JobExecution jobExecution =
        jobTestUtils
            .getJobTester(DailyRankingJobConfig.JOB_NAME)
            .launchJob(jobTestUtils.makeJobParameters(parameters));

    // then
    assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
    // reader의 경우 chunk의 갯수만큼 호출
    verify(dailyRankingMatchCntReader, times(3)).read();
    verify(dailyRankingGameMoneyReader, times(2)).read();

    // writer의 경우 chunk 당 한번 호출(여기선 갯수가 적어 스텝 당 한 번임)
    final ArgumentCaptor<Chunk> captor = ArgumentCaptor.forClass(Chunk.class);
    verify(dailyRankingWriter, times(2)).write(captor.capture());
    List<Chunk> chunks = captor.getAllValues();
    assertThat(chunks.size()).isEqualTo(2);
    assertThat(chunks.get(0).size()).isEqualTo(2);
    assertThat(chunks.get(1).size()).isEqualTo(1);
  }

그러면 given.. willReturn/willThrow 등 기존에 사용하던 mocking 함수를 사용할 수 있게 된다!!


참고

https://jojoldu.tistory.com/236

 

SpringBatch에서 ItemReader를 Mock객체로 교체하기

안녕하세요? 이번 시간엔 SpringBatch에서 ItemReader를 Mock객체로 교체하는 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용

jojoldu.tistory.com

 

728x90
반응형
반응형

환경: 자바 17, springboot3.1.5, springCloud 2022.0.4

목표: 배치가 하루에 한 번 돌아야 하고 (성공했어도) 종종 수동으로 한번 더 돌릴 수 있어야 함.

 

trial1: program argument로 date를 넘겨 중복 실행을 막아보자

step1. job parameter를 program argument로 넘겨야 한다.

시도한 방법

java -jar aaa.jar --job.name=sampleBatchJob dateParam=2022-09-09

에러 발생

Caused by: org.springframework.batch.core.converter.JobParametersConversionException: Unable to decode job parameter 2024-08-22
...
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('-' (code 45)): Expected space separating root-level values
 at [Source: (String)"2024-08-22"; line: 1, column: 6]
...

그 어떤 글을 봐도 job param을 넘길 때 그냥 넘기길래.. 계속 저 방식으로 시도했지만.. 실패가 났다..

'-'가 문제 되나 싶어 지우고 해 봐도, 숫자가 아닌 임의의 문자열을 줘도 비슷한 에러가 나길래, 타입의 문제는 아닌 것 같았다.

 

step2. 혹시 springboot2 와 3의 차이로 인해 발생?

구글링 한 자료들이 outdated 된 것일 수 있다고 판단하였다.

그 이유는 springboot3 로 오면서 크게 변한 것 중 하나가 javax -> jakarta로 패키지명이 변한 것인데

사실 그 때는 jackson이라고 착각했다. 어쨌건 라이브러리 변화가 있어서 추가 설정이나 파라미터 넘기는 방식이 변했을지도 모르겠다고 생각했다.

그러다 구글링하다 파라미터에 type을 주는 예시를 봤는데, 아래와 같이 시도해 보았지만 역시나 같은 에러가 발생하였다.

dateParam(String)=2024-08-22

더 파고들어보니 위 방식은 fade out 되었고 boot3으로 버전이 오르면서 아래와 같이 바뀌었다는 글을 보게 된다.

parameter=value,type,identifying

그래서 아래와 같이 시도해 보았지만 여전히 실패하였다.

dateParam=2024-08-22,String,true

 

 

step3. 파고들기

관련 글을 좀 더 보다 보니 위와 같은 형태로 파라미터를 전달하려면 아래의 잡 파라미터 컨버터를 사용해야 한다고 한다. 이름 그대로 설정이 없을 경우 "기본적"으로 사용하는 컨버터이다.

DefaultJobParametersConverter

해당 프로젝트의 컨버터 설정이 뭔지 찾아보니 맙소사.. 다른 것이었다.

 @Bean
  public JobParametersConverter jobParametersConverter() {
    return new JsonJobParametersConverter();
  }

해당 컨버터를 사용할 경우 잡 파라미터를 아래와 같은 형태로 넘겨야 한다고 한다.

parameterName='{"value": "parameterValue", "type":"parameterType", "identifying": "booleanValue"}'

그래서 비슷하게 만들고 실행해 본다.

--job.name=sampleBatchJob
dateParam='{"value":"2024-08-22","type":"java.lang.String","identifying":"true"}'

아래의 에러가 발생한다.

Caused by: org.springframework.batch.core.converter.JobParametersConversionException: Unable to decode job parameter '{value:2024-08-22,type:java.lang.String,identifying:true}'
...
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character (''' (code 39)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
...

json parsing 에러다. single quotaion, double quotation 문제인가 싶어서 여러 조합으로 수정해 봤는데도 비슷한 에러만 발생한다.

그러던 중 한 글을 보게 되는데, json 안의 quote에는 escape 처리를 해주어야 한다는 것! (https://github.com/spring-projects/spring-batch/issues/4299)

그래서 아래처럼 수정했더니 드디어 돌아간다!

--spring.profiles.active=local
--job.name=sampleBatchJob
dateParam="{\"value\":\"2024-08-22\",\"type\":\"java.lang.String\",\"identifying\":\"true\"}

복수개의 파라미터를 넘기게 된다면 아래와 같다. 필요없는 콤마, 따옴표.. 등등이 들어가면 뜬금없는 에러가 나며 인식이 되지 않는다(에러 상황을 알기 어려움).

--spring.profiles.active=local
--job.name=DailyRankingJob
date="{\"value\":\"2024-08-21\",\"type\":\"java.lang.String\",\"identifying\":\"true\"}"
version="{\"value\":\"1\",\"type\":\"java.lang.Integer\",\"identifying\":\"true\"}"

json job converter 예시: https://spring.io/blog/2022/11/24/spring-batch-5-0-goes-ga

배치 최신 문서: https://docs.spring.io/spring-batch/reference/job/running.html

 

trial2. 날짜는 넘겼는데, 잡에서 사용하게 해야 하네!

job parameter를 프로젝트에서 보이게 하려면 우선 빈으로 등록되어 있어야 한다.

아래와 같이 일반적인 string으로 받을 경우 아래의 에러를 만난다.

  @Value("#{jobParameters[dateParam]}")
  public String dateParam;
Caused by: org.springframework.expression.spel.SpelEvaluationException: 
	EL1008E: Property or field 'jobParameters' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?

빈만 등록되면 안 되고 scope에서 보이게끔 선언해야 한다.

Job 안에서 보이게 하려면 JobScope, Step 안에서 보이려면 StepScope 안에서 사용하게 끔 아래와 같이 빈 선언부에 등록한다.

@Bean(STEP_NAME)
@JobScope
public Step rankingStep(
    JobRepository jobRepository,
    PlatformTransactionManager transactionManager,
    MyBatisCursorItemReader<Ranking> dailyRankingReader,
    ItemWriter<Ranking> dailyRankingWriter,
    @Value("#{jobParameters[dateParam]}") String dataParam) {
    	...
    }

이때 Job Parameter의 타입으로 사용할 수 있는 것으로는 Double, Long, Date, String이 있다.(배치4 기준)

LocalDate나 LocalDateTime같은 타입은 String으로 받아서 타입 변환을 해야 한다. 반환하는 방법은 크게 세 가지가 있다(https://jojoldu.tistory.com/490). 여기서는 setter주입 방식으로 해본다.

@Getter
@NoArgsConstructor
@Component
@JobScope
public class DailyRankingJobParameter {
  private LocalDate date;

  @Value("#{jobParameters[dateParam]}")
  public void setDate(String date) {
    this.date = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  }
}

빈으로 등록되어야 JobScope이 먹기 때문에 굳이? 싶어도 Component 등록을 해줘야 한다.

사용하고자 하는 Job이나 Step에서는 argument로 전달할 필요 없이 클래스에서 생성자로 받아서 바로 사용하면 된다.

@Configuration
@RequiredArgsConstructor
public class DailyRankingJobConfig {

	private final DailyRankingJobParameter jobParameter;
    
    ...
    
  @Bean(STEP_NAME)
  @JobScope
  public Step sinyutnoriDailyRankingStep(
      JobRepository jobRepository,
      PlatformTransactionManager transactionManager,
      MyBatisCursorItemReader<SinyutnoriRanking> dailyRankingReader,
      ItemWriter<SinyutnoriRanking> dailyRankingWriter
      //      @Value("#{jobParameters[dateParam]}") String dataParam
      ) {
    System.out.println(jobParameter);
    return new StepBuilder(STEP_NAME, jobRepository)
        .<SinyutnoriRanking, SinyutnoriRanking>chunk(CHUNK_SIZE, transactionManager)
        .reader(dailyRankingReader)
        .writer(dailyRankingWriter)
        .build();
  }

 

위 내용은 fade out된 내용이고(물론 위처럼 해도 작동은 됨) 실제로는 job parameter class를 만들 필요도 없이! argument에 아래와 같이 전달하면 된다.

--job.name=DailyRankingJob
date="{\"value\":\"2024-08-21\",\"type\":\"java.time.LocalDate\",\"identifying\":\"true\"}"
version="{\"value\":\"2\",\"type\":\"java.lang.Integer\",\"identifying\":\"true\"}"

사용하려는 job/step에서 바로 땡겨다 사용 가능. 클래스에 선언하면 scope이 정의되지 않아 에러가 난다.

@Bean(STEP1_NAME)
@JobScope
public Step DailyRankingMatchCntStep(
    JobRepository jobRepository,
    PlatformTransactionManager transactionManager,
    @Value("#{jobParameters[date]}") LocalDate date) {

batch 5에 추가된 내용

In Spring Batch 5, when job parameters are passed as strings, Spring Batch will automatically infer the correct type (
String, Long, Double, or Date) based on the format of the input.

 

trial3. 날짜는 같은데도 반복 실행이 된다?

위에서 날짜를 받아서 job parameter로 넘기는 것을 해봤다. 근데도 여전히 반복 실행이 된다. 왜 그런가 싶어 job execution에 사용된 파라미터를 확인해 보니 아래와 같이 run.id와 복합 키로 잡고 있어서 매번 다르게 인식하고 있었다.

해당 부분은 소스로 보면 아래와 같은데, RunIdIncrementer가 run.id의 키로 하나씩 키를 증가시키면서 실행하기 때문이다.

  @Bean(JOB_NAME)
  public Job rankingJob(JobRepository jobRepository, Step rankingStep) {
    return new JobBuilder(JOB_NAME, jobRepository)
        .incrementer(new RunIdIncrementer())  // <----
        .start(rankingStep)
        .build();
  }

따라서 해당 부분을 주석하면 여러 번 실행되지 않는 것을 확인할 수 있다.

16:32:21.071 [main] ERROR o.s.boot.SpringApplication - Application run failed
java.lang.IllegalStateException: Failed to execute ApplicationRunner
...
Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={'dateParam':'{value=2024-08-21, type=class java.lang.String, identifying=true}'}.  If you want to run this job again, change the parameters.

 

체크포인트

  • RunIdIncrementer와 같이 매번 다른 키를 생성하는 job incrementer를 사용하지 않았는지 확인
  • argument로 파라미터를 넘길 때 identifying 값을 true로 넘겼는지 확인
    • 해당 의미는 고유 키값인지 의미로 true면 execution key로 인식한다.
dateParam="{\"value\":\"2024-08-21\",\"type\":\"java.lang.String\",\"identifying\":\"true\"}
  • 이전 execution 결과, params 확인
    • 이전 execution이 fail이면 execution id는 달라도 같은 job instance의 execution으로 묶여 재실행이 가능하다.
    • 해당 내용을 확인할 수 있는 쿼리 참고..
select execution.JOB_EXECUTION_ID, execution.JOB_INSTANCE_ID, execution.CREATE_TIME, execution.STATUS, execution.EXIT_CODE, execution.EXIT_MESSAGE,
params.PARAMETER_NAME, params.PARAMETER_TYPE, params.PARAMETER_VALUE, params.IDENTIFYING
FROM BATCH_CASUAL_JOB_EXECUTION execution inner join BATCH_CASUAL_JOB_EXECUTION_PARAMS params on execution.JOB_EXECUTION_ID = params.JOB_EXECUTION_ID 
order by JOB_EXECUTION_ID  DESC
;

job instance 45의 경우 dateParam, identifying: true로 실패 -> 성공을 했고(execution 47, 48)

job instance 46번의 경우, 같은 dateParam이지만 identifying:false로 실행을 하니 다른 job parameter로 인식을 해서 실행을 되었고 실패 난 것을 알 수 있다.

 

결론

배치가 하루에 한 번 돌아야 하고 종종 수동으로 한번 더 돌릴 수 있으려면..

배치 당 unique 한 값(날짜 등)을 argument로 넘기고 job parameter로 받아서 적당한 scope의 빈에 등록해야 한다.

나의 경우, json의 형식으로 parameter를 넘기고 혹시 재실행이 필요할 경우 identifying: false로 재실행하려고 한다.

 


참고

argument로 파라미터 보내는 방법

https://velog.io/@guswns3371/Spring-Boot-Framework-%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EA%B3%BC%EC%A0%95

 

Spring Boot Framework 버전 업그레이드 과정

new features of jdk 17 & spring boot 3major spring projectsJDK를 최소 17부터 19까지 지원함.Java 11과 비교하여 GC 등 성능 개선문자열, 리스트 등 다양한 API 지원타입 추론 키워드 추가switch 문 확장r

velog.io

 

파라미터를 bean으로 등록한다는 것의 의미

https://velog.io/@lxxjn0/Spring-Batch-Guide-05.-Spring-Batch-Scope-Job-Parameter

 

Spring Batch Guide - 05. Spring Batch Scope & Job Parameter

Spring Batch Guide 시리즈는 이동욱 개발자님의 Spring Batch 가이드를 보고 학습한 내용을 정리한 글입니다.많은 내용이 원 글과 유사할 수 있습니다. 이 점 양해바랍니다 🙏🏻 이번에는 Spring Batch의 S

velog.io

batch5에 추가된 내용

https://devfunny.tistory.com/931

 

[Kotlin + SpringBatch5] SpringBatch5의 다양한 파라미터 지원 - Job 생성해서 테스트 및 메타테이블 확인, i

SpringBatch5의 다양한 파라미터 지원 https://devfunny.tistory.com/930 SpringBatch5 변경사항 정리 (vs SpringBatch4) SpringBatch 5.0 이전 SpringBatch 공부할때 SpringBatch 4.0 버전이였다. 최근, SpringBatch 복습을 위해 새로

devfunny.tistory.com

 

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

+ Recent posts