티스토리 뷰

1. 이슈와 해결

레거시 코드가 있는 상태에서 기존 코드의 동작을 확장시키는 코드를 만들일이 있었다. 한 문장이지만 표현이 거창해서 그렇지 사실 대부분의 개발은 첫 문장과 같은 형태가 될것이다. 기존 코드를 어떤방식으로 확장하면 좋을까를 고민하다가 상속과 오버라이딩을 이용하기로 했다. 간략화된 코드는 아래와 같다. 

@Service
@RequiredArgsConstructor
class AService {
    private final BService bService;

    @Transactional
    void aMethod() {
        bService.bMethod1();
        bService.bMethod2();
    }
}

@Service
class BService {
    @Transactional
    void bMethod1() {

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void bMethod2() {

    }
}

위에있는 AService 와 BService 가 기존 레거시 코드이다. 실제 코드에서는 훨씬 복잡한 형태였으며 테스트 코드도 충분하지 않았기에 AService 와 BService 를 수정하는건 썩 좋은 방법이 아니라고 생각했다. 이중에 내가 확장해야할 클래스는 BService 였기에 이에 대한 코드를 작성했다.

@Service
class SubBService extends BService {
    @Override
    @Transactional
    void bMethod1() {
        // Entity 를 업데이트함
    }

    @Override
    void bMethod2() {
        // Entity 를 조회함
    }
}

BService 를 상속받는 SubBService 클래스를 만들었고, AService 에서 특정 조건에 따라 bService 필드에 SubBService 를 주입받도록했다. SubBService 는 주석으로 작성한대로 bMethod1() 에서 특정 entity 를 업데이트하고, bMethod2() 에서 업데이트된 entity 를 조회한다. 이 두 메서드를 모두 AService 의 aMethod() 에서 호출하는데 aMethod() 에 트랜잭션이 걸려있기때문에 이슈없이 동작하리라 생각했다.

 

하지만 코드는 내가 생각한대로 작동하지않았다. bMethod2() 메서드에서 조회한 entity 는 업데이트 되지않은 기존의 entity 였던것이다. 이유는 무엇일까? 간략화된 예제코드에서는 문제의 원인으로 추정되는 부분이 꽤나 금방 보이지만 실제 복잡한 레거시코드에서는 이부분을 확인하기가 쉽지않았다. SubBService 의 상위 클래스인 BService 에서 bMethod2() 메서드에 @Transactional 애노테이션 설정이 걸려있었던게 문제의 원인이었다. propagation 설정이 무엇을 의미하는지는 검색하면 쉽게 알 수 있으니 이번 포스팅에서는 다루지 않는다.

 

여튼 상위 클래스에 @Transactional 애노테이션 설정으로 인해 하위 클래스에서 내가 기대했던대로 동작하지않는것이다. 누차 말하지만 BService 의 모델이 된 실제 클래스는 프로덕션 환경에서 작동하고있는 코드기때문에 함부로 기존 트랜잭션 설정을 건드리는건 어떤 사이드 이펙트가 있을지 알 수 없었다. 이 문제는 어떻게 해결 할 수 있을까?

 

내가 SubBService 의 bMethod2() 메서드에 트랜잭션 애노테이션을 붙이지않은 이유는 bMethod2() 메서드에서는 조회 1번만 하기때문이다. 특별히 트랜잭션을 걸어둘 이유가 없다고 판단해서 걸지않았던 것인데 하위 클래스에서도 트랜잭션 애노테이션을 붙이면 상위 설정을 셰도우(shadow) 하여 설정을 덮어쓰게된다.

@Service
@RequiredArgsConstructor
class SubBService extends BService {
    @Override
    @Transactional
    void bMethod1() {
        // Entity 를 업데이트함
    }

    @Override
    @Transactional
    void bMethod2() {
        // Entity 를 조회함
    }
}

 

2. Spring 의 Annotation scanner

비단 이 이슈뿐만 아니라도 애노테이션과 상속이 서로 엮여있을때 어떤식으로 작동하는지 명확히 이해하기란 쉽지않다. 그리고 Spring 은 더 깊은 구조의 상속관계에서도 상위 클래스에 있는 애노테이션을 읽어오는데, 실제 리플렉션 API 를 활용해서 그 코드를 흉내내보고자하면 생각보다 복잡해지는걸 알 수 있다. 그래서 스프링은 어떤식으로 트랜잭션 애노테이션을 읽어오고있는지 코드를 확인해보기로 했다.

 

스프링은 다양한 종류의 Annotation Parser 를 구현하고있고, parser 들은 또 다양한 종류의 strategy 를 활용하고있다. 복잡한 객체 구조를 파고들어 AnnotationScanner 라는 클래스를 찾았다. 현재 내가 예제로 올린 코드와 같은 형태로 트랜잭션 애노테이션을 활용하고있는 경우엔(트랜잭션 애노테이션을 메서드에 달았을때) processMethodHierarchy() 메서드를 이용해 트랜잭션을 읽게 된다. 현재 메서드에 트랜잭션 애노테이션이 없다면 현재 메서드가 선언된 클래스의 상위 클래스로 이동한다. 이때 재귀호출 방식으로 이루어지기때문에 아마도 클래스 상속계층이 엄청나게 깊다면 스택오버 플로우가 날수도 있을것 같다(그렇게 깊은 구조가 있을지는...). 이런식으로 구현되어있기때문에 하위 클래스에서 트랜잭션 애노테이션을 선언하는 경우 상위 클래스까지 올라가지 않게 되는것이다.

 

https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java

 

내가 확인한 코드는 2020-07-29 master 브랜치 기준이기때문에 시간이 흐른다면 구현 내용은 변경 될 수 있다.(private 메서드라 더더욱 구현 변경에 부담이 없을것 같다.)

댓글
댓글쓰기 폼