예약 시스템 API - 전시 상세 정보 조회

Seyeong·2023년 4월 8일
0
post-thumbnail

이번엔 전시 상품에 대해서 상세한 정보를 조회하는 API를 만들겠습니다.

먼저 API는 아래와 같습니다.

API 문서

요청

응답

...

문서를 보면 필요한 값들이 매우 많습니다. 이는 DTO 객체에서 문서를 생성하는 부분의 코드가 길어지고, 책임이 많아진다는 것을 의미합니다.

따라서 DTO 객체들이 문서를 생성하는 부분을 리팩토링 해줄 필요가 있습니다.
이 부분은 모든 구현을 마치고 나서 하도록 하고, 우선은 비즈니스 로직을 구현해보겠습니다.

먼저 Repository 입니다.

Repository 테스트 코드 작성

우선 전시 상세 정보에 필요한 데이터들을 살펴보면 아래와 같습니다.

DetailDisplayInfosResponseDto

public class DetailDisplayInfosResponseDto {
    private final DisplayInfoResponseDto product;
    private final List<ProductImageDto> productImages;
    private final List<DisplayInfoImageDto> displayInfoImages;
    private final int avgScore;
    private final List<ProductPrice> productPrices;
}

이러한 데이터를 응답하기 위해선 아래와 같은 데이터들이 필요합니다.

  • DisplayInfoResponseDto
  • ProductImageDto
  • DisplayInfoImageDto
  • ProductPrice

따라서 각각의 DTO들을 조회하는 테스트 코드를 작성해봅시다.

DisplayInfo 조회 테스트

JdbcDisplayInfoRepositoryTest

...
public class JdbcDisplayInfoRepositoryTest {
	
    ...
    
    @Test
    void testDisplayInfo() {
        // given
        DisplayInfoResponseDto expected = getDisplayInfo();

        // when
        DisplayInfoResponseDto actual = repository.getDisplayInfo(1);

        // then
        assertThat(actual.getId()).isEqualTo(expected.getId());
        assertThat(actual.getName()).isEqualTo(expected.getName());
    }

    public static DisplayInfoResponseDto getDisplayInfo() {
        return DisplayInfoResponseDto.builder()
                .id(1)
                .name("전시")
                .build();
    }
}

ProductImages 조회 테스트

JdbcProductRepositoryTest

@DataJdbcTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class JdbcProductRepositoryTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    private ProductRepository repository;

    @BeforeEach
    void setUp() {
        repository = new JdbcProductRepository(jdbcTemplate);
    }

    @Test
    void testProductImages() {
        // given
        List<ProductImageDto> expected = getProductImages();

        // when
        List<ProductImageDto> actual = repository.getProductImages(1);

        // then
        assertThat(actual.size()).isEqualTo(expected.size());
        assertThat(actual.get(0).getProductId()).isEqualTo(expected.get(0).getProductId());
        assertThat(actual.get(0).getProductImageId()).isEqualTo(expected.get(0).getProductImageId());
        assertThat(actual.get(0).getFileInfoId()).isEqualTo(expected.get(0).getFileInfoId());
    }

    public static List<ProductImageDto> getProductImages() {
        return List.of(
                ProductImageDto.builder()
                        .productId(1)
                        .productImageId(2)
                        .fileInfoId(61)
                        .build()
        );
    }
}

DisplayInfoImages 조회 테스트

JdbcDisplayInfoRepositoryTest

...
public class JdbcDisplayInfoRepositoryTest {

	...

    @Test
    void testDisplayInfoImages() {
        // given
        List<DisplayInfoImageDto> expected = getDisplayInfoImages();

        // when
        List<DisplayInfoImageDto> actual = repository.getDisplayInfoImages(1);

        // then
        assertThat(actual.size()).isEqualTo(expected.size());
        assertThat(actual.get(0).getId()).isEqualTo(expected.get(0).getId());
        assertThat(actual.get(0).getDisplayInfoId()).isEqualTo(expected.get(0).getDisplayInfoId());
        assertThat(actual.get(0).getFileId()).isEqualTo(expected.get(0).getFileId());
    }

    public static List<DisplayInfoImageDto> getDisplayInfoImages() {
        return List.of(
                DisplayInfoImageDto.builder()
                        .id(1)
                        .displayInfoId(1)
                        .fileId(1)
                        .build()
        );
    }
}

ProductPrice 조회 테스트

JdbcProductRepositoryTest

...
public class JdbcProductRepositoryTest {

	...

    @Test
    void testProductPrice() {
        // given
        List<ProductPrice> expected = getProductPrice();

        // when
        List<ProductPrice> actual = repository.getProductPrice(1);

        // then
        assertThat(actual.size()).isEqualTo(expected.size());
        assertThat(actual.get(0).getPrice()).isEqualTo(expected.get(0).getPrice());
        assertThat(actual.get(1).getPrice()).isEqualTo(expected.get(1).getPrice());
        assertThat(actual.get(2).getPrice()).isEqualTo(expected.get(2).getPrice());
    }

    public static List<ProductPrice> getProductPrice() {
        return List.of(
                ProductPrice.builder().price(6000).build(),
                ProductPrice.builder().price(3000).build(),
                ProductPrice.builder().price(2000).build()
        );
    }
}

Score 조회 테스트

JdbcCommentRepositoryTest

@DataJdbcTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class JdbcCommentRepositoryTest {
    @Autowired
    JdbcTemplate jdbcTemplate;

    private CommentRepository repository;

    @BeforeEach
    void setUp() {
        repository = new JdbcCommentRepository(jdbcTemplate);
    }

    @Test
    void testCommentsScoreByProductId() {
        // given
        CommentScoreResponseDto expected = getCommentsScoreByProductId();

        // when
        CommentScoreResponseDto actual = repository.getCommentsScoreByDisplayId(1);

        // then
        assertThat(actual).isEqualTo(expected);
    }

    public static CommentScoreResponseDto getCommentsScoreByProductId() {
        return CommentScoreResponseDto.builder()
                .avgScore(3)
                .build();
    }
}

DetailDisplayInfos 조회 테스트

DetailDisplayInfoServiceTest

class DetailDisplayInfoServiceTest {
    @InjectMocks
    private DetailDisplayInfoService service;

    @Mock private JdbcDisplayInfoRepository displayInfoRepository;
    @Mock private JdbcProductRepository productRepository;
    @Mock private JdbcCommentRepository commentRepository;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testDetailDisplayInfos() {
        // given
        DisplayInfoResponseDto expectedDisplayInfo = JdbcDisplayInfoRepositoryTest.getDisplayInfo();
        List<ProductImageDto> expectedProductImages = JdbcProductRepositoryTest.getProductImages();
        List<DisplayInfoImageDto> expectedDisplayInfoImages = JdbcDisplayInfoRepositoryTest.getDisplayInfoImages();
        CommentScoreResponseDto expectedComment = JdbcCommentRepositoryTest.getCommentsScoreByProductId();
        List<ProductPrice> expectedProductPrices = JdbcProductRepositoryTest.getProductPrice();
        DetailDisplayInfosResponseDto expected = getDetailDisplayInfos();

        // when
        when(displayInfoRepository.getDisplayInfo(1)).thenReturn(expectedDisplayInfo);
        when(productRepository.getProductImages(1)).thenReturn(expectedProductImages);
        when(displayInfoRepository.getDisplayInfoImages(1)).thenReturn(expectedDisplayInfoImages);
        when(commentRepository.getCommentsScoreByDisplayId(1)).thenReturn(expectedComment);
        when(productRepository.getProductPrice(1)).thenReturn(expectedProductPrices);
        DetailDisplayInfosResponseDto actual = service.getDetailDisplayInfos(new DetailDisplayInfosRequestDto(1));

        // then
        assertThat(actual.getProduct().getId()).isEqualTo(expected.getProduct().getId());
        assertThat(actual.getProductImages().size()).isEqualTo(expected.getProductImages().size());
        assertThat(actual.getDisplayInfoImages().size()).isEqualTo(expected.getDisplayInfoImages().size());
        assertThat(actual.getAvgScore()).isEqualTo(expected.getAvgScore());
        assertThat(actual.getProductPrices().size()).isEqualTo(expected.getProductPrices().size());
    }

    public static DetailDisplayInfosResponseDto getDetailDisplayInfos() {
        return DetailDisplayInfosResponseDto.builder()
                .product(DisplayInfoResponseDto.builder().id(1).build())
                .productImages(List.of(ProductImageDto.builder().productId(1).build()))
                .displayInfoImages(List.of(DisplayInfoImageDto.builder().id(1).build()))
                .avgScore(3)
                .productPrices(
                        List.of(
                                ProductPrice.builder().id(3).build(),
                                ProductPrice.builder().id(2).build(),
                                ProductPrice.builder().id(1).build()
                        )
                )
                .build();
    }
}

여기선 특이하게 RepositoryTest가 아니라 ServiceTest로 작성해주었습니다. 그 이유는 전시 상세 정보를 얻기 위해서는 여러 도메인들의 Repository를 사용해야 할 뿐만 아니라, 전시 상세라는 그 자체의 조회는 하지 않기 때문에 Service에서 여러 Repository의 의존성을 주입받고 Service에서 각 Repository들을 이용해 조회한 결과를 만들어 반환하는 식으로 만들어주었습니다.

Repository 코드 구현

DisplayInfo 조회 구현

DisplayInfoRepository

public interface DisplayInfoRepository {
    DisplayInfoResponseDto getDisplayInfo(int displayId);
    
    ...
    
}

JdbcDisplayInfoRepository

...
public class JdbcDisplayInfoRepository implements DisplayInfoRepository {
	
    ...

    @Override
    public DisplayInfoResponseDto getDisplayInfo(int displayId) {
        return jdbcTemplate.queryForObject(DisplayInfoSQLMapper.SELECT_DISPLAY_INFO_QUERY,
                DisplayInfoResponseDto.displayInfoMapper,
                displayId);
    }
    
    ...
    
}

DisplayInfoSQLMapper

public class DisplayInfoSQLMapper {
    public static final String SELECT_DISPLAY_INFO_QUERY =
            "SELECT p.id, p.category_id categoryId, di.id displayInfoId, c.name, p.description, p.content, p.event, di.opening_hours openingHours, di.place_name placeName, di.place_lot placeLot, di.place_street placeStreet, di.tel, di.homepage, di.email, di.create_date createDate, di.modify_date modifyDate,\n"
                    + "    (\n"
                    + "        SELECT fi.id\n"
                    + "        FROM product_image pi, file_info fi\n"
                    + "        WHERE pi.file_id = fi.id and pi.product_id = p.id and fi.file_name LIKE CONCAT('%', 'ma' '%')\n"
                    + "    ) as fileId\n"
                    + "FROM product p, display_info di, category c\n"
                    + "WHERE p.id = di.product_id and p.category_id = c.id and di.id = ?";
                    
    ...
    
}

이렇게 하면 단일 전시 정보를 조회하는 테스트가 성공하게 됩니다.

이제 상품 이미지 조회를 구현해봅시다.

ProductImages 조회 구현

ProductRepository

public interface ProductRepository {
    List<ProductImageDto> getProductImages(int displayId);
}

JdbcProductRepository

@Repository
@RequiredArgsConstructor
public class JdbcProductRepository implements ProductRepository {
	private final JdbcTemplate jdbcTemplate;

    @Override
    public List<ProductImageDto> getProductImages(int displayId) {
        return jdbcTemplate.query(ProductSQLMapper.SELECT_PRODUCT_IMAGES,
                ProductImageDto.productImageMapper,
                displayId);
    }
}

ProductSQLMapper

public class ProductSQLMapper {
    public static final String SELECT_PRODUCT_IMAGES = "SELECT pi.product_id productId,pi.id productImageId, pi.type type, pi.file_id fileInfoId, fi.file_name fileName, fi.save_file_name saveFileName, fi.content_type contentType, fi.delete_flag deleteFlag, fi.create_date createDate, fi.modify_date modifyDate\n"
            + "FROM product_image pi, file_info fi\n"
            + "WHERE pi.file_id = fi.id AND pi.type = 'ma' AND pi.product_id = (\n"
            + "    SELECT di.product_id\n"
            + "    FROM display_info di\n"
            + "    WHERE di.id = ?\n"
            + ");";
}

ProductImageDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class ProductImageDto implements RestDocsTemplate {
    private final int productId;
    private final int productImageId;
    private final String type;
    private final int fileInfoId;
    private final String fileName;
    private final String saveFileName;
    private final String contentType;
    private final int deleteFlag;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;

    public static final RowMapper<ProductImageDto> productImageMapper = (rs, rowNum) -> {
            return ProductImageDto.builder()
                .productId(rs.getInt("productId"))
                .productImageId(rs.getInt("productImageId"))
                .type(rs.getString("type"))
                .fileInfoId(rs.getInt("fileInfoId"))
                .fileName(rs.getString("fileName"))
                .saveFileName(rs.getString("saveFileName"))
                .contentType(rs.getString("contentType"))
                .deleteFlag(rs.getInt("deleteFlag"))
                .createDate(rs.getObject("createDate", LocalDateTime.class))
                .modifyDate(rs.getObject("modifyDate", LocalDateTime.class))
                .build();
    };
}

여기까지 하면 상품 이미지 조회 테스트가 성공합니다.

이제 전시 상품 이미지 조회를 구현해봅시다.

DisplayInfoImages 조회 구현

DisplayInfoRepository

public interface DisplayInfoRepository {
	
    ...
    
    List<DisplayInfoImageDto> getDisplayInfoImages(int displayId);
}

JdbcDisplayInfoRepository

public class JdbcDisplayInfoRepository implements DisplayInfoRepository {
	
    ...

    @Override
    public List<DisplayInfoImageDto> getDisplayInfoImages(int displayId) {
        return jdbcTemplate.query(
                DisplayInfoSQLMapper.SELECT_DISPLAY_INFO_IMAGES,
                DisplayInfoImageDto.displayInfoImageMapper,
                displayId
        );
    }
}

DisplayInfoSQLMapper

public class DisplayInfoSQLMapper {
	
    ...
    
    public static final String SELECT_DISPLAY_INFO_IMAGES =
            "SELECT dii.id id, dii.display_info_id displayInfoId, fi.id fileId, fi.file_name fileName, fi.save_file_name saveFileName, fi.content_type contentType, fi.delete_flag deleteFlag, fi.create_date createDate, fi.modify_date modifyDate\n"
            + "FROM display_info_image dii, file_info fi\n"
            + "WHERE dii.file_id = fi.id AND dii.display_info_id = ?";
}

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DisplayInfoImageDto implements RestDocsTemplate {
    private final int id;
    private final int displayInfoId;
    private final int fileId;
    private final String fileName;
    private final String saveFileName;
    private final String contentType;
    private final int deleteFlag;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;

    public static final RowMapper<DisplayInfoImageDto> displayInfoImageMapper = (rs, rowNum) -> {
        return DisplayInfoImageDto.builder()
                .id(rs.getInt("id"))
                .displayInfoId(rs.getInt("displayInfoId"))
                .fileId(rs.getInt("fileId"))
                .fileName(rs.getString("fileName"))
                .saveFileName(rs.getString("saveFileName"))
                .contentType(rs.getString("contentType"))
                .deleteFlag(rs.getInt("deleteFlag"))
                .createDate(rs.getObject("createDate", LocalDateTime.class))
                .modifyDate(rs.getObject("modifyDate", LocalDateTime.class))
                .build();
    };
    
    ...
    
}

여기까지 입력하면 전시 상품 이미지 조회 테스트에 성공합니다.

이제 전시상품의 평균 평점을 조회해봅시다.

Score 조회 구현

CommentRepository

public interface CommentRepository {
    CommentScoreResponseDto getCommentsScoreByDisplayId(int displayId);
}

JdbcCommentRepository

@Repository
@RequiredArgsConstructor
public class JdbcCommentRepository implements CommentRepository {
    private final JdbcTemplate jdbcTemplate;

    @Override
    public CommentScoreResponseDto getCommentsScoreByDisplayId(int displayId) {
        return jdbcTemplate.queryForObject(
                CommentSQLMapper.SELECT_COMMENTS_AVERAGE_SCORE,
                CommentScoreResponseDto.commentScoreMapper,
                displayId
        );
    }
}

CommentSQLMapper

public class CommentSQLMapper {
    public static final String SELECT_COMMENTS_AVERAGE_SCORE =
            "SELECT AVG(score) avgScore\n"
            + "FROM reservation_user_comment ruc, product p, display_info di\n"
            + "WHERE p.id = ruc.product_id AND p.id = di.product_id AND di.id = ?";
}

CommentScoreResponseDto

@Builder
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
public class CommentScoreResponseDto {
    private final int avgScore;

    public static final RowMapper<CommentScoreResponseDto> commentScoreMapper = (rs, rowNum) -> {
        return CommentScoreResponseDto.builder()
                .avgScore(rs.getInt("avgScore"))
                .build();
    };
}

여기까지 입력하면 전시상품의 평균 평점 조회 테스트가 성공합니다.

이제 상품 가격 조회를 구현해봅시다.

ProductPrice 조회 구현

ProductRepository

public interface ProductRepository {
	
    ...
    
    List<ProductPrice> getProductPrice(int displayId);
}

JdbcProductRepository

...
public class JdbcProductRepository implements ProductRepository {
	
    ...

    @Override
    public List<ProductPrice> getProductPrice(int displayId) {
        return jdbcTemplate.query(
                ProductSQLMapper.SELECT_PRODUCT_PRICES,
                ProductPrice.productPriceMapper,
                displayId
        );
    }
}

ProductSQLMapper

public class ProductSQLMapper {
	
    ...
    
    public static final String SELECT_PRODUCT_PRICES =
            "SELECT pp.id id, pp.product_id productId, pp.price_type_name priceTypeName, pp.price price, pp.discount_rate discountRate, pp.create_date createDate, pp.modify_date modifyDate\n"
            + "FROM product_price pp, display_info di\n"
            + "WHERE pp.product_id = di.product_id AND di.id = ?";
}

ProductPrice

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class ProductPrice implements RestDocsTemplate {
    private final int id;
    private final int productId;
    private final String priceTypeName;
    private final int price;
    private final int discountRate;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;

    public static final RowMapper<ProductPrice> productPriceMapper = (rs, rowNum) -> {
        return ProductPrice.builder()
                .id(rs.getInt("id"))
                .productId(rs.getInt("productId"))
                .priceTypeName(rs.getString("priceTypeName"))
                .price(rs.getInt("price"))
                .discountRate(rs.getInt("discountRate"))
                .createDate(rs.getObject("createDate", LocalDateTime.class))
                .modifyDate(rs.getObject("modifyDate", LocalDateTime.class))
                .build();
    };
    
    ...
    
}

여기까지 작성하면 상품 가격 조회 테스트가 성공합니다.

이제 전시 상품의 모든 상세 정보를 조회하는 것을 구현해봅시다.

DetailDisplayInfos 조회 구현

DetailDisplayInfoService

@Service
@RequiredArgsConstructor
public class DetailDisplayInfoService {
    private final DisplayInfoRepository displayInfoRepository;
    private final ProductRepository productRepository;
    private final CommentRepository commentRepository;

    public DetailDisplayInfosResponseDto getDetailDisplayInfos(DetailDisplayInfosRequestDto requestDto) {
        return DetailDisplayInfosResponseDto.builder()
                .product(displayInfoRepository.getDisplayInfo(requestDto.getDisplayId()))
                .productImages(productRepository.getProductImages(requestDto.getDisplayId()))
                .displayInfoImages(displayInfoRepository.getDisplayInfoImages(requestDto.getDisplayId()))
                .avgScore(commentRepository.getCommentsScoreByDisplayId(requestDto.getDisplayId()).getAvgScore())
                .productPrices(productRepository.getProductPrice(requestDto.getDisplayId()))
                .build();
    }
}

지금까지 만들었던 조회들을 하나식 호출하여 반환된 결과로부터 전시 상세 객체를 생성해주기만 하면 됩니다.

이렇게 작성하면 테스트가 성공하게 됩니다.

이번 장에서는 전시 상세정보를 구하는 API를 만들었습니다.
다음 장에서는 마지막 API인 댓글 목록 조회를 만들어 보겠습니다.













DTO 리팩토링?????????????????????????????????????????????????????????????????

위의 API의 경우 현재 DTO 객체가 문서를 생성하는 부분의 코드는 아래와 같습니다.

DetailDisplayInfosResponseDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DetailDisplayInfosResponseDto implements RestDocsTemplate {
    private final DisplayInfoResponseDto product;
    private final ProductImageDto productImages;
    private final DisplayInfoImageDto displayInfoImages;
    private final int avgScore;
    private final List<ProductPrice> productPrices;

    @Override
    public List<RestDocsDto> generateRestDocsFields() {
        return List.of(
                //// products
                RestDocsDto.builder().path("product").description("전시 상품 정보").build(),
//                RestDocsDto.builder().path("product[]").description("전시 상품 정보").build(),
                RestDocsDto.builder().path("product.id").description("전시 상품 ID").build(),
                RestDocsDto.builder().path("product.categoryId").description("카테고리 ID").build(),
                RestDocsDto.builder().path("product.displayInfoId").description("전시 상품 ID").build(),
                RestDocsDto.builder().path("product.name").description("전시 상품명").build(),
                RestDocsDto.builder().path("product.description").description("전시 상품 설명").build(),
                RestDocsDto.builder().path("product.content").description("전시 상품 내용").build(),
                RestDocsDto.builder().path("product.event").description("이벤트").build(),
                RestDocsDto.builder().path("product.openingHours").description("오픈 시각").build(),
                RestDocsDto.builder().path("product.placeName").description("장소").build(),
                RestDocsDto.builder().path("product.placeLot").description("위치").build(),
                RestDocsDto.builder().path("product.placeStreet").description("도로명").build(),
                RestDocsDto.builder().path("product.tel").description("연락처").build(),
                RestDocsDto.builder().path("product.homepage").description("홈페이지").build(),
                RestDocsDto.builder().path("product.email").description("이메일").build(),
                RestDocsDto.builder().path("product.createDate").description("생성일").build(),
                RestDocsDto.builder().path("product.modifyDate").description("수정일").build(),
                RestDocsDto.builder().path("product.fileId").description("파일 ID").build(),
                //// productImages
                RestDocsDto.builder().path("productImages").description("상품 이미지 정보들").build(),
                RestDocsDto.builder().path("productImages.productId").description("상품 아이디").build(),
                RestDocsDto.builder().path("productImages.productImageId").description("상품 이미지 아이디").build(),
                RestDocsDto.builder().path("productImages.type").description("상품 이미지 타입").build(),
                RestDocsDto.builder().path("productImages.fileInfoId").description("상품 이미지 파일 아이디").build(),
                RestDocsDto.builder().path("productImages.fileName").description("상품 이미지 파일명").build(),
                RestDocsDto.builder().path("productImages.saveFileName").description("상품 이미지 저장된 파일명").build(),
                RestDocsDto.builder().path("productImages.contentType").description("상품 이미지 Content Type").build(),
                RestDocsDto.builder().path("productImages.deleteFlag").description("상품 이미지 삭제 여부").build(),
                RestDocsDto.builder().path("productImages.createDate").description("상품 이미지 생성일").build(),
                RestDocsDto.builder().path("productImages.modifyDate").description("상품 이미지 수정일").build(),
                //// displayInfoImages
                RestDocsDto.builder().path("displayInfoImages").description("전시 이미지 정보들").build(),
                RestDocsDto.builder().path("displayInfoImages.id").description("전시 이미지 아이디").build(),
                RestDocsDto.builder().path("displayInfoImages.displayInfoId").description("전시 정보 아이디").build(),
                RestDocsDto.builder().path("displayInfoImages.fileId").description("전시 이미지 파일 이미지").build(),
                RestDocsDto.builder().path("displayInfoImages.fileName").description("전시 이미지 파일명").build(),
                RestDocsDto.builder().path("displayInfoImages.saveFileName").description("전시 이미지 저장된 파일명").build(),
                RestDocsDto.builder().path("displayInfoImages.contentType").description("전시 이미지 Content Type").build(),
                RestDocsDto.builder().path("displayInfoImages.deleteFlag").description("전시 이미지 삭제 여부").build(),
                RestDocsDto.builder().path("displayInfoImages.createDate").description("전시 이미지 생성일").build(),
                RestDocsDto.builder().path("displayInfoImages.modifyDate").description("전시 이미지 수정일").build(),
                //// avgScore
                RestDocsDto.builder().path("avgScore").description("평점").build(),
                //// productPrices
                RestDocsDto.builder().path("productPrices[]").description("상품 가격 정보들").build(),
                RestDocsDto.builder().path("productPrices[].id").description("상품 가격 아이디").build(),
                RestDocsDto.builder().path("productPrices[].productId").description("상품 아이디").build(),
                RestDocsDto.builder().path("productPrices[].priceTypeName").description("상품 가격 타입명").build(),
                RestDocsDto.builder().path("productPrices[].price").description("상품 가격").build(),
                RestDocsDto.builder().path("productPrices[].discountRate").description("상품 가격 할인률").build(),
                RestDocsDto.builder().path("productPrices[].createDate").description("상품 가격 생성일").build(),
                RestDocsDto.builder().path("productPrices[].modifyDate").description("상품 가격 수정일").build()
        );
    }
}

DTO 객체에 문서 생성 로직이 너무 많이 담겨 있어 가독성이 떨어집니다.

이 부분을 리팩토링 해줍시다.

DetailDisplayInfosResponseDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DetailDisplayInfosResponseDto implements RestDocsTemplate {
    private final DisplayInfoResponseDto product;
    private final ProductImageDto productImages;
    private final DisplayInfoImageDto displayInfoImages;
    private final int avgScore;
    private final List<ProductPrice> productPrices;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        List<RestDocsDto> results = new ArrayList<>();
        generateProduct(results);
        generateProductImages(results);
        generateDisplayInfoImages(results);
        generateAvgScore(results);
        generateProductPrices(results);
        return results;
    }

    private void generateProduct(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("product").description("전시 상품 정보").build());
        results.addAll(new DisplayInfoResponseDto().generateRestDocsFields("product"));
    }

    private void generateProductImages(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("productImages").description("상품 이미지 정보들").build());
        results.addAll(new ProductImageDto().generateRestDocsFields("productImages"));
    }

    private void generateDisplayInfoImages(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("displayInfoImages").description("전시 이미지 정보들").build());
        results.addAll(new DisplayInfoImageDto().generateRestDocsFields("displayInfoImages"));
    }

    private void generateAvgScore(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("avgScore").description("평점").build());
    }

    private void generateProductPrices(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("productPrices[]").description("상품 가격 정보들").build());
        results.addAll(new ProductPrice().generateRestDocsFields("productPrices[]"));
    }
}

이전에는 DTO 객체가 직접 가지고 있던 필드와 그 필드 타입이 포함하고 있던 필드들까지 모두 표현하였으나, 그렇게 하지 않고 자신이 가지고 있는 필드만 표현하고 필드 객체가 포함하고 있는 필드들은 자신이 표현하도록 위임해주었습니다.

그런데 이 과정에서 문제가 생겨서 REST Docs Template 인터페이스를 수정해주었습니다. 아래는 기존의 인터페이스입니다.

RestDocsTemplate

public interface RestDocsTemplate {
    List<RestDocsDto> generateRestDocsFields();
}

위의 인터페이스를 아래와 같이 수정해주었습니다.

public interface RestDocsTemplate {
    List<RestDocsDto> generateRestDocsFields(String rootField);
}

그 이유는 DetailDisplayInfosResponseDto 객체가 가지고 있는 필드인 DisplayInfoResponseDto 타입의 객체는 다른 API 문서에서도 사용하고 있습니다.

그런데 현재 DetailDisplayInfosResponseDto 에서는 객체 단일로만 사용하고 있지만 다른 API 문서에서는 이를 List 인터페이스로 사용하고 있습니다.

여기서 문제가 발생했는데 단일 객체로만 사용하면 API 문서로 아래와 같이 사용하면 됩니다.

DisplayInfoResponseDto

public class DisplayInfoResponseDto implements RestDocsTemplate {
    private final int id;
    private final int categoryId;
    ...

    @Override
    public List<RestDocsDto> generateRestDocsFields() {
        return List.of(
                RestDocsDto.builder().path("product.id").description("전시 상품 ID").build(),
                RestDocsDto.builder().path("product.categoryId").description("카테고리 ID").build()
                ...
    }
}

그런데 리스트를 이용해서 API 문서를 작성하려고 하면 아래와 같이 표현해야 합니다.

	@Override
    public List<RestDocsDto> generateRestDocsFields() {
        return List.of(
                RestDocsDto.builder().path("product[].id").description("전시 상품 ID").build(),
                RestDocsDto.builder().path("product[].categoryId").description("카테고리 ID").build()
                ...
    }

따라서 이 부분의 차이를 극복하기 위해서 root 필드명을 받아와 아래와 같이 개선해주기 위함입니다.

	@Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("전시 상품 ID").build(),
                RestDocsDto.builder().path(rootField + ".categoryId").description("카테고리 ID").build()
                ...
    }

이러한 부분을 토대로 남은 DTO들은 아래와 같습니다.

DisplayInfoResponseDto

@Builder
@Getter
@ToString
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DisplayInfoResponseDto implements RestDocsTemplate {
    private final int id;
    private final int categoryId;
    private final int displayInfoId;
    private final String name;
    private final String description;
    private final String content;
    private final String event;
    private final String openingHours;
    private final String placeName;
    private final String placeLot;
    private final String placeStreet;
    private final String tel;
    private final String homepage;
    private final String email;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;
    private final int fileId;

	... // Mapper 부분

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("전시 상품 ID").build(),
                RestDocsDto.builder().path(rootField + ".categoryId").description("카테고리 ID").build(),
                RestDocsDto.builder().path(rootField + ".displayInfoId").description("전시 상품 ID").build(),
                RestDocsDto.builder().path(rootField + ".name").description("전시 상품명").build(),
                RestDocsDto.builder().path(rootField + ".description").description("전시 상품 설명").build(),
                RestDocsDto.builder().path(rootField + ".content").description("전시 상품 내용").build(),
                RestDocsDto.builder().path(rootField + ".event").description("이벤트").build(),
                RestDocsDto.builder().path(rootField + ".openingHours").description("오픈 시각").build(),
                RestDocsDto.builder().path(rootField + ".placeName").description("장소").build(),
                RestDocsDto.builder().path(rootField + ".placeLot").description("위치").build(),
                RestDocsDto.builder().path(rootField + ".placeStreet").description("도로명").build(),
                RestDocsDto.builder().path(rootField + ".tel").description("연락처").build(),
                RestDocsDto.builder().path(rootField + ".homepage").description("홈페이지").build(),
                RestDocsDto.builder().path(rootField + ".email").description("이메일").build(),
                RestDocsDto.builder().path(rootField + ".createDate").description("생성일").build(),
                RestDocsDto.builder().path(rootField + ".modifyDate").description("수정일").build(),
                RestDocsDto.builder().path(rootField + ".fileId").description("파일 ID").build());
    }
}

ProductImageDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class ProductImageDto implements RestDocsTemplate {
    private final int productId;
    private final int productImageId;
    private final String type;
    private final int fileInfoId;
    private final String fileName;
    private final String saveFileName;
    private final String contentType;
    private final int deleteFlag;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".productId").description("상품 아이디").build(),
                RestDocsDto.builder().path(rootField + ".productImageId").description("상품 이미지 아이디").build(),
                RestDocsDto.builder().path(rootField + ".type").description("상품 이미지 타입").build(),
                RestDocsDto.builder().path(rootField + ".fileInfoId").description("상품 이미지 파일 아이디").build(),
                RestDocsDto.builder().path(rootField + ".fileName").description("상품 이미지 파일명").build(),
                RestDocsDto.builder().path(rootField + ".saveFileName").description("상품 이미지 저장된 파일명").build(),
                RestDocsDto.builder().path(rootField + ".contentType").description("상품 이미지 Content Type").build(),
                RestDocsDto.builder().path(rootField + ".deleteFlag").description("상품 이미지 삭제 여부").build(),
                RestDocsDto.builder().path(rootField + ".createDate").description("상품 이미지 생성일").build(),
                RestDocsDto.builder().path(rootField + ".modifyDate").description("상품 이미지 수정일").build());
    }
}

DisplayInfoImageDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DisplayInfoImageDto implements RestDocsTemplate {
    private final int id;
    private final int displayInfoId;
    private final int fileId;
    private final String fileName;
    private final String saveFileName;
    private final String contentType;
    private final int deleteFlag;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("전시 이미지 아이디").build(),
                RestDocsDto.builder().path(rootField + ".displayInfoId").description("전시 정보 아이디").build(),
                RestDocsDto.builder().path(rootField + ".fileId").description("전시 이미지 파일 이미지").build(),
                RestDocsDto.builder().path(rootField + ".fileName").description("전시 이미지 파일명").build(),
                RestDocsDto.builder().path(rootField + ".saveFileName").description("전시 이미지 저장된 파일명").build(),
                RestDocsDto.builder().path(rootField + ".contentType").description("전시 이미지 Content Type").build(),
                RestDocsDto.builder().path(rootField + ".deleteFlag").description("전시 이미지 삭제 여부").build(),
                RestDocsDto.builder().path(rootField + ".createDate").description("전시 이미지 생성일").build(),
                RestDocsDto.builder().path(rootField + ".modifyDate").description("전시 이미지 수정일").build());
    }
}

ProductPrice

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class ProductPrice implements RestDocsTemplate {
    private final int id;
    private final int productId;
    private final String priceTypeName;
    private final int price;
    private final int discountRate;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("상품 가격 아이디").build(),
                RestDocsDto.builder().path(rootField + ".productId").description("상품 아이디").build(),
                RestDocsDto.builder().path(rootField + ".priceTypeName").description("상품 가격 타입명").build(),
                RestDocsDto.builder().path(rootField + ".price").description("상품 가격").build(),
                RestDocsDto.builder().path(rootField + ".discountRate").description("상품 가격 할인률").build(),
                RestDocsDto.builder().path(rootField + ".createDate").description("상품 가격 생성일").build(),
                RestDocsDto.builder().path(rootField + ".modifyDate").description("상품 가격 수정일").build());
    }
}

이렇게 DetailDisplayInfosResponseDto 객체가 모든 필드를 문서로 기록하던 것에서 객체 자신의 필드는 스스로 표현하도록 리팩토링해주었습니다.

이렇게 바꾸게 되면 다른 남은 DTO들도 모두 변경해주어야 합니다.

다른 DTO들도 아래와 같이 바꿔봅시다.

카테고리 조회

CategoriesResponseDto

@Builder
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class CategoriesResponseDto implements RestDocsTemplate {
    private final int size;
    private final List<CategoryResponseDto> items;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        List<RestDocsDto> results = new ArrayList<>();
        generateSize(results);
        generateItems(results);
        return results;
    }

    private void generateSize(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("size").description("카테고리 개수").build());
    }

    private void generateItems(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("items[]").description("카테고리 정보").build());
        results.addAll(new CategoryResponseDto().generateRestDocsFields("items[]"));
    }
}

CategoryResponseDto

@Getter
@ToString
@EqualsAndHashCode
@Builder
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class CategoryResponseDto implements RestDocsTemplate {
    private final int id;
    private final String name;
    private final int count;

	...

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("카테고리 id").build(),
                RestDocsDto.builder().path(rootField + ".name").description("카테고리 이름").build(),
                RestDocsDto.builder().path(rootField + ".count").description("카테고리에 포함된 전시 상품(display_info)의 수").build()
        );
    }
}

전시 정보 조회

DisplayInfosRequestDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DisplayInfosRequestDto implements RestDocsTemplate {
    private final int categoryId;
    private final int start;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        List<RestDocsDto> results = new ArrayList<>();
        generateCategoryId(results);
        generateStart(results);
        return results;
    }

    private void generateCategoryId(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("categoryId").description("카테고리 아이디 (0 또는 없을 경우 전체 조회)").build());
    }

    private void generateStart(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("start").description("조회 시작 위치").build());
    }
}

DisplayInfosResponseDto

@Builder
@Getter
@ToString
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DisplayInfosResponseDto implements RestDocsTemplate {
    private final int totalCount;
    private final int productCount;
    private final List<DisplayInfoResponseDto> products;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        List<RestDocsDto> results = new ArrayList<>();
        generateTotalCount(results);
        generateProductCount(results);
        generateProducts(results);
        return results;
    }

    private void generateTotalCount(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("totalCount").description("해당 카테고리의 전시 상품 수").build());
    }

    private void generateProductCount(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("productCount").description("읽어온 전시 상품 수").build());
    }

    private void generateProducts(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("products[]").description("전시 상품 정보").build());
        results.addAll(new DisplayInfoResponseDto().generateRestDocsFields("products[]"));
    }
}

DisplayInfoResponseDto

@Builder
@Getter
@ToString
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class DisplayInfoResponseDto implements RestDocsTemplate {
    private final int id;
    private final int categoryId;
    private final int displayInfoId;
    private final String name;
    private final String description;
    private final String content;
    private final String event;
    private final String openingHours;
    private final String placeName;
    private final String placeLot;
    private final String placeStreet;
    private final String tel;
    private final String homepage;
    private final String email;
    private final LocalDateTime createDate;
    private final LocalDateTime modifyDate;
    private final int fileId;

    ... // Mapper 부분

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("전시 상품 ID").build(),
                RestDocsDto.builder().path(rootField + ".categoryId").description("카테고리 ID").build(),
                RestDocsDto.builder().path(rootField + ".displayInfoId").description("전시 상품 ID").build(),
                RestDocsDto.builder().path(rootField + ".name").description("전시 상품명").build(),
                RestDocsDto.builder().path(rootField + ".description").description("전시 상품 설명").build(),
                RestDocsDto.builder().path(rootField + ".content").description("전시 상품 내용").build(),
                RestDocsDto.builder().path(rootField + ".event").description("이벤트").build(),
                RestDocsDto.builder().path(rootField + ".openingHours").description("오픈 시각").build(),
                RestDocsDto.builder().path(rootField + ".placeName").description("장소").build(),
                RestDocsDto.builder().path(rootField + ".placeLot").description("위치").build(),
                RestDocsDto.builder().path(rootField + ".placeStreet").description("도로명").build(),
                RestDocsDto.builder().path(rootField + ".tel").description("연락처").build(),
                RestDocsDto.builder().path(rootField + ".homepage").description("홈페이지").build(),
                RestDocsDto.builder().path(rootField + ".email").description("이메일").build(),
                RestDocsDto.builder().path(rootField + ".createDate").description("생성일").build(),
                RestDocsDto.builder().path(rootField + ".modifyDate").description("수정일").build(),
                RestDocsDto.builder().path(rootField + ".fileId").description("파일 ID").build());
    }
}

프로모션 정보 조회

PromotionsResponseDto

@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class PromotionsResponseDto implements RestDocsTemplate {
    private final int size;
    private final List<PromotionResponseDto> items;

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        List<RestDocsDto> results = new ArrayList<>();
        generateSize(results);
        generateItems(results);
        return results;
    }

    private void generateSize(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("size").description("프로모션 정보의 수").build());
    }

    private void generateItems(List<RestDocsDto> results) {
        results.add(RestDocsDto.builder().path("items[]").description("프로모션 상품 정보").build());
        results.addAll(new PromotionResponseDto().generateRestDocsFields("items[]"));
    }
}

PromotionResponseDto

@Builder
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class PromotionResponseDto implements RestDocsTemplate {
    private final int id;
    private final int productId;
    private final int categoryId;
    private final String categoryName;
    private final String description;
    private final int fileId;

    ... // Mapper 부분

    @Override
    public List<RestDocsDto> generateRestDocsFields(String rootField) {
        return List.of(
                RestDocsDto.builder().path(rootField + ".id").description("프로모션 상품 PK").build(),
                RestDocsDto.builder().path(rootField + ".productId").description("프로모션 상품 ID").build(),
                RestDocsDto.builder().path(rootField + ".categoryId").description("프로모션 상품 카테고리 ID").build(),
                RestDocsDto.builder().path(rootField + ".categoryName").description("프로모션 상품 카테고리명").build(),
                RestDocsDto.builder().path(rootField + ".description").description("프로모션 상품 설명").build(),
                RestDocsDto.builder().path(rootField + ".fileId").description("file_info 테이블의 id (product_image의 타입중 ma인 경우만)").build());
    }
}

여기까지 DTO가 문서를 생성하는 로직을 리팩토링 해주었습니다.

그런데 이 과정에서 RestDocsTemplate 인터페이스를 수정하였기 때문에 이에 대한 사이드 이펙트가 발생했었는데, 그 영향은 테스트 코드에도 있었습니다.

테스트 코드에서 문서를 생성하는 로직 중 RestDocsGenerator 클래스에 변경 사항을 적용해주어야 합니다.

RestDocsGenerator

public class RestDocsGenerator {
	
    ...
	
 	/* 이러한 부분들을
    RestDocsFieldsGenerator.generateRequest(request.generateRestDocsFields()),
	RestDocsFieldsGenerator.generateResponse(response.generateRestDocsFields()));
    */
    
    // 이렇게 null을 넣어준다.
    RestDocsFieldsGenerator.generateRequest(request.generateRestDocsFields(null)),
	RestDocsFieldsGenerator.generateResponse(response.generateRestDocsFields(null)));
}

이렇게 모든 리팩토링이 끝났습니다.

0개의 댓글