Development/썬카(SunCar) - 개인 프로젝트

[썬카/백엔드] 차량 판매등록 기능 구현 - 더미 데이터 생성과 객체지향적 설계

양선규 2025. 4. 3. 16:43
728x90
반응형

 

잔뜩 들어간 더미 데이터

 

 

차량 판매등록 기능을 구현하며 객체지향적 설계를 한 것에 대해 정리해 보고자 한다. 차량 등록에 필요한 엔티티는 10개이며, 판매되는 자동차 1대가 10개 엔티티의 데이터를 전부 필요로 한다.

 

현재 차량 정보를 조회하는 카히스토리 api는 도입하지 않았기 때문에(너무 비싸..) 도입했다는 가정 하에 api 리턴값과 동일한 형태의 테이블을 설계했고, 그에 맞는 더미 데이터 생성 로직을 작성했다. 그 더미 데이터를 이용해 차량 등록 기능을 구현해 보았다. 다만 엔티티가 너무 많았기 때문에 코드가 길어졌고, 하나의 서비스 클래스에 이 모든 로직을 (엔티티 저장, dto변환, 더미데이터 생성 등) 담는 건 매우 좋지 않은 설계라고 생각되었다.

 

따라서 각 엔티티별 서비스 클래스에 create 메서드를 각각 만들고, 더미 데이터 생성 로직은 util 패키지 하위에 따로 클래스로 만들어 두었다. 그리고 facade 서비스에서 이것들을 호출해 차량 등록 기능을 완성했으며, 마지막으로 더미 데이터 관련 클래스는 전부 dev환경에서만 동작하도록 구분해 두었다.

 

 

 

차량 관련 Facade 서비스 ( 컨트롤러에서 호출되는 최상단 서비스 )

// service/facade/CarFacadeDummyServiceImpl
// 클래스 레벨에 @Profile("dev")

/**
 * 차량을 판매등록합니다.
 * - 더미 데이터를 이용합니다.
 */
@Override
@Transactional
public RegisterCarResponseDto registerCar(RegisterCarDummyRequestDto dto, String userId) {

    /** Model */
    ModelDto modelDto = carDummyDataGenerator.generateModelDto(); // 더미 데이터 생성
    Model model = modelService.createModel(modelDto); // create (DB에 저장)

    /** Car */
    CarDto carDto = carDummyDataGenerator.generateCarDto(model.getId(), dto.getCarNumber());
    Car car = carService.createCar(carDto);

	// 생략 ...

    /**
     * 결과를 dto로 만든 후 리턴
     */
    RegisterCarResponseDto registerCar = carMapper.toRegisterCarResponseDto(listing, car, model);
    return registerCar;
}

 

 

10개나 되는 엔티티의 더미 데이터 생성 로직과, DB 저장을 위한 엔티티 생성 및 저장 로직을 전부 이곳 facade 서비스에 넣는건 말이 안 되게 긴 작업이었다. 애초에 단일 책임 원칙(SRP)를 지키기 위해 엔티티 별 서비스를 따로 두고 facade 서비스를 도입한 것이기도 하니 분리는 필수적이었다.

 

그래서 더미 데이터 생성 메서드와 create 메서드를, 각각 util 패키지와 엔티티 별 서비스 클래스에 따로 구현하고 facade 서비스에서는 호출만 하는 형태로 구현하였다. 이렇게만 해도 코드가 매우 길다. 마지막엔 Mapper를 이용해서 dto에 담은 후 리턴해 주었다.

 

각 엔티티별 서비스 클래스에서 호출되는 create 메서드는 단순히 엔티티로 변환 후 save만 하기도 하지만, 일부 메서드(image 관련 메서드)들은 로직이 추가되기도 한다. 따라서 save만 있는 메서드더라도 다른 메서드와의 일관성을 위해 서비스 계층을 꼭 거치도록 했다. 이것이 계층 별 관심사 분리에도 좋은 방향이다.

 

 

더미 데이터 생성 클래스

// util/CarDummyDataGenerator

/**
 * 차량 판매등록 시 사용될 더미 데이터 생성을 위한 클래스 입니다.
 *
 * - 차량 관련 10개 엔티티에 대한 더미 데이터 생성 메서드를 제공합니다.
 * - 개발 환경에서만 활성화됩니다.
 */
@Component
@Profile("dev") // 개발 환경에서만 동작
public class CarDummyDataGenerator {

    /** Model */
    public ModelDto generateModelDto() {

        String uuid = RandomUtils.createUuid(6); // 6자리 UUID 생성
        String brandName = "브랜드-" + uuid;
        String modelName = "모델-" + uuid;


        return ModelDto.builder()
                .brandName(brandName)
                .modelName(modelName)
                .isForeign(RandomUtils.randomBoolean()) // 랜덤 true OR false
                .build();
    }

    // 생략 ...
    // 생략 ...

    /** CarListingImage -> 일반 이미지 등록 (여러장) */
    public List<CarListingImageDto> generateCarListingDtosFromAdditionalImages(
            Long listingId,
            List<String> additionalImages
    ) {

        List<CarListingImageDto> images = new ArrayList<>();
        for (String imageUrl : additionalImages) {
            CarListingImageDto image = CarListingImageDto.builder()
                    .listingId(listingId)
                    .imageUrl(imageUrl)
                    .isPrimary(false)
                    .build();

            images.add(image);
        }
        return images;
    }
}

 

더미 데이터 생성 클래스는 util 패키지에 따로 빼 놓았다.

 

더미 데이터라는 것은 테스트 또는 개발 과정에서 필요한 것인데, 실제 배포용 비즈니스 로직과 같은 공간에 있는 것은 바람직하지 않다. 또한, 추후 다른 로직에서 사용될지도 모르므로 util 패키지 하위에 따로 구현해 두었다.

 

클래스 레벨엔 @Profile("dev") 어노테이션을 달아서, dev 환경이 아닐 때는 동작하지 않도록 설정했다.

generateModelDto() 쪽을 보면 createUuid(), randomBoolean() 메서드가 있는데 이것들은 직접 구현한 static 메서드이다. 이것들도 마찬가지로 util 패키지 하위에 별도 클래스로 빼 놓았다.

 

나는 중복되면 안 되는 데이터엔 6자리 uuid를 추가해 놓았는데, 더미 데이터를 실제 데이터처럼 만들까도 생각해 봤지만 그건 너무 엄청난 반복작업이고, 개발단계에서는 딱히 문제도 없으며, 어차피 카히스토리 api가 도입되면 사용되지 않을 것들이라 그냥 uuid 추가로 합의를 보기로 했다.

 

엔티티 별 서비스 클래스와 create 메서드

// service/car/CarServiceImpl
/**
 * Car 엔티티 관련 서비스 클래스 입니다.
 */
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CarServiceImpl implements CarService {

    private final CarRepository carRepository;
    private final CarMapper carMapper;

    /**
     * 차량 정보를 생성합니다.
     */
    @Override
    @Transactional
    public Car createCar(CarDto dto) {
        Car car = carMapper.fromCarDto(dto);
        Car saved = carRepository.save(car);

        return saved;
    }
}

// service/car/CarListingImageServiceImpl

    // 생략 ..
    /**
     * 차량 판매등록 이미지를 다수 생성합니다.
     */
    @Override
    @Transactional
    public List<CarListingImage> createImages(List<CarListingImageDto> dtos) {

        // 요청받은 이미지들이 일반 이미지인지 확인 ( isPrimary 확인 )
        boolean hasPrimaryImage = dtos.stream()
                .anyMatch(CarListingImageDto::getIsPrimary);

        if (hasPrimaryImage) {
            throw new InvalidArgumentException(ErrorMessages.PRIMARY_IMAGES_NOT_ALLOWED);
        }

        // 모든 DTO를 엔티티로 변환 후 DB 저장 및 리턴
        List<CarListingImage> carListingImages = dtos.stream()
                .map(carMapper::fromListingImageDto)
                .collect(Collectors.toList());

        List<CarListingImage> savedImages = carListingImageRepository.saveAll(carListingImages);

        return savedImages;
    }


// 이외의 8개 서비스들

 

create 메서드는 각 엔티티 별 서비스 클래스에 만들었다. 더미 데이터 생성과 마찬가지로 10개 엔티티의 create 로직을 한 군데 모아둔다는 건 코드가 매우 길어지고 바람직하지 않기 때문에 따로 분리해 두었다.

 

생성된 더미데이터 Dto를 create 메서드에 넣기만 하면 데이터가 DB에 저장되도록 만들어 두었다.

 

dev 환경 설정

// resources/application.properties

// 생략 ..
spring.profiles.active=dev

 

이제 더미 데이터 클래스들이 @Profile("dev") 어노테이션에 의해 dev환경에서만 돌아가도록 설정해 준다.

application.properties 파일에 위 설정을 추가해 주기만 하면 dev환경으로 서버가 돌아가는 것이다.

 

테스트/개발 환경에서만 사용되는 더미 데이터 및 관련 로직들이 prod 환경에서 돌아가는 것은 바람직하지 않다. 따라서 prod 환경에서 호출되거나 로직이 섞이지 않도록 dev 환경을 구분해 두었다.

 

컨트롤러

// controller/car/CarController

/**
 * 차량 관련 컨트롤러 입니다.
 */
@RestController
@RequestMapping("/cars")
@ResponseStatus(HttpStatus.OK)
@Tag(name = "Car")
@RequiredArgsConstructor
public class CarController {

    /**
     * 더미 데이터 입력용 구현체
     *
     * TODO
     * 카히스토리 API 및 S3 도입 후, CarFacadeService 로 교체
     */
    private final CarFacadeDummyService carFacadeDummyService;
	
    
    // 생략..
    // 생략..
    
    
    /**
     * 차량을 판매등록합니다. - 더미 데이터 입력용 컨트롤러 입니다.
     */
    @PostMapping("/dummy")
    @SecurityRequirement(name = "bearer-jwt")
    @Operation(summary = "차량 판매 등록 - 더미 데이터 입력용")
    public ResponseEntity<ResponseDto<RegisterCarResponseDto>> registerCarDummy(
            @AuthenticationPrincipal CustomUserDetails userDetails,
            @RequestBody RegisterCarDummyRequestDto dto
    ) {
        RegisterCarResponseDto registeredCar = carFacadeDummyService.registerCar(dto, userDetails.getUserId());
        ResponseDto<RegisterCarResponseDto> response = ResponseDto.of(ResponseMessages.CAR_REGISTERED, registeredCar);

        /**
         * TODO
         * 등록된 차량 상세조회 페이지 경로 리턴해주기
         */
        return ResponseEntity.created(URI.create("/cars"))
                .body(response);
    }
}

 

마지막으로 컨트롤러이다.

 

위에서 만든 로직들은 전부 추상화 되어 있으며, 컨트롤러는 단지 facade 서비스 메서드를 호출한 후 리턴값을 ResponseDto로 감싸 주기만 하면 된다.

 

객체지향에서 추상화란 건 너무나도 중요한 것 같다. 메서드가 어떤 리턴값을 반환하는지만 안다면, 사용자는 이 메서드들을 조합해서 다양한 행위를 할 수 있다. 안에 있는 로직을 생각할 필요가 없다. 당연하면서도, 복잡한 프로그래밍을 단순하게 생각할 수 있게 해주는 것 같다.

 

마무리

차량 판매등록 기능을 구현하면서 여러 가지 객체지향적 설계법을 경험해 본 것 같다.

 

단일 책임 원칙(SRP)

-> 각 엔티티 별 서비스 클래스 제작, 더미 데이터 생성 클래스 분리

캡슐화

-> 각 기능을 적절한 클래스 내부로 캡슐화하여 복잡한 로직을 숨기고 필요한 메서드만 외부에 노출

관심사 분리

-> 비즈니스 로직(서비스), 유틸(더미 데이터 생성, 랜덤값 생성), 통합 서비스 로직(Facade 서비스)을 명확히 분리

 

 

내가 짠 코드들이 그렇게 대단한 건 아니다. 어쩌면 누구나 하는 것일 수 있고, 어거지로 끼워맞추는 것일 수도 있다.

 

그러나 다양한 객체지향적 설계방법을 끊임없이 생각하며 적용하려고 노력하는 것,

내가 짠 코드가 어떤 원칙에 부합할까/부합하지 않을까 역으로 생각해 보는 것 등,

항상 객체지향을 적용하려는 습관을 들인다면, 이것이 당연하게 몸에 익어 어느 순간 완벽한 구조의 코드를 숨쉬듯이 짜고 있지 않을까.. 그렇게 점점 시니어에 가까워지지 않을까.. 생각한다.

 

=============

 

 

 

728x90
반응형