728x90
반응형
728x90
반응형
반응형
ORDER BY NULL은 쿼리의 결과를 정렬하지 않도록 지정하는 구문

 

언제 쓸까?

데이터베이스에서 정렬 작업은 비용이 많이 드는 작업이다. 결과를 정렬할 필요가 없는 경우, ORDER BY NULL을 사용하여 불필요한 정렬 작업을 피할 수 있다.

정렬이 필요 없다면 성능 향상 가능!

GROUP BY와 함께 사용할 때: MySQL은 GROUP BY를 실행할 때 암묵적으로 정렬을 수행한다. 하지만 특정 상황에서는 이 정렬이 불필요할 수 있다. ORDER BY NULL을 사용하면 MySQL에 정렬을 생략하도록 지시하여 성능을 향상할 수 있다.

 

728x90
반응형

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

[파티셔닝] 하는법, 쓰는법  (0) 2024.11.25
비관락/낙관락 쓰기락/읽기락 베타락/공유락  (1) 2024.11.09
2 Phase Lock & mysql -> MVCC  (3) 2024.11.06
[분산] mysql 네임드락  (0) 2024.11.01
[p6spy] 설정 방법  (0) 2024.10.21
반응형

환경: springboot3.4, java17

 

최신 버전의 스프링부트를 쓰면 어느 새부터 아래와 같은 워닝을 만나는데 상당히 신경 쓰인다. 그동안 Page 인터페이스를 아주 많이 사용했던 터라 혹시 안되거나 deprecated 된다면 난감하기 때문이다..

찾아보니 springboot3.3부터 변경되었다고 한다!

2024-12-12 13:57:23 WARN  [ration$PageModule$WarningLoggingModifier.changeProperties    : 156] Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
	For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
	or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.

내용은 직렬화 시 안정적이지 않으니 Spring Data의 PagedModel 또는 PagedResourcesAssembler를 사용하여 안정적인 JSON 구조를 생성하라는 것이다.

그동안 직렬화할 때 PageImpl을 직접 직렬화하였는데, 더이상 안정적인 방식이 아니니 아래 두 방식 중 하나를 고르라는 뜻

  • HATEOAS를 쓰면 PagedResourcesAssembler, 그렇지 않으면  PagedModel

프로젝트 전역으로 설정하는 방식은 아래와 같다.

@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)

https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables

PageSerializationMode 에는 아래와 같이 두 가지가 있다.

DIRECT

PageImpl 객체를 직접 JSON으로 직렬화함

  • 직렬화된 JSON 구조는 PageImpl의 내부 구조에 따라 다를 수 있음
  • JSON 구조가 API의 변경이나 버전 업그레이드에 따라 변할 수 있으므로, 안정성이 떨어질 수 있음
  • 이 모드는 이전 버전의 Spring Data에서 기본적으로 사용되던 방식

DTO

DTO(Data Transfer Object)를 사용하여 페이지 데이터를 직렬화

  • 안정적이고 일관된 JSON 구조를 제공
  • DTO를 사용함으로써 페이지 데이터의 구조가 API 변경에 영향을 덜 받음
  • 이 모드는 PagedModel 또는 PagedResourcesAssembler와 함께 사용되며, 이를 통해 클라이언트가 예측 가능한 형식의 데이터를 수신할 수 있다.

 

설정하고 기존과 똑같이 페이징하면 된다.

@GetMapping
public Page<BaseResponse> getPrices(

참고로 HATEOAS

HATEOAS는 REST API 설계의 원칙 중 하나로, 클라이언트가 서버 응답에 포함된 하이퍼미디어(hypermedia)를 통해 애플리케이션 상태를 동적으로 탐색할 수 있도록 하는 방식이다. 이 원칙은 REST의 자기 설명(self-descriptive) 특성을 강화한다

HATEOAS의 구성 요소

  • 링크(Link): API 응답에 포함된 URL. 다음 가능한 액션을 안내.
  • 상태(State): 현재 리소스의 상태.
  • 동작(Action): 링크를 따라가면 수행할 수 있는 작업.

HATEOAS의 사용 이유

  • API 탐색성 증가:
    • 클라이언트는 추가적인 문서 없이 서버 응답에 포함된 링크를 통해 어떤 작업이 가능한지 동적으로 파악할 수 있음
  • 클라이언트-서버 결합도 감소:
    • 클라이언트는 서버가 제공하는 링크를 따라가기 때문에 특정 엔드포인트에 강하게 의존하지 않음
  • 유연한 확장성:
    • 서버에서 새로운 액션이나 엔드포인트를 추가하더라도, 클라이언트는 변경 없이 새로운 기능을 사용할 수 있음
  • 자기 설명적 API:
    • 서버 응답에 포함된 하이퍼미디어가 클라이언트에게 리소스 상태 및 가능한 작업을 설명하므로 API의 문서화와 유지보수가 용이
728x90
반응형
반응형

 

Redis의 Sorted Set은 특정 요소들의 집합으로, 각 요소는 고유한 값(value)과 정수나 부동소수점 형식의 점수(score)를 함께 가집니다. 이 점수에 따라 요소들이 자동으로 정렬됩니다.

특징:

  1. 자동 정렬: 요소들이 점수에 따라 오름차순으로 자동 정렬됩니다.
  2. 빠른 조회: 특정 범위에 있는 요소들을 빠르게 조회할 수 있습니다.
    • 범위 조회 지원: 점수나 멤버 인덱스를 기반으로 부분 집합을 효율적으로 검색 가능합니다.
  3. 순위 계산: 요소들의 순위(rank)를 쉽게 계산할 수 있습니다.
  4. 중복 불가: 멤버 값은 중복될 수 없습니다. (단, 점수는 중복 가능)
  5. O(log(N)) 복잡도: 추가, 삭제, 조회 연산의 시간 복잡도는 O(log(N))입니다.

주요 명령어:

  • ZADD: Sorted Set에 요소를 추가합니다.
  • ZRANGE: 지정한 범위의 요소를 가져옵니다.
  • ZREM: 요소를 삭제합니다.
  • ZSCORE: 요소의 점수를 확인합니다.
  • ZRANK: 요소의 순위를 확인합니다.
//ZADD key score member [score member ...] 추가
ZADD le/aderboard 100 alice
ZADD leaderboard 200 bob 150 charlie

//ZSCORE key member 조회
ZSCORE leaderboard alice  # 결과: 100

//ZRANGE key start stop [WITHSCORES] 인덱스기반(시작; 0) 범위 조회 [점수도 같이 반환]
ZRANGE leaderboard 0 -1 WITHSCORES  # 전체 조회

//ZRANGEBYSCORE key min max [WITHSCORES] 점수 기반 범위 조회
ZRANGEBYSCORE leaderboard 100 200 WITHSCORES

//ZRANK key member 0시작 순위 반환(오름차순)
ZRANK leaderboard bob  # 결과: 2

//ZREVRANK key member 내림차순 순위 반환
ZREVRANK leaderboard bob  # 결과: 0

//ZREM key member [member ...] 특정 맴버 삭제
ZREM leaderboard alice

활용 사례

(1) 리더보드

게임에서 점수에 따라 순위를 관리할 때 유용합니다.

  • 점수를 score로, 사용자 이름이나 ID를 member로 저장.
  • 순위 조회, 점수 범위 내 사용자 검색 등이 가능.

(2) 태스크 스케줄링

  • 점수를 타임스탬프로 사용하여 작업을 스케줄링.
  • 특정 시간 범위의 작업을 조회하거나 삭제 가능.

(3) 우선순위 큐

  • 점수를 우선순위로 사용하여 작업을 관리.

주의점

  • 메모리 사용량: 점수와 멤버를 함께 저장하므로 메모리 사용량이 단순 Set보다 큽니다.
  • 점수 정밀도: 점수는 부동소수점(Floating Point)이므로 정밀도에 주의해야 합니다.

 

TTL 설정

Redis의 Sorted Set 자체에는 직접적인 TTL(Time To Live) 설정이 지원되지 않습니다. Redis는 Key-Value 기반으로 동작하므로, TTL은 키(key) 단위로 설정됩니다. 따라서 zset 안의 각 맴버에게 만료시간을 설정하는 것이 아닌 Sorted Set 전체에 대해 TTL을 설정해야 합니다.

 

  • TTL은 키 전체에 적용됩니다. Sorted Set의 개별 멤버에 TTL을 설정할 수는 없습니다.
    • 개별 멤버 TTL 관리가 꼭 필요하다면, 점수를 TTL처럼 사용하거나 별도 키로 TTL 관리하는 방법이 가장 실용적입니다.
  • TTL 설정 후, 키이 삭제되면 Sorted Set에 저장된 모든 멤버도 함께 삭제됩니다.
  • TTL은 주로 일시적인 데이터에 사용됩니다. 예를 들어, 일일 리더보드나 임시 작업 큐 등에 적합합니다.
  • 키가 이미 만료된 상태에서 접근하면, Redis는 키를 자동으로 삭제하고, 해당 키에 대한 작업은 무시됩니다.

레디스로 구현하려면..

sorted set으로 랭킹 저장하고 각 맴버별로 hash만들어서 ttl 설정한 후 노티피케이션이나 주기적으로 확인하여 set에서 삭제하는 로직 작성하여 수동으로 관리해야함..

# Sorted Set에 멤버 추가
ZADD myset 100 user1
ZADD myset 200 user2

# user1의 TTL이 필요하면 별도 키 생성 후 TTL 적용
SET user1_temp_value some_value
EXPIRE user1_temp_value 3600  # user1_temp_value 키에 TTL 1시간 설정

 

728x90
반응형

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

분산락 - redis 사용  (0) 2024.11.08
[redis] 기초  (0) 2023.02.08
반응형

환경: springboot3.3

url의 path variable로 enum을 받을 경우 아래와 같이 한 번에 받을 수 있지만, enum이 대문자로 관리될 경우, url에 대소문자가 혼용되게 된다.

이 경우 localhost:8080/AGAME/member-no 이렇게 된다(이는 StringToEnumConverterFactory 때문이다).

대소문자가 혼용되는 게 싫어서 아래와 같이 변환을 해서 검증을 하곤 하는데, 이게 또 계속 반복되는 문제점이 생긴다.

path variable의 값을 string의 코드로 보내도 자동으로 enum으로 변환할 수 없을까?!

 

HandlerMethodArgumentResolver 사용해 보기

resolver? Spring MVC에서 HTTP 요청 데이터를 Controller의 메서드 매개변수로 바인딩함

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class GamePathVariableResolver implements HandlerMethodArgumentResolver {

 @Override
  public boolean supportsParameter(MethodParameter parameter) {
    boolean hasAnnotation = parameter.hasParameterAnnotation(PathVariable.class);
    boolean usedEnum = GameType.class.isAssignableFrom(parameter.getParameterType());
    return hasAnnotation && usedEnum;
  }

  @Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
      
    String value = (String) delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    return GameType.findByPath(value);
  }
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new GamePathVariableResolver());
    }
}

근데? Resolver를 타지 않음

resolver가 스프링에 등록된 것은 확인했다.

그런데 url을 String game으로 호출하면 올바른 enum값이 아니라는 에러가 난다.

org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type

GamePathResolver에 가보지도 못한 건가? 그럼 호출 순서는 StringToEnumConverterFactory -> GamePathResolver??

 

우선 방식을 파악하기 위해 enum값으로 호출하고 resolver에 디버그를 걸어 호출 시 resolver를 타는지 확인해 본다.

그런데 resolver를 아애 타지 않는다..? 디버깅이 안 걸린다.

조금 더 다양한 방법으로 테스트해 본다.

  • 아래와 같이 PathVariable도 아니고 RequestParam도 아닌 일반 객체가 있을 때는 탄다.
  • 근데 잘 타다가도 아래 memberNo에 어노테이션(RequestParam 나 ModelAttribute)을 걸면 또 안탄다.
  • 디버깅을 걸어 MethodParam의 값을 확인해보면 어노테이션 PathVariable로 선언된 GameType이 없다....
@GetMapping("/{game}")
public ResponseEntity<?> checkGameFallbackToInGame(@PathVariable GameType game, String memberNo) {

왜?

먼저 등록된 기본 PathVariableMethodArgumentResolver가 처리할 수 있는 파라미터를 모두 소화하기 때문에 커스텀 리졸버가 호출되지 않을 수 있다... 흐름은 아래 참고..

  • 어노테이션이 있는 매개변수는 기본적으로 Spring의 내장 리졸버에 의해 처리됨
    • RequestParamMethodArgumentResolver는 @RequestParam을 처리.
    • PathVariableMethodArgumentResolver는 @PathVariable을 처리.
    • ModelAttributeMethodProcessor는 @ModelAttribute
  • 어노테이션이 없는 경우 아래의 룰로 기본 매핑된다. 
    • 단순 타입(String, int, boolean 등): 단순한 데이터 타입이면 @RequestParam으로 간주
      • RequestParamMethodArgumentResolver
    • 객체 타입 (사용자 정의 객체): 객체 타입이면 @ModelAttribute로 간주
      • ModelAttributeMethodProcessor
    • 그 외의 룰은 커스텀 어노테이션을 개발하여 매핑할 수 있음

궁금한 사항..

근데! WebMvcConfigurer에 커스텀 리졸버가 등록되면, Spring의 기본 리졸버보다 우선 적용되어 커스텀 리졸버에서 기존 동작을 완전히 덮어쓴다고 한다(그래서 확장하거나 위임해야 한다). 근데 아래와 같이 해보니 그냥 실행 자체가 안된다.. 왜?!

우선 PathVariableMethodArgumentResolver 자체를 override 할 수 있는지 궁금해서 임시로라도 해보려고 했는데 그저 안된다.. 

public class GamePathResolver implements HandlerMethodArgumentResolver {

  PathVariableMethodArgumentResolver resolver;

  public GamePathResolver() {
    this.resolver = new PathVariableMethodArgumentResolver(); // 기본 구현 생성
  }

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    if (resolver.supportsParameter(parameter)) {
      PathVariable path = parameter.getParameterAnnotation(PathVariable.class);
      return "game".equals(path.value());
    }
    return false;
  }

  @Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
    // 기본 처리 로직 호출
    Object resolvedValue = resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    return GameType.findByPath((String) resolvedValue);
  }
}

그럼 어떻게 해결? ConverterFactory 구현

우선 시간이 없어서 위 resolver 방법은 잠시 중단하고, 커스텀 어노테이션을 이용하여 컨버터와 연결하였다.

public class GamePathConverter implements ConditionalGenericConverter {

  @Override
  public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    return targetType.hasAnnotation(ConvertGamePath.class);
  }

  @Override
  public Set<ConvertiblePair> getConvertibleTypes() {
    return Set.of(new ConvertiblePair(String.class, GameType.class));
  }

  @Override
  public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    return GameType.findByPath((String) source);
  }
}
 
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

...

  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new GamePathConverter());
  }
}

그러면 enum의 코드(string)로 보내도 enum으로 바로 가져올 수 있다.


참고로 포매터와 컨버터는 비슷하지만 아래와 같은 용어적 차이가 있다.

리졸버랑 컨버터는.. 여기서는 사실 두 개의 짬뽕을 하고 싶었고 그걸 리졸버에서 구현하고 싶었는데 안돼서 컨버터를 달아버린..


 

why not AOP?

1. AOP로 파라미터 값을 직접 변경 불가

  • Java의 AOP는 메서드 실행 전에 메서드 파라미터 값을 직접 변경할 수 없음

2. string으로 game argument관리하여 실수 잦음 + 순서도 중요..

@Before("@annotation(org.springframework.web.bind.annotation.PathVariable) && args(game,..)")
  • @PathVariable로 선언된 메서드 파라미터를 가지는 메서드 중, 첫 번째 파라미터 이름이 game인 경우를 대상으로 함..
728x90
반응형
반응형

환경: springboot3.3

 

기존에 멀티 데이터베이스를 쓸 때 분산 트랜젝션을 위해 아래와 같이 ChainedTransaction을 사용하였는데..

@Configuration
public class ChainedTransactionConfiguration {

  @Primary
  @Bean(Constants.CHAINED_TRANSACTION_MANAGER)
  public PlatformTransactionManager transactionManager(@Qualifier(Constants.USER_TRANSACTION_MANAGER) PlatformTransactionManager userPlatformTransactionManager,
      @Qualifier(Constants.LOG_TRANSACTION_MANAGER) PlatformTransactionManager logPlatformTransactionManager,
      @Qualifier(Constants.STATIC_TRANSACTION_MANAGER) PlatformTransactionManager staticPlatformTransactionManager) {
    return new ChainedTransactionManager(userPlatformTransactionManager, logPlatformTransactionManager, staticPlatformTransactionManager);
  }
}

아래와 같이 Deprecated 되었다.

여러 대안을 찾다가 JtaTransaction이 있어 사용가능한지 확인해 본다.

 

조건은

1. 멀티 데이터베이스이기 때문에 각각에 대해 단일 Transactional을 설정할 수 있어야 하고

2. 필요에 따라 복합 트랜젝션도 가능해야 한다.

 

우선 JtaTransaction이 뭔지 간단히 알아보자.

JtaTransactionManager는 기본적으로 여러 데이터베이스에 걸쳐 트랜잭션을 처리하는 역할을 한다. 그러나 이를 제대로 활성화하려면 다음과 같은 조건을 충족해야 한다:

  • XA 데이터 소스 설정: 분산 트랜잭션을 사용하려면 XADataSource를 사용해야 한다. 예를 들어, MySQL을 사용할 경우 MysqlXADataSource를 사용해야 하며, 다른 데이터베이스도 XA 지원을 해야 한다.
  • 트랜잭션 관리자의 설정: JtaTransactionManager는 기본적으로 JTA를 사용하여 트랜잭션을 관리하지만, 분산 트랜잭션을 활성화하려면 여러 데이터 소스를 연결하고 이를 관리할 수 있는 TransactionManager 설정이 필요하다.
@Configuration
public class JtaDbConfig {

    @Bean(name = "chainedTransaction")
    public JtaTransactionManager transactionManager() {
        JtaTransactionManager transactionManager = new JtaTransactionManager();
        // JTA 트랜잭션 매니저 설정
        return transactionManager;
    }

    // DataSource 1 설정 (XA DataSource)
    @Bean
    public DataSource dataSource1() {
        MysqlXADataSource dataSource = new MysqlXADataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/db1");
        dataSource.setUser("user1");
        dataSource.setPassword("pass1");
        return dataSource;
    }

    // DataSource 2 설정 (XA DataSource)
    @Bean
    public DataSource dataSource2() {
        MysqlXADataSource dataSource = new MysqlXADataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/db2");
        dataSource.setUser("user2");
        dataSource.setPassword("pass2");
        return dataSource;
    }

    // EntityManagerFactory 설정 (각각의 데이터베이스용)
    @Bean(name = "entityManagerFactory1")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory1(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(dataSource1())
                .packages("com.example.entity1")
                .build();
    }

    @Bean(name = "entityManagerFactory2")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory2(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(dataSource2())
                .packages("com.example.entity2")
                .build();
    }
}

JtaTransactionManager 도 단일 트랜젝션 관리 가능한가?

  1. JTA 트랜잭션 관리의 범위:
    • JtaTransactionManager는 분산 트랜잭션(XA 트랜잭션)을 관리하는 데 최적화되어 있지만, 단일 데이터 소스에서의 트랜잭션도 처리할 수 있음
    • 단일 데이터 소스만 사용할 경우에도 JTA 프로토콜을 통해 트랜잭션이 시작되고 종료됨
  2. 단일 트랜잭션 시 처리 동작:
    • 단일 데이터 소스에서 JtaTransactionManager는 해당 데이터 소스에서의 트랜잭션을 관리함
    • 단일 데이터 소스 환경에서는 JpaTransactionManager나 DataSourceTransactionManager처럼 작동함
    • 단일 트랜잭션 환경에서는 JpaTransactionManager나 DataSourceTransactionManager가 더 효율적일 수 있다. 이는 JTA 오버헤드가 없기 때문
    • 분산 트랜잭션이 필요 없는 경우 굳이 JtaTransactionManager를 사용할 필요는 없음

 

참고:

  1. JpaTransactionManager는 JPA에 특화되어 있으며, 트랜잭션이 하나의 데이터베이스일 경우에 적합
  2. JtaTransactionManager는 JTA를 지원하며, XA 데이터 소스를 사용하는 분산 트랜잭션을 관리할 수 있음

JtaTransactionManager는 단일 트랜잭션도 처리할 수 있지만, 분산 트랜잭션이 필요 없는 경우에는 더 가벼운 트랜잭션 매니저(JpaTransactionManager 또는 DataSourceTransactionManager)를 사용하는 것이 더 효율적임. 하지만 프로젝트 환경에서 단일 및 분산 트랜잭션이 모두 필요하다면 JtaTransactionManager를 사용해 통합적으로 관리 가능

그럼 단일 트랜젝션이 필요할 경우 더 가볍게 설정할 수는 없을까?

그거슨 불가..

JtaTransactionManager가 기본적으로 JTA 규격에 따라 동작하며, 트랜잭션의 범위는 리소스에 따라 자동으로 결정되기 때문이다..

정 필요하면 아래처럼 JpaTransactionManager / JtaTransactionManager 각각 만들어서 필요에 따라 transactionManager를 지정하는 방법뿐.. 이라는데 이건 좀 아닌 듯....

@Configuration
public class DataSourceConfig {

    // 첫 번째 데이터베이스 - 단일 트랜잭션용
    @Bean(name = "dataSource1Hikari")
    public DataSource dataSource1Hikari() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db1");
        dataSource.setUsername("user1");
        dataSource.setPassword("password1");
        return dataSource;
    }

    // 첫 번째 데이터베이스 - 분산 트랜잭션용
    @Bean(name = "dataSource1XA")
    public DataSource dataSource1XA() {
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUrl("jdbc:mysql://localhost:3306/db1");
        xaDataSource.setUser("user1");
        xaDataSource.setPassword("password1");
        return xaDataSource;
    }

    // 두 번째 데이터베이스 - 단일 트랜잭션용
    @Bean(name = "dataSource2Hikari")
    public DataSource dataSource2Hikari() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db2");
        dataSource.setUsername("user2");
        dataSource.setPassword("password2");
        return dataSource;
    }

    // 두 번째 데이터베이스 - 분산 트랜잭션용
    @Bean(name = "dataSource2XA")
    public DataSource dataSource2XA() {
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUrl("jdbc:mysql://localhost:3306/db2");
        xaDataSource.setUser("user2");
        xaDataSource.setPassword("password2");
        return xaDataSource;
    }
}
@Configuration
public class TransactionManagerConfig {

    // 첫 번째 데이터베이스 - 단일 트랜잭션
    @Bean(name = "transactionManager1")
    public DataSourceTransactionManager transactionManager1(
            @Qualifier("dataSource1Hikari") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // 두 번째 데이터베이스 - 단일 트랜잭션
    @Bean(name = "transactionManager2")
    public DataSourceTransactionManager transactionManager2(
            @Qualifier("dataSource2Hikari") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // JTA 트랜잭션 매니저 (분산 트랜잭션 관리)
    @Bean(name = "jtaTransactionManager")
    public JtaTransactionManager jtaTransactionManager(
            @Qualifier("dataSource1XA") DataSource dataSource1XA,
            @Qualifier("dataSource2XA") DataSource dataSource2XA) {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        return new JtaTransactionManager(userTransactionImp, userTransactionManager);
    }
}
@Service
public class DbService {

    @Transactional(transactionManager = "transactionManager1")
    public void performDb1Operation() {
        // 첫 번째 데이터베이스 트랜잭션 작업
    }

    @Transactional(transactionManager = "transactionManager2")
    public void performDb2Operation() {
        // 두 번째 데이터베이스 트랜잭션 작업
    }

    @Transactional(transactionManager = "jtaTransactionManager")
    public void performMultiDbOperation() {
        // DB1과 DB2를 조율하는 분산 트랜잭션 작업
    }
}

JTA 프로토콜: 2단계 커밋 (2PC)

JTA는 2PC (Two-Phase Commit) 프로토콜을 사용하여 분산 트랜잭션의 원자성과 일관성을 보장한다. 이 프로토콜은 다음 두 단계를 포함한다.

  1. 1단계: Prepare
    • 트랜잭션 관리자(Transaction Manager)는 모든 자원 관리자(XAResource)에 "Prepare" 메시지를 보냄
    • 각 자원 관리자는 트랜잭션을 준비하고, 성공 여부를 반환(예: VoteCommit 또는 VoteRollback)
  2. 2단계: Commit or Rollback
    • 모든 자원이 VoteCommit을 반환하면 트랜잭션 관리자는 "Commit" 메시지를 보내 트랜잭션을 커밋
    • 하나라도 VoteRollback을 반환하면 모든 자원에 "Rollback" 메시지를 보내 트랜잭션을 롤백
728x90
반응형
반응형

ForkJoinPool은 Java 7에 도입된 병렬 처리 프레임워크로, 작업을 작은 단위로 분할(fork)하고 병렬로 처리한 후 다시 합치는(join) 방식으로 동작한다. 병렬 프로그래밍작업 스케줄링을 위한 강력한 도구로, 특히 대규모 데이터 처리나 계산 집약적인 작업에 유용하다.

이게 프레임워크?

  • 고수준의 작업 관리: ForkJoinPool은 작업 스케줄링, 워크 스틸링, 병렬 처리 등을 관리하는 메커니즘을 제공한다. 개발자가 세부적인 스레드 관리나 큐 처리 등을 직접 코딩하지 않아도 된다.
    • 제어 역전 (IoC, Inversion of Control): 작업 실행과 스레드 관리는 ForkJoinPool이 수행하며, 개발자는 작업의 논리만 작성
  • 작업 분할 및 병합 전략: RecursiveTaskRecursiveAction이라는 추상 클래스를 기반으로 작업을 설계하며, 내부적으로는 효율적인 작업 분할 및 병합을 자동으로 처리한다.
  • 워크 스틸링 (Work Stealing): 스레드 풀에서 작업 큐를 관리하며, 비활성 스레드가 다른 활성 스레드의 큐에서 작업을 가져와 실행하는 동적 작업 분배를 한다.(처리량을 최적화); 개발자가 구현할 필요 없이 forkjoinpool이 자동으로 처리
  • 표준화된 인터페이스: 개발자가 사용할 수 있는 명확한 API (invoke, submit, fork, join 등)를 제공. 이로 인해 복잡한 병렬 프로그래밍을 간단히 구현할 수 있음.

장점

  • 멀티코어를 활용하여 작업을 병렬로 처리하므로 CPU 사용률이 최적화
  • 워크 스틸링을 통해 비효율적인 작업 분배를 방지

단점

  • 작업 분할 및 병합에 대한 오버헤드가 존재
  • I/O 중심 작업에서는 비효율적이며, CPU 집약적인 작업에 적합

적합한 상황

  • 데이터가 많고, 병렬로 처리할 수 있는 작업
  • 재귀작업/반복적으로 작업을 나눌 수 있을 때 (예: 합계, 정렬)
  • CPU 집약적인 작업에서 최적의 성능을 얻고자 할 때

개발 시 전체적인 흐름

  1. 큰 작업이면 Fork하여 병렬 처리.
  2. 작은 작업이면 직접 계산으로 효율적 처리.
  3. 모든 계산이 끝나면 병렬 결과를 Join하여 최종 결과를 얻음.

작은 작업은 직접 계산하는 이유

  1. 작업 분할의 비용 문제:
    • Fork/Join Framework는 큰 작업을 작은 작업으로 나누고 각 작업을 병렬적으로 실행
    • 하지만 작업을 너무 많이 나누면 작업 분할과 작업 병합(merge)에 드는 오버헤드(비용)가 커질 수 있음
    • 작은 작업에 대해서는 작업 분할을 하지 않고 직접 계산하여 오버헤드를 줄임
  2. 효율성 최적화:
    • 일정 크기 이하의 작업은 더 이상 병렬로 처리할 필요가 없으므로 직접 계산이 더 효율적
    • 예를 들어, 배열의 일부를 합산하거나 특정 범위의 숫자를 더하는 간단한 작업이라면, 병렬처리 대신 반복문을 통해 순차적으로 계산하는 것이 빠름

 

ForkJoinPool의 주요 메서드

  • invoke(ForkJoinTask<?> task): 기다리고 결과를 받음
  • execute(ForkJoinTask<?> task): 작업을 비동기로 실행
  • submit(ForkJoinTask<?> task): 작업을 실행하고 Future를 반환

ForkJoinPool 개발 시 RecursiveTaskRecursiveAction의 역할

  1. RecursiveTask<V>:
    • 반환값이 있는 병렬 작업을 정의할 때 사용
    • 작업을 분할하고 결과를 합산하여 반환(compute() 메서드)
  2. RecursiveAction:
    • 반환값이 없는 병렬 작업을 정의할 때 사용.
    • 단순히 작업을 수행하고 결과를 반환하지 않는 경우 적합(compute() 메서드)

꼭 써야해?

RecursiveTask를 상속하지 않고도 직접 ForkJoinTask 또는 Runnable과 같은 인터페이스를 사용할 수 있다. 하지만 이는 더 복잡하고 비효율적이며 코드 복잡성을 증가시킨다.

ForkJoinPool

스레드 갯수를 생략하면, 기본적으로 가용한 CPU 코어 수에 따라 동작

  • 스레드 수 = Runtime.getRuntime().availableProcessors()
    즉, 현재 시스템의 CPU 코어 수(논리적 코어 포함)가 기본 스레드 수로 사용됨
ForkJoinPool pool = new ForkJoinPool(); //내부적으로 가용한 프로세서 수를 기반으로 스레드 풀 크기를 결정

ForkJoinPool pool = new ForkJoinPool(4); // 스레드 4개 사용

예시

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {
    static class SumTask extends RecursiveTask<Integer> {
        private final int[] arr;
        private final int start, end;
        private static final int THRESHOLD = 10;

        public SumTask(int[] arr, int start, int end) {
            this.arr = arr;
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if (end - start <= THRESHOLD) {
                // 작은 작업은 직접 계산
                int sum = 0;
                for (int i = start; i < end; i++) {
                    sum += arr[i];
                }
                return sum;
            } else {
                // 작업 분할
                int mid = (start + end) / 2;
                SumTask leftTask = new SumTask(arr, start, mid);
                SumTask rightTask = new SumTask(arr, mid, end);

                leftTask.fork(); // 병렬 처리
                int rightResult = rightTask.compute(); // 동기 처리
                int leftResult = leftTask.join(); // 병합

                return leftResult + rightResult;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < arr.length; i++) arr[i] = i + 1;

        ForkJoinPool pool = new ForkJoinPool();
        SumTask task = new SumTask(arr, 0, arr.length);
        int result = pool.invoke(task);

        System.out.println("Sum: " + result); // 출력: Sum: 5050
    }
}

1. leftTask.fork();

  • 작업 분할:
    • leftTask를 병렬로 처리할 수 있도록 Fork-Join Pool에 작업 큐로 제출
    • fork() 메서드는 현재 작업을 Fork-Join Pool의 스레드가 처리하도록 요청하며, 비동기적으로 실행
    • 이 시점에서 leftTask는 아직 결과를 계산하지 않음

2. int rightResult = rightTask.compute();

  • 동기 실행:
    • rightTask는 직접 현재 스레드에서 동기적으로 계산
    • compute() 메서드는 RecursiveTask에서 작업을 처리하는 메인 로직
    • 이렇게 함으로써 하나의 스레드가 rightTask를 바로 계산하여, 자원을 최대한 활용

3. int leftResult = leftTask.join();

  • 결과 병합:
    • join() 메서드는 leftTask가 완료될 때까지 대기하고 결과를 반환
    • 만약 leftTask가 이미 완료되었으면, 바로 결과를 반환
    • 이를 통해 leftTask와 rightTask의 결과를 병합

왜 이런 방식으로 처리?

  • 자원의 효율적 활용:
    • leftTask는 병렬로 실행하도록 요청 (fork())
    • 한편, rightTask는 현재 스레드에서 처리 (compute())
    • 이렇게 하면 다른 작업 스레드가 leftTask를 처리하는 동안, 현재 스레드가 놀지 않고 rightTask를 계산하여 자원을 최대한 활용
  • 병렬성과 동기화의 조합:
    • fork()로 비동기 작업을 시작하고 join()으로 결과를 기다리며 동기화.
    • 병렬성과 동기화의 균형을 유지하면서 성능을 최적화

ForkJoinPool

  • 특징:
    • Java 7에서 도입.
    • 작업 분할(divide-and-conquer)을 기반으로 병렬 처리를 수행.
    • Work-Stealing 알고리즘을 사용해 작업이 끝난 스레드가 다른 스레드의 작업을 훔쳐 효율성을 높임.
    • 주로 재귀적인 작업 처리작업 분할에 사용.
    • RecursiveTask(결과 반환)와 RecursiveAction(결과 없음)을 통해 작업 정의.
  • 사용 사례:
    • 큰 작업을 작은 작업으로 나눠 처리하는 경우.
    • 예: 대규모 데이터 처리, 배열 합산, 병렬 검색.
  • 장점:
    • 스레드 수를 효율적으로 관리 (스레드 풀 크기 설정 가능).
    • Idle(대기) 상태인 스레드가 다른 작업을 훔쳐 병렬 처리 최적화.
  • 단점:
    • 작업 분할이 필요 없는 간단한 병렬 작업에는 적합하지 않을 수 있음.
    • Work-Stealing 비용이 단순 작업에서는 오히려 비효율적.

ExecutorService

  • 특징:
    • Java 5에서 도입.
    • 병렬 작업을 스레드 풀에서 실행하여 스레드 관리를 자동화.
    • Java의 스레드 풀을 관리하는 인터페이스로, 스레드의 생성, 실행, 종료를 간편하게 처리.
    • 개발자는 스레드 풀을 직접 관리할 필요가 없음!
    • 스레드 풀이 다양한 종류로 제공:
      • FixedThreadPool: 고정된 크기의 스레드 풀.
      • CachedThreadPool: 동적으로 크기가 변하는 스레드 풀.
      • ScheduledThreadPool: 예약 및 지연 실행 작업용.
      • SingleThreadExecutor: 단일 스레드로 작업 처리.
  • 사용 사례:
    • 병렬 작업이 분할되지 않거나 작업 분할을 수동으로 처리해야 할 때.
    • 예: 웹 서버 요청 처리, 비동기 작업 관리.
  • 장점:
    • API가 간단하고 다양한 스레드 풀 종류 제공.
    • 반복적이고 독립적인 병렬 작업에 적합.
    • 작업 분할 없이 단순 병렬 실행 가능.
  • 단점:
    • ForkJoinPool만큼 작업 분할에 최적화되지 않음.
    • 대규모 데이터 병렬 처리에는 적합하지 않을 수 있음.
import java.util.concurrent.*;

public class ExecutorServiceExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //4개의 스레드로 구성된 고정 크기 풀 생성
        ExecutorService executor = Executors.newFixedThreadPool(4);

        Callable<Integer> task1 = () -> {
            Thread.sleep(1000);
            return 1;
        };
        Callable<Integer> task2 = () -> {
            Thread.sleep(1000);
            return 2;
        };

        Future<Integer> result1 = executor.submit(task1);
        Future<Integer> result2 = executor.submit(task2);

		//Future.get() 호출로 각각의 결과를 대기하고 출력
        //task1과 task2는 1초 동안 대기 후 각각 1과 2를 반환
        System.out.println("Result 1: " + result1.get());
        System.out.println("Result 2: " + result2.get());

		//스레드 풀 종료
        executor.shutdown();
    }
}

 


728x90
반응형

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

[동기화] 뮤텍스/세마포어  (0) 2024.11.24
DB로 분산락 구현  (2) 2024.11.21
[test] org.mockito.exceptions.misusing.PotentialStubbingProblem  (1) 2024.11.15
자바와 스프링에서 thread pool  (0) 2024.11.11
[test] object mother  (2) 2024.09.26
반응형

환경: MySql

예전에 거래내역의 데이터를 파티셔닝 해서 조회한 적이 있는데 직접 설정했던 게 아니라서 구체적으로 알아본다.

파티셔닝 (Partitioning)

파티셔닝하나의 데이터베이스 내에서 데이터를 논리적으로 나누는 방법. 특정 컬럼을 기준으로 데이터를 여러 파티션으로 분할하여 성능을 향상시킬 수 있다.

파티셔닝의 특징:

  • 단일 데이터베이스 인스턴스 내에서 분할
  • 쿼리 성능 향상: 데이터를 작은 블록으로 나누어 특정 파티션만 조회할 수 있어 성능이 향상
  • 트랜잭션 처리: 트랜잭션의 일관성을 유지할 수 있음
  • 관리 용이성: 데이터를 분할해도 동일한 데이터베이스 인스턴스를 사용하므로 관리가 상대적으로 단순

파티셔닝을 선택할 때:

  • 단일 서버에서 성능을 향상시키고 싶을 때
  • 데이터가 일정한 기준으로 나누어지고, 쿼리가 특정 범위 (예: 날짜, 지역 등)로 자주 조회될 때
  • 트랜잭션 일관성 및 데이터 무결성을 유지해야 할 때

예시:

  • 날짜 기반 파티셔닝: order_date가 날짜 범위에 따라 파티셔닝되어, 특정 날짜 범위만 조회하면 해당 파티션만 스캔하여 빠른 성능을 제공

 

파티션 생성

CREATE TABLE orders (
    id INT NOT NULL,
    order_date DATE NOT NULL,
    amount DECIMAL(10, 2),
    PRIMARY KEY (id, order_date)
)
PARTITION BY RANGE (YEAR(order_date) * 100 + MONTH(order_date)) (
    PARTITION p202301 VALUES LESS THAN (202302),
    PARTITION p202302 VALUES LESS THAN (202303),
    PARTITION p202303 VALUES LESS THAN (202304),
    PARTITION pMax VALUES LESS THAN MAXVALUE
);

PARTITION BY RANGE는 각 파티션의 VALUES LESS THAN 조건에 따라 데이터를 할당

MySQL에서는 파티션 조건이 겹치지 않도록 설계되어 있으므로, 데이터가 특정 파티션에만 할당됨

 

  • YEAR(order_date) * 100 + MONTH(order_date) 값이 202302 미만인 데이터가 이 파티션에 들어감
  • 예: 2023년 1월의 데이터(202301), 2022년 12월 이전의 데이터

파티션 확인 가능?

MySQL에서는 내부적으로 파티션이 관리되지만, 사용자 관점에서는 단일 테이블로만 동작하며 클라이언트에서 개별 파티션을 노출하지 않는다. 파티션 정보를 조회하거나 특정 파티션만 쿼리 하는 기능은 있지만, 파티션에 직접 들어가서 작업하는 방식은 지원되지 않는다.

하지만 오라클에서는 파티션이 클라이언트나 관리 도구에서 명시적으로 노출되며, 개별 파티션에 대해 직접 접근하고 작업할 수 있다.

내부 동작 방식

물리적 파일 분리

  • MySQL은 각 파티션을 내부적으로 별도의 파일 형태로 저장
  • 데이터 파일(.ibd 파일, InnoDB 기준)이 각 파티션별로 생성
  • 이로 인해 파티셔닝 된 테이블은 디스크 I/O 및 데이터 관리를 효율적으로 수행

논리적 테이블

  • 사용자는 하나의 테이블로 모든 데이터를 다룸
  • SQL 문장에서 특정 파티션을 명시적으로 참조할 필요가 없음
  • MySQL은 파티션 조건에 따라 자동으로 적절한 파티션에 데이터를 삽입하거나 조회

 

플로우

1. 삽입 시 MySQL은 파티션 조건을 평가하여 해당 파티션에 데이터를 저장

2. 조회 시에도 사용자는 특정 파티션을 신경 쓰지 않아도 됨. MySQL이 자동으로 필요한 파티션만 읽음(파티션 프루닝).

3. 필요하다면 특정 파티션만 직접 조회할 수도 있음

INSERT INTO orders (id, order_date, amount) VALUES (1, '2023-01-15', 100.00);
-- 이 데이터는 p202301 파티션에 저장됨

SELECT * FROM orders WHERE order_date = '2023-01-15';
-- MySQL은 p202301 파티션만 스캔

SELECT * FROM orders PARTITION (p202301);
-- 명시적인 조회도 가능..

 

 

조회 시 파티션 프루닝

MySQL의 파티션 프루닝(Partition Pruning)은 쿼리 실행 시 WHERE 절의 조건에 따라 필요한 파티션만 읽도록 최적화하는 기법

파티션 프루닝이 작동하는 조건

  1. WHERE 절에 파티션 키가 포함될 것
    • MySQL은 WHERE 절에 파티션 키(파티셔닝 기준이 되는 컬럼)가 있을 때만 프루닝을 수행
  2. 상수 또는 단순 연산 사용
    • MySQL은 조건이 상수 값이거나 단순 연산으로 평가될 수 있을 때만 프루닝을 적용
    • 예를 들어, order_date = '2023-01-15'는 가능하지만 order_date = NOW()는 모든 파티션을 탐색
  3. 범위 조건
    • BETWEEN, <, >, = 등의 조건도 프루닝이 가능
  4. IN 조건
    • IN 조건도 가능한 경우 특정 파티션만 선택

그럼 아래와 같이 여러 월에 걸쳐진 데이터도 프루닝이 적용될까?

SELECT * FROM orders WHERE order_date >= '2023-02-01' AND order_date < '2023-03-01';

mysql의 경우 가능, oracle의 경우 불가능..

1. mysql

  • 위 쿼리는 order_date 컬럼이 파티션 키이므로 프루닝이 가능
  • >=, <, BETWEEN과 같은 범위 조건은 프루닝이 지원
    • -> p202301과 p202302 파티션만 검색
  • MySQL에서 프루닝 여부를 확인하려면 EXPLAIN PARTITIONS를 사용
  • 여기서 partitions 열에 p202301, p202302와 같은 값이 표시되면 프루닝이 성공적으로 적용된 것
EXPLAIN PARTITIONS SELECT * FROM orders WHERE order_date >= '2023-01-15' AND order_date < '2023-02-21';

id | partitions    | type  | possible_keys | key  | key_len | ref  | rows  | Extra
---|---------------|-------|---------------|------|---------|------|-------|------
 1 | p202301,p202302 | ALL  | NULL          | NULL | NULL    | NULL | 1000  |

단, 아래와 같은 쿼리는 프루닝 불가

WHERE YEAR(order_date) = 2023 AND MONTH(order_date) = 1; -- 프루닝 불가

 

2. Oracle

  • 조건이 파티션의 경계값과 비교 가능해야 한다.
    • order_date= TO_DATE('2024-01-15', 'YYYY-MM-DD')
      • 파티션 p202401만 접근
    • order_date BETWEEN TO_DATE('2024-01-01', 'YYYY-MM-DD') AND TO_DATE('2024-01-31', 'YYYY-MM-DD')
      • 파티션 p202401만 접근
    • order_date >= TO_DATE('2023-01-15', 'YYYY-MM-DD') AND order_date < TO_DATE('2023-02-10', 'YYYY-MM-DD');
      • 조건이 파티션 경계와 명확히 매칭되므로 Oracle은 p202301과 p202302 두 개의 파티션만 읽습니다.

 

  • 파티션 키에 함수나 연산을 적용하면 Oracle이 프루닝을 할 수 없다.
SELECT * FROM sales WHERE TRUNC(order_date) = TO_DATE('2024-01-15', 'YYYY-MM-DD');
-- 파티션 프루닝 작동하지 않음

 

  • 파티션 키에 연산을 하는 것은 위험하다. 모든 파티션을 스캔할 수 있으니 확인해야한다.
SELECT * FROM orders 
WHERE TRUNC(order_date) >= TO_DATE('2023-01-15', 'YYYY-MM-DD')
  AND TRUNC(order_date) < TO_DATE('2023-02-21', 'YYYY-MM-DD');

 

  • 프루닝 여부 확인
EXPLAIN PLAN FOR
SELECT * FROM orders 
WHERE order_date >= TO_DATE('2023-01-15', 'YYYY-MM-DD') 
  AND order_date < TO_DATE('2023-02-21', 'YYYY-MM-DD');
  
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

--------------
PARTITION START  | PARTITION STOP
-----------------|---------------
202301           | 202302
728x90
반응형

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

[mysql] order by null  (0) 2024.12.19
비관락/낙관락 쓰기락/읽기락 베타락/공유락  (1) 2024.11.09
2 Phase Lock & mysql -> MVCC  (3) 2024.11.06
[분산] mysql 네임드락  (0) 2024.11.01
[p6spy] 설정 방법  (0) 2024.10.21
반응형

프로세스 또는 스레드 간의 동기화공유 자원 관리를 위해 사용하는 동기화 도구

원리인가, 구현인가?

  1. 원리 측면:
    • 뮤텍스와 세마포어는 동시성 제어 문제를 해결하기 위한 개념적인 원리
    • 프로세스 간 공유 자원 접근 문제(Critical Section Problem)를 해결하기 위해 설계
    • "P/V 연산", "락/언락" 등의 수학적 원리에 기반
  2. 구현 측면:
    • 운영 체제는 뮤텍스와 세마포어를 시스템 콜로 구현하여 동기화를 제공
    • 프로그래밍 언어에서는 이를 감싸는 고급 추상화 클래스/메서드로 구현

 

뮤텍스; 단일 스레드/프로세스가 임계 구역을 독점적으로 보호

특징

  • 초기값이 0인 상태에서 스레드가 자원을 점유하면 값을 0 -> 1로 변경.
  • 뮤텍스는 한 번에 하나의 스레드만 자원을 점유할 수 있도록 제한하는 도구
  • 뮤텍스는 자원의 소유권 개념이 있어, 락을 획득한 스레드만 언락이 가능
  • Java의 ReentrantLock

뮤텍스 초기값 0의 의미

  • 0:
    • 뮤텍스가 열려 있는 상태(사용 가능)
    • 어떤 스레드도 해당 뮤텍스를 점유하고 있지 않은 상태

뮤텍스의 동작 과정

  1. 락(Lock):
    • 스레드가 뮤텍스를 점유하려고 하면, 뮤텍스 값을 1로 변경
    • 동시에 다른 스레드가 뮤텍스를 점유하려고 하면 대기 상태로 전환
  2. 언락(Unlock):
    • 스레드가 작업을 완료하고 뮤텍스를 해제하면, 뮤텍스 값을 0으로 
    • 대기 중인 다른 스레드가 뮤텍스를 점유할 수 있도록 허용
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexExample {
    private final Lock lock = new ReentrantLock();

    public void criticalSection() {
        lock.lock(); // 임계 구역 접근
        try {
            System.out.println(Thread.currentThread().getName() + " is in critical section.");
        } finally {
            lock.unlock(); // 접근 해제
        }
    }
/////
    public static void main(String[] args) {
        MutexExample example = new MutexExample();

        Runnable task = example::criticalSection;

        // 스레드 2개 실행
        new Thread(task).start();
        new Thread(task).start();
    }
}

 

  • 첫 번째 스레드가 실행되어 lock.lock()으로 락 획득
    • 임계 구역에 진입해 메시지를 출력
    • lock.unlock()로 락 해제
  • 두 번째 스레드는 첫 번째 스레드가 락을 해제하기 전까지 대기
    • 첫 번째 스레드가 락을 해제하면, 두 번째 스레드가 락을 획득하고 임계 구역에 진입

 

  • 임계 구역 보호:
    • ReentrantLock을 사용하여 임계 구역에 동시에 하나의 스레드만 접근하도록 보장
  • 스레드 간 동기화:
    • 두 스레드는 순차적으로 criticalSection 메서드에 접근하며, 동시에 실행되지 않음
  • 락 해제 보장:
    • try-finally 구조를 통해 락이 반드시 해제되도록 보장하여 데드락을 방지

 

 

 

세마포어; 여러 스레드/프로세스가 제한된 자원을 공유

특징

세마포어는 정수 값(카운터)을 기반으로 동작하며, 공유 자원에 대한 접근을 제어하는 데 사용; Java의 Semaphore

 

  • 정수 값 기반 제어:
    • 세마포어는 정수 값을 사용하여 현재 공유 자원에 접근할 수 있는 허용 가능한 스레드 또는 프로세스의 수를 나타냄
    • 이 값은 초기화된 이후, 특정 연산을 통해 증가하거나 감소
  • P 연산 (wait 또는 acquire):
    • 세마포어 값을 감소
    • 값이 0보다 작아질 경우, 현재 프로세스나 스레드는 대기 상태에 들어가고, 다른 스레드가 세마포어 값을 증가시킬 때까지 블록
  • V 연산 (signal 또는 release):
    • 세마포어 값을 증가
    • 대기 중인 스레드가 있으면 이를 깨워서 실행

세마포어의 초기값

세마포어의 초기값은 관리할 자원의 개수를 나타냄

  • 초기값 1: 바이너리 세마포어(Binaray Semaphore)처럼 동작하여 뮤텍스와 비슷한 역할
  • 초기값 N (N > 1): 자원을 N개까지 동시 허용하는 카운팅 세마포어(Counting Semaphore) 역할
  • 세마포어는 다음과 같은 연산으로 동작한다:
    • P(Wait):
      • 자원의 개수를 감소시킴.
      • 값이 0 이하가 되면 스레드는 자원이 해제될 때까지 대기.
    • V(Signal):
      • 자원의 개수를 증가시킴.
      • 대기 중인 스레드가 있다면 해제된 자원을 할당받아 실행을 재개.

세마포어의 값과 자원의 상태

  1. 값 > 0:
    • 자원이 하나 이상 사용 가능한 상태
    • 대기 중인 스레드가 즉시 자원에 접근 가능.
  2. 값 = 0:
    • 모든 자원이 이미 사용 중
    • 자원을 사용하려는 스레드는 대기 상태로 전환
  3. 값 < 0 (특정 구현에서 허용):
    • 대기 중인 스레드의 수를 나타낼 수 있
    • 하지만 일반적인 세마포어 구현에서는 음수값을 사용하지 않고 **대기열(queue)**로 관리

 

세마포어의 용도

  1. 임계 구역(Critical Section) 보호:
    • 공유 자원에 대한 동시 접근을 제어하여 데이터 무결성을 보장합니다.
  2. 스레드 동기화:
    • 여러 스레드가 특정 순서대로 작업을 수행하도록 제어합니다.
  3. 제한된 자원 관리:
    • 예를 들어, 네트워크 연결, 데이터베이스 연결과 같이 제한된 수의 자원을 사용하는 경우, 세마포어를 사용하여 접근 수를 제한할 수 있습니다.

 

  • 데이터베이스 커넥션 풀 관리
  • 생산자-소비자 문제
  • 네트워크 리소스 관리 (동시에 처리 가능한 연결 제한)
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private final Semaphore semaphore = new Semaphore(2); // 동시에 2개 허용

    public void accessResource() {
        try {
            semaphore.acquire(); // P 연산
            System.out.println(Thread.currentThread().getName() + " accessing resource.");
            Thread.sleep(1000); // 작업 수행
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release(); // V 연산
            System.out.println(Thread.currentThread().getName() + " released resource.");
        }
    }
////
    public static void main(String[] args) {
        SemaphoreExample example = new SemaphoreExample();

        Runnable task = example::accessResource;

        for (int i = 0; i < 5; i++) {
            new Thread(task).start(); // 스레드 5개 실행
        }
    }
}
/////////
Thread-0 accessing resource.
Thread-1 accessing resource.
Thread-0 released resource.
Thread-2 accessing resource.
Thread-1 released resource.
Thread-3 accessing resource.
Thread-2 released resource.
Thread-4 accessing resource.
Thread-3 released resource.
Thread-4 released resource.
  • 초기 상태에서 Semaphore의 카운터는 2
  • 5개의 스레드가 실행되며, 다음과 같이 동작
  1. Thread-0Thread-1이 먼저 자원을 획득하여 작업을 수행
  2. 다른 스레드(Thread-2, Thread-3, Thread-4)는 자원이 반환될 때까지 대기
  3. 작업이 끝난 스레드가 자원을 반환(release())하면, 대기 중인 스레드 중 하나가 자원을 획득
  4. 이 과정이 반복되며, 모든 스레드가 차례로 작업을 완료

 

 

뮤텍스와 바이너리 세마포어는 같은 것? NO

  • 뮤텍스는 락을 가진 자만 해제 가능, 세마포어는 그렇지 않다.
  • 뮤텍스는 priority inheritance 속성을 가지나 세마포어는 그렇지 않다(누가 시그널을 날릴지 모름)
    • 우선순위 높은 작업이 락에 의해 블라킹되면 그 블라킹 작업의 우선순위를 높여 우선순위작업이 빨리 처리되게끔 하는 것

 

상호배제(단일 자원에 대한 독점적 접근 보장)만 필요하다면 뮤텍스를, 작업 간의 실행순서 동기화가 필요하면 세마포어 사용

언제 뮤텍스를 사용할까?

  • 자원에 대한 단일 접근만 보장하면 되는 경우.
  • 예:
    • 공유 변수나 데이터 구조 보호.
    • 파일 쓰기 작업.

언제 세마포어를 사용할까?

  • 작업 간 실행 순서를 동기화해야 하는 경우.
  • 여러 스레드가 동시에 제한된 자원(예: DB 연결, 네트워크 포트)에 접근할 때.
  • 예:
    • 생산자-소비자 문제.
    • 연결 풀 관리(최대 N개 동시 연결).
      • 디비풀; 커넥션풀
728x90
반응형
반응형

환경: springboot3.3

 

1. Repository 사용

  • Repository는 Spring Data JPA의 가장 기본 인터페이스로, 기본적인 CRUD 메서드를 제공하지 않는다.
  • 따라서 save(), findById(), delete()와 같은 기본적인 CRUD 기능을 사용하려면 CrudRepository 또는 JpaRepository를 확장해야 한다.
  • Repository는 보통 커스텀 리포지토리 인터페이스에서 사용되며, 실제 데이터베이스 관련 작업은 별도로 구현해야 한다.
public interface RatingRepository extends Repository<Rating, String> {

  Optional<Rating> findByMemberId(String memberId);
}

 

하지만 위와 같이 작성가능하다!?

Repository 인터페이스 자체는 Spring Data JPA에서 기본적으로 제공하는 CRUD 기능을 직접적으로 제공하지 않지만, Spring Data JPA가 제공하는 쿼리 메서드 이름 규칙을 사용하면 findBy와 같은 쿼리 메서드를 Repository 인터페이스에서도 사용할 수 있다. 즉, Repository는 기본적으로 CRUD 메서드를 제공하지 않지만, Spring Data JPA는 메서드 이름 기반의 쿼리 생성 기능을 지원한다.

왜 findBy 메서드가 동작할까?

Spring Data JPA의 인터페이스 상속 구조

  • Repository는 Spring Data JPA의 최상위 마커 인터페이스
    • 실제로 인터페이스 열면 아무 함수도 없음
  • CrudRepository와 JpaRepository는 이 Repository를 상속받아 확장된 기능을 제공한다.
  • findBy 메서드는 실제로 Repository에 구현된 것이 아니라, Spring Data JPA가 런타임에 자동으로 구현해 주는 기능

어떻게?

  • Spring Data JPA는 @EnableJpaRepositories를 통해 등록된 리포지토리를 스캔
  • 리포지토리가 Repository를 상속받으면 Spring은 이를 프록시 객체로 생성하고, 쿼리 메서드(findByXxx)를 자동으로 구현
  • 따라서 JpaRepository나 CrudRepository가 아니라도 Repository를 상속하면 동작한다.

 

2. CrudRepository

CrudRepository는 기본적인 CRUD 메서드를 제공

  • save(): 엔티티 저장 또는 업데이트
  • findById(): ID로 엔티티 조회
  • delete(): 엔티티 삭제
  • deleteById(): ID로 엔티티 삭제
  • count(): 엔티티 개수 조회
  • existsById(): 엔티티 존재 여부 확인

페이징 및 정렬 기능은 제공하지 않으며, 추가 기능이 필요하다면 JpaRepository로 확장해야 합니다.

 

3. JpaRepository

  • CrudRepository에 추가적으로 JPA에 특화된 기능인 페이징, 정렬, 배치 저장 등을 제공.
  • 또한, findAll(Pageable pageable)이나 saveAll()과 같은 메서드를 제공하여 더 고급 기능을 사용할 수 있다.

  • 기본 CRUD만 필요한 경우:
    • CrudRepository 또는 PagingAndSortingRepository를 사용.
  • JPA 관련 기능 사용 필요:
    • JpaRepository를 사용.
  • 커스터마이징된 최소 구현이 필요한 경우:
    • Repository를 상속하여 직접 메서드를 정의.

 

728x90
반응형
반응형

환경: springboot3

 

@Transactional에서 속성을 명시하지 않으면 Spring의 기본값이 적용된다. 알고 써야 한다는!!

1. transactionManager의 기본값

@EnableJpaRepositories로 각 데이터베이스마다 별도의 리포지토리를 설정했더라도, @Transactional을 명시적으로 특정 데이터베이스와 연결하지 않으면 기본 설정된 데이터베이스가 사용된다..! 즉, @Primary로 설정된 빈이 사용됨. 멀티 데이터베이스를 쓰는 프로젝트에서 아무 생각 없이 사용하는 경우 의미 없는 트랜젝션이 설정될 수 있어 조심해야 한다.

2. timeout의 기본값

  • 기본값: -1 (무제한)
  • 트랜잭션이 실행되는 데 시간이 얼마나 걸리든 제한을 두지 않음
  • 데이터베이스에 설정된 타임아웃 값이 있을 경우, 그 값이 적용될 수 있음
  • 명시적으로 설정할 경우, 초 단위로 지정

3. rollbackFor의 기본값

  • 기본값: RuntimeException 및 Error
  • 기본적으로 트랜잭션은 RuntimeException(unchecked exception)이나 Error가 발생했을 때 롤백됨
  • CheckedException(예: SQLException)은 기본적으로 롤백 대상이 아님!!
  • rollbackFor = Throwable.class로 설정하면 모든 예외(Checked와 Unchecked 포함)가 발생 시 트랜잭션이 롤백

4. readOnly

  • 기본값: false
  • 읽기 전용 트랜잭션으로 설정되지 않음
  • 데이터 변경 작업이 가능하며, 최적화를 위해 읽기 전용 작업에서는 readOnly = true를 설정하기

5. propagation

  • 기본값: Propagation.REQUIRED
  • 기존 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 생성함

6. isolation

  • 기본값: Isolation.DEFAULT
  • 데이터베이스의 기본 격리 수준이 적용됨

커스텀 @Transactional 만들기

매번 트랜젝션 설정을 달아야 하는 게 번거로워서 전용 어노테이션을 만든다.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = TransactionConstants.TRANSACTION_MANAGER, timeout = 10, rollbackFor = Throwable.class)
public @interface WriteTransactional {

}

1. @Target

@Target은 애노테이션이 적용될 수 있는 대상(타깃)을 지정

  • ElementType.METHOD: 메서드에 적용 가능
  • ElementType.TYPE: 클래스, 인터페이스, 열거형(enum)에 적용 가능
  • ElementType.FIELD: 필드(멤버 변수)에 적용 가능
  • ElementType.PARAMETER: 메서드 매개변수에 적용 가능
  • ElementType.CONSTRUCTOR: 생성자에 적용 가능
  • ElementType.LOCAL_VARIABLE: 지역 변수에 적용 가능
  • ElementType.ANNOTATION_TYPE: 다른 애노테이션에 적용 가능
  • ElementType.PACKAGE: 패키지에 적용 가능
  • ElementType.TYPE_PARAMETER (Java 8 이상): 제네릭 타입 매개변수에 적용 가능.
  • ElementType.TYPE_USE (Java 8 이상): 모든 타입 선언에 적용 가능

2. @Retention

@Retention은 애노테이션이 얼마나 오래 유지되는지 지정

  • RetentionPolicy.SOURCE:
    • 소스 코드에서만 유지되고, 컴파일 시 제거
    • 코드 문서화나 컴파일러 경고용으로 사용.
    • 예: @Override
  • RetentionPolicy.CLASS:
    • 컴파일된 .class 파일에 포함되지만, 런타임에는 JVM에 의해 로드되지 않음
    • 기본값(Default).
  • RetentionPolicy.RUNTIME:
    • 런타임에도 JVM에 의해 유지
    • 리플렉션(Reflection)으로 접근 가능
    • 예: @Autowired, @RequestMapping.
728x90
반응형

+ Recent posts