📝 TIL

[TIL] 3주차 주특기 입문ㅣCRUD API 만들기 3편

오늘 ONEUL 2022. 11. 30. 22:37

 

👇 [스프링 부트로 CRUD API 만들기 1편] 보러 가기 👇

 

[TIL] 3주차 주특기 입문ㅣCRUD API 만들기 1편

Spring 입문 주차 개인 과제 이번 주차는 처음으로 개인 과제를 받았다. 스프링 부트를 이용하여 CRUD API를 만드는 것인데 자세한 요구사항은 다음과 같다. 🚩 Goal: "스프링 부트로 로그인 기능이

oneul-losnue.tistory.com

 

👇 [스프링 부트로 CRUD API 만들기 2편] 보러 가기 👇

 

[TIL] 3주차 주특기 입문ㅣCRUD API 만들기 2편

👇 [스프링 부트로 CRUD API 만들기 1편] 보러 가기 👇 new IllegalArgumentException("아이디가 존재하지 않습니다.") ); board.update(requestDto); // 엥 이게 끝? return new BoardResponseDto(board); } 어제 기술 매니저님

oneul-losnue.tistory.com

 

 

오늘의 궁금증

1. 성공 여부를 어떻게 반환하면 좋을까?

"결과 메시지나 상태 코드를 담을 클래스를 만들고, 상속을 이용해 보세요!"

 

어제 기술 매니저님이 조언해주신 대로 클래스 구조를 변경해서 성공 여부를 반환해 보려 한다.
먼저 내가 원하는 Response는 이런 형태이다.

Response

1) success(성공 여부): true or false
2) statusCode(Http 상태 코드) : 200 or 404
3) boardOne or boardList: 제목, 이름, 내용, 생성 날짜, 수정 날짜

 

요청을 보내는 BoardRequestDto를 제외하고
나머지 4개의 응답용 클래스 파일을 만들어보자.

 

성공 여부와 상태 코드를 담은 BaseResponse

package com.sparta.crud.dto;

import lombok.Getter;

@Getter
public class BaseResponse {
    public Boolean success; // 성공 여부
    public int statusCode; // Http 상태 코드

    public BaseResponse(Boolean success, int statusCode) {
        this.success = success;
        this.statusCode = statusCode;
    }
}

이 클래스에는 딱 2가지의 필드만 존재한다. Boolean 타입의 성공 여부와 int 타입의 Http 상태 코드.
BaseResponse 클래스가 부모 클래스가 되어 다른 클래스를 상속해줄 것이다.

 

Entity를 DTO로 변환하는 BoardToDto

package com.sparta.crud.dto;

import com.sparta.crud.entity.Board;
import lombok.Getter;
import java.time.LocalDateTime;

@Getter
public class BoardToDto {
    private Long id;
    private String title;
    private String name;
    private String content;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    public BoardToDto(Board board) { // 생성자를 통해 Entity to Dto
        this.id = board.getId();
        this.title = board.getTitle();
        this.name = board.getName();
        this.content = board.getContent();
        this.createdAt = board.getCreatedAt();
        this.modifiedAt = board.getModifiedAt();
    }

}

기존에 BoardResponseDto로 쓰고 있던 이 클래스는 이제부터 딱 1가지의 기능만 한다.
바로 Entity인 Board를 받아서 DTO로 변신시켜 주는 것!

 

상세 조회 시 사용하는 BoardOneResponseDto

package com.sparta.crud.dto;

import com.sparta.crud.entity.Board;
import lombok.Getter;

@Getter
public class BoardOneResponseDto extends BaseResponse { // BaseResponse 상속

    BoardToDto boardOne; // DTO로 받아줄 필드 선언

    public BoardOneResponseDto(Boolean success, int statusCode, Board board) { // Entity를 받아서
        super(success, statusCode);
        this.boardOne = new BoardToDto(board); // 생성자를 통해 Entity to Dto
    }
}

BaseResponse를 상속받아 성공 여부와 상태 코드 필드를 이 클래스에서도 사용할 수 있다.
super() 메소드를 이용해 부모 클래스의 생성자를 호출한다.
BoardToDto 타입의 boardOne 필드를 선언하고, 생성자의 매개변수로 Entity인 board를 받아 BoardToDto 타입으로 변환해준다.

 

전체 조회 시 사용하는 BoardListResponseDto

package com.sparta.crud.dto;

import lombok.Getter;
import java.util.ArrayList;
import java.util.List;

@Getter
public class BoardListResponseDto extends BaseResponse {

    List<BoardToDto> boradList = new ArrayList<>();

    public BoardListResponseDto(Boolean success, int statusCode) {
        super(success, statusCode);
    }

    public void addBoard(BoardToDto boardToDto) {
        boradList.add(boardToDto);
    }
}

위의 상세 조회용 DTO 클래스와 다른 점은 그저 타입이 리스트라는 것뿐이다.
마찬가지로 BaseResponse를 상속받았고, super() 메소드를 이용해주었다.
객체가 생성되면서 리스트가 바로 초기화될 수 있도록 boardList를 ArrayList로 초기화해주고,
addBoard 메소드를 만들어 리스트에 값이 추가될 수 있도록 해준다.

이제 Service와 Controller에서 Entity가 아닌 DTO로 반환 타입을 지정하고,
Service 내부에서 Repository부터 받아온 Entity 객체를 DTO로 변환해주는 작업을 하면 된다.

여기서 포인트는, 상속을 통한 다형성을 이용하는 것!

 

다형성(polymorphism)이란?

  • 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미한다.
  • 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있다.
  • 이때 참조 변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 같거나 적어야 참조할 수 있다.
  • 상속 관계에 있는 클래스 사이의 참조 변수는 서로 타입 변환을 할 수 있다.
    자식 클래스 타입 > 부모 클래스 타입(변환 생략 가능) / 부모 클래스 타입 > 자식 클래스 타입(변환 반드시 명시)
class Parent { ... }
class Child extends Parent { ... }
 
...
 
Parent pa = new Parent(); // 허용
Child ch = new Child();   // 허용
Parent pc = new Child();  // 허용
Child cp = new Parent();  // 오류 발생

 

게시글을 업데이트하는 메소드는 2가지의 리턴이 있을 수 있다.

1) 비밀번호가 일치했을 시, 성공 여부와 함께 변경된 객체 리턴
2) 비밀번호가 불일치했을 시, 성공 여부(=실패)만 리턴

그렇다면 Service 클래스에서 update 메소드의 리턴 타입은 무엇이 되어야 할까?

// 리턴타입 주목!
public BaseResponse update(Long id, BoardRequestDto requestDto) {
    Board board = boardRepository.findById(id).orElseThrow(
            () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
    );

    // 비밀번호 일치
    if (requestDto.getPw().equals(board.getPw())) {
        board.update(requestDto);
        Board savedBoard = boardRepository.save(board);
        return new BoardOneResponseDto(true, HttpStatus.OK.value(), savedBoard);
        // 성공 여부와 수정된 객체 리턴
    }
    // 비밀번호 불일치
    else {
        return new BaseResponse(false, HttpStatus.NOT_FOUND.value());
        // 성공 여부(=실패)만 리턴
    }

}

바로 부모 클래스인 BaseResponse이다!
BoardOneResponseDto는 BaseResponse를 상속받고 있기 때문에 부모 클래스 타입으로 참조할 수 있는 것이다.
이렇게 하면 경우에 따라 원하는 형태로 응답을 보낼 수 있다.

와 이게 정말 가능하다니..!
기술 매니저님께서 왜 의미심장한 미소를 지으며 상속을 이용해 보라 했는지 이해가 되는 순간이었다.
머릿속에 개념만 존재하던 다형성을 직접 필요한 때에 사용해보니 재사용성이라는 장점을 직접 느낄 수 있었다.

 

 

2. 개발 순서는 어디서부터?

나는 보통 Entity → Repository → Service → Controller → DTO 순서로 개발을 했는데,
Controller부터 개발하시는 분을 보고 문득 다른 분들은 어떤 순서로 개발을 하시는지 궁금해졌다.
그에 대한 기술 매니저님의 답변!

"비즈니스 로직이 어떻게 구동되는지를 DB 입장에서만 생각하면 안 돼요.
저는 클라리언트와의 데이터 전달을 생각하면서 Controller부터 개발합니다.
그렇지만 DB에서부터 개발하는 게 맞습니다.(?)"

뭔가 내용과 결론이 앞뒤가 안 맞지만(ㅋㅋㅋㅋ)
정답은 없고, 관점의 차이가 존재할 수 있겠다는 생각이 들었다.
그래도 나는 역시 DB부터...

 

 

3. RESTful 한 API가 뭐지?

API를 설계할 때마다 지겹도록 들었던 REST API.
도대체 그게 무슨 의미일까?

REST API를 알기 전에, REST부터 무슨 의미인지 알아보자.

 

REST(Representational State Transfer)란?

  • 자원을 ‘이름’으로 구분하여 해당 자원의 ‘상태(정보)’를 주고받는 모든 것!
  • 즉 자원(Resource)의 표현(Representation)에 의한 상태 전달을 말한다.
  • 구체적으로 HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미한다.
  • 한마디로 URI와 HTTP 메소드를 이용해 객체화된 서비스에 접근하는 것!

 

REST가 필요한 이유는?

  • 애플리케이션 분리 및 통합
  • 다양한 클라이언트의 등장
  • 멀티 플랫폼에 대한 지원

 

REST 기반으로 서비스 API를 구현하는 것을 바로 REST API라고 한다.
쉽게 말해, URI는 자원의 정보를 표현하고 그 행위는 HTTP Method(POST, GET, PUT, DELETE)로 표현하는 것이다.
자세한 가이드는 다음과 같다.

 

REST API 디자인 가이드

  • URI는 정보의 자원을 표현
    • resource는 동사보다는 명사, 대문자보다는 소문자
    • resource의 도큐먼트 이름으로는 단수 명사를 사용
    • resource의 컬렉션 이름으로는 복수 명사를 사용
    • reousrce의 스토어 이름으로는 복수 명사를 사용
      • ex) GET /Member/1 (x) → GET /members/1 (o)
  • 자원에 대한 행위는 HTTP Method로 표현(GET, POST, PUT, DELETE)
    • URI에 HTTP Method 사용 X
      • ex) GET /members/get/1 (x) → GET /members/1 (o)
    • URI에 행위에 대한 동사 표현 X
      • ex) GET /members/show/1 (x) → GET /members/1 (o)
  • 경로 부분 중 변하는 부분은 유일한 값으로 대체
    • id는 하나의 특정 resource를 나타내는 고유 값

 

이 외에도 더 많은 가이드가 존재한다.

그럼 나는 RESTful 하게 API를 설계했는가?

URI에 동사 표현이 있다거나 하지 않고, HTTP Method를 이용해서 행위를 표현한 것은 RESTful 하다고 생각한다.
조금 아쉬운 점은, 단수 복수를 생각하지 못했던 점과 HTTP 상태 코드를 더 적절하게 사용하지 못한 점이다.
RESTful이 비공식적 구현 가이드이긴 하지만 다음 API 설계는 이러한 점들을 더 고려해봐야겠다.

 

📚 참고자료

 

REST API 제대로 알고 사용하기 : NHN Cloud Meetup

REST API 제대로 알고 사용하기

meetup.toast.com

 

 

 

오늘의 나는

작은 힌트로 깨달음을 얻고 구현에 성공했을 때의 쾌감은 정말...
머릿속 퍼즐이 하나로 딱 맞춰지는 기분이다.

이제 제출할 수 있을 정도로 과제를 완성하였다.
이 외의 개인적으로 추가하고 싶은 기능들은 제출 후에 시간이 허락한다면(...) 구현해보려 한다.

내일은 주특기 입문 주차 시험이 있는 날이다.
어떻게 나올지 모르겠어서 걱정이긴 하지만..
시험 이후에 여유가 좀 있을 테니 여태까지 한 과제를 문서화하고, 제출까지 후다닥 해버려야겠다.