티스토리 뷰

experiences/과제리뷰

설계

lingi04 2021. 12. 22. 23:25

개발 들어가기 앞서 요구사항을 완벽히 분석하고 설계한 후 개발에 들어가진 못했다. 변명을 하자면... 그런 습관이 잘 들어있지 않았고 시간이 부족했다. 그래서 개발을 하다가 더 좋은 방법이 생각나 중간 중간 변경하기도 했다. 채점자 리뷰에서도 이 부분과 관련해 지적을 받은게 있어서 앞으로 설계에 대해 신경을 좀 더 써야겠다고 생각했다.

DB 설계

테이블

테이블 이름 역할
song 노래 정보 저장
album 앨범정보 저장
playlist playlist 정보 저장
playlist_song playlist와 playlist에 들어있는 곡 정보를 담고 있는 테이블

테이블간 관계

한개의 앨범에 여러 노래가 담길 수 있으니 album과 song의 관계는 1:n 관계로 간단하게 정했다. playlist와 song의 관계를 정할 때 조금 멈칫했다. 한개의 playlist에는 여러 song이 담길 수 있고, 한개의 노래가 여러 플레이리스트에 속할 수도 있다. m:n 관계인데, 표현 하기가 어려울 것 같아 playlist_song 테이블을 만들고 song과 playlist_song 1:n 관계로, playlist_song과 playlist는 n:1관계로 정리하였다.

erd

테이블 관계
album - song 1:n
song - playlist_song 1:n
playlist_song - playlist n:1

공통 기능 설계

공통 변수

전역으로 사용할 수 있는 변수를 뽑아서 모아놓았다.

public class Constants {

    public enum OrderBy{
        ASC("ASC", "Asc Order"),
        DESC("DESC", "Desc Order");

        private final String value;
        private final String reasonPhrase;

        OrderBy(String value, String reasonPhrase) {
            this.value = value;
            this.reasonPhrase = reasonPhrase;
        }

        public String value() {
            return this.value;
        }
    }

    public static int ROW_PER_PAGE = 10;
}

예외처리

예외처리를 한곳에서 할 수 있도록 @ControllerAdvice 어노테이션을 붙여 ExceptionAdvice 클래스를 만들었다. 발생한 Exception의 처리를 한다. 예외처리를 한 곳에 몰아놓는 것과 발생한 자리에서 catch 한 후 처리하는 것 사이에 어떤게 나은지는 잘 모르겠다. 각 장단점이 있는 것 같은데, 실무에선 팀원들과 어떻게 할지 상의한 후 결정한다면 어느 방향을 사용하던 상관없을 것 같다.

@ControllerAdvice
@RestController
public class ExceptionAdvice {

    @ExceptionHandler(BindException.class)
    public ResponseEntity<?> requestParamValidationError(BindException exception) {
        return bindResult(exception.getBindingResult());
    }
    ...
}

Response body 정리

보통 API 설계할 때 공통 폼이 있다. 이번 과제를 하며 내 경우는 간단하게

{
    "statusCode": ...,
    "errorMsg":...(에러 발생한 경우),
    "data": {
        ...
    }
}

이런 식으로 정의했다.

공통으로 사용할 형식을 정의했기에 재사용하며 사용할 수 있는 클래스를 만들기로 했고, ResponseBody클래스를 만들었다. 그리고 여기에 Builder패턴을 적용했다. 한번 사용해보고 싶기도 했고,

  • 생성자 방식과 비교해선 필요한 파라미터만 골라서 넘길 수 있다는 장점이
    • 지금 경우엔 해당되지 않지만, 같은 타입의 파라미터가 여러 개 존재할 때 파라미터의 순서를 헷갈릴 수도 있고, 필요한 파라미터에 따라 생성자를 여러 개 만들어야 한다는 수고로움이 있을 수 있다.
  • setter방법 보단 좀 더 간단하게 사용할 수 있을것
    • 생성자로 생성 후 세터를 또 호출
    • (솔직히 이건 좀 억지같긴 하다...)

같았기 때문이다.

페이징 처리

페이징은 보통 쿼리 에서 limitorder by를 이용해서 한다. 얼마전에 잠깐 jpa를 공부해 봤는데, jpa에서는 Paging이라는 객체를 넘기면 알아서 해주는 것을 보고 비슷하게 적용해봐야겠다 생각했다. 그래서 Order, Paging 클래스를 만들었고, 쿼리 실행 시 넘기는 파라미터 dto클래스에 조합해서 사용하는 방식으로 개발 했다. 좀 더 나은 방식으로 개발할 수 있었을것 같은데 조금 아쉽다.

API 설계

여기에 사용된 도메인은 이렇게 분류할 수 있을 것 같다.

  • 앨범 관련
    • album, song
  • 플레이리스트 관련
    • playlist, playlistSong

따라서 패키지도 album 패키지와 playlist 패키지로 나누었다.

공통

  • 파라미터 검증에서 조건에 맞지 않으면 예외를 발생시킨다.
    • @Valid어노테이션, 기타 다른 어노테이션
  • 리턴 타입으로 ResponseEntity<?>, 위에 언급한 ResponseBody를 사용했다.
  • Http 메소드와 HttpStatusCode를 상황에 맞게 알맞은 값을 사용하도록 노력했다.
    기능 Http Method Http Status Code
    생성 Post HttpStatus.CREATED
    조회 Get HttpStatus.OK
    수정 Put HttpStatus.CREATED
  • 공통 헤더

    헤더명 필수 여부
    Content-Type application/json 필수
    UserId {userId} 필수
  • 공통 Response

    필드 필드명 설명
    statusCode 상태코드 요청에 대한 결과를 HttpStatus Code로 내려준다.
    errMsg 에러메시지 에러가 생길 경우 에러 메시지가 세팅된다.
    data 데이터 요청에 대한 결과 데이터.

1. album

  • 앨범 관련 요청을 처리한다.
  • /album, /search 등...

2. playlist

  • 플레이리스트 관련 요청을 처리한다.

  • /api/playlist/*

  • Playlist 생성 API

    POST /api/playlist

    • HTTP REQUEST

        POST /api/playlist HTTP/1.1
        Host: localhost:5000
        Content-Type: application/json
        UserId: 1
      
        {
            "playlistName":"new playlist"
        }
    • 파라미터

      Parameter Description 필수 여부
      playlistName 생성할 playlist의 이름 필수
    • HTTP RESPONSE

        HTTP/1.1 201 CREATED
        Content-Length: 224
        Content-Type: application/json;charset=UTF-8
      
        {
            "statusCode": 201,
            "data": {
                "playlist": {
                    "userId": 1,
                    "playlistName": "new playlist",
                    "id": 16
                }
            }
        }
    • Response 필드

      필드 설명
      data.playlist.id 생성된 플레이리스트의 아이디
      data.playlist.playlistName 생성된 플레이리스트의 이름
      data.playlist.userId 유저 아이디
  • Playlist 노래, 앨범 추가 API

    • 노래 개별 추가

      POST /api/playlist/{playlistId}/song

    • Path Variable

      Path Variable Description 필수 여부
      playlistId 플레이리스트 id 필수
    • HTTP REQUEST

        POST /api/playlist/14/song HTTP/1.1
        Host: localhost:5000
        Content-Type: application/json
        UserId: 1
      
        {
            "songId":101,
            "albumId":13
        }
    • 파라미터

      Parameter Description 필수 여부
      songId 내 플레이리스트에 추가할 노래의 id 필수
      albumId 내 플레이리스트에 추가할 노래의 albumId 필수
    • HTTP RESPONSE

        HTTP/1.1 201 CREATED
        Content-Length: 230
        Content-Type: application/json;charset=UTF-8
      
        {
            "statusCode": 201,
            "data": {
                "playlistSong": {
                    "id": 154,
                    "playlistId": 14,
                    "songId": 101,
                    "albumId": 13
                }
            }
        }
    • Response 필드

      필드 설명
      data.playlistSong[].id 생성된 playlistSong의 아이디
      data.playlistSong[].playlistId 노래가 추가된 playlist의 아이디
      data.playlistSong[].songId 노래 아이디
      data.playlistSong[].albumId 앨범 아이디
    • 노래 앨범단위로 추가

      POST /api/playlist/{playlistId}/album

    • Path Variable

      Path Variable Description 필수 여부
      playlistId 플레이리스트 id 필수
    • HTTP REQUEST

        POST /api/playlist/14/album HTTP/1.1
        Host: localhost:5000
        Content-Type: application/json
        UserId: 1
      
        {"albumId":14}
    • 파라미터

      Parameter Description 필수 여부
      albumId 내 플레이리스트에 추가할 노래의 albumId 필수
    • HTTP RESPONSE

        HTTP/1.1 201 CREATED
        Content-Length: 742
        Content-Type: application/json;charset=UTF-8
      
        {
            "statusCode": 201,
            "data": {
                "playlistSong": [
                    {
                        "id": 155,
                        "playlistId": 14,
                        "songId": 111,
                        "albumId": 14
                    },
                    {
                        "id": 0,
                        "playlistId": 14,
                        "songId": 112,
                        "albumId": 14
                    },
                    ...
                    {
                        "id": 165,
                        "playlistId": 14,
                        "songId": 121,
                        "albumId": 14
                    }
                ]
            }
        }
    • Response 필드

      필드 설명
      data.playlistSong[].id 생성된 playlistSong의 아이디
      data.playlistSong[].playlistId 노래가 추가된 playlist의 아이디
      data.playlistSong[].songId 노래 아이디
      data.playlistSong[].albumId 앨범 아이디
    • Playlist 목록 API

      GET /api/playlist

    • HTTP REQUEST

        GET /api/playlist HTTP/1.1
        Host: localhost:5000
        UserId: 1
    • HTTP RESPONSE

        HTTP/1.1 200 OK
        Content-Length: 265
        Content-Type: application/json;charset=UTF-8
        {
            "statusCode": 200,
            "data": {
                "playlist": [
                    {
                        "userId": 1,
                        "playlistName": "abcde",
                        "id": 14
                    },
                    {
                        "userId": 1,
                        "playlistName": "new playlist",
                        "id": 16
                    }
                ]
            }
        }
    • Response 필드

      필드 설명
      data.playlist[].id playlist의 아이디
      data.playlist[].playlistName playlist의 이름
      data.playlist[].userId 사용자 id
  • Playlist 삭제 API

    DELETE /api/playlist/{playlistId}

    • Path Variable

      Path Variable Description 필수 여부
      playlistId 플레이리스트 id 필수
    • HTTP REQUEST

        DELETE /api/playlist/15 HTTP/1.1
        Host: localhost:5000
        UserId: 1
        Content-Type: application/json
      
    • HTTP RESPONSE

        HTTP/1.1 200 OK
        Content-Length: 148

컨벤션

  • 카멜케이스
  • ResponseBody 정의
  • 쿼리 앞부분에 /* mapperName.queryId */ 형식으로 주석 추가

'experiences > 과제리뷰' 카테고리의 다른 글

Intro  (0) 2021.12.22
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday