<졸업작품> 01. Calendar Restful API(1)

박서연·2023년 7월 19일
0

Graduation

목록 보기
2/6

1. 개요

1. 구현사항

> 회의록 생성 / 수정 / 삭제 / 조회

2. ERD

3. 설명

프론트엔드에서 캘린더를 눌렀을 때 날짜(즉 date 데이터)가 전달되며, 해당 날짜에 해당하는 회의록이 존재할 경우 그 회의록을 수정하고 존재하지 않을 경우 회의록을 생성할 것

📢 위의 경우는 Monolithic의 경우이고, 현재 구현은 API 형식으로 할 것이므로 기능별로 구현할 예정

2. 개념

1. API(Application Programming Interface)

> 프로그램을 실행하는 인터페이스로, 프로그램에 요청을 전달하는 방식을 의미

2. REST API

> URI와 HTTP method 사용해 리소스 식별해 요청 처리하는 아키텍쳐 스타일

REST API 디자인 가이드

  1. REST API 중심 규칙
    1) URI는 정보의 자원을 표현해야함 => 리소스명은 동사보다 명사 사용
ex. GET  /members/delete/1

2) 자원에 대한 행위는 HTTP method(GET, POST, PUT, DELETE)로 표현

HTTP method역할
POST해당 URI를 요청하면 리소스 생성
GET리소스 조회
PUT리소스 수정
DELETE리소스 삭제

📢 아래와 같이 행위는 HTTP method로 나타내므로 굳이 URI 넣을 필요 없고(1), 사용하는 목적에 맞는 method를 선택해야함(2)

#1. 회원 정보 가져오는 URI
Get /members/show/1		(X)
Get /members/1			(O)

#2. 회원 추가 
GET /members/insert/2   (X)
POST /members/2			(O)
  1. URI 설계 시 주의해야할 점

3. RESTful API

REST의 원칙을 잘 지키면서 API의 의미를 표현하기 쉽고, 파악하기 쉬운 방식으로 데이터를 교환이 이루어지도록 설계된 API

조건

  1. 클라이언트-서버의 communication: 요청에 클라이언트 정보가 저장되지 않으며, 각 요청은 분리되어있고 연결되어 있지 않음
  2. Stateless 상태

장점

  1. URL만 보고 어떤 자원에 접근할 것인지, 메소드를 보고 어떤 행위를 할 지 알 수 있기 때문에 개발 시 용이
  2. 1개의 URI로 4개의 행위(CRUD)를 명시할 수 있기에 효율적
  3. stateless한 상태 유지 가능 ✨

4. @RequestParam vs @RequestBody vs @PathVariable

@RequestParam

장점
쿼리스트링 또는 폼 데이터를 간편하게 받을 수 있음
매개변수의 기본값을 설정할 수 있어 오류 방지에 도움이 됨
단점
많은 매개변수를 사용하는 경우, 코드가 복잡해질 수 있음
쿼리스트링으로 데이터를 전달하면 보안상의 이슈가 발생할 수 있음

@RequestBody

장점
복잡한 데이터 구조를 가진 객체를 쉽게 받아서 처리할 수 있음
JSON, XML과 같은 여러 데이터 형식을 처리할 수 있음
유효성 검사와 데이터 바인딩이 자동으로 처리되므로 편리
단점
클라이언트에서 보낸 데이터가 바로 자바 객체로 변환되기 때문에 보안상 주의가 필요

@PathVariable

장점
RESTful API에서 경로에 담긴 데이터를 쉽게 추출할 수 있음
경로에 데이터가 포함되어 있어 URL이 직관적이고 읽기 쉬움

단점
반드시 하나의 요청만 사용할 수 있음
URL 경로에 데이터를 노출하기 때문에, 민감한 정보를 처리할 때 보안상 주의가 필요합니다.
경로가 복잡해지는 경우, 코드의 유지보수가 어려울 수 있습니다.

참고
https://meetup.nhncloud.com/posts/92

3. 코드

1. 코드 설명

1. Annotation

@RestController: @Controller + @ResponseBody. Json 형태로 객체 데이터 반환

@ReponseBody
HTTP 응답 Body에 데이터를 직접 넣어주는 것을 의미

@Data: @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode를 한 번에 설정해주는 annotation

@RequiredArgsConstructor: final이나 @NonNull인 필드 값만 파라미터로 받는 생성자 만들어줌

@GetMapping(): API 메서드 지정으로 GET 방식의 API 지정
@Entity: 이 클래스가 Entity가 될 클래스임을 명시. 테이블과 해당 클래스가 링크되며 @Column으로 지정된 변수가 테이블의 column이 됨
@Id: 테이블의 PK(Primary Key)가 됨을 선언
@GeneratedValue: PK를 스프링부트가 자동으로 생성
@Transactional: DB 접근 도중 오류 발생 시 해당 기능을 모두 완수하지 않은 상태로 멈추는 것이 아니라, DB 접근 이전 상태로 돌려 데이터에 오류가 섞이지 않
@NoArgsConstructor: 파라미터가 없는 기본 생성자 생성
@AllArgsConstructor: 모든 필드 값을 파라미터로 받는 생성자 생성

2. 코드

extends CrudRepository: 간단한 CRUD 사용 가능(Repository)
Optional <T>: null이 올 수 있는 값을 감싸는 wrapper 클래스로 NPE(NullPointerException)가 발생하지 않도록 하며 각종 메소드 지원

2. 구조

  1. Controller
    클라이언트로부터 요청을 받고 Service에 처리 요청, 이후 클라이언트에게 응답
  2. Service
    Controller의 호출에 따라 사용자의 요구사항을 처리. DB 정보 등 필요에 따라 Repository에 요청 => service 인터페이스와 serviceImpl 클래스 필요
  3. Repository
    데이터베이스 관련 처리 담당
  4. Dto
    Entity에 접근하는 객체
  5. Domain
    Entity를 의미하며 DB에 저장된 값

3. 코드

java/com.example.graduation/controller/CalendarController

package com.example.graduation.controller;

import com.example.graduation.dto.MinutesForm;
import com.example.graduation.entity.Minutes;
import com.example.graduation.repository.MinutesRepository;
import com.example.graduation.service.MinutesService;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@Slf4j //log
@RequiredArgsConstructor
public class CalendarController {

    private final MinutesService minutesService;

    //1. 회의록 생성
    @PostMapping("/calendars/create")
    public Optional<Minutes> createMinutes(@RequestBody MinutesForm form) {
        Optional<Minutes> target = minutesService.create(form);
        return target;
    }

    //2-1. 회의록 조회(전체)
    @GetMapping("/calendars/watchAll")
    public List<Minutes> getAllMinutes() {
        List<Minutes> minutesList = minutesService.watchAll();
        return minutesList;
    }

    //2-2. 회의록 조회(세부)
    @GetMapping("/calendars/watch")
    public Optional<Minutes> getMinutes(@RequestParam String date) {
        Optional<Minutes> minutes = minutesService.watch(date);
        if (minutes.isEmpty()) {
            System.out.println("NOT EXIST");
        }
        return minutes;
    }

    //3. 회의록 수정
    @PatchMapping("/calendars/edit")
    public Minutes editMinutes(@RequestParam String date, @RequestBody MinutesForm form) {
        Minutes minutes = minutesService.edit(date, form);
        return minutes;
    }
    
    //4. 회의록 삭제
    @DeleteMapping("/calendars/delete")
    public String deleteMinutes(@RequestParam String date) {
        minutesService.delete(date);
        return "Completely Deleted";
    }

}

.../com.example.graduation/dto/MinutesForm

package com.example.graduation.dto;

import com.example.graduation.entity.Minutes;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Min;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;
import java.util.SimpleTimeZone;


@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MinutesForm {
    @JsonProperty("id")
    private  Long id;

    @JsonProperty("userId")
    private Long userId;

    @JsonProperty("teamId")
    private Long teamId;
    @JsonProperty("date")
    private String date;
    @JsonProperty("title")
    private String title;
    @JsonProperty("content")
    private String content;


    // dto -> entity 연결
    public Minutes toEntity() {
        return new Minutes(id, userId, teamId, date, title, content);
    }
}

.../com.example.graduation/entity/Minutes

package com.example.graduation.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

//요청용
@Entity
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Getter
public class Minutes {
    @Id
    @GeneratedValue
    private Long id;

    @Column
    private Long userId;

    @Column
    private Long teamId;

    @Column
    private String date;
    @Column
    private String title;
    @Column
    private String content;

    public void update(String title, String content, Long userId) {
        this.title = title;
        this.content = content;
        this.userId = userId;
    }

}

.../com.example.graduation/repository/MinutesRepository

package com.example.graduation.repository;

import com.example.graduation.entity.Minutes;
import org.springframework.data.repository.CrudRepository;

import java.util.ArrayList;
import java.util.Optional;

//extends 뒤에는 CrudRepository를 작성하여 여러 기능을 사용할 수 있도록 하고, <> 안의 파라미터는 사용할 entity와 그 대푯값(이 프로젝트에서는 id)의 타입 작성
public interface MinutesRepository extends CrudRepository<Minutes, Long> {
    @Override
    ArrayList<Minutes> findAll();

    Optional<Minutes> findByDate(String date);
}

.../com.example.graduation/service/MinutesService

package com.example.graduation.service;

import com.example.graduation.dto.MinutesForm;
import com.example.graduation.entity.Minutes;
import com.example.graduation.repository.MinutesRepository;
import jakarta.transaction.Transactional;
import jakarta.websocket.server.ServerEndpoint;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
@Slf4j
@RequiredArgsConstructor
public class MinutesService {

    private  final MinutesRepository minutesRepository;

    @Transactional
    public Optional<Minutes> create(MinutesForm dto) {
        Minutes minutes= minutesRepository.save(dto.toEntity());
        Optional<Minutes> target = minutesRepository.findById(minutes.getId());
        return target;
    }

    @Transactional
    public List<Minutes> watchAll() {
        return minutesRepository.findAll();
    }

    @Transactional
    public Optional<Minutes> watch(String date) {
        Optional<Minutes> minutes = minutesRepository.findByDate(date);
        return minutes;
    }

    @Transactional
    public Minutes edit(String date, MinutesForm dto) {
        //현재 입력한 date가 repository에 존재할 경우 edit. orElseThrow()
        Minutes minutes = minutesRepository.findByDate(date).orElse(null);
        minutes.update(dto.getTitle(), dto.getContent(), dto.getUserId());
        return minutes;
    }

    @Transactional
    public void delete(String date) {
        Minutes minutes = minutesRepository.findByDate(date).orElse(null);
        minutesRepository.delete(minutes);
    }
}

4. 추가 코드

lombok
logging => Slf4j
thymeleaf

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.1'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencies {
	//thymeleaf
	//implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

	//UserDto 생성 시 필요
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-mustache'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

application.properties

spring.h2.console.enabled=true
spring.jpa.defer-datasource-initialization = true

5. Postman

1. POST: http://localhost:8080/calendars/create

Body에 아래와 같이 데이터 2개 입력

2. GET: http://localhost:8080/calendars/watchAll

3. GET: http://localhost:8080/calendars/watch?date=2023-07-11

4. PATCH: http://localhost:8080/calendars/edit?date=2023-07-11

5. DELETE: http://localhost:8080/calendars/delete?date=2023-07-12

아래 사진과 같이 수정, 삭제가 정상적으로 이루어짐

📢 위의 동작이 정상적으로 이루어지지만 피드백에 따른 수정 필요
(1) watchAll에서 특정 년도와 월을 입력받을 시 해당 년도와 월에 해당하는 회의록만 조회
(2) id와 teamId의 경우 데이터를 어떻게 전송할지 생각하여 수정

0개의 댓글