2023. 9. 13. 18:55ㆍ기술 창고/Spring
음악 플랫폼을 만들어보고 싶은 생각이 들어서 음악 등록 api를 만들게 되었습니다.
내부 라이브러리를 사용하면 mp3 파일의 가사, 아티스트와 같은 정보들을 추출할 수 없기 때문에 외부의 라이브러리를 사용하게 되었습니다.
https://mvnrepository.com/artifact/net.jthink/jaudiotagger/3.0.1
다양한 라이브러리가 있겠지만 저는 jaudiotagger 를 사용해주었습니다.
maven repository 에서 jaudiotagger 를 가져와서 build.gradle에 넣어주었습니다.
이제 api를 구현하는 것부터 음악 파일을 넣는 과정까지 차례대로 정리해보겠습니다.
Music
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Entity
public class Music extends TimeStamped {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long musicId;
@Column(nullable = false)
private String musicUuidName; // 난수화된 곡 이름
@Column(nullable = false)
private String musicRealName; // 곡 원래 이름
@Column(nullable = false)
private String musicUploadUrl; // 곡 업로드된 경로 url
@Column(columnDefinition = "LONGTEXT")
private String lyrics; // 가사
@Column(nullable = false)
private String genre; // 음악 장르
@Column(nullable = false)
private String artist; // 아티스트
@Column(nullable = false)
private String playTime; // 재생시간
@Column(nullable = false)
private String albumImg; // 앨범 이미지
@Column(nullable = false)
private String albumImgUploadUrl; // 업로드 된 이미지 경로 url
@Column(nullable = false)
private Long memberId; // 업로드한 유저 아이디
}
음악 정보를 저장하기 위한 Music 엔티티를 만들어줍니다.
필요한 정보는 uuid로 난수화된 곡 이름, 원래 곡 이름, 업로드된 파일의 경로, 가사, 음악 장르, 아티스트, 재생 시간, 앨범 이미지, 이미지 업로드 경로, 업로드한 유저 id 라고 가정하였습니다.
가사 정보는 매우 긴 텍스트로 이루어져 있기 때문에 Column 어노테이션에 columnDefinition 옵션을 LongText 로 지정하여 기존의 String 크기 범위를 넘어갈 수 있게끔하였습니다.
MusicRepository
@Repository
public interface MusicRepository extends JpaRepository<Music, Long> {
}
데이터를 저장할 MusicRepository를 생성합니다.
MusicController
// 음악 등록 api
@PostMapping("/upload")
public ResponseEntity<ResponseBody> musicUpload(
HttpServletRequest request,
@RequestPart MultipartFile music,
@RequestPart MultipartFile thumbnailImg) throws IOException, CannotReadException, TagException, InvalidAudioFrameException, ReadOnlyFileException {
log.info("음악 등록 api");
return musicService.musicUpload(request, music, thumbnailImg);
}
이제 controller를 만들어 줍니다.
Spring Security와 JWT를 적용했기 때문에 정상적인 토큰이 발급된 HttpServletRequest 가 필요하고, 음악 파일, 그리고 음악 썸네일 이미지 파일을 매개변수로 받습니다.
MusicUploadInterface / MusicUpload
@Component
public interface MusicUploadInterface {
/** 업로드 될 경로 **/
String getFullPath(String filename, String fileDir);
/** 업로드 시킬 파일 **/
HashMap<String, String> serverUploadFile(MultipartFile multipartFile, String fileDir) throws IOException, CannotReadException, TagException, InvalidAudioFrameException, ReadOnlyFileException;
/** 난수화한 업로드할 파일 이름 **/
String createServerFileName(String originalFilename);
/** 업로드 파일 확장자 정보 추출 **/
String extractExt(String originalFilename);
}
@Component
public class MusicUpload implements MusicUploadInterface{
// 업로드 될 경로
@Override
public String getFullPath(String filename, String fileDir) {
// 파일명과 업로드될 경로 반환
return fileDir + filename;
}
// 업로드 시킬 파일
@Override
public HashMap<String, String> serverUploadFile(MultipartFile multipartFile, String fileDir) throws IOException, CannotReadException, TagException, InvalidAudioFrameException, ReadOnlyFileException {
// 업로드할 음악 파일의 진짜 이름 추출
String originalFilename = multipartFile.getOriginalFilename();
// 난수화된 파일 이름과 확장자를 합친 파일명 추출
String serverUploadFileName = createServerFileName(originalFilename);
// 업로드한 mp3 파일과 경로를 합친 업로드 경로를 File 객체에 넣어 생성
File file = new File(getFullPath(serverUploadFileName, fileDir));
// multipartfile에서 지원하는 transferTo 함수 사용. (해당 파일을 지정한 경로로 전송)
multipartFile.transferTo(file);
// mp3 파일 정보들을 저장하여 전달하기 위한 HashMap 생성
HashMap<String, String> musicinfo = new HashMap<>();
if(extractExt(originalFilename).equals("mp3")){
// 업로드한 mp3 파일을 jaudiotagger 에서 지원해주는 MP3File 객체 클래스로 읽어서 반영 생성
MP3File mp3 = (MP3File) AudioFileIO.read(file);
// mp3 파일 내부에 존재하는 기타 태그 정보들을 Tag 객체 클래스로 따로 생성
Tag tag = mp3.getTag();
String title = tag.getFirst(FieldKey.TITLE); // 곡 타이틀
String artist = tag.getFirst(FieldKey.ARTIST); // 곡 아티스트
String album = tag.getFirst(FieldKey.ALBUM); // 곡 앨범
String genre = tag.getFirst(FieldKey.GENRE); // 곡 장르
String lyrics = tag.getFirst(FieldKey.LYRICS); // 곡 가사
// 추출한 정보들을 저장
musicinfo.put("uuidName", serverUploadFileName);
musicinfo.put("title", title);
musicinfo.put("artist", artist);
musicinfo.put("playTime", mp3.getMP3AudioHeader().getTrackLengthAsString());
musicinfo.put("album", album);
musicinfo.put("genre", genre);
musicinfo.put("lyrics", lyrics);
}
// 이후 배포 시 mp3 파일과 썸네일 이미지 파일을 s3 와 같은 클라우드 플랫폼에 저장 후 발급된 url 정보도 같이 hashMap에 넣어서 전달해야됨.
return musicinfo;
}
// 난수화한 업로드할 파일 이름
@Override
public String createServerFileName(String originalFilename) {
// 원래 이름이 아닌 난수화한 uuid 이름 추출
String uuid = UUID.randomUUID().toString();
// 파일의 원래 이름 중에 . 기호 기준으로 확장자 추출
String ext = extractExt(originalFilename);
// 난수화된 이름과 확장자를 합쳐 난수화된 파일명 반환
return uuid + "." + ext;
}
// 업로드 파일 확장자 정보 추출
@Override
public String extractExt(String originalFilename) {
// 파일명의 . 기호가 몇번째에 존재하는지 인덱스 값 추출
int pos = originalFilename.lastIndexOf(".");
// 원래 이름에서 뽑은 인덱스값에 위치한 . 기호 다음 확장자 추출
return originalFilename.substring(pos + 1);
}
}
실질적으로 음악 파일을 업로드 시킬 인터페이스를 따로 만들어 주었습니다.
지금은 로컬 환경에서 업로드할 것이기 때문에 프로젝트 내부 경로에 저장되게 하였습니다.
이후에는 아마존의 s3 와 같은 기능을 연동하게 될 경우, 해당 업로드 경로를 적용시킬 것입니다.
인터페이스에는 총 네가지의 함수로 구성되어있습니다.
1. 파일명과 경로를 합친 업로드 경로를 뽑아낼 getFullPath 함수
- 파일 명과 업로드 경로를 매개변수로 받아 붙여서 최종 업로드 경로 반환합니다.
2. 파일을 업로드하고 파일 정보를 추출하여 반환할 serverUploadFile 함수
- 음악 파일과 업로드 경로를 매개변수로 받아서 transferTo 함수로 파일을 미리 프로젝트 내부 경로에 저장하고 jaudiotagger 에서 지원해주는 MP3File 클래스와 Tag 클래스를 사용하여 음악 파일 내부 정보 추출 후, HashMap을 사용하여 저장 후 반환
MP3File 객체 클래스로 변환한 음악 파일의 tag에 음악파일의 정보들이 저장되어있습니다.
Tag 객체 클래스로 해당 정보들을 가져오고, FieldKey.{태그명} 을 통해 가져올 정보들을 지정한뒤, tag의 getFirst함수로 실제로 정보들을 가져옵니다.
재생 시간은 MP3File 객체 클래스에서 지원하는 .getMP3AudioHeader().getTrackLengthAsString() 함수들을 통해 플레이 시간을 흔히 볼 수 있는 mm:ss 형식으로 추출해줍니다.
이제 추출한 모든 정보들을 HashMap에 담아 반환시켜줍니다.
3. 파일명을 난수화해서 확장자를 다시 붙여 재정의한 createServerFileName 함수
- 파일명을 매개변수로 받고 UUID.randomUUID 함수로 랜덤 파일명을 만든 뒤, 전달받은 파일명을 extractExt 함수에 넣어 확장자를 따로 뽑아서 난수화된 파일명과 확장자를 합친 파일명으로 추출합니다.
4. 확장자를 추출할 extractExt 함수
전달받은 파일명의 . 기호 기준으로 뒤에 있는 확장자를 따로 추출합니다.
MusicService
// 음악 등록 service
public ResponseEntity<ResponseBody> musicUpload(
HttpServletRequest request,
MultipartFile music, MultipartFile thumbnailImg) throws IOException, CannotReadException, TagException, InvalidAudioFrameException, ReadOnlyFileException {
// 발급된 jwt 토큰의 정합성이 옳바르지 않을 때 예외 처리
if (tokenExceptionInterface.checkToken(request.getHeader("Refresh-Token"))) {
new ResponseEntity<>(new ResponseBody(StatusCode.UNAUTHORIZE_TOKEN, null), HttpStatus.BAD_REQUEST);
}
// 로컬 환경 음악 파일 및 썸네일 이미지 파일 저장 경로 (이후 배포 시 s3를 연동하면서 경로를 따로 지정해주어야함.)
String filePath = "C:" + File.separator + "MyProject" + File.separator + "MusicIsMyLife" + File.separator + "MusicIsMyLife" + File.separator + "src" + File.separator + "main" + File.separator + "webapp" + File.separator;
// 음악 파일 업로드 경로 지정 및 이름 추출
HashMap<String, String> musicinfo = musicUploadInterface.serverUploadFile(music, filePath + "upload-music/");
// 썸네일 이미지 파일 업로드 경로 지정 및 이름 추출
musicUploadInterface.serverUploadFile(thumbnailImg, filePath + "upload-thumbnail/");
Music uploadMusic = Music.builder()
.musicUuidName(musicinfo.get("uuidName"))
.musicRealName(music.getOriginalFilename())
.musicUploadUrl(filePath + "upload-music/") // 이후 서버에 배포 시 s3 연동 후 url 정보로 대체
.lyrics(musicinfo.get("lyrics"))
.genre(musicinfo.get("genre"))
.artist(musicinfo.get("artist"))
.playTime(musicinfo.get("playTime"))
.albumImg(thumbnailImg.getOriginalFilename())
.albumImgUploadUrl(filePath + "upload-thumbnail/") // 이후 서버에 배포 시 s3 연동 후 url 정보로 대체
.memberId(jwtTokenProvider.getMemberFromAuthentication().getMemberId())
.build();
// 음악 정보 DB 저장
musicRepository.save(uploadMusic);
// 결과 확인용 Dto 객체
MusicUploadResponseDto responseDto = MusicUploadResponseDto.builder()
.musicId(uploadMusic.getMusicId())
.musicUuidName(uploadMusic.getMusicUuidName())
.musicRealName(uploadMusic.getMusicRealName())
.musicUploadUrl(uploadMusic.getMusicUploadUrl())
.lyrics(uploadMusic.getLyrics())
.genre(uploadMusic.getGenre())
.artist(uploadMusic.getArtist())
.playTime(uploadMusic.getPlayTime())
.albumImg(uploadMusic.getAlbumImg())
.albumImgUploadUrl(uploadMusic.getAlbumImgUploadUrl())
.memberId(uploadMusic.getMemberId())
.build();
HashMap<String, Object> resultSet = new HashMap<>();
resultSet.put("uploadData", responseDto);
return new ResponseEntity<>(new ResponseBody(StatusCode.OK, resultSet), HttpStatus.OK);
}
구현한 코드의 시나리오는 이렇습니다.
(1)
발급된 유저의 토큰 정보가 옳바른지 처음에 확인합니다.
(2)
File.separator 와 프로젝트 폴더 명을 합쳐서 업로드 경로 변수를 만듭니다.
(3)
MusicUploadInterface 의 음악 업로드 함수 serverUploadFile 에 음악 파일과 업로드 경로를 매개변수로 넘겨 추출받은 HashMap 음악 파일 정보들을 가져옵니다.
(4)
썸네일 용 음악 이미지 파일도 업로드 될 수 있게끔 MusicUploadInterface 의 음악 업로드 함수 serverUploadFile 에 이미지 파일과 업로드 경로를 매개변수로 넘겨 이미지 파일이 저장만 되게끔 합니다.
(5)
가져온 음악 정보 파일들 musicInfo 를 기반으로 Music 엔티티에 넣어줍니다.
(6)
MusicRepository 로 jpa save 함수를 통해 데이터 저장합니다.
(7)
저장된 결과를 확인하기 위해서 MusicUploadResponseDto 객체를 생성하여 결과 정보들 반영
그리고 결과 HashMap 에 저장합니다.
(8)
api 가 호출되면 저장된 MusicUploadResponseDto 를 반환합니다.
이제 Postman을 통해 api를 호출해봅시다.
Headers 에는 발급받은 JWT 토큰 정보(Authorization : 액세스 토큰, Refresh-Token : 리프레시 토큰) 을 넣어주고, Body에는 음악 파일과 썸네일 이미지를 넣어주었습니다.
정상적으로 데이터가 저장되고 추출되었습니다.
가사도 길지만 정상적으로 저장되었습니다.
무사히 저장 및 추출 완료했습니다!
'기술 창고 > Spring' 카테고리의 다른 글
[Spring Boot] Springdoc-openapi Swagger 적용 (+ JWT 사용 시 적용법) (0) | 2023.10.04 |
---|---|
[Spring Boot] 구글 Oauth2 인증 및 구글 소셜 로그인 기능 구현 (0) | 2023.09.20 |
[Spring Boot] JPA 엔티티 String 속성을 더욱 큰 속성으로 변경 (0) | 2023.09.13 |
[Spring Boot] Spring Boot 3.XX 버전 이상 QueryDSL 설정 및 실행 과정 (0) | 2023.09.11 |
[Spring Boot] Spring Boot로 프로덕션 환경 배포 준비하기 - (2) Configuration Properties (0) | 2023.09.02 |