알쓸코지
article thumbnail

이번에는 지난 시간에 만들었던 `아티스트 정보 검색 API`로 응답받은 데이터를 데이터베이스에 저장하는 기능을 구현해보려고 한다.

 

사전 설정

이번 프로젝트에서는 PostgreSQL을 사용해보기로 해서 설치해 두었다. PostgreSQL에 데이터를 저장하기 위해서는 다음과 같이 설정을 추가해야 한다.

 

build.gradle

dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.postgresql:postgresql:42.7.3'
}

 

application.properties

# PostgreSQL
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

 

Domain

Artist

  • `artistId`: Spotify API 내에서 사용하는 고유 ID (String)
  • `artistName`: 아티스트 이름
  • `profileImg`: 아티스트 대표 이미지
@Entity
@Getter
@NoArgsConstructor
@Table(name = "artist")
public class Artist {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "artist_id")
    private String artistId;

    @Column(name = "artist_name")
    private String artistName;

    @Column(name = "profile_img")
    private String profileImg;

    @Builder
    public Artist(String artistId, String artistName, String profileImg) {
        this.artistId = artistId;
        this.artistName = artistName;
        this.profileImg = profileImg;
    }

}

 

Track

  • `trackName`: 곡 제목
  • `albumName`: 앨범 제목
  • `previewUri`: 트랙 미리 듣기 URL
@Entity
@Getter
@AllArgsConstructor
@Table(name = "track")
public class Track {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "track_name")
    private String trackName;

    @Column(name = "album_name")
    private String albumName;

    @Column(name = "preview_uri")
    private String previewUri;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "artist_id")
    @JsonIgnore
    private Artist artist;

}

 

Repository

ArtistRepository

public interface ArtistRepository extends JpaRepository<Artist, Long> {

    Artist findByArtistId(String artistId);
    Optional<Artist> findByArtistName(String artistName);

}

 

TrackRepository

public interface TrackRepository extends JpaRepository<Track, Long> {

    List<Track> findByArtist_ArtistName(String artistName);

}

이 `findByArtist_ArtistName` 메서드는 `artistName`에 해당하는 모든 `Track` 객체를 반환하는 메서드로, 내부적으로 다음과 같은 SQL 쿼리를 실행한다.

SELECT * FROM track t
JOIN artist a ON t.artist_id = a.id
WHERE a.artist_name = :artistName

 

Service

ArtistService

@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class ArtistService {

    private final ArtistRepository artistRepository;
    private final TrackRepository trackRepository;
    private final SpotifyService spotifyService;

    @Transactional
    public Artist saveArtistFromSpotify(String artistName) {
        List<SearchResponseDto> searchResults = spotifyService.search(artistName);

        // 첫 번째 결과가 없으면 예외 발생
        SearchResponseDto artistInfo = searchResults.stream()
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("No artist found with name: " + artistName));

        // 아티스트 저장
        Artist artist = artistRepository.findByArtistId(artistInfo.getArtistId());
        if (artist == null) {
            artist = Artist.builder()
                    .artistId(artistInfo.getArtistId())
                    .artistName(artistInfo.getArtistName())
                    .profileImg(artistInfo.getImageUrl())
                    .build();

            artist = artistRepository.save(artist);
            saveTopTracks(artistInfo.getTracks(), artist);
        }

        return artist;
    }

    private void saveTopTracks(List<TrackInfo> tracks, Artist artist) {
        for (TrackInfo trackInfo : tracks) {
            Track track = new Track(trackInfo.getTrackName(), trackInfo.getAlbumName(), trackInfo.getPreviewUri(), artist);
            trackRepository.save(track);
        }
    }

    public Artist getArtistById(Long id) {
        return artistRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당하는 가수가 없습니다."));
    }

    public Artist getArtistByArtistName(String artistName) {
        return artistRepository.findByArtistName(artistName)
                .orElseThrow(() -> new IllegalArgumentException("해당하는 가수가 없습니다."));
    }

    @Transactional
    public void deleteArtist(Long id) {
        artistRepository.deleteById(id);
    }

}

 

TrackService

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TrackService {

    private final TrackRepository trackRepository;

    public List<Track> getTracksByArtist(String artistName) {
        return trackRepository.findByArtist_ArtistName(artistName);
    }

    @Transactional
    public void deleteTrack(Long trackId) {
        trackRepository.deleteById(trackId);
    }

}

 

Controller

ArtistController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/artists")
public class ArtistController {

    private final ArtistService artistService;

    @GetMapping("/spotify")
    public ResponseEntity<Artist> saveArtistInfo(@RequestParam("artistName") String artistName) {
        return ResponseEntity.ok(artistService.saveArtistFromSpotify(artistName));
    }

    @GetMapping("/search")
    public ResponseEntity<Artist> getArtistByArtistName(@RequestParam("artistName") String artistName) {
        return ResponseEntity.ok(artistService.getArtistByArtistName(artistName));
    }

    @GetMapping("/{id}")
    public ResponseEntity<Artist> getArtistById(@PathVariable("id") Long id) {
        return ResponseEntity.ok(artistService.getArtistById(id));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteArtistById(@PathVariable("id") Long id) {
        artistService.deleteArtist(id);
        return ResponseEntity.noContent().build();
    }

}

 

TrackController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/tracks")
public class TrackController {

    private final TrackService trackService;

    @GetMapping
    public ResponseEntity<List<Track>> getTracksByArtist(@RequestParam("artistName") String artistName) {
        List<Track> tracks = trackService.getTracksByArtist(artistName);
        return ResponseEntity.ok(tracks);
    }

    @DeleteMapping("/{trackId}")
    public ResponseEntity<Void> deleteTrack(@PathVariable Long trackId) {
        trackService.deleteTrack(trackId);
        return ResponseEntity.noContent().build();
    }

}

 

정리 ✏️

지난 시간에 구현했던 API를 호출해서 데이터를 저장하는 코드와 `아티스트명`으로 데이터 조회 및 데이터 삭제 등 기본적인 기능을 구현해 보았다. 하지만 현재 `아티스트명`으로 데이터 조회 시, 데이터베이스에 저장된 아티스트 이름과 정확히 일치하는 이름만 검색이 가능하다는 문제가 있다.

 

즉, 현재 `artist` 테이블에 저장되어 있는 아티스트명이 `DAY6`일 경우, 다음과 같은 이름으로 검색했을 때, 모두 검색이 되지 않는다.

  • day6
  • Day6
  • 데이식스
  • 데식

즉, 대소문자 구분 없이 검색이 가능해야 하며, 한글명과 별칭으로도 검색이 가능해야 한다.

다음 시간에는 이러한 요구사항을 만족하고, 예외 처리도 추가해보려고 한다.

profile

알쓸코지

@chocoji

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!