ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -2,18 +2,18 @@
 
 import com.requestrealpiano.songrequest.domain.song.searchapi.response.SearchApiResponse;
 import com.requestrealpiano.songrequest.global.response.ApiResponse;
+import com.requestrealpiano.songrequest.global.response.StatusCode;
 import com.requestrealpiano.songrequest.service.SongService;
 import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import javax.validation.constraints.Size;
 
 import static com.requestrealpiano.songrequest.global.constant.ValidationCondition.*;
-import static com.requestrealpiano.songrequest.global.response.ApiResponse.OK;
+import static com.requestrealpiano.songrequest.global.response.ApiResponse.SUCCESS;
+import static com.requestrealpiano.songrequest.global.response.StatusCode.OK;
 
 @RequiredArgsConstructor
 @Validated
@@ -23,12 +23,13 @@ public class SongController {
 
     private final SongService songService;
 
+    @ResponseStatus(HttpStatus.OK)
     @GetMapping
     public ApiResponse<SearchApiResponse> search(@RequestParam(defaultValue = "artist") @Size(max = ARTIST_MAX)
                                                  String artist,
                                                  @RequestParam(defaultValue = "title") @Size(max = TITLE_MAX)
                                                  String title) {
         SearchApiResponse searchApiResponse = songService.searchSong(artist, title);
-        return OK(searchApiResponse);
+        return SUCCESS(OK, searchApiResponse);
     }
 }

리뷰: 지난번 리뷰에 같은 의견을 주셔서 변경을 고려 해봤었는데요. 

여기 이 두 값은 빈 값으로 요청이 왔을 때 `NotEmpty` 와 같은 처리로 에러를 반환할게 아니라 default value로 검색을 계속 진행 시킬 생각 입니다. 만약 `@ModelAttribute` DTO로 받을 경우에는 setter로 값을 바인딩 해야 하는 것으로 알고 있는데요.
그렇다면 setter에서 빈 값일 경우 기본 값을 주는 로직이 필요할 듯 보이는데, 

가능하다면 `@RequestParam` 처럼 `@Something(defaultValue = "Default value")` 이런식으로 애노테이션 기반으로 수정 하고 싶은데 원래 스펙에 없는건지 제가 못 찾는건지...ㅎㅎ setter에서 직접 처리하는 것 말고 혹시 알고 계시는 더 좋은 방법이 있을까요?
@@ -1,5 +1,6 @@
 package com.requestrealpiano.songrequest.domain.account;
 
+import com.requestrealpiano.songrequest.security.oauth.OAuthAttributes;
 import com.requestrealpiano.songrequest.domain.base.BaseTimeEntity;
 import com.requestrealpiano.songrequest.domain.letter.Letter;
 import lombok.AccessLevel;
@@ -8,9 +9,6 @@
 import lombok.NoArgsConstructor;
 
 import javax.persistence.*;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -25,7 +23,7 @@ public class Account extends BaseTimeEntity {
     private Long id;
 
     @Column(name = "google_oauth_id")
-    private Long googleOauthId;
+    private String googleOauthId;
 
     private String name;
 
@@ -44,12 +42,36 @@ public class Account extends BaseTimeEntity {
     private List<Letter> letters = new ArrayList<>();
 
     @Builder
-    private Account(Long googleOauthId, String name, String email, Role role, String avatarUrl, Integer requestCount) {
+    private Account(String googleOauthId, String name, String email, Role role, String avatarUrl, Integer requestCount) {
         this.googleOauthId = googleOauthId;
         this.name = name;
         this.email = email;
         this.role = role;
         this.avatarUrl = avatarUrl;
         this.requestCount = requestCount;
     }
+
+    public String getRoleKey() { return role.getKey(); }
+
+    public String getRoleValue() {
+        return role.getValue();
+    }
+
+    public Account updateProfile(OAuthAttributes attributes) {
+        this.name = attributes.getName();
+        this.email = attributes.getEmail();
+        this.avatarUrl = attributes.getAvatarUrl();
+        return this;
+    }
+
+    public static Account from(OAuthAttributes oAuthAttributes) {
+        return Account.builder()
+                      .googleOauthId(oAuthAttributes.getGoogleOauthId())
+                      .name(oAuthAttributes.getName())
+                      .email(oAuthAttributes.getEmail())
+                      .role(Role.MEMBER)
+                      .avatarUrl(oAuthAttributes.getAvatarUrl())
+                      .requestCount(0)
+                      .build();
+    }
 }

리뷰: 계정을 최초 생성 했을 때의 기본 값 (신청곡 등록 수) 입니다. 이전 리뷰에서 이렇게 직관적으로 이해할 수 있는 이런 값은 변수로 빼지 않아도 될거 같다는 피드백을 받았었는데, 그걸 반영해서 코드 작성을 했던 듯 싶습니다. 
우선 이 코드는 컬럼에 기본 값을 추가 할 예정이기 때문에 없어질 코드가 될 듯 합니다.

그런데 혹시 써니는 매직넘버를 지정하는 것에 대한 어떤 기준이 있으실까요?
예전에 팀 프로젝트 할 때도 매직 넘버 사용을 무조건 피하려고 모두 변수로 지정 했다가 오히려 의미 해석이 어려워졌다는 피드백을 리뷰어 분께 받은적이 있었습니다. 이런 경우에는 그냥 숫자 값을 사용하고 주석으로 부가 설명을 적는 것도 괜찮다고 하시더라구요.

써니는 어떤 기준을 가지고 계시는지 궁금합니다.
@@ -1,5 +1,6 @@
 package com.requestrealpiano.songrequest.domain.account;
 
+import com.requestrealpiano.songrequest.security.oauth.OAuthAttributes;
 import com.requestrealpiano.songrequest.domain.base.BaseTimeEntity;
 import com.requestrealpiano.songrequest.domain.letter.Letter;
 import lombok.AccessLevel;
@@ -8,9 +9,6 @@
 import lombok.NoArgsConstructor;
 
 import javax.persistence.*;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -25,7 +23,7 @@ public class Account extends BaseTimeEntity {
     private Long id;
 
     @Column(name = "google_oauth_id")
-    private Long googleOauthId;
+    private String googleOauthId;
 
     private String name;
 
@@ -44,12 +42,36 @@ public class Account extends BaseTimeEntity {
     private List<Letter> letters = new ArrayList<>();
 
     @Builder
-    private Account(Long googleOauthId, String name, String email, Role role, String avatarUrl, Integer requestCount) {
+    private Account(String googleOauthId, String name, String email, Role role, String avatarUrl, Integer requestCount) {
         this.googleOauthId = googleOauthId;
         this.name = name;
         this.email = email;
         this.role = role;
         this.avatarUrl = avatarUrl;
         this.requestCount = requestCount;
     }
+
+    public String getRoleKey() { return role.getKey(); }
+
+    public String getRoleValue() {
+        return role.getValue();
+    }
+
+    public Account updateProfile(OAuthAttributes attributes) {
+        this.name = attributes.getName();
+        this.email = attributes.getEmail();
+        this.avatarUrl = attributes.getAvatarUrl();
+        return this;
+    }
+
+    public static Account from(OAuthAttributes oAuthAttributes) {
+        return Account.builder()
+                      .googleOauthId(oAuthAttributes.getGoogleOauthId())
+                      .name(oAuthAttributes.getName())
+                      .email(oAuthAttributes.getEmail())
+                      .role(Role.MEMBER)
+                      .avatarUrl(oAuthAttributes.getAvatarUrl())
+                      .requestCount(0)
+                      .build();
+    }
 }

리뷰: 저는 부가적 설명으로 주석넣는것도 괜찮다고 생각해요 그러나 만약 테스트 코드에서 여러번 중복됐을때는 따로 관리하는게 낫겠네요
@@ -11,6 +11,12 @@ public enum ErrorCode {
     METHOD_NOT_ALLOWED(405, "지원하지 않는 요청 메소드 입니다."),
     INTERNAL_SERVER_ERROR(500, "서버에서 요청을 처리하지 못했습니다."),
 
+    // Auth
+    UNAUTHENTICATED_ERROR(401, "인증이 필요합니다. 로그인 이후 다시 시도 해주세요."),
+    JWT_INVALID_ERROR(400, "올바른 인증 정보가 아닙니다. 다시 로그인 해주세요."),
+    JWT_EXPIRATION_ERROR(401, "인증 정보가 만료 되었습니다. 다시 로그인 해주세요."),
+    ACCESS_DENIED_ERROR(403, "해당 요청에 대한 접근 권한이 없습니다."),
+
     // Account
     ACCOUNT_NOT_FOUND(404, "해당 계정이 존재하지 않습니다."),
 

리뷰: JWT 관련 예외를 구상할 때 아래의 네가지 예외가 맨 처음 고려했던 JWT 관련 예외였습니다.
1. ExpiredJwtException : 유효기간이 지난 Token으로 요청 
2. InvalidClaimException : Claim 파싱 실패로 인한 예외
3. MalformedJwtException : 구조적 문제가 있는 JWT 토큰으로 인한 예외
4. SignatureException : 올바르지 않은 JWT 시그니처로 인한 검증 실패

이 때 1번 '유효기간 만료' 와  2, 3, 4에 해당하는 'JWT 검증 실패'
두가지 유형으로만 나누어서 처리를 한 뒤 클라이언트에 알려주면 될거라 생각하고 다음의 두가지로 처리 하도록 구현하였습니다.
`JWT_INVALID_ERROR(400, "올바른 인증 정보가 아닙니다. 다시 로그인 해주세요.") - JwtException`
`JWT_EXPIRATION_ERROR(401, "인증 정보가 만료 되었습니다. 다시 로그인 해주세요.") - ExpiredJwtException`

말씀하신 `JWT가 아닌 Token 사용으로 인한 에러`와 `JWT 이지만  올바르지 않은 JWT를 사용` 두 가지 경우를 구분 할 때 어떤 부분에서 장점이 있는 것인지 궁금합니다ㅎㅎ

물론 이런 인증과 보안에 대한 것은 최대한 견고하게 하는 것이 좋다는 생각을 가지고 있는데요. 
혹시 이 둘을 구분하지 않았을 때 어떤 큰 문제가 생길 여지가 있을까요? 

제가 모르고 지나치고 있는 것이 있는지에 대한 우려와 써니의 의견이 궁금해서 여쭈어 봅니다!
@@ -0,0 +1,63 @@
+language: java
+jdk:
+  - openjdk11
+
+branches:
+  only:
+    - main
+
+cache:
+  directories:
+    - '$HOME/.m2/repository'
+    - '$HOME/.gradle'
+
+script: "./gradlew clean build"
+
+before_deploy:
+  - mkdir -p before-deploy
+  - cp scripts/*.sh before-deploy/
+  - cp appspec.yml before-deploy/
+  - cp build/libs/*.jar before-deploy
+  - cd before-deploy && zip -r before-deploy *
+  - cd ../ && mkdir -p deploy
+  - mv before-deploy/before-deploy.zip deploy/songrequest-backend.zip
+
+deploy:
+  - provider: s3
+    access_key_id: $AWS_ACCESS_KEY
+    secret_access_key: $AWS_SECRET_KEY
+
+    bucket: songrequest-backend-build
+    region: ap-northeast-2
+
+    skip_cleanup: true
+    acl: private
+    local_dir: deploy
+
+    wait-until-deployed: true
+
+    on:
+      all_branches: true
+
+  - provider: codedeploy
+    access_key_id: $AWS_ACCESS_KEY
+    secret_access_key: $AWS_SECRET_KEY
+
+    bucket: songrequest-backend-build
+    key: songrequest-backend.zip
+
+    bundle_type: zip
+    application: songrequest-backend
+
+    deployment_group: songrequest-backend-group
+
+    region: ap-northeast-2
+    wait-until-deployed: true
+
+    on:
+      all_branches: true
+
+notifications:
+  email:
+    recipients:
+      - museopkim0214@gmail.com

리뷰: travis CI 이군요. 저도 써봤지만 Github Actions보다 느리고 디버깅하는데 불편해서 전 쓰다가 바꿨습니다.
현재는 Github Actions + AWS CodeBuild를 병합해서 사용하고 있는데 CI툴 중 travis ci를 사용하신 이유가 궁금합니다.
@@ -0,0 +1,18 @@
+version: 0.0
+os: linux
+files:
+  - source: /
+    destination: /home/ubuntu/app/songrequest/zip/
+    overwrite: yes
+
+permissions:
+  - object: /
+    pattern: "**"
+    owner: ubuntu
+    group: ubuntu
+
+hooks:
+  ApplicationStart:
+    - location: deploy.sh
+      timeout: 60
+      runas: ubuntu

리뷰: 여기서 아셔야할건 만약 blue/green 배포를 하실 경우 overwrite가 먹질 않습니다. 그래서 beforeInstall 쉘스크립트를 작성하셔서 파일 삭제하는 스크립트를 짜셔야합니다. 이 내용은 언젠가 제 블로그에 적을 예정이니 궁금하시면 보세용
@@ -0,0 +1,18 @@
+version: 0.0
+os: linux
+files:
+  - source: /
+    destination: /home/ubuntu/app/songrequest/zip/
+    overwrite: yes
+
+permissions:
+  - object: /
+    pattern: "**"
+    owner: ubuntu
+    group: ubuntu
+
+hooks:
+  ApplicationStart:
+    - location: deploy.sh
+      timeout: 60
+      runas: ubuntu

리뷰: 만약 본인이 blue/green 무중단 배포를 하실경우 여기서 BeforeInstall 쉘스크립트 넣으셔서 이전 빌드된파일 삭제하시는 쉘스크립트를 넣어야 정상적으로 작동되니 꼭 기억하세요
@@ -23,13 +23,33 @@ ext {
     set('snippetsDir', file("build/generated-snippets"))
 }
 
+bootJar {
+    dependsOn asciidoctor
+    from("${asciidoctor.outputDir}/html5") {
+        into 'static/docs'
+    }
+}
+
+test {
+    outputs.dir snippetsDir
+    useJUnitPlatform()
+}
+
+asciidoctor {
+    inputs.dir snippetsDir
+    dependsOn test
+    attributes 'snippets': snippetsDir
+}
+
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-web'
-    compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.1'
-    compile 'org.apache.commons:commons-lang3:3.11'
+    implementation 'javax.validation:validation-api:2.0.1.Final'
+    implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.1'
+    implementation 'org.hibernate.validator:hibernate-validator:6.1.7.Final'
+    implementation 'org.apache.commons:commons-lang3:3.11'
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'com.h2database:h2'
     runtimeOnly 'mysql:mysql-connector-java'

리뷰: 이것도 좋지만 DTO <-> Entity역할을 하는 기존의 modelmapper도 좋지만 Mapstruct도 훨씬 성능도 좋습니다. 실무에서도 사용하는 중입니다.
https://huisam.tistory.com/entry/mapStruct
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+REPOSITORY=/home/ubuntu/app/songrequest
+PROJECT_NAME=song-request
+
+echo "> Build 파일 복사"
+
+cp $REPOSITORY/zip/*.jar $REPOSITORY/
+
+echo "> 현재 구동 중인 애플리케이션 pid 확인"
+
+CURRENT_PID=$(pgrep -fl song-request | grep jar | awk '{print $1}')
+
+echo "현재 구동 중인 애플리케이션 pid : $CURRENT_PID"
+
+if [ -z "$CURRENT_PID" ]; then
+  echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
+else
+  echo "> kill -15 $CURRENT_PID"
+  kill -15 $CURRENT_PID
+  sleep 5
+fi
+
+echo "> 새 애플리케이션 배포"
+
+JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
+
+echo "> JAR Name : $JAR_NAME"
+
+echo "> $JAR_NAME에 실행 권한 추가"
+
+chmod +x $JAR_NAME
+
+echo "> $JAR_NAME 실행"
+
+nohup java -jar \
+  -Dspring.profiles.active=prod \
+  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

리뷰: home에 그대로 배포하는 것보다 폴더를 생성해서 관리하는 게 좋습니다. 잘 적용하셨네요
@@ -9,14 +9,16 @@
 import javax.annotation.PostConstruct;
 import java.util.TimeZone;
 
+import static com.requestrealpiano.songrequest.global.constant.JpaProperties.ASIA_SEOUL;
+
 @EnableConfigurationProperties(value = {ManiaDbProperties.class, LastFmProperties.class})
 @SpringBootApplication
 public class SongRequestApplication {
 
     @PostConstruct
     public void setTimeZone() {
-        String SEOUL = "Asia/Seoul";
-        TimeZone.setDefault(TimeZone.getTimeZone(SEOUL));
+        TimeZone KST = TimeZone.getTimeZone(ASIA_SEOUL);
+        TimeZone.setDefault(KST);
     }
 
     public static void main(String[] args) {

리뷰: ```suggestion

      Timezone.setDefault(TimeZone.getTimeZone(ASIA_SEOUL));
```
으로 바꾸면 어떻게 되나요?
@@ -1,27 +1,33 @@
 package com.requestrealpiano.songrequest.controller;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.requestrealpiano.songrequest.domain.song.searchapi.response.SearchApiResponse;
 import com.requestrealpiano.songrequest.global.response.ApiResponse;
 import com.requestrealpiano.songrequest.service.SongService;
 import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.validation.constraints.Size;
+
+import static com.requestrealpiano.songrequest.global.constant.ValidationCondition.*;
 import static com.requestrealpiano.songrequest.global.response.ApiResponse.OK;
 
 @RequiredArgsConstructor
+@Validated
 @RestController
-@RequestMapping("/songs")
+@RequestMapping("/api/songs")
 public class SongController {
 
     private final SongService songService;
 
     @GetMapping
-    public ApiResponse<SearchApiResponse> search(@RequestParam String artist,
-                                                 @RequestParam String title) throws JsonProcessingException {
+    public ApiResponse<SearchApiResponse> search(@RequestParam(defaultValue = "artist") @Size(max = ARTIST_MAX)
+                                                 String artist,
+                                                 @RequestParam(defaultValue = "title") @Size(max = TITLE_MAX)
+                                                 String title) {
         SearchApiResponse searchApiResponse = songService.searchSong(artist, title);
         return OK(searchApiResponse);
     }

리뷰: RequestParam도 좋지만 artist, title도 한번에 관리하는 DTO를 만들면 어떤가요? 자주 쓰일지 모르니 DTO를 만들고 거기서 Validation처리해도 괜찮아 보입니다.
@@ -0,0 +1,32 @@
+package com.requestrealpiano.songrequest.domain.letter.request.inner;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+import static com.requestrealpiano.songrequest.global.constant.ValidationCondition.*;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class SongRequest {
+
+    @NotEmpty(message = NOT_EMPTY_MESSAGE)
+    @Size(min = TITLE_MIN, max = TITLE_MAX, message = TITLE_MESSAGE)
+    private String title;
+
+    @NotEmpty(message = NOT_EMPTY_MESSAGE)
+    @Size(min = ARTIST_MIN, max = ARTIST_MAX, message = ARTIST_MESSAGE)
+    private String artist;
+
+    @Size(max = IMAGE_MAX, message = IMAGE_MESSAGE)
+    private String imageUrl;
+
+    SongRequest(String title, String artist, String imageUrl) {
+        this.title = title;
+        this.artist = artist;
+        this.imageUrl = imageUrl;
+    }
+}

리뷰: validation 괜찮네요
@@ -0,0 +1,25 @@
+package com.requestrealpiano.songrequest.global.constant;
+
+public interface ValidationCondition {
+
+    // Common
+    String NOT_EMPTY_MESSAGE = "필수 입력 정보 입니다.";
+
+    // Artist
+    String ARTIST_MESSAGE = "아티스트는 30자 미만 입니다.";
+    int ARTIST_MAX = 30;
+    int ARTIST_MIN = 1;
+
+    // Title
+    String TITLE_MESSAGE = "제목은 30자 미만 입니다.";
+    int TITLE_MAX = 30;
+    int TITLE_MIN = 1;
+
+    // Image URL
+    String IMAGE_MESSAGE = "유효한 이미지 정보가 아닙니다.";
+    int IMAGE_MAX = 100;
+
+    // Song Story
+    String SONG_STORY_MESSAGE = "사연은 500자 미만이어야 합니다.";
+    int SONG_STORY_MAX = 500;
+}

리뷰: interface에 그대로 value값을 지정하면 좋지않습니다. 이펙티브 자바에서 본 거같은데 내용이 정확하게 기억은 안나지만 이렇게 하지말라고 강하게 경고했던 기억이 나네요.

그리고 내용상 interface보단 enum에 어울리는데 enum은 어떤가요?
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+REPOSITORY=/home/ubuntu/app/songrequest
+PROJECT_NAME=song-request
+
+echo "> Build 파일 복사"
+
+cp $REPOSITORY/zip/*.jar $REPOSITORY/
+
+echo "> 현재 구동 중인 애플리케이션 pid 확인"
+
+CURRENT_PID=$(pgrep -fl song-request | grep jar | awk '{print $1}')
+
+echo "현재 구동 중인 애플리케이션 pid : $CURRENT_PID"
+
+if [ -z "$CURRENT_PID" ]; then
+  echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
+else
+  echo "> kill -15 $CURRENT_PID"
+  kill -15 $CURRENT_PID
+  sleep 5
+fi
+
+echo "> 새 애플리케이션 배포"
+
+JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
+
+echo "> JAR Name : $JAR_NAME"
+
+echo "> $JAR_NAME에 실행 권한 추가"
+
+chmod +x $JAR_NAME
+
+echo "> $JAR_NAME 실행"
+
+nohup java -jar \
+  -Dspring.profiles.active=prod \
+  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

리뷰: p4 : 자동 배포 스크립트 군요 👍 
간단한 팁이지만.. 여기서 사용하고 있는 `tail`, `grep`등등 에 대한 기본적인 이해 정도는 있으시면 좋을듯 합니다!
@@ -0,0 +1,25 @@
+package com.requestrealpiano.songrequest.global.constant;
+
+public interface ValidationCondition {
+
+    // Common
+    String NOT_EMPTY_MESSAGE = "필수 입력 정보 입니다.";
+
+    // Artist
+    String ARTIST_MESSAGE = "아티스트는 30자 미만 입니다.";
+    int ARTIST_MAX = 30;
+    int ARTIST_MIN = 1;
+
+    // Title
+    String TITLE_MESSAGE = "제목은 30자 미만 입니다.";
+    int TITLE_MAX = 30;
+    int TITLE_MIN = 1;
+
+    // Image URL
+    String IMAGE_MESSAGE = "유효한 이미지 정보가 아닙니다.";
+    int IMAGE_MAX = 100;
+
+    // Song Story
+    String SONG_STORY_MESSAGE = "사연은 500자 미만이어야 합니다.";
+    int SONG_STORY_MAX = 500;
+}

리뷰: p3: 저도 enum이나 다른 class으로 관리하시는 게 좋다고 생각되네요!
@@ -0,0 +1,63 @@
+language: java
+jdk:
+  - openjdk11
+
+branches:
+  only:
+    - main
+
+cache:
+  directories:
+    - '$HOME/.m2/repository'
+    - '$HOME/.gradle'
+
+script: "./gradlew clean build"
+
+before_deploy:
+  - mkdir -p before-deploy
+  - cp scripts/*.sh before-deploy/
+  - cp appspec.yml before-deploy/
+  - cp build/libs/*.jar before-deploy
+  - cd before-deploy && zip -r before-deploy *
+  - cd ../ && mkdir -p deploy
+  - mv before-deploy/before-deploy.zip deploy/songrequest-backend.zip
+
+deploy:
+  - provider: s3
+    access_key_id: $AWS_ACCESS_KEY
+    secret_access_key: $AWS_SECRET_KEY
+
+    bucket: songrequest-backend-build
+    region: ap-northeast-2
+
+    skip_cleanup: true
+    acl: private
+    local_dir: deploy
+
+    wait-until-deployed: true
+
+    on:
+      all_branches: true
+
+  - provider: codedeploy
+    access_key_id: $AWS_ACCESS_KEY
+    secret_access_key: $AWS_SECRET_KEY
+
+    bucket: songrequest-backend-build
+    key: songrequest-backend.zip
+
+    bundle_type: zip
+    application: songrequest-backend
+
+    deployment_group: songrequest-backend-group
+
+    region: ap-northeast-2
+    wait-until-deployed: true
+
+    on:
+      all_branches: true
+
+notifications:
+  email:
+    recipients:
+      - museopkim0214@gmail.com

리뷰: travis CI를 사용한 기술적인 이유는 없었습니다ㅎㅎ
CI / CD를 다양한 툴로 경험 해보고 싶어서 백엔드는 travis CI, 프론트는 Github Actions로 배포 환경을 구성 했습니다.

travis CI를 써보고 느낀 것은... 디버깅 과정에서 불편함은 없었고, 다만 말씀 하신 것처럼 조금 느린 것이 약간 불편 했습니다.
프론트와 빌드 과정이 다르기 때문에 절대적인 비교는 불가 하지만 저도 Github Actions가 약간 더 빠른 느낌이었네요.
@@ -0,0 +1,18 @@
+version: 0.0
+os: linux
+files:
+  - source: /
+    destination: /home/ubuntu/app/songrequest/zip/
+    overwrite: yes
+
+permissions:
+  - object: /
+    pattern: "**"
+    owner: ubuntu
+    group: ubuntu
+
+hooks:
+  ApplicationStart:
+    - location: deploy.sh
+      timeout: 60
+      runas: ubuntu

리뷰: 만약 개인 프로젝트로 blue / green 배포까지 하게 된다면 매우 큰 성과겠네요ㅎㅎ
써니 블로그에 잘 보고 있습니다. 😄 좋은 팁 감사합니다.
@@ -1,27 +1,33 @@
 package com.requestrealpiano.songrequest.controller;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.requestrealpiano.songrequest.domain.song.searchapi.response.SearchApiResponse;
 import com.requestrealpiano.songrequest.global.response.ApiResponse;
 import com.requestrealpiano.songrequest.service.SongService;
 import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.validation.constraints.Size;
+
+import static com.requestrealpiano.songrequest.global.constant.ValidationCondition.*;
 import static com.requestrealpiano.songrequest.global.response.ApiResponse.OK;
 
 @RequiredArgsConstructor
+@Validated
 @RestController
-@RequestMapping("/songs")
+@RequestMapping("/api/songs")
 public class SongController {
 
     private final SongService songService;
 
     @GetMapping
-    public ApiResponse<SearchApiResponse> search(@RequestParam String artist,
-                                                 @RequestParam String title) throws JsonProcessingException {
+    public ApiResponse<SearchApiResponse> search(@RequestParam(defaultValue = "artist") @Size(max = ARTIST_MAX)
+                                                 String artist,
+                                                 @RequestParam(defaultValue = "title") @Size(max = TITLE_MAX)
+                                                 String title) {
         SearchApiResponse searchApiResponse = songService.searchSong(artist, title);
         return OK(searchApiResponse);
     }

리뷰: 아직은 여기서 밖에 쓰이지 않는데 사용 되는 곳이 더 많아지면 `@ModelAttribute` 객체로 한번 관리 해볼게요!
@@ -0,0 +1,25 @@
+package com.requestrealpiano.songrequest.global.constant;
+
+public interface ValidationCondition {
+
+    // Common
+    String NOT_EMPTY_MESSAGE = "필수 입력 정보 입니다.";
+
+    // Artist
+    String ARTIST_MESSAGE = "아티스트는 30자 미만 입니다.";
+    int ARTIST_MAX = 30;
+    int ARTIST_MIN = 1;
+
+    // Title
+    String TITLE_MESSAGE = "제목은 30자 미만 입니다.";
+    int TITLE_MAX = 30;
+    int TITLE_MIN = 1;
+
+    // Image URL
+    String IMAGE_MESSAGE = "유효한 이미지 정보가 아닙니다.";
+    int IMAGE_MAX = 100;
+
+    // Song Story
+    String SONG_STORY_MESSAGE = "사연은 500자 미만이어야 합니다.";
+    int SONG_STORY_MAX = 500;
+}

리뷰: 인터페이스가 static 상수와 추상 메서드만 접근 가능해서 이렇게 적용 해봤었는데, 다시 보니 Enum으로 관리 하는게 더 적합 할거 같습니다.
(+) 언급하신 이펙티브 자바의 내용은 (3rd edition 기준) `Item 22` 에 있네요!