๐ก๊ฒฐ๋ก
Request/Response DTO๋ API ์ ์ถ๋ ฅ ํ์์ ๊ฐ๊น๊ณ , API ์๊ตฌ์ฌํญ์ FE ํ๋ฉด ๊ตฌ์ฑ์ด๋ ๊ธฐํ ๋ณ๊ฒฝ์ ๋ฐ๋ผ ์์ฃผ ๋ฐ๋๋ค. ๊ทธ๋์ Service๊ฐ DTO๋ฅผ ์ง์ ๋ฐํํ๋ฉด ๋น์ฆ๋์ค ๋ก์ง์ด ํ๋ ์ ํ ์ด์ ๊ณ์ธต์ ๋ณํ์ ๋๋ ค๋ค๋ ์ ์๋ค. ์ด๋ฒ ํ๋ก์ ํธ์์๋ Service๋ Entity, VO, primitive, Map ๊ฐ์ ๋น์ฆ๋์ค์ ๊ฐ๊น์ด ๊ฐ์ ๋ฐํํ๊ณ , Controller์์ Response DTO๋ก ๋ณํํ๋ ๋ฐฉ์์ ์ฌ์ฉํ๋ค.
1. ๋ฌธ์ ๋ฐ์ ์ํฉ
์ด๋ฒ SOPT์์ ํฉ๋ ์ธ๋ฏธ๋๋ฅผ ์งํํ๊ณ ์๋์ ๊ฐ์ ์ผ์ด ์์๋ค.
๋ด๊ฐ ์๋ ๋ฐฐ์ ๋ ๊ตฌ์กฐ๋ ์ฐจํธ API, ์ข์์ ํ ๊ธ API, ์ํฐ์คํธ ๊ณก ๋ชฉ๋ก API๋ฅผ ๊ตฌํํ๋ฉด์ Service๊ฐ ๋ฐ๋ก Response DTO๋ฅผ ๋ฐํํ ์ ์์๋ค.
ํ์ง๋ง ๋ฆฌ๋๋๊ป์๋ Business Layer์ Presentation Layer ๋ฅผ ํ์คํ ์ํ๊ณ ํฉ๋ ์ธ๋ฏธ๋ ๊ทธ๋ผ์ด๋ ๋ฃฐ์ ๊ท์น์ ์ ์ผ์ จ๋ค.
ํฉ๋ ์ธ๋ฏธ๋ ๊ทธ๋ผ์ด๋ ๋ฃฐ์ ๋ง๋ค๋ฉด์ ”๊ท์ฐฎ๊ฒ ์ ๋ถ๋ฆฌ์ํค๋๊ฑฐ์ง? ํ ๋ฒ์ ํด๊ฒฐํ๋ฉด ์ฝ๊ณ ํ์ผ์๋ ์ค์ผ ์ ์์ด์ ์ด๋ ์๋๊ฐ..?” ๋ผ๋ ์๊ฐ์ ์ฌ๋ก ์กํ์์๋ค.
ํ์ง๋ง ์ด๋ ๊ฒ ํ๋ฉด Service๊ฐ API ์๋ต ํํ๋ฅผ ์๊ฒ ๋์ด๋ฒ๋ฆฌ๋ ๋ฌธ์ ๊ฐ ์์๋ค.
2. ํฉ๋ ์ธ๋ฏธ๋ ๊ทธ๋ผ์ด๋ ๋ฃฐ
- ๋น์ฆ๋์ค ๋ก์ง: ์๋ฒ๊ฐ ์ฑ
์์ง๋ ๊ท์น
- ์: ์ ์ ๊ฒ์ฆ, ๊ถํ ํ์ธ, ์ข์์ ์ฌ๋ถ ํ๋จ, ์ข์์ ์ ์ฆ๊ฐ/๊ฐ์, ์ ๋ ฌ ๊ธฐ์ค์ ๋ฐ๋ฅธ ๊ณก ์กฐํ
- ํ๋ ์ ํ
์ด์
๋ก์ง: API ์๋ต์ ์ด๋ป๊ฒ ๋ณด์ฌ์ค์ง ์กฐ๋ฆฝํ๋ ์์ญ
- ์: FE๊ฐ ํ์ํ ํํ๋ก songId, title, artist, albumImageUrl์ ๋ฌถ์ด์ ๋ด๋ ค์ฃผ๊ธฐ
- ์ฆ, ์ฐ๋ฆฌ๋ ํ๋ก ํธ์๋๊ฐ ๋ญ ํ์ง ์๊ณ ๋ฑ ๋ณด์ฌ์ฃผ๋ ํํ๋ก ๋ณด๋ด์ฃผ์!
- ์: FE๊ฐ ํ์ํ ํํ๋ก songId, title, artist, albumImageUrl์ ๋ฌถ์ด์ ๋ด๋ ค์ฃผ๊ธฐ
⇒ Service๋ ์ต๋ํ ์๊ฒ, Controller์์๋ ์กฐ๋ฆฝ ์ํค๊ธฐ!
3. ํฉ๋์ธ๋ฏธ๋ ์์
๋ฌธ์ ๋ฐ์ ์ํฉ
public record ChartSongInfo(
Long songId,
String title,
Long artistId,
String artistName,
String albumImageUrl
) {
}
- ๋งจ ์ฒ์์๋ Vo๊ฐ์ฒด๋ ๊ด์ฌ์ฌ๋ฅผ ๋ชจ์ผ๋ ๊ฑธ๋ก ์ดํดํด์ ๊ณก๊ณผ ์ํฐ์คํธ๋ฅผ ํ๊บผ๋ฒ์ ๋ชจ์ผ๋ฉด ๋๊ฒ ๋ค ํ๊ณ Voํจํค์ง์ ์ ์ฉ์์ผฐ๋ค
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ChartService {
private final SongDetailRepository songDetailRepository;
public ChartResponse findChartByFilter(final ChartFilterType chartFilterType) {
List<ChartSongInfo> chartSongs = switch (chartFilterType) {
case TOP100 -> songDetailRepository.findTop100Chart();
case HOT100 -> songDetailRepository.findHot100Chart();
case ABROAD_2006, SWEET -> songDetailRepository.readChart();
};
return ChartResponse.of(chartSongs);
}
}
- ๊ทธ ํ Service์์๋ dto/reponse/์ ์๋ ChartResponse๋ฅผ ๋ฐ๋ก ๋ฐํํ๋ ์ค์๋ฅผ ํ์๋ค
- ๊ทธ๋ผ์ด๋ ๋ฃฐ์ ์ํด Service์์๋ dto๋ฅผ ๋ฐํํ๋ฉด ์๋จ
ํด๊ฒฐ ๋ฐฉ๋ฒ
- vo ๊ฐ์ฒด๊ฐ ํ์์๋ค๊ณ ํ๋จ์ด ๋ค์ด์ ์ญ์ ์์ผฐ๋ค
- ChartResponse์์ ์๋์ ๊ฐ์ด ๋ฐํํ๊ฒ ๋๋ฉด join๋ ํ์์์ด ๋ฐํํ ์ ์๋ค๋ ์ฅ์ ์์ ์ญ์
public record ChartResponse( @Schema(description = "์ฐจํธ ๊ณก ๋ชฉ๋ก") List<SongInfo> songs ) { public static ChartResponse of(List<Song> songs, Map<Long, List<Artist>> artistsMap) { return new ChartResponse( songs.stream() .map(song -> SongInfo.of( song, artistsMap.getOrDefault(song.getId(), List.of()) )) .toList() ); } public record SongInfo( @Schema(description = "๊ณก ID", example = "1") Long songId, String albumImageUrl ) { public static SongInfo of(Song song, List<Artist> artists) { Artist artist = artists.getFirst(); return new SongInfo( song.getId(), song.getTitle(), new ArtistInfo(artist), song.getAlbum().getImageUrl() ); } } public record ArtistInfo( String name ) { public ArtistInfo(Artist artist) { this(artist.getId(), artist.getName()); } } } - Service๋ Entity, VO, primitive, Map ๊ฐ์ ๋น์ฆ๋์ค์ ๊ฐ๊น์ด ๊ฐ์ ๋ฐํํ๊ธฐ
- Mapper๋ฅผ ์ฌ์ฉํ ์ํฐ์คํธ ๊ณก ๋ชฉ๋ก ์กฐํ Service
package org.sopt.collaboration.melon.domain.song.service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.sopt.collaboration.melon.domain.artist.entity.Artist;
import org.sopt.collaboration.melon.domain.song.entity.Song;
import org.sopt.collaboration.melon.domain.song.entity.SongArtist;
import org.sopt.collaboration.melon.domain.song.repository.SongArtistRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SongArtistService {
private final SongArtistRepository songArtistRepository;
public List<Artist> findAllArtistsBySongId(Long songId) {
return songArtistRepository.findAllBySongId(songId)
.stream()
.map(SongArtist::getArtist)
.toList();
}
public Map<Long, List<Artist>> findArtistAsMap(List<Song> songs) {
List<Long> songIds = songs.stream()
.map(Song::getId)
.toList();
return songArtistRepository.findAllBySongIdIn(songIds).stream()
.collect(Collectors.groupingBy(
SongArtist::getSongId,
Collectors.mapping(SongArtist::getArtist, Collectors.toList())
));
}
public List<Song> findArtistSongBySort(Long artistId, List<Song> sortedSong) {
List<Long> artistSongIds = songArtistRepository.findSongIdsByArtistId(artistId);
return sortedSong.stream()
.filter(song -> artistSongIds.contains(song.getId()))
.toList();
}
}
3. Controller๋ ์กฐ๋ฆฝํด์ Dto๋ก
@RestController
@RequestMapping("/v1/chart")
@RequiredArgsConstructor
public class ChartController implements ChartApi {
private final ChartService chartService;
private final SongArtistService songArtistService;
@GetMapping
public CommonResponse<ChartResponse> getChartByFilter(
@RequestParam(defaultValue = "TOP100") String chartFilter
) {
List<Song> chartSongsByFilter = chartService.findChartSongByFilter(ChartFilterType.from(chartFilter));
Map<Long, List<Artist>> artistsMap = songArtistService.findArtistAsMap(chartSongsByFilter);
ChartResponse response = ChartResponse.of(chartSongsByFilter, artistsMap);
return CommonResponse.success(ChartSuccessCode.GET_CHART, response);
}
}
์ ๋ฆฌ
๊ฐ๋ ์ ์๊ธฐ ์ ์๋ Business Layer์ Presentation Layer ๋ถ๋ฆฌํ๋ ๊ฒ ๋๋ฌด ๊ท์ฐฎ๊ณ ์ค์๋ ๋ง์์๋ค.
ํ์ง๋ง ํฉ๋ ์ธ๋ฏธ๋๋ฅผ ์งํํ๋ฉด์ ๋ง์ API์ด ์ญ์ ๋๊ฑฐ๋ ์ถ๊ฐ๋๊ฑฐ๋ ์์ ๋๋ ์ผ์ด ๋ง์ด ๋ฐ์ํ์๋ค. ์ด ๋๋ง๋ค Service์์ dto๋ฅผ ๋ฐํํ์ง ์์์ ๊ฐํธํ๊ฒ Controller์ dto๋ง ์ ๊ฒฝ ์ฐ๋ฉด ๋์๋ค.
๊ฐ์ฅ ํฌ๊ฒ ๋๋ ๋ถ๋ถ์ ์ฐจํธ API์๋ค. ํํฐ ์ ๋ ฌ ์์๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ํ ๋ด์์ ์ฌ๋ฌ ๋ฒ ๋ ผ์๊ฐ ์ค๊ฐ๊ณ , ์๊ตฌ์ฌํญ๋ ๊ณ์ ๋ฐ๋์๋๋ฐ, ์ ๋ ฌ๊ณผ ๊ณ์ฐ ๋ก์ง์ Service ์ชฝ์์ ์ง์ค์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด์ ์์ ๋ถ๋ด์ด ์ค์๋ค.
๋ํ ์ฐจํธ API์ ์ํฐ์คํธ ๊ณก ๋ชฉ๋ก ์กฐํ API์์ ๊ณ์ฐ ๋ก์ง์ด ๊ฒน์น๋ ํํฐ๊ฐ ์์๋๋ฐ, ๊ณตํต ๋ก์ง์ ์ฌ์ฌ์ฉํ๊ณ ํํฐ๋ช ๋ง ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํ ์ ์๋๋ก ๊ตฌํํ๋ฉด์ ์ฝ๋๋๋ ์ค์ผ ์ ์์๋ค.
์ฐธ๊ณ ์๋ฃ
Command ์ฝ์ด๋ดค๋ ์๋ฃ
'Back-end > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Spring, Docker] ๋ชจ๋์ ๋ชจ๋๋ฆฌ์ค์์ Docker ๋ฐฐํฌํ๊ธฐ (0) | 2026.06.24 |
|---|