[프로젝트]/[백엔드] LAITEU

작품 대표 이미지 저장

danhan 2024. 11. 20. 15:38

들어가며

작품 포트폴리오 서비스를 개발하면서, 대표 이미지 지정 기능이 새롭게 추가되었습니다. 기존에는 업로드된 첫 번째 이미지를 대표 이미지로 사용했는데, 이는 MVP(Minimum Viable Product) 단계에서 충분했습니다. 하지만 알파테스터들의 피드백을 통해 대표 이미지를 직접 선택하고 싶다는 요구사항이 생겼고 이에 따라 저장 로직을 개선하게 되었습니다.

문제 인식

초기 개발 시에는 단순히 이미지 배열의 첫 번째 요소를 대표 이미지로 사용했습니다. 이는 다음과 같은 이유 때문이었습니다:

  1. 단순한 구현: 별도의 필드나 로직 없이도 대표 이미지를 결정할 수 있었습니다.
  2. 데이터 중복 최소화: 이미지 ID를 한 번만 저장하면 되었습니다.
  3. 조회 성능: 대표 이미지를 가져올 때 추가적인 필드 조회가 필요 없었습니다.
// 초기 MongoDB 데이터 구조
{
  "_id": "6673d61d8508511115152bdf",
  "artworkImageIds": [
    "6673d61d8508511115152bd9",// 첫 번째 이미지가 자동으로 대표 이미지
    "6673d61d8508511115152bda",
    "6673d61d8508511115152bdb"
  ]
}

하지만 사용자가 대표 이미지를 직접 선택할 수 있는 기능이 추가되면서, 이 방식은 다음과 같은 한계점을 드러냈습니다:

  1. 유연성 부족: 사용자가 원하는 이미지를 대표 이미지로 지정하려면 배열의 순서를 조작해야 했습니다.
  2. 관심사 분리: 이미지 순서와 대표 이미지 지정은 서로 다른 관심사인데, 하나의 배열로 두 가지를 모두 표현하려다 보니 코드가 복잡해질 우려가 있었습니다.
  3. 향후 확장성: 이미지 순서 변경 기능이 추가될 경우, 대표 이미지 관리 로직과 충돌할 가능성이 있었습니다.

개선된 구현

새로운 요구사항을 수용하기 위해 대표 이미지 ID를 직접 관리하는 방식으로 개선했습니다. 엔티티 내부에서 validation 로직을 캡슐화하고, 외부에서는 인덱스 기반으로 접근할 수 있도록 구현했습니다.

java
Copy
@Document(collection = "artworks")
public class Artwork {
    @Id
    private String artworkId;
    private List<String> artworkImageIds;
    private String representativeImageId;

    private void validateAndSetRepresentativeImage(String imageId) {
        if (imageId != null && !artworkImageIds.contains(imageId)) {
            imageId = null;
        }
        this.representativeImageId = imageId;
    }

    public void setRepresentativeImageByIndex(Integer index) {
        if (index == null || artworkImageIds == null || artworkImageIds.isEmpty() ||
            index < 0 || index >= artworkImageIds.size()) {
            validateAndSetRepresentativeImage(null);
            return;
        }
        validateAndSetRepresentativeImage(artworkImageIds.get(index));
    }

    public String getRepresentativeImageId() {
        return representativeImageId != null ? representativeImageId :
               (artworkImageIds != null && !artworkImageIds.isEmpty() ? artworkImageIds.get(0) : null);
    }

    public void update(ArtworkUpdateRequest request, List<String> imageIds) {
        this.artworkImageIds = imageIds;
        setRepresentativeImageByIndex(request.getRepresentativeImageIndex());
// ... 다른 필드 업데이트
    }
}

이미지 조회 시에는 성능을 고려하여 한 번의 iteration으로 대표 이미지를 포함한 전체 목록을 생성합니다:

@Service
public class ArtworkImageService {
    public List<String> fetchPreviewImageUrlsByImageIds(List<String> imageIds, String representativeImageId) {
        if (imageIds == null || imageIds.isEmpty()) {
            return Collections.emptyList();
        }

        if (representativeImageId == null || !imageIds.contains(representativeImageId)) {
            return imageIds.stream()
                    .map(this::fetchPreviewImageUrlByImageId)
                    .collect(Collectors.toList());
        }

        List<String> orderedUrls = new ArrayList<>(imageIds.size());
        orderedUrls.add(fetchPreviewImageUrlByImageId(representativeImageId));

        for (String imageId : imageIds) {
            if (!imageId.equals(representativeImageId)) {
                orderedUrls.add(fetchPreviewImageUrlByImageId(imageId));
            }
        }

        return orderedUrls;
    }
}

개선된 점

  1. 명확한 의도: 대표 이미지가 별도 필드로 관리되어 코드의 의도가 명확해졌습니다.
  2. 독립적인 기능: 이미지 순서와 대표 이미지 지정이 독립적으로 동작합니다.
  3. 하위 호환성: 대표 이미지가 지정되지 않은 경우 기존처럼 첫 번째 이미지를 사용합니다.
  4. 안정적인 데이터 관리: ID 기반으로 저장되어 이미지 순서가 변경되어도 대표 이미지는 유지됩니다.

마치며

이번 개선 작업은 MVP에서 시작해서 점진적으로 기능을 개선해나가는 좋은 사례였습니다. 특히 신규 서비스의 특성상 기획이 자주 변경되고 새로운 기능들이 빠르게 추가되는 상황에서, 처음부터 모든 케이스를 고려한 복잡한 설계보다는 필요한 시점에 적절한 개선을 진행하는 것이 더 효율적이었습니다.

예를 들어, 대표 이미지 지정 기능 외에도 이미지 순서 변경, 이미지 그룹핑, 섬네일 최적화 등 다양한 기능들이 기획 중에 있었습니다. 이러한 변경 가능성을 모두 예측하여 처음부터 복잡한 구조를 만들기보다는, MVP 단계에서는 가장 단순한 형태로 시작하고 실제 필요한 시점에 맞춰 리팩토링을 진행하는 것이 더 합리적인 선택이었습니다.

특히 MongoDB를 사용하는 상황에서, 문서의 구조를 변경하는 것이 비교적 용이했던 점도 이러한 접근 방식에 도움이 되었습니다. 하지만 동시에 스키마 변경이 쉽다는 점이 오히려 독이 될 수 있다는 것도 배웠습니다. 변경이 쉽다고 해서 무분별하게 스키마를 변경하기보다는, 신중하게 설계 변경을 검토하고 적용하는 것이 중요했습니다.

앞으로도 "적절한 시기의 적절한 추상화"를 통해 코드를 개선해나갈 계획입니다. 새로운 기능이 추가될 때마다 기존 코드와의 결합도, 유지보수성, 확장성을 고려하면서도, 과도한 조기 최적화는 피하고자 합니다. 현재 대표 이미지 관리 기능이 그러했듯이, 앞으로의 기능 개발도 실제 필요한 시점에 맞춰 점진적으로 개선해 나갈 예정입니다.

결과적으로 이번 개선 작업을 통해, 빠르게 변화하는 서비스 요구사항 속에서 어떻게 코드를 발전시켜 나갈지에 대한 좋은 인사이트를 얻을 수 있었습니다. 완벽한 설계보다는 현재 상황에 맞는 적절한 복잡도를 유지하면서, 필요에 따라 유연하게 확장할 수 있는 구조를 만드는 것이 중요하다는 것을 배웠습니다.