715 단어
4 분
Strategy for Registering SNS Feed Posts

Overview#

In the previous post, I covered how to upload and synchronize images to a cloud storage bucket (OCI). Now, I’ll walk through implementing the feed registration API, which handles post content and multiple images. Let’s design and implement this feature while considering key requirements and potential edge cases.

Feature Requirements#

We need to build a feature to post SNS-style feed content.

To register a post in the app, both images and text must be included. The user can upload up to 10 images before finalizing the post. Once the post is ready, the images stored in a buffer bucket should be moved to a disk bucket, and the originals in the buffer bucket should be deleted.

Here’s the current design for the core feed-related entities:

Image

Note that location and media (images) are optional fields when registering a feed. In other words, the system must handle cases where those values are absent.

Functional Flow#

The requirements are relatively complex, so I modeled the high-level flow to better understand the behavior. Here’s a mermaid-based diagram:

Image

In short:

  1. Images are uploaded before the post is created.
  2. The client holds the image URLs, not the server.
  3. When the post is finalized, those images are moved from the buffer bucket to the disk bucket.
  4. If location data is present, it is also saved. Otherwise, only feed and feed media are persisted.

Checklist of Features#

  • Images uploaded to the buffer bucket are not saved in the DB, so we name them using identifiers for easy tracking and log each upload (stored in MongoDB).
  • Upload multiple images to the buffer bucket and return the upload result.
  • After registering the feed, move the images to the disk bucket and return updated URLs.
  • Save the updated image URLs in the feed media entity using JPA.
  • If a location exists, also persist the location entity to the DB.

Additional Consideration#

  • Each image must have an index to maintain order.

Because feed images need to preserve order, an index must be associated with each image. However, since Stream.parallel() processes data in parallel threads, it’s not possible to increment a shared index safely. So I decided to have the client send the index along with the upload request to ensure consistency.


Implementation#

Here’s the implementation of the service logic for the checklist above (excluding full controller or repository code for brevity):

@Service
@RequiredArgsConstructor  
public class FeedService {
	private final FeedRepository feedRepository;  
	private final FeedMediaRepository feedMediaRepository;  
	private final MemberRepository memberRepository;  
	private final LocationRepository locationRepository;  
	private final ObjectStorageService objectStorageService;  
	private final MemberService memberService;

	@Transactional
	public FeedPostResponse registerFeed(List<FileMetadataDto> metadataList, FeedRequest feedRequest) {  
	    MemberDto memberDto = memberService.getMember();  
	    Member member = memberRepository.findById(memberDto.getId()).orElseThrow();  
	  
	    // Save the feed entity  
	    Feed feed = Feed.of(member, feedRequest.getContent(), feedRequest.getCategory(), LocalDateTime.now(), LocalDateTime.now());  
	    feedRepository.save(feed);
	  
	    // Move images from buffer bucket to disk bucket  
	    List<FeedTempDto> finalizedFeed = metadataList.parallelStream()  
	            .map(metadata -> {
	                try {  
	                    String finalFileName = String.format("%s/%s/%s?%s&%s",  
	                            member.getId(),  
	                            "feed",  
	                            feed.getId(),  
	                            "width=" + metadata.getWidth() + "&height=" + metadata.getHeight(),  
	                            "index=" + metadata.getIndex());
	                    String diskUrl = objectStorageService.copyToDisk(metadata, finalFileName);  
	  
	                    return FeedTempDto.builder()  
	                            .url(diskUrl)  
	                            .index(metadata.getIndex())  
	                            .build();  
	                } catch (BmcException e) {  
	                    logger.error("Failed to copy file: {}", metadata.getOriginalFileName(), e);  
	                    throw new RuntimeException("Failed to copy file: " + metadata.getOriginalFileName(), e);  
	                }  
	            })  
	            .toList();  
	  
	    // Save media info to DB  
	    finalizedFeed.forEach(feedTempData -> {  
	        FeedMedia feedMedia = FeedMedia.of(feed, "IMAGE", feedTempData.getUrl(), feedTempData.getIndex(), LocalDateTime.now(), LocalDateTime.now());  
	        feedMediaRepository.save(feedMedia);  
	    });  
	  
	    // Save location if it exists  
	    LocationRequest locationRequest = feedRequest.getLocation();  
	    if (locationRequest.getAddress() != null) {
	        Location location = Location.of(feed, locationRequest.getCoordinate(), locationRequest.getAddress(), locationRequest.getName(), LocalDateTime.now(), LocalDateTime.now());  
	        locationRepository.save(location);  
	    }  
	  
	    return FeedPostResponse.builder()  
	            .id(feed.getId().toString())  
	            .feedTempDtos(finalizedFeed)  
	            .content(feed.getContent())  
	            .build();  
	}
}

Summary of Key Logic#

Retrieve the current member (user):

MemberDto memberDto = memberService.getMember();  
Member member = memberRepository.findById(memberDto.getId()).orElseThrow();

Save the feed’s main info:

Feed feed = Feed.of(member, feedRequest.getContent(), feedRequest.getCategory(), LocalDateTime.now(), LocalDateTime.now());  
feedRepository.save(feed);

Process the images:

List<FeedTempDto> finalizedFeed = metadataList.parallelStream()
    .map(metadata -> {
        ...
        String diskUrl = objectStorageService.copyToDisk(metadata, finalFileName);  
        ...
    })
    .toList();

Since we’re using parallel streams, I avoided incrementing a shared index and instead had the client specify the order via metadata.

Save feed media:

finalizedFeed.forEach(feedTempData -> {
    FeedMedia feedMedia = FeedMedia.of(feed, "IMAGE", feedTempData.getUrl(), feedTempData.getIndex(), LocalDateTime.now(), LocalDateTime.now());  
    feedMediaRepository.save(feedMedia);  
});

Handle optional location info:

if (locationRequest.getAddress() != null) {
    Location location = Location.of(feed, locationRequest.getCoordinate(), locationRequest.getAddress(), locationRequest.getName(), LocalDateTime.now(), LocalDateTime.now());  
    locationRepository.save(location);  
}

Return the final response:

return FeedPostResponse.builder()  
        .id(feed.getId().toString())  
        .feedTempDtos(finalizedFeed)  
        .content(feed.getContent())  
        .build();

Final Thoughts#

There are definitely areas in this service that could use a lot of refactoring. But for now, my priority is to complete and test all critical features. Once that’s done, I’ll revisit this and polish the code structure for readability, maintainability, and scalability.

you can see the code in

Team-SynApps
/
resona-api-server
Waiting for api.github.com...
00K
0K
0K
Waiting...

Strategy for Registering SNS Feed Posts
https://blog-full-of-desire-v3.vercel.app/posts/resona/sns-registration-strategy-en/
저자
SpeculatingWook
게시일
2025-04-06
라이선스
CC BY-NC-SA 4.0