본문 바로가기

개발/루퍼스(Loopers)

Facade 패턴으로 레이어 책임 분리하기

반응형

나는 지난주 이커머스 기능을 개발하며, Point를 VO(Value Object)로 할지, 별도의 Entity 테이블로 만들지 고민하다가 후자를 선택하였다. 

가장 큰 이유는 별도의 테이블로 구현하는 것이 VO로 구현하여 User 테이블에 포함되게 하는 것보다 추후 복잡한 연산이나 비즈니스 로직을 적용하기에 적합하다고 판단했기 때문이다. (물론 이 또한 지난주 피드백을 통해 처음부터 미리 이렇게 설계하지 말고, 점진적으로 발전시키라고 하셨다)

 

그리하여 별 문제 없이 

public class UserController {
    private final UserService userService;
    private final UserDtoMapper userDtoMapper;
    private final PointService pointService;
    @Operation(summary = "회원 가입")
    @PostMapping("/users")
    public ApiResponse<UserResponseDto> signUp(
            @RequestBody UserRequestDto userRequestDto){
        User user = userDtoMapper.toEntity(userRequestDto);
        User saved = userService.saveUser(user);
        pointService.create(saved.getId());
        return ApiResponse.success(userDtoMapper.toResponse(saved));
    }

 

다음과 같이 Controller를 구현하였고, 멘토님께 중요한 부분을 피드백 받을 수 있었다. 

 

멘토님의 피드백에 의하면 내 코드에선 2가지가 잘못 되었다.

 

1. 컨트롤러에서 도메인을 조합하고 있어 레이어 책임을 위반 한다는 점

2. User 저장과 Point 생성이 별도 트랜잭션이라는 점 

 

Controller는 단순히 인터페이싱 역할을 해야하기에 HTTP 요청/응답 처리와 DTO 변환 정도만 작성해야 한다. 

내가 지금 구현한 도메인 조합은 Application Layer(Facade)에서 진행해야 한다.  그 것이 '레이어의 책임 분리'다. 

 

 

추가적으로 나는 피드백 받은 내용 중 2번을 실제로 검증해보기 위해 E2E 테스트 한 개를 작성해보았다. 

@DisplayName("회원 가입 시 Point 생성이 실패하면, User만 저장되어 데이터 불일치가 발생한다")
        @Test
        void data_inconsistency_when_point_creation_fails() {
            //given
            UserRequestDto requestDto = new UserRequestDto(
                    "testUser",
                    "test@test.com",
                    "1998-02-21",
                    "MALE"
            );

            doThrow(new RuntimeException("Point 생성 실패"))
                    .when(pointService).create(anyString());

            //when
            try {
                testRestTemplate.exchange(
                        "/api/v1/users",
                        HttpMethod.POST,
                        new HttpEntity<>(requestDto),
                        new ParameterizedTypeReference<ApiResponse<UserResponseDto>>() {}
                );
            } catch (Exception e) {
            }

            //then
            assertAll(
                    () -> assertThat(userJpaRepository.findById("testUser")).isPresent(), // User는 저장됨
                    () -> assertThat(pointJpaRepository.findById("testUser")).isEmpty()  // Point는 생성되지 않음
            );
        }

 

예상대로 user에는 정상적으로 저장되었으나, point에는 유저id가 저장되지 않는 문제가 발생하였다.

 

이를 통해 "회원가입 = 유저 생성 + 포인트 생성" 을 원자적(Atomic)하게 처리해야 한다는 것을 알게 되었다. 

 

그리하여 나는 Facade 패턴을 적용하여 

 

@RequiredArgsConstructor
public class UserFacade {
    private final UserService userService;
    private final PointService pointService;
    
    @Transactional
    public User execute(User user) {
        userService.saveUser(user);
        pointService.create(user.getId());
        return user;
        }
    }

 

다음과 같이 유저 생성과 포인트 생성을 한 트랜잭션에 처리되도록 하였고, 그 말인 즉, 하나의 작업이라도 실패하면 모두 실패해야 한다는 ACID 특성 중 Atomicity를 보장하도록 하였다. 

 

//개선된 Controller
@RestController
public class UserController {
    private final UserFacade userFacade;
    private final UserDtoMapper userDtoMapper;
    
    @PostMapping("/users")
    public ApiResponse<UserResponseDto> signUp(@RequestBody UserRequestDto userRequestDto) {
        User user = userDtoMapper.toEntity(userRequestDto);
        User saved = userFacade.execute(user);  
        return ApiResponse.success(userDtoMapper.toResponse(saved));
    }
}

 

개선 후에 다시 테스트 해본 결과 

 

 

원하는 대로 동작하는 것을 확인하였다. 

 

이번 기회를 통해 각 레이어의 책임과 역할에 대해 생각해볼 수 있었다. 지금까지는 1인 프로젝트를 하는 일이 많아 기능 작동에만 초점을 두었다면, 앞으로는 협업을 염두에 둔 구조와 테스트를 해야겠다고 느꼈다.   

반응형