post-custom-banner

개요

서비스를 만들던 중 실 사용자를 만들게 될 제품을 만드는 것이니 Transaction을 도입하면 좋지 않겠나하는 피드백을 42내 캐비넷 서비스의 동료에게 받게 되었다. 트랜잭션에는 읽기 전용 옵션이 있는데 이를 적용하면 최적화가 된다는 내용을 접했고, CUD와 Read를 나누기 위해 주석으로 서비스 클래스를 나누던 도중, 'findBoard()'와 같이 특정 키를 이용해 Repository를 직접 접근하여 도메인을 가져오는 메서드는 트랜잭션을 걸어야 할까라는 고민을 하게 되었다.

// BoardService.java
    public Board findBoard(String encryptedBoardId) {
        Long boardId = secureDataUtils.decrypt(encryptedBoardId);
        Optional<Board> optionalBoard = boardRepository.findById(boardId);

        if (optionalBoard.isEmpty()) {
            throw new NotAcceptableException("존재하지 않는 보드입니다.");
        }
        return optionalBoard.get();
    }

이와 같이 작은 단위의 메서드를 사용하는 또다른 서비스 메서드가 존재하는 상황인데 별다른 이유 없이 트랜잭션을 두 번 걸게 되는 상황을 우려하게 된 것이다. 그리하여 필요 없는 메서드는 걸지 않고, 보기에 좋게 다음과 같은 형식으로 주석을 달아 서비스 계층 내 메서드를 특징 별로 분류하기로 했다.

  • 읽기 전용 API(조회, Read)가 사용하는 메서드
  • CUD 메서드
  • Repository를 직접 조회하는 작은 단위의 비즈니스 로직

트랜잭션을 사용한 메서드와 사용하지 않은 메서드, 읽기 전용 트랜잭션인 메서드들이 따로 있고, 주석이 많아지니 코드 가독성을 해치겠다는 판단을 하여 다른 사람들은 어떻게 해결했을까가 궁금해졌다. 그리하여 찾아낸 것이 퍼사드 패턴이고, 이는 서비스에서 트랜잭션의 책임을 전부 전담하는 객체를 따로 만들어 낼 수 있다는 생각을 하게 되었다.

Facade(퍼사드) 디자인 패턴

Refactoring.Guru

출처: Refactoring.Guru

Facade는 건물의 외관이라는 뜻으로 프로그래밍에선 복잡한 기능을 추상화하여 인터페이스를 제공한다는 목적을 지닌 디자인 패턴이다. 위 사진처럼 추상화 된 VideoConverter라는 퍼사드 객체에 Application이 요청을 보내면 아래 있는 AudioMixer, VideoFile 등의 객체를 사용하는 방식이다.

우리의 상황으로 본다면 현재 진행 중인 Spring 프로젝트에서는 MVC 패턴을 사용한다. 이는 보통 Client - Controller - "?" - Service - Repository - ORM - DB의 관계를 갖는데 내가 고려한 퍼사드 패턴을 적용하면 Controller와 Service 사이에 표시한 "?"에 하나의 새로운 계층을 생성하는 것이다.

이로써 우리 프로젝트에서는 @Transactional 어노테이션을 달게 될 책임만 부여할 Facade라는 객체가 생기고, Service는 더 세부적인 비즈니스 로직을 다루게 되는 것이다.

트랜잭션을 지금 도입해야 하는가?

현재 우리 서비스는 CRUD 중 Create(생성)과 Read(조회)만 있는 상황에서 트랜잭션을 도입하면 어떤 이점을 가질지 고민하였고, 트랜잭션의 ACID 성질 중 Atomicity(원자성)을 고려하여 성공하면 전부 성공하거나 실패하면 전부 실패하는 이점을 보고 도입을 결정하였다. 심지어 조회 시에도 서로 다른 도메인을 가져오는 과정에서 실패 시 그대로 진행 되는 것이 아닌 중간의 변경 사항들을 전부 롤백하고, 개발자가 의도한대로 동작하도록 제어하기 위함이다.(하지만 이 부분을 내가 자세하게 고려하지 못했다는 것을 이후에 알게 된다)

원자성(Atomicity)

원자성이란 하나의 논리적인 작업 단위를 이야기하는 트랜잭션에서 하나라도 실패하면 전부 실패하고, 성공할 것이라면 전부 성공하도록 하는 성질을 이야기한다. 여기에서 트랜잭션이라고 하는 논리적인 작업의 단위는 하나 이상의 비즈니스 로직이 포함 되어있다는 것을 고려한 것이다.

예를 들면 게임에서 유저를 생성하는 API를 호출할 때 아바타와 랭킹 쪽에 데이터가 생긴다. 이 때 유저 생성 -> 아바타 생성 -> 랭킹 정보 생성의 순서로 진행 될 때 유저까지 생성했는데 아바타 생성에서 실패했다. 이렇게 되면 정상적인 상황은 유저, 아바타, 랭킹은 전부 한 번에 만들어져야 하는 상황으로 세 개가 원자성을 지녀야 하는데 트랜잭션을 걸지 않으면 유저가 생성 된 채로 아바타는 생성 되지 않고, 에러 상황 맞이를 기다리게 된다.

팀 회의 내용

트랜잭션 지금 필요해?

Q) 그래서 지금 진짜 트랜잭션이 필요한가? 생성 및 조회만 있는 상황에서 이것을 하지 않는다고 치명적인 에러 상황을 맞이할 수 있나?

A) 기존에 생각했던 두 번 이상의 조회 후 프로퍼티가 하나 이상인 JSON을 만들어 응답을 주는 경우에 몇 개만 담기고, 몇 개는 담기지 못하는 상황을 고려했다. 이 상황에서 게시물 제목은 보이는데 컨텐츠들이 보이지 않은 경우를 생각할 수 있지 않은가?

Q) SQL문이 실패하면 해당 위치에서 예외를 던져 이후에 catch하게 되기 때문에 애초에 그런 상황이 발생할 수 없다. 우리가 문제로 삼을만한 문제는 그나마 하나의 API에서 두 개 이상의 도메인을 생성할 때이다. 현재는 그것이 User 생성 후 Oauth User를 생성하는 과정이고, User를 생성했는데 Oauth User를 생성하지 못할 때 이를 롤백하여 User마저 성공하지 못한 이전 상황으로 돌리는 것이다. 하지만 이걸 제외하면 없다.

순환 참조 문제를 해결할 수 있나?

Q) 의존성 주입(Dependency Injection)을 통해 BoardGroupService가 BoardService에 있는 findBoard()를 사용하고, BoardService에서 BoardGroupService에 있는 findBoardGroup()을 사용하기 때문에 순환 참조가 일어나 스프링 빌드 시 에러가 발생한다.

A) 작은 단위의 비즈니스 로직을 전부 Service 단에 넣고, 이를 사용하는 계층을 퍼사드에 넣으면 해결할 수 있다.(하지만 이번 기획에서 BoardGroup은 삭제 되었다)

서비스가 서비스를 의존성 주입 받으면서 의도치 않게 너무 많은 권한을 특정 서비스에게 많이 준다.

Q) 원론적인 객체지향적으로 봤을 때 올바르게 보이지 않는다.

A) 객체가 각각의 역할과 책임을 부여 받고, 다른 객체에 응답으로 주는 정보를 제한한다면 충분히 객체지향적으로 좋은 방향으로 만들 수 있다고 생각한다. 결국 가장 중요한 것은 내부 정보를 공개하지 않고, 캡슐화 하여 객체끼리 메시지로 협력한다면 크게 걱정하지 않아도 될 것 같다. 하지만 DI를 받으면 결국 'Has a' 관계와 같은 수준으로 되는 것이니 분명 좋은 패턴은 아니다.

규모가 크지 않은 상태에서 계층이 많다면 오히려 코드 복잡성을 키운다.

Q) 코드가 많지 않은 상황에서 퍼사드 계층이 생긴다면 오히려 코드가 복잡해진다.

A) 맞다. 이후에 Update, Delete 기능이 추가 되면 그때 다시 고려해보자!

우리 서비스에서 동시성 문제를 고려해도 치명적이지 않다.

Q) 생성 되는 board의 id를 추정하여 해당 id를 포함하는 board 조회 API를 생성 API와 함께 조회하면 생성이 보통 조금 더 걸리니 조회 페이지는 없는 상태를 조회하게 되어 404 에러가 발생할 것이다. 하지만 이는 새로고침하거나 다음에 다시 조회하면 쉽게 해결한다. 이게 사용자의 사용성을 낮추는 부분이 될 수 있을까? ACID의 Isolation을 고려해도 당장 트랜잭션을 고려할 문제가 있나?

A) 기존에 있는 board라고 해도 이를 변경이나 삭제가 없으니 공유 자원에 대해 현재로써 치명적인 문제가 발생하지 않는다.

결론

  • '트랜잭션을 지금 도입해야 하는가?' 파트에서 마지막에 깨닫게 된 부분은 두 번의 repository 조회 시 하나라도 실패하면 사실 끝까지 진행한 뒤 RESTful 응답이 생성되는 것이 아니라 Data JPA 예외가 발생하여 이에 따른 에러 상황 로직으로 흘러갈 것이다. 애초에 정상적인 응답이 가공하지 않기 때문에 내가 고려한 '개발자가 의도한 데이터만 담기지 않는다'는 문제가 발생할 수가 없다.

  • 기존에 문제 삼았던 서비스가 서비스를 호출하는 부분은 괜찮은데 객체지향 프로그래밍의 원론적인 객체지향을 벗어난다라는 피드백을 받았다. 하지만 내가 생각하는 객체지향은 역할, 책임을 나누고 이에 대한 메시지를 통한 협력 부분을 추상화 하여 구현한다면 큰 문제는 없다고 판단 된다. 하지만...지금은 트랜잭션 도입 자체를 고민하므로 이 부분은 공부를 더 해보는 것으로 결정! 현재 '객체지향의 사실과 오해'라는 책을 읽고 있는데 이 부분에 대해서 고려하며 읽으면 재미있을 것 같다.

  • 현재는 생성 및 조회 기능만 있기 때문에 ACID를 고려하여 유저의 사용성에 치명적인 문제는 없다. 극한의 상황에서도 새로고침을 하면 전부 해결되는 문제이고, 특정 기능에 복잡하게 얽혀 있는 것이 없다.

  • 때문에 이후 MVP에서 Update, Delete 기능이 추가 되면 다시 고려해보기로 했다.

  • 하지만 공부는 했으므로 트랜잭션과 (readOnly = true) 관련한 최적화 작업은 어떻게 이루어지는지, 그럼 읽기 전용 트랜잭션은 도대체 언제 사용하는 것이 올바른지 등 더 심도 깊은 공부를 하고, 팀원들에게 강의해주기로 했다. 아래는 오늘 백엔드 파트의 회의 내용을 칠판으로 미리 적어둔 부분이다.

  • 트랜잭션 공부를 위한 42Cabi 버그 제보를 통해 트랜잭션에 대한 이해가 조금 더 깊어졌다. Transaction의 propagation(전파)에 대해 더 찾아볼 예정이다.

profile
안녕하세요? 개발자 jayoon입니다.
post-custom-banner

0개의 댓글