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

1. 트랜젝션 안에 API 호출이 있고 실패가 난다면 롤백함

 

2. 트랜젝션 안에 다른 트랜젝션이 있고 JTA아님, 두번째 트랜젝션에서 에러 발생 시?

라고 하는데 아무래도 구라같다..

내 생각엔...

  • saveEvent()에서 DB관련 RuntimeException 발생 → txManagerB 트랜잭션 롤백
  • 예외가 someService()로 전파됨
  • Spring은 someService()도 @Transactional("txManagerA")로 되어 있으므로
    → 예외를 감지하고 txManagerA의 트랜잭션도 롤백

 

흠흠흠흠흠흠흠흠.... 뭐가 진실일지는 테스트만이 답?

728x90
반응형
반응형

컨트롤러 전/후 처리를 위한 어노테이션 @ControllerAdvice

  • 전처리: @InitBinder, @ModelAttribute
  • 후처리: @ExceptionHandler
@ControllerAdvice
public class GlobalControllerAdvice {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        //form-data, application/x-www-form-urlencoded에 적용
        //POST 요청에서 setter가 없어도 되게끔
        binder.initDirectFieldAccess(); // 컨트롤러 실행 전 바인딩 설정
    }

    @ModelAttribute
    public void addGlobalAttributes(Model model) {
        model.addAttribute("appName", "MyApp"); // 모든 뷰에 공통 속성 추가
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleAll(Exception ex) {
        return ResponseEntity.status(500).body("서버 에러 발생");
    }
}

순서

요청 수신
 ↓
@InitBinder
 ↓
@ModelAttribute
 ↓
@RequestMapping (실제 컨트롤러 메서드 실행)
 ↓
@ExceptionHandler (예외 발생 시)

 

참고로 

binder.initDirectFieldAccess();

 

  • @InitBinder는 주로 form-data, application/x-www-form-urlencoded에 적용된다.
  • JSON 요청이라면 @InitBinder는 영향이 없음
    • @RequestBody로 받는 JSON 요청은 WebDataBinder가 아니라 Jackson이 처리

 

728x90
반응형
반응형

환경: springboot3.2

 

기존과 같이 new PageImpl()을 이용하여 Page<T> 객체를 내리는데 아래와 같은 에러(warn)가 발생한다.

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))

 

에러 발생 이유

  • Spring Boot 3.x
  • 내부적으로 사용하는:
    • Spring Data Commons 3.x (2023.0.x 이상)
    • Spring Framework 6.x

이 조합에서 PageImpl을 그대로 @RestController에서 반환하면 Jackson 직렬화 구조가 불안정하다는 경고가 발생한다.

 

Spring 팀은 PageImpl<T>의 직렬화가 다음 문제를 가진다고 판단했다:

  • Jackson 직렬화 시 필드 순서나 구조가 변경될 수 있음
  • 필드 이름이 내부 구현에 의존
  • API 스펙을 안정적으로 유지하기 어려움

그래서 Spring Data 팀이 공식적으로 PagedModel<T> 또는 DTO 변환을 권장하게 됨.

 

해결 방법

1. hate oas 를 사용하는 경우

PagedModel<T> 사용

return pagedResourcesAssembler.toModel(page);
 
: 결과가 PagedModel<EntityModel<T>> 형태가 되어 안정적인 JSON 구조를 보장
 

2. hate oas 사용 안하는 경우

 
1) 설정 추가로 해결
@Configuration
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
public class WebConfig {
}

2) 직접 커스텀 DTO를 만들어서 사용

public class PageResponse<T> {
    private List<T> content;
    private int page;
    private int size;
    private long totalElements;

    public PageResponse(List<T> content, int page, int size, long totalElements) {
        this.content = content;
        this.page = page;
        this.size = size;
        this.totalElements = totalElements;
    }

    // getters, setters
}
728x90
반응형
반응형
728x90
반응형
반응형

환경: java17, spring boot 3.1.5, spring batch 5.0.3, mysql5.7

 

이슈:

아래 에러가 간헐적으로 발생하며 배치 실패. 재실행 시 정상 처리

Caused by: com.mysql.cj.jdbc.MysqlXAException: XAER_DUPID: The XID already exists
	at com.mysql.cj.jdbc.MysqlXAConnection.mapXAExceptionFromSQLException(MysqlXAConnection.java:344)
	at com.mysql.cj.jdbc.MysqlXAConnection.dispatchCommand(MysqlXAConnection.java:329)
	at com.mysql.cj.jdbc.MysqlXAConnection.start(MysqlXAConnection.java:290)
	at com.atomikos.datasource.xa.XAResourceTransaction.resume(XAResourceTransaction.java:217)
	... 81 common frames omitted
Caused by: java.sql.SQLException: XAER_DUPID: The XID already exists
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:130)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.StatementImpl.executeInternal(StatementImpl.java:763)
	at com.mysql.cj.jdbc.StatementImpl.execute(StatementImpl.java:648)
	at com.mysql.cj.jdbc.MysqlXAConnection.dispatchCommand(MysqlXAConnection.java:323)

디비에서 xa recover; 로 검색 시 남아있는 트랜젝션 없는 것 확인

 

해결:

XID 중복이라 우선 XID가 어떻게 생성되는지 확인

XID: <gtrid>:<bqual>
  • gtrid (Global Transaction ID): 분산 트랜잭션을 식별하는 고유한 값
  • bqual (Branch Qualifier): 트랜잭션 내에서 개별 브랜치를 구분하는 값

16진수 → ASCII 디코딩
제공된 XID는 16진수(Hex)로 인코딩 되어 있음. 이를 ASCII 문자로 변환해야 함

XID: 172.18.0.3.tm174046320874700011:172.18.0.3.tm11
XID: <IP>:tm<TIMESTAMP>:<BRANCH_ID>

gtrid = 172.18.0.3.tm174046320874700011
bqual = 172.18.0.3.tm11

  • 172.18.0.3 → 트랜잭션을 실행한 클라이언트 서버의 IP(여기서는 배치 서버)
  • tm174046320874700011 → 글로벌 트랜잭션 ID (gtrid)
    • 174046320874700011  타임스탬프 기반의 유니크 ID
  • tm11 → 브랜치 ID (bqual)

 

XID에 배치 서버 ip와 timestamp가 들어가므로 해당 서버에서 그 시간에 동시에 돌았던 'jta transaction'과 연관이 있었을 것으로 생각..

그걸 확인하기 위해 xid에 프로젝트 명이나 job 이름을 넣어보기로 했다.

상수 값도 줄 수 있고 아래처럼 spel을 사용하여 동적으로 줄 수도 있다.

spring.jta.atomikos.properties.transaction-manager-unique-name=${TRANSACTION_MANAGER_NAME:defaultTxManager}

 

  • 설정 대상: Atomikos 전체 트랜잭션 관리자
  • 역할: 트랜잭션 관리자(Transaction Manager)의 고유한 이름 지정
  • 사용 위치: 전역적으로 하나만 설정 (애플리케이션 전체에서 단 하나)
  • 중복되면 안 됨: 동일 네트워크 내 여러 인스턴스에서 서로 다른 값 필요

 

우선 해당 시간에 돈 배치 중 jta transaction 을 사용하는 배치는 이 배치 밖에 없을 것 같고

그래서 중복이 나도 한 프로젝트 안에서, 특히 이 job 안에서 발생했을 것 같긴 한데

우선 그걸 확증하기 위해 위와 같은 설정을 추가해 본다.

그래도 나면 그땐 진짜 우리끼리의 싸움

+ 그때는 XID에 UUID를 심을 수 있는 빈을 위 설정에 연결하는 게 좋을 것 같다.


추가로 

spring.jta.atomikos.properties.transaction-manager-unique-name=${TRANSACTION_MANAGER_NAME:defaultTxManager}

이 설정 값과 소스의 아래 부분이 동일한 설정이라고 생각했는데.. 그것은 아니었다..

AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName(resourceNameCreator.createResourceName(BATCH_DATASOURCE_NAME));

 

  • 설정 대상: 특정 DataSource(XA 리소스)
  • 역할: 각 XA 리소스를 식별하는 고유한 이름(Unique Name) 지정
  • 사용 위치: 각각의 AtomikosDataSourceBean 객체에 대해 개별적으로 설정
  • 중복되면 안 됨: 각 DataSource마다 고유해야 함 (여러 개의 XA 리소스를 사용할 경우 필수)

 

 

AtomikosDataSourceBean은 데이터 소스에 해당하는 고유 이름을 주는 것이고 

transaction-manager-unique-name는 atomikos transaction manager의 고유한 이름을 지정하는 것이었다..

 

우선 아래와 같이 세팅하고 추이를 지켜보기로 했다.

728x90
반응형
반응형

환경: 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
반응형
반응형

환경: 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" 메시지를 보내 트랜잭션을 롤백

JTA (Java Transaction API)

  • javax.transaction 패키지
  • UserTransaction, TransactionManager, XAResource 같은 인터페이스를 제공
  • 트랜잭션을 시작/종료하는 표준 방식 정의
  • 스프링, Java EE, Jakarta EE에서 트랜잭션을 추상화할 때 사용
  • 단독으로 동작 X → 구현체가 필요함
    • 예: Atomikos, Bitronix, Narayana 등이 JTA를 구현

Atomikos

  • JTA 구현체이자 분산 트랜잭션 관리 라이브러리
  • 다중 데이터소스 (예: MySQL + Oracle), 메시지 브로커 (Kafka, JMS) 등 여러 리소스에 걸친 트랜잭션을 지원
  • XA 프로토콜 기반의 **2PC (Two-Phase Commit)**을 수행
  • 트랜잭션 복구, 타임아웃, 로그 등 고급 기능 포함
  • Spring Boot나 Spring에서 사용하려면 AtomikosJtaPlatform, AtomikosDataSourceBean 설정 필요

즉,

  • JTA = 인터페이스
  • Atomikos = 구현체 (라이브러리)
  • 스프링에서는 JtaTransactionManager를 사용하고, 실제 구현체로 Atomikos를 등록해 줌

 

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