[Spring] Business Layer์™€ Presentation Layer ๋ถ„๋ฆฌ์˜ ์ค‘์š”์„ฑ

2026. 6. 16. 17:39ยทBack-end/Spring

 ๐Ÿ’ก๊ฒฐ๋ก 

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์„ ๋ฌถ์–ด์„œ ๋‚ด๋ ค์ฃผ๊ธฐ
      • ์ฆ‰, ์šฐ๋ฆฌ๋Š” ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋ญ˜ ํ•˜์ง€ ์•Š๊ณ  ๋”ฑ ๋ณด์—ฌ์ฃผ๋Š” ํ˜•ํƒœ๋กœ ๋ณด๋‚ด์ฃผ์Ÿˆ!

⇒ 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๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์•ˆ๋จ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. 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());
            }
        }
    }
  2. 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 ์ฝ์–ด๋ดค๋˜ ์ž๋ฃŒ

https://lets-do-the-odessey.tistory.com/47

'Back-end > Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Spring, Docker] ๋ชจ๋“ˆ์‹ ๋ชจ๋†€๋ฆฌ์Šค์—์„œ Docker ๋ฐฐํฌํ•˜๊ธฐ  (0) 2026.06.24
'Back-end/Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • [Spring, Docker] ๋ชจ๋“ˆ์‹ ๋ชจ๋†€๋ฆฌ์Šค์—์„œ Docker ๋ฐฐํฌํ•˜๊ธฐ
dev_Hyeonjoon
dev_Hyeonjoon
์Šคํ”„๋ง์ด๋ž‘ ์ž๋ฐ” ๊ณต๋ถ€ ์ค‘ ์ž…๋‹ˆ๋‹ค!!
  • dev_Hyeonjoon
    Hyeonjoon's code
    dev_Hyeonjoon
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (61) N
      • CS (6)
        • Architecture (4)
        • Algorithm (2)
      • Front-end (10)
        • JavaScript (0)
        • React-project (5)
        • Vanilla[JS]-project (5)
        • study (0)
      • Tools (1)
        • GitHub (1)
        • AI (0)
      • Back-end (35) N
        • Java (27)
        • Architecture (2) N
        • JPA (0)
        • Trouble Shooting (1)
        • Spring (2)
        • DevOps (2)
      • Trouble Shooting (2)
        • Python (1)
      • ์ž๊ฒฉ์ฆ (1)
      • ์•Œ๊ณ ๋ฆฌ์ฆ˜ (0)
      • ํšŒ๊ณ  (6)
        • ๊ธฐํƒ€ (6)
        • ๋…์„œ๋ก (0)
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํ™ˆ
    • ํƒœ๊ทธ
    • ๋ฐฉ๋ช…๋ก
  • ๋งํฌ

  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    OOP
    ์ž๋ฐ”๊ธฐ์ดˆ
    ์‚ฌ์ง€๋ฐฉ์ฝ”๋”ฉ
    ํŒŒ์ด์ฌ
    ๊ตฐ๋Œ€์—์„œ์ฝ”๋”ฉ
    ํ˜•๋ณ€ํ™˜
    ๋ฐ”๋‹๋ผjs
    ๊ฐ์ฒด์ง€ํ–ฅ
    ์ž๋ฐ”
    ์ฝ”๋”ฉ
    ๋ฐฑ์—”๋“œ
    Spring
    backend
    ๋ฆฌ์•กํŠธ
    Til
    ํ† ์ดํ”„๋กœ์ ํŠธ
    ์•„ํ‚คํ…์ฒ˜
    ์ž๋ฐ”๊ณต๋ถ€
    vanillajs
    Java
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.1
dev_Hyeonjoon
[Spring] Business Layer์™€ Presentation Layer ๋ถ„๋ฆฌ์˜ ์ค‘์š”์„ฑ
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”