본문 바로가기
Side Project/Socket

[Self-Invocation]@Transactional 어느 Layer에 두는게 맞을까? (1)

by asdft 2024. 2. 4.

개요

프로젝트 진행중, 내가 작성한 코드가 @Transactional 어노테이션을 무의식적으로 Service 패키지 안에,

메소드 선언만 하는 인터페이스인 PostSaveUseCase와, 선언된 메소드의 구현체가 있는 PostSaveService 의 메소드 두군데에다 선언을 하고 있는것이 눈에 들어왔다.

아래 코드에서 선언한 방식만 봐도 알 수 있듯  @Transactional을 사용할때, 나는 그저

  1. 트랜젝션이 시작되었을때 다른시점에 시작된 트랜젝션과 서로 영향을 줄수 없어 격리성이 유지되며 에러 발생시 Rollback 하는 방법으로 일부만 남아있는것이 아닌 정상 전체저장 비정상 전체롤백을 통해 그 원자성을 유지함.
  2. 영속성 컨텍스트의 엔티티 자원관리 기능을 효율적으로 사용하기 위함.

이 두가지에만 초점을 두고 사용했었다. 

public interface PostSaveUseCase {

  @Transactional
  Post createPost(PostSaveCommand postSaveInfo);

  @Transactional
  void mapPostWithSkill(Post post, List<String> skillNames);
}

 

@Service
@RequiredArgsConstructor
public class PostSaveService implements PostSaveUseCase {

  @Override
  @Transactional
  public Post createPost(PostSaveCommand postSaveInfo) {
    User user = findUser(postSaveInfo.userId());

    Post postToSave = savePost(postSaveInfo.toEntity(user));

    if (!isEmptyTag(postSaveInfo)) {
      mapPostWithSkill(postToSave, postSaveInfo.skillNames());
    }
    return postToSave;
  }

  @Transactional
  @Override
  public void mapPostWithSkill(Post post, List<String> skillNames) {
    skillNames.stream()
        .map(hashtag -> findSkillName(hashtag)
            .orElseGet(() -> saveSkillName(hashtag))
        )
        .forEach(hashTag -> mapHashTagToPost(post, hashTag));
  }

 

이에 대해서 더 자세히 알아보고 @Transactional의 동작을 하나하나 따라가보려 한다.


우선 위의 트랜잭션의 과정과 코드의 문제점을 짚어보자면,

 

createPost()의 트랜잭션의 과정은 아래와 같다

1. 등록된 유저인지 check

2. postSaveInfo.toEntity(user)를 통해 게시물 작성시 유저가 입력한  title, postContent, postType, postMeeting 4가지 항목      을 묶어서 Post 엔티티로 저장.

3. 이때 만약 skillNames(태그 내용)에 입력된 값이 있을경우 mapPostWithSkill() 메소드 실행.

4. String타입의 태그내임이 Skill repository에 존재할 경우 해당 ID값을 가져오고 존재하지 않을 경우 Skill repository에            저장.

5. 마지막으로 생성된 게시물의 Post_id와 Skill_id를 맵핑해서 중간테이블인 PostSkill  저장.

 

 

그리고 문제점은 @Transactional의 선언을 메소드마다 해줘야 한다고 알고 있었다는 점이다.

위 코드에서 보면 @Transactional 어노테이션이 createPost( )메소드에 선언되어 있고,
createPost( ) 메소드에서 mapPostWithSkill( ) 메소드를 호출하고 있기 때문에,
mapPostWithSkill( ) 메소드에 @Transactional을 하지 않아도 자동으로 createPost( )메소드의 트랜잭션에 참여하게 된다.

만약에 두 메소드가 서로 다른 트랜잭션으로 실행하려는 의도였다면, 위 코드에서는 Self Invocation 때문에  @Transactional의 기본 전략은 상위 트랜잭션에 참여하게 되므로,
mapPostWithSkill( ) 메소드는 트랜잭션이 적용되지 않고, 결과적으로 createPost( ) 메소드의 트랜잭션만 실행되게 된다. 


스프링 공식문서는 @Transactional 메소드 내부 호출시 프록시가 적용되지 않는다고 적혀있다 즉 Self-invocation 을 하지 않는것이다. 즉, 내부 호출시 @Transactional 이 동작하지 않는다.

 

Self Invocation에 대해서 궁금하다면 아래 사이트에서 알아보자

https://gmoon92.github.io/spring/aop/2019/04/01/spring-aop-mechanism-with-self-invocation.html

 

나는 createPost( ) 메소드의 트랜잭션에 mapPostWithSkill( )의 비즈니스 로직이 같은 트랜잭션 내에서 돌아가기를 원했으므로 mapPostWithSkill( ) 메소드에서 @Transactional 어노테이션을 지웠다.

 

 

생각보다 내용이 많아져서 @Transactional을 어느 Layer에 두는지에 대한 얘기는 다음 게시물에서 이어서 하겠다.