Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5f1d5e3
CONFETI-75 feat: facade 계층 도입을 위한 커스텀 어노테이션 작성
jher235 Dec 24, 2025
3ee50a8
CONFETI-75 feat: ConfetiSong VO 작성
jher235 Dec 24, 2025
3a017b3
CONFETI-75 feat: Music sync를 위한 스레드 풀 추가
jher235 Dec 24, 2025
6191c58
CONFETI-75 feat: Artist Song 업데이트 시 벌크 쿼리를 사용하기 위한 JDBC 설정
jher235 Dec 24, 2025
3b042e0
CONFETI-75 feat: AppleMusic 관련 로직을 분리해서 관리하기 위한 MusicAPIHandler 추가
jher235 Dec 24, 2025
0b6f578
CONFETI-75 feat: topArtist API에서 offset을 사용할 수 있도록 파라미터 추가
jher235 Dec 24, 2025
ff87642
CONFETI-75 feat: 빌더 및 비교 메서드 추가
jher235 Dec 24, 2025
8efb133
CONFETI-75 feat: SongService 구현
jher235 Dec 24, 2025
56c930c
CONFETI-75 feat: VO 변환 메서드 추가
jher235 Dec 24, 2025
2368fce
CONFETI-75 feat: Music Sync 관련 작업을 위한 facade 구현
jher235 Dec 24, 2025
11ddaca
CONFETI-75 feat: BulkArtistSongUpsertWriter 구현
jher235 Dec 24, 2025
85dae5f
CONFETI-75 feat: artistSyncJob 에 artistSongSyncStep 등록
jher235 Dec 24, 2025
c64ef53
CONFETI-75 feat: StepConfig 구조 변경 및 추가
jher235 Dec 24, 2025
2409f97
CONFETI-75 feat: artistId 기반으로 Song을 찾는 메서드 추가
jher235 Dec 24, 2025
40961dd
CONFETI-75 feat: projection을 사용해서 Song을 엔티티가 아닌 데이터로 조회하도록 변경
jher235 Dec 25, 2025
0ca236b
CONFETI-75 feat: 로직에서 Song 조회 시projection을 사용하도록 변경
jher235 Dec 25, 2025
f9e9f15
CONFETI-75 feat: isDifferentData 로직을 ConfetiSong에서 갖도록 변경
jher235 Dec 25, 2025
e81fad1
CONFETI-75 feat: id 기반으로 Artist 호출하는 로직을 MusicAPIHandler 가 갖도록 변경
jher235 Dec 25, 2025
1be66d0
CONFETI-75 refactor: writer에서 facade의 로직을 호출하는 형태로 변경
jher235 Dec 25, 2025
302a3db
CONFETI-75 refactor: ArtistSyncStep 시 엔티티가 아닌 VO를 사용하는 형태로 변경
jher235 Dec 25, 2025
a1ec44a
CONFETI-75 refactor: 필드를 상수로 관리하도록 변경
jher235 Dec 25, 2025
2adade1
CONFETI-75 refactor: ArtistSong 배치 작업 시 reader에서 Id값만 읽어도로록 변경
jher235 Dec 25, 2025
4a5b3b6
CONFETI-75 refactor: upsert 로직을 SongService 내부에 두도록 변경
jher235 Dec 25, 2025
466fa01
CONFETI-75 chore: host 변경
jher235 Dec 27, 2025
9dc33f1
CONFETI-75 refactor: 패키지 구조 변경.
jher235 Dec 27, 2025
4a7c33d
CONFETI-75 refactor: Reader 클래스 분리
jher235 Dec 27, 2025
6ec8335
CONFETI-75 refactor: Projection 대신 ConfetiSong을 사용하도록 변경
jher235 Dec 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package confeti.confetibatchserver.api.music.facade;

import confeti.confetibatchserver.domain.music.artist.Artist;
import confeti.confetibatchserver.domain.music.artist.application.ArtistService;
import confeti.confetibatchserver.domain.music.artist.vo.ConfetiArtist;
import confeti.confetibatchserver.domain.music.song.application.SongService;
import confeti.confetibatchserver.domain.music.song.vo.ConfetiSong;
import confeti.confetibatchserver.external.service.MusicAPIHandler;
import confeti.confetibatchserver.global.annotation.Facade;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;

@Facade
@RequiredArgsConstructor
public class MusicSyncFacade {

private final MusicAPIHandler musicAPIHandler;
private final ArtistService artistService;
private final SongService songService;

public void upsertSongByArtistId(String artistId) {
List<ConfetiSong> fetchedSongs = musicAPIHandler.getAllSongsByArtistId(artistId);
songService.upsert(artistId, fetchedSongs);
}

public void upsertArtists(List<ConfetiArtist> artists) {
Set<String> artistIds = artists.stream().map(ConfetiArtist::getId)
.collect(Collectors.toSet());
List<ConfetiArtist> fetchedArtists = musicAPIHandler.getArtistsByIds(artistIds);
List<Artist> updatedArtists = artistService.getUpdatedArtists(artists, fetchedArtists);
artistService.upsertArtists(updatedArtists);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package confeti.confetibatchserver.config;

import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class ThreadPoolConfig {

public static final String MUSIC_SYNC_EXECUTOR = "musicSyncExecutor";

@Bean(MUSIC_SYNC_EXECUTOR)
public ThreadPoolTaskExecutor batchThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("music-sync-worker-");

executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()); // 큐가 꽉 차면 메인 스레드가 작업을 처리
executor.setWaitForTasksToCompleteOnShutdown(true); // 종료 시 작업 계속하도록
executor.initialize();

return executor;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package confeti.confetibatchserver.domain.batch.stepconfig;

import confeti.confetibatchserver.job.JobInfo;
import confeti.confetibatchserver.job.StepInfo;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -20,9 +19,6 @@ public class StepConfig {
@Enumerated(EnumType.STRING)
private StepInfo stepInfo;

@Enumerated(EnumType.STRING)
private JobInfo jobInfo;

@Column(nullable = false)
private int chunkSize;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -55,10 +54,4 @@ public static Artist from(ConfetiArtist artist) {
.build();
}

public boolean isDifferentData(ConfetiArtist artist) {
return !Objects.equals(id, artist.getId())
|| !Objects.equals(name, artist.getName())
|| !Objects.equals(artworkUrl, artist.getProfileUrl());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@ public void upsertArtists(Collection<Artist> artists) {
}

public List<Artist> getUpdatedArtists(
List<Artist> savedArtists,
List<ConfetiArtist> savedArtists,
List<ConfetiArtist> newArtists
) {
Map<String, Artist> savedArtistById = artistsToMap(savedArtists);
Map<String, ConfetiArtist> savedArtistById = artistsToMap(savedArtists);

return newArtists.stream()
.filter(artist -> {
Artist savedArtist = savedArtistById.get(artist.getId());
ConfetiArtist savedArtist = savedArtistById.get(artist.getId());
return savedArtist != null && savedArtist.isDifferentData(artist);
})
.map(Artist::from)
.toList();
}

private Map<String, Artist> artistsToMap(Collection<Artist> artists) {
private Map<String, ConfetiArtist> artistsToMap(Collection<ConfetiArtist> artists) {
return artists.stream()
.collect(Collectors.toMap(
Artist::getId,
ConfetiArtist::getId,
Function.identity(),
(exist, replacement) -> exist));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package confeti.confetibatchserver.domain.music.artist.vo;

import java.util.Objects;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -18,4 +19,10 @@ public static ConfetiArtist of(String id, String name, String profileUrl) {
return new ConfetiArtist(id, name, profileUrl);
}

public boolean isDifferentData(ConfetiArtist artist) {
return !Objects.equals(id, artist.getId())
|| !Objects.equals(name, artist.getName())
|| !Objects.equals(profileUrl, artist.getProfileUrl());
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
package confeti.confetibatchserver.domain.music.artistsong.infra.repository;

import confeti.confetibatchserver.domain.music.artistsong.ArtistSong;
import confeti.confetibatchserver.domain.music.song.Song;
import feign.Param;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;


public interface ArtistSongRepository extends JpaRepository<ArtistSong, Long> {

@Query(value = """
SELECT s
FROM Song as s
JOIN ArtistSong AS a_s ON a_s.artistId = :artistId
""")
List<Song> findAllSongByArtistId(@Param("artistId") String artistId);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package confeti.confetibatchserver.domain.music.song;

import confeti.confetibatchserver.domain.music.song.vo.ConfetiSong;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
Expand All @@ -24,12 +26,12 @@ public class Song {
@Column(nullable = false)
private String id;

@Column(length = 100, nullable = false)
@Column(length = 1000, nullable = false)
private String trackName;

@Column(length = 100)
@Column(length = 1000)
private String artistName;

@Column(length = 3000)
private String artworkUrl;

Expand All @@ -42,5 +44,24 @@ public class Song {

@LastModifiedDate
private LocalDateTime updatedAt;
}

@Builder
private Song(String id, String trackName, String artistName, String artworkUrl,
String previewUrl) {
this.id = id;
this.trackName = trackName;
this.artistName = artistName;
this.artworkUrl = artworkUrl;
this.previewUrl = previewUrl;
}

public static Song from(ConfetiSong confetiSong) {
return Song.builder()
.id(confetiSong.getId())
.trackName(confetiSong.getTrackName())
.artistName(confetiSong.getArtistName())
.artworkUrl(confetiSong.getArtworkUrl())
.previewUrl(confetiSong.getPreviewUrl())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package confeti.confetibatchserver.domain.music.song.application;

import confeti.confetibatchserver.domain.music.song.infra.repository.SongRepository;
import confeti.confetibatchserver.domain.music.song.vo.ConfetiSong;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class SongService {

private final SongRepository songRepository;

@Transactional
public void upsert(String artistId, List<ConfetiSong> songs) {
Map<String, ConfetiSong> songMapByArtistId = getConfetiSongMapByArtistId(
artistId);
List<ConfetiSong> upsertSongs = getUpsertSongs(songs, songMapByArtistId);

songRepository.upsertSongsWithArtistId(artistId, upsertSongs);
}

private Map<String, ConfetiSong> getConfetiSongMapByArtistId(String artistId) {
return songRepository.findAllConfetiSongsByArtistId(artistId).stream()
.collect(Collectors.toMap(ConfetiSong::getId, Function.identity()));
}

private List<ConfetiSong> getUpsertSongs(
List<ConfetiSong> fetchedSongs,
Map<String, ConfetiSong> songMapByArtistId
) {
List<ConfetiSong> upsertSongs = new ArrayList<>();
for (ConfetiSong fetchedSong : fetchedSongs) {
ConfetiSong song = songMapByArtistId.get(fetchedSong.getId());
if (song == null) {
upsertSongs.add(fetchedSong);
continue;
}
if (fetchedSong.isDifferentData(song)) {
upsertSongs.add(fetchedSong);
}
}
return upsertSongs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package confeti.confetibatchserver.domain.music.song.infra.repository;

import confeti.confetibatchserver.domain.music.song.vo.ConfetiSong;
import java.util.List;

public interface SongJdbcRepository {

void upsertSongsWithArtistId(String artistId, List<ConfetiSong> songs);

List<ConfetiSong> findAllConfetiSongsByArtistId(String artistId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package confeti.confetibatchserver.domain.music.song.infra.repository;

import confeti.confetibatchserver.domain.music.song.vo.ConfetiSong;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@RequiredArgsConstructor
public class SongJdbcRepositoryImpl implements SongJdbcRepository {

private final NamedParameterJdbcTemplate namedJdbcTemplate;

private final String BULK_UPSERT_SONGS_SQL = """
INSERT INTO songs (id, track_name, artwork_url, artist_name, preview_url, created_at, updated_at)
VALUES (:id, :trackName, :artworkUrl, :artistName, :previewUrl, NOW(), null)
ON DUPLICATE KEY UPDATE
track_name = VALUES(track_name),
artwork_url = VALUES(artwork_url),
artist_name = VALUES(artist_name),
preview_url = VALUES(preview_url),
updated_at = NOW()
""";
private final String BULK_INSERT_ARTIST_SONGS_SQL = """
INSERT IGNORE INTO artist_songs (song_id, artist_id)
VALUES (:songId, :artistId)
""";
private final String SELECT_CONFETI_SONGS_BY_ARTIST_ID_SQL = """
SELECT s.id as id,
s.track_name as trackName,
s.artwork_url as artworkUrl,
s.artist_name as artistName,
s.preview_url as previewUrl
FROM songs as s
INNER JOIN artist_songs as a_s ON a_s.song_id = s.id AND a_s.artist_id = :artistId
""";

@Transactional
public void upsertSongsWithArtistId(String artistId, List<ConfetiSong> songs) {
SqlParameterSource[] songParams = SqlParameterSourceUtils.createBatch(songs);
namedJdbcTemplate.batchUpdate(BULK_UPSERT_SONGS_SQL, songParams);

MapSqlParameterSource[] artistSongParams = songs.stream()
.map(song -> new MapSqlParameterSource()
.addValue("songId", song.getId())
.addValue("artistId", artistId))
.toArray(MapSqlParameterSource[]::new);
namedJdbcTemplate.batchUpdate(BULK_INSERT_ARTIST_SONGS_SQL, artistSongParams);
}

@Override
public List<ConfetiSong> findAllConfetiSongsByArtistId(String artistId) {
Map<String, Object> params = Map.of("artistId", artistId);

return namedJdbcTemplate.query(
SELECT_CONFETI_SONGS_BY_ARTIST_ID_SQL,
params,
(rs, rowNum) -> ConfetiSong.builder()
.id(rs.getString("id"))
.trackName(rs.getString("trackName"))
.artworkUrl(rs.getString("artworkUrl"))
.artistName(rs.getString("artistName"))
.previewUrl(rs.getString("previewUrl"))
.build()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import confeti.confetibatchserver.domain.music.song.Song;
import org.springframework.data.jpa.repository.JpaRepository;

public interface SongRepository extends JpaRepository<Song, String> {
public interface SongRepository extends JpaRepository<Song, String>, SongJdbcRepository {

}

Loading