• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

JPA 최적화 강의 수강 후, 개인 프로젝트 수행하면서 생긴 질문입니다.

23.08.29 02:20 작성 조회수 324

0

안녕하세요. JPA 최적화 강의를 수강하고, 개인적으로 DDD를 책으로 공부해본 후에 개인 토이 프로젝트를 진행하던 중, 뜻밖에 의문이 생겼는데 여쭐 곳이 없어서 이렇게 질문 올리게 되었습니다. 맨 땅에 헤딩으로 이런저런 강의, 책, 다른 분들의 소스를 참고 하려다보니 여러 개념이 뒤섞여서 혼동이 옵니다.. ㅠㅠ

현재 프로젝트에서는 크게 에그리거트를 CUSTOMER, EXTERNAL, SECURITYMEDIA 3개로 나누어 설계했는데요. 강의에서도, 책에서도 DOMAIN 계층에 있는 서비스는 해당 도메인에 대한 순수한 CRUD를 수행하는 것으로 보았습니다. DDD 책에서는 여러 에그리거트가 필요로 하는 기능을 구현할 때는 도메인 서비스로 구현하라고 이야기 했구요.

처음에는 책에서 조언하는 대로 도메인 서비스로 구현해보고자 하다가, 좀처럼 구현이 안되어서 다른 분들이 구현한 소스를 참조하다 보니 application(응용)영역을 FACADE라는 상위 계층을 두는 것을 방식을 알게 되었습니다. 소스를 따라가보니 각 애그리거트의 DOMAIN 영역에 있는 서비스를 주입하여, 각 도메인 영역에 있는 서비스를 적절히 호출하기에 책에서 본 도메인 서비스와 같은 역할을 하겠구나 하여,, 해당 프로젝트 구성 방식을 따라 개발해보기로 했습니다.

그런데 개발을 하다보니,, 참조하는 소스에서 메소드 단위의 트랜잭션의 적용을 facade 영역이 아닌, 도메인 영역의 서비스 구현체에서 하는 것을 알게 되었습니다. 제가 개발하고자 기능은 여러 애그리거트를 생성, 변경하는 하나의 행위가 하나의 트랜잭션으로 묶여야 하는데 말이죠.

이러한 이유 때문에 현재 소스는 FACADE에서는 하나의 도메인 영역의 서비스를 주입하여 하나의 메소드를 호출하도록 되어있고, 도메인 영역에 있는 해당 서비스의 구현체에서 여러 애그리거트의 서비스, 레포지토리를 주입받아 하나의 메소드에서 트랜잭션 단위로 수행하도록 구현되어있습니다..

@Service
@RequiredArgsConstructor
public class SecurityMediaFacade {

    private final SecurityMediaService securityMediaService;
    

    public SecurityMediaInfo.Main registerOtp(SecurityMediaCommand.RegisterSecurityMediaRequest req) { //디지털otp 발급
        // 디지털 otp 발행
        return securityMediaService.issueSecurityMedia(req, SecurityMediaType.DIGITAL_OTP);
    }
...
}
public interface SecurityMediaService {

    public SecurityMediaInfo.Main issueSecurityMedia(SecurityMediaCommand.RegisterSecurityMediaRequest req, SecurityMediaType type);
...
}
@Slf4j
@Service
@RequiredArgsConstructor
public class SecurityMediaServiceImpl implements SecurityMediaService {

    private final CustomerReader customerReader;
    private final SecurityMediaStore securityMediaStore;

    private final TokenStore tokenStore;
    private final ExternalClientService externalClientService;

    @Override
    @Transactional
    public SecurityMediaInfo.Main issueSecurityMedia(SecurityMediaCommand.RegisterSecurityMediaRequest req, SecurityMediaType type) {

        // 요청고객 찾기
        Customer customer = customerReader.findCustomerByRnn(req.getRnn());

        SecurityMedia newOtp = null;

        if(!customer.existActiveSecurityMedia()) {
            // otp 신규
            SecurityMedia initOtp = req.toEntity(SecurityMediaType.DIGITAL_OTP, customer);
            newOtp  = securityMediaStore.store(initOtp);

            // 토큰 발급 요청
            Token newToken = externalClientService.getToken(customer, newOtp);
            newOtp.addToken(newToken);
            tokenStore.store(newToken);
        }
        return new SecurityMediaInfo.Main(newOtp);
    }

화면 캡처 2023-08-29 015927.jpg

위에는 프로젝트의 구성인데.. 첫 단추부터 잘못 끼운 것도 같아서 시작 단계인 지금에서라도 좀 개선을 해보려고 하는데요.

  1. 사실 도메인 서비스가 제가 의도로 하는 여러 애그리거트의 서비스 기능을 묶어서 하는 건지 아무리 읽어봐도 혼선이 옵니다. 혹시 DDD 책에서 이벤트라는 개념이 나오는데 도메인 서비스가 아니라, 이 이벤트를 통해 다른 애그리거트의 응용 서비스를 호출하도록 핸들링 하는게 올바른 방법일까요?

  2. 지금과 같은 구조를 유지해도 된다면.. facade 영역의 메소드를 트랜잭션으로 묶고, 각 도메인 계층의 서비스들에서 선언된 해당 도메인에 대한 crud 메소드를 적절히 호출해가면서 facade 영역에서 비즈니스 로직을 처리해도 될까요?

 

너무 글이 장황하고 기네요.. ㅠㅠ 혹시 도움을 주신다면 너무나도 감사드리겠습니다.

답변 1

답변을 작성해보세요.

2

안녕하세요. 인생은회전목마님

DDD에 대해서 열심히 공부중이시군요 :)

Q: 사실 도메인 서비스가 제가 의도로 하는 여러 애그리거트의 서비스 기능을 묶어서 하는 건지 아무리 읽어봐도 혼선이 옵니다. 혹시 DDD 책에서 이벤트라는 개념이 나오는데 도메인 서비스가 아니라, 이 이벤트를 통해 다른 애그리거트의 응용 서비스를 호출하도록 핸들링 하는게 올바른 방법일까요?

A: 도메인 서비스는 여러 엔티티의 기능을 묶어서 처리할 수 있습니다. 참고로 도메인 서비스가 A, B 엔티티를 둘다 처리해야 하는 상황이라면 해당 도메인 서비스를 특정 엔티티 패키지에 꼭 넣지 않아도 됩니다. 그러니까 도메인 서비스를 A나, B 패키지에 넣지 않고, 별도의 패키지를 만들고 거기에 도메인 서비스만 넣어두어도 괜찮습니다.

말씀하신 이벤트를 사용해도 됩니다. 이벤트를 사용하는 경우 유연성은 증가하지만 모호함도 함께 증가하기 때문에 각각 장단점이 있습니다.

여기서 말하는 유연성이라는 것은 이벤트를 소비하고 싶을 때 기존 코드를 손대지 않고 쉽게 기능을 추가할 수 있습니다.

모호함이라는 것은 개발자가 코드를 추적할 때 발행하는 이벤트를 찾아야 하고, 또 어디서 이 이벤트를 소비하는지 찾아야 합니다. 단순히 어떤 메서드가 호출되는지 바로 찾을 수 있는 것과 비교하면 이 부분이 직관적이지는 않습니다.

Q: 지금과 같은 구조를 유지해도 된다면.. facade 영역의 메소드를 트랜잭션으로 묶고, 각 도메인 계층의 서비스들에서 선언된 해당 도메인에 대한 crud 메소드를 적절히 호출해가면서 facade 영역에서 비즈니스 로직을 처리해도 될까요?

A: 먼저 도메인 서비스에 트랜잭션이 걸려있는 것은 적절합니다. 하지만 생각해보면 여러 도메인 서비스를 묶어서 트랜잭션을 처리해야 하는 경우도 필요합니다. 이런 경우 단순한 해결 방법은 facade에 트랜잭션을 거는 방법입니다. 결국 facade와 도메인 서비스 둘다에 트랜잭션이 걸리게 되는데요. 도메인 서비스에도 트랜잭션이 걸려있고 facade에도 트랜잭션이 걸려 있어도 스프링의 트랜잭션 전파덕분에 하나의 트랜잭션으로 모두 묶어서 사용할 수 있습니다. 이 방식의 단점으로는 트랜잭션이 너무 길어질 수 있다는 점입니다. 이 방식을 사용할 때는 가급적 트랜잭션 처리에 꼭 필요한 로직만 묶어서 트랜잭션을 처리하는 것이 좋습니다.

다른 대안으로는 이벤트를 발행해서 처리하는 방법이 있습니다. 첫번째 도메인 서비스에서 이벤트를 발행하고 다른 곳에서 해당 이벤트를 받아서 처리하는 것이지요. 이때는 첫번째 도메인 서비스의 트랜잭션을 그대로 이어 받아서 두번째 도메인 서비스에서 처리하는 방법도 있고, 또는 첫번째 도메인 서비스에서 트랜잭션을 완전히 완료하고, 두번째 도메인 서비스에서 별도의 트랜잭션으로 처리하는 방법도 있습니다. 트랜잭션을 각각 사용하는 경우 트랜잭션은 격리되지만, 트랜잭션이 이어지지 않기 때문에 Transactional Outbox Pattern 같은 다른 대안들이 필요합니다.

이벤트를 사용하는 방법은 유연성이 증가하지만 모호함도 함께 증가합니다. 추가로 이벤트를 발행하고, 소비하는 코드를 유지하고 관리해야 합니다.

DDD에서 설명하는 여러 기법들은 복잡한 비즈니스를 처리할 때는 유용할 수 있지만, 대부분의 경우에는 단순한 계층형 방식이 더 나은 선택일 수 있습니다.

그리고 DDD의 모든 규칙을 지키려고 하기 보다는 필요한 부분은 우리 상황에 맞도록 선택해서 적용하는 것이 필요합니다.

인생은회전목마님 죄송하지만 앞으로는 질문 안내에 말씀드린 것처럼 강의 학습에 관련된 질문을 올려주시길 부탁드립니다. 저도 마음으로는 도움을 드리고 싶지만, 하루에도 수많은 분들이 질문을 올려주십니다. 그래서 강의 학습과 관련된 질문에 초점을 맞추는 것이 맞다 생각합니다. 다시한번 이해를 부탁드립니다 :)

 

강의에 대한 질문이 아닌데 이렇게 친절하게 설명해(자비를 베풀어)주셔서 너무나 감사합니다!!

이런 질문 다시 없도록 하겠습니다.