API 명세서

서비스 : 회원가입 및 로그인 ( 화면 생략, Postman으로 확인)

기능 Method URL Request Response
가입 POST /api/user/signup
{
    "username" : "test",
    "password" : "testtest!"
}
{
    "status": 200,
    "message": "OK"
}
로그인 POST /api/user/login
{
    "username" : "test",
    "password" : "testtest!"
}
Header : 
Authorization : Bearer <JWT>

Body :
{
    "status": 200,
    "message": "OK"
}

 

서비스 : 메모  

기능 Method URL Request Response
모든
게시글

조회
GET /api/memos -
{
    "status": 200,
    "message": "OK",
    "data": [
        {
            "id": 7,
            "title": "제목",
            "username": "test",
            "content": "내용",
            "createdAt": "2022-12-08T13:10:11.10534",
            "modifiedAt": "2022-12-08T13:10:11.10534",
            "replies": []
        },
        {
            "id": 6,
            "title": "제목",
            "username": "test",
            "content": "내용",
            "createdAt": "2022-12-08T13:10:09.808928",
            "modifiedAt": "2022-12-08T13:10:09.808928",
            "replies": []
        },
        {
            "id": 3,
            "title": "수정한 제목",
            "username": "test",
            "content": "수정한 내용입니다",
            "createdAt": "2022-12-08T13:08:30.122685",
            "modifiedAt": "2022-12-08T13:12:28.841916",
            "replies": [
               {
                    "replyId": 10,
                    "memoId": 8,
                    "replyName": "test",
                    "replyContent": "작성한 댓글"
                },
                {
                    "replyId": 9,
                    "memoId": 3,
                    "replyName": "test",
                    "replyContent": "작성한 댓글"
                }
            ]
        }
    ]
}
선택한
게시글

조회
GET /api/memos/{id}
@pathVariable : id
{
    "status": 200,
    "message": "OK",
    "data": {
        "id": 6,
        "title": "제목",
        "username": "test",
        "content": "내용",
        "createdAt": "2022-12-08T13:10:09.808928",
        "modifiedAt": "2022-12-08T13:10:09.808928",
        "replies": [
            {
                "replyId": 8,
                "memoId": 6,
                "replyName": "test",
                "replyContent": "작성한 댓글"
            }
        ]
    }
}
 게시글
작성
POST /api/memos Header : 
Authorization : Bearer <JWT>

Body :
{
    "title" : "제목",
    "content" : "내용"
}
{
    "status": 200,
    "message": "OK",
    "data": {
        "id": 3,
        "title": "제목",
        "username": "test",
        "content": "내용",
        "createdAt": "2022-12-08T13:08:30.1226851",
        "modifiedAt": "2022-12-08T13:08:30.1226851",
        "replies": []
    }
}
선택한
게시글

수정
PUT /api/memos/{id} @pathVariable : id,

Header : 
Authorization : Bearer <JWT>

Body :
{
    "title" : "수정한 제목",
    "content" : "수정한 내용입니다"
}
{
    "status": 200,
    "message": "OK",
    "data": {
        "id": 3,
        "title": "수정한 제목",
        "username": "test",
        "content": "수정한 내용입니다",
        "createdAt": "2022-12-08T13:08:30.122685",
        "modifiedAt": "2022-12-08T13:08:30.122685",
        "replies": [
            {
                "replyId": 9,
                "memoId": 3,
                "replyName": "test",
                "replyContent": "작성한 댓글"
            }
        ]
    }
}
선택한
게시글

삭제
DELETE /api/memos/{id}
@pathVariable : id

Header : 
Authorization : Bearer <JWT>
{
    "status": 200,
    "message": "OK"
}
댓글
작성
POST /api/memos/{id} @pathVariable : id

Header : 
Authorization : Bearer <JWT>

{
    "replyContent" : "작성한 댓글"
}
{
    "status": 200,
    "message": "OK",
    "data": {
        "replyId": 4,
        "memoId": 3,
        "replyName": "test",
        "replyContent": "작성한 댓글"
    }
}
댓글
수정
PUT /api/memos/{id}/{replyId} @pathVariable : id, replyId

Header : 
Authorization : Bearer <JWT>

{
    "replyContent" : "수정한 댓글"
}
{
    "status": 200,
    "message": "OK",
    "data": {
        "replyId": 13,
        "memoId": 5,
        "replyName": "test",
        "replyContent": "수정한 댓글"
    }
}
댓글
삭제
DELETE /api/memos/{id}/{replyId} @pathVariable : id, replyId

Header : 
Authorization : Bearer <JWT>
{
    "status": 200,
    "message": "OK"
}

좋아요
선택
POST /api/memos/{id}/like @pathVariable : id

Header : 
Authorization : Bearer <JWT>
{
    "status": 200,
    "message": "OK",
    "data": {
        "id": 3,
        "like_cnt": 4
    }
}

좋아요
취소
DELETE /api/memos/{id}/like @pathVariable : id

Header : 
Authorization : Bearer <JWT>
{
    "status": 200,
    "message": "OK",
    "data": {
        "id": 3,
        "like_cnt": 3
    }
}
댓글
좋아요
선택
POST /api/memos/{id}/{replyId}/like @pathVariable : id, replyId

Header : 
Authorization : Bearer <JWT>
{
    "status": 200,
    "message": "OK",
    "data": {
        "replyId": 4,
        "like_cnt": 3
    }
}
댓글
좋아요
취소
DELETE /api/memos/{id}/{replyId}/like @pathVariable : id, replyId

Header : 
Authorization : Bearer <JWT>
{
    "status": 200,
    "message": "OK",
    "data": {
        "replyId": 4,
        "like_cnt": 2
    }
}

 

 

 

ERD

 

스프링 심화 주차 ERD

Draw ERD with your team members. All states are shared in real time. And it's FREE. Database modeling tool.

www.erdcloud.com

 

 

 

 

Q1. 처음 설계한 API와 ERD에 변경사항이 있었나요? 변경되었다면 어떤 점 때문일까요? 첫 설계의 중요성에 대해 생각해보세요.

 

빠뜨린 요구 사항에 대한 요소가 추가되었고, 유저와 글에 관한 연관 관계가 추가되었으며, 출력할 형태를 데이터 만으로 하다가 요청 Status와 메시지에 대한 처리를 추가하면서 많은 부분이 바뀌었다.  

 

설계에 대한 큰 깨달음을 얻은 것이 ResponseEntity를 사용해보면서와 예외처리에 대한 부분을 추가하면서 였는데 이부분에 대한 설계를 진행하지 않고, 다 구현한 후 추가하려니 대대적으로 바꿔야 하는 부분이 많았다. 이미 ResponseEntity아니나 역할을 하는 비슷한 Dto를 만들어버려서 이를 다시 대체 하는데에 혼란을 겪어야 했다. 

처음 설계할 때 충분히 생각하고 설계했다면 후에 대대적으로 수정하는 일을 줄였을 걸로 생각된다.

 

완성된 API문서 : https://documenter.getpostman.com/view/24654654/2s8YzMX4uu

 

Memo API

The Postman Documenter generates and maintains beautiful, live documentation for your collections. Never worry about maintaining API documentation again.

documenter.getpostman.com

완성된 ERD : https://www.erdcloud.com/d/gnWBcoa9Hu339urMd

 

스프링 숙련 주차 ERD

Draw ERD with your team members. All states are shared in real time. And it's FREE. Database modeling tool.

www.erdcloud.com

 

 

 


Q2. ERD를 먼저 설계한 후 Entity를 개발했을 때 어떤 점이 도움이 되셨나요?

 

 

참조에 대한 구상을 먼저 ERD를 통해 서로의 관계를 이미 명확하게 하고 설계했기 때문에 Entity로 구현하는 것이 훨씬 쉬워진다. 
참조키는 어떤 것으로 할 것인지, 일대일, 일대다, 다대일의 관계 역시 ERD의 그림을 통해 관계로 정리해 머리나 글로 이해하는 것보다 명확하게 다가 오기 때문에 구현에 큰 도움이 된다.

 


Q3. JWT를 사용하여 인증/인가를 구현 했을 때의 장점은 무엇일까요?

 

  • 쿠키-세션 방식과 달리 JWT는 서버 쪽에 따로 저장하지 않기 때문에 저장소가 필요없다. 따라서 서버의 부담을 줄일 수 있다.
  • 저장소를 오갈 일이 없기에 불필요한 인증 과정 감소해 트래픽의 위험을 줄일 수 있다.
  • 세션 방식 경우 자원이 공유되지 않아 같은 서비스여도 서버가 달라지면 새롭게 인증을 해야했는데 Secret Key만 공유된다면 각 서버에서 토큰 검증이 가능하고 사용이 가능하기 때문에 별도의 인증을 새롭게 할 불편함이 사라져 서버를 유지 보수, 확장하기 편해진다.

 


 

Q4. 반대로 JWT를 사용한 인증/인가의 한계점은 무엇일까요?

 

  • JWT는 토큰이 만료될 때까지는 중간에 만료시키는 것이 불가능하다. 또한 Client에 토큰을 저장하고 있기 때문에 Client와 Server간에 토큰이 오갈때 Hijacking 하여 토큰을 탈취하게 되면 만료될 때 까지 해커에 의한 공격을 당할 수도 있다. 
  • Token에 정보를 많이 담게되면 그만큼 길어지게 되고 길어긴 Token이 오가면 서버에 부담이 될 수 잇다.
  • Secret Key만 있으면 모두 해독이 가능하기 때문에 보관에 신경써야한다
  • payload 자체는 암호화 된것이 아니라 BASE64로 인코딩 되어 있는 것이다. payload에는 민감한 내용을 담을 수 없거나 암호화를 별도로 해야하는 등 담을 수 있는 내용에 한계가 있다.

 


 

Q5. 만약 댓글 기능이 있는 블로그에서 댓글이 달려있는 게시글을 삭제하려고 한다면 무슨 문제가 발생할까요? Database 테이블 관점에서 해결방법이 무엇일까요?

 

연관되어 있는 댓글들 때문에 삭제가 되지 않는 문제가 생긴다. 댓글과 연결이 되어있는데 글만 삭제가 되어버린다면 댓글들은 연관 관계에서 공중에 뜨게 되고 이는 참조 무결성에 위배가 되게 된다. 참조 무결성이란 데이터베이스 상의 참조가 "모두" 유효함을 말한다.
따라서 무결성을 지키기 위해 CASCADE라는 옵션을 사용하게 되는데, 이는 참조 관계에 있어 신뢰성 있는 상태로 유지하기 위한 것이다. CASCADE옵션을 사용하면 값을 수정 또는 삭제 시 이 값을 참조하고 있는 레코드 역시 종속적으로 수정/삭제할 수 있다.
그런데 만약 값이 하나 삭제될 때 참조하는 값은 지우지 않다면?  ON DELETE SET NULL 라는 옵션이 외래키를 NULL로 바꾸어 유지할 수 있게 한다. 

 

 

 


 

Q6. IoC / DI 에 대해 간략하게 설명해 주세요!

 

* IoC / Invention Of Control / 제어의 역전
스프링의 가장 큰 특징 중 하나로 간단히 말해서 객체의 생성 및 제어권을 사용자가 아닌 스프링에게 맡기는 것이다. 객체에 IoC가 적용된 경우에는 이러한 객체의 생성과 사용자의 제어권을 스프링에게 넘기게 되며 스프링의 DI(Dependency Injection) Container에 의하여 관리당하는 자바 객체를 사용자는 사용하게 된다. 

* DI / Dependency Injection / 의존성 주입
외부에서 두 객체 간의 의존 관계를 결정하고 주입해 주는 것으로, 스프링은 두 객체 사이에 인터페이스를 두어 클래스 레벨에서는 의존 관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.

 

 

 

 

ERD와 SQL 참고하여 요구사항에 맞게 연관관계 구현

step 1

책 재고가 부족하여 다음주에 책들이 새롭게 들어오기로 했습니다.

책을 서점에 등록하려고 합니다. 책(Book) 과 서점(BookStore)의 연관관계를 맺어보세요!

다대일 단방향 연관관계를 적용해주세요.

//--------------------------------------------------------------------------------------
//  경로1.  /com/exmple/spring_week_2_test/entity/Book 
//  id제외) 기본 컬럼 생성 부분과 서점으로 단방향 구현
//--------------------------------------------------------------------------------------

   @Column(nullable = false)
    private String author;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private int price;

    @Column(nullable = false)
    private Long quantity;

    @ManyToOne
    @JoinColumn(name = "book_store_id")
    private BookStore bookStore;

//-------------------------------------------------------------------------------------------
//  경로2. /com/exmple/spring_week_2_test/entity/BookStore
//  id제외) 기본 컬럼 생성 부분, 연관 관계 없음
//-------------------------------------------------------------------------------------------

    @Column(nullable = false)
    private String location;

    @Column(nullable = false)
    private String name;

 

step 2

서점(BookStore)에서 책(Book)을 관리하려고 하는데 현재 구조로는 개발 하기가 불편하네요.

연관관계를 수정해 보세요!

다대일 양방향 연관관계를 적용해주세요.

//--------------------------------------------------------------------------------------
//  경로1.  /com/exmple/spring_week_2_test/entity/Book 
//  step1과 동일 변동사항 없음
//--------------------------------------------------------------------------------------
//  경로2. /com/exmple/spring_week_2_test/entity/BookStore
//  OneToMany추가로 양방향 구현
//--------------------------------------------------------------------------------------

    @OneToMany(mappedBy = "bookStore")
    private List<Book> books;

 

step 3

항해서점 제주점이 드디어 회원제를 적용하기로 했습니다.

서점(BookStore)에서 회원(Member)을 관리할 수 있도록 연관관계를 맺어보세요!

일대다 단방향 연관관계를 적용해주세요.

//--------------------------------------------------------------------------------------
//  경로1.  /com/exmple/spring_week_2_test/entity/BookStore
//  일대다 단방향 구현
//--------------------------------------------------------------------------------------

    @OneToMany
    @JoinColumn(name="book_store_id")
    private List<Member> member;

//--------------------------------------------------------------------------------------
//  경로2. /com/exmple/spring_week_2_test/entity/Member
//  id제외) 기본 컬럼 생성 부분, 연관 관계 없음
//--------------------------------------------------------------------------------------

    @Column(nullable = false)
    private String address;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String phoneNumber;

 

step 4

항해서점 제주점에서 회원(Member)이 구매한 책(Book)을 관리하려고 합니다.

회원(Member)과 책(Book)의 연관관계를 맺어보세요!

다대다를 사용하지 말고 구현해 보세요.

구매(Purchase) Entity를 사용하세요.

// * 다대다 관계를, 구매를 Many로 설정하고 다대일과 일대다로 풀어냄

//--------------------------------------------------------------------------------------
//  경로1. /com/exmple/spring_week_2_test/entity/Member
//  일대다 관계 설정
//--------------------------------------------------------------------------------------

    @OneToMany(mappedBy = "member")
    private List<Purchase> purchase;

//--------------------------------------------------------------------------------------
//  경로2. /com/exmple/spring_week_2_test/entity/Book
//  일대다 관계 설정
//--------------------------------------------------------------------------------------

    @OneToMany(mappedBy = "book")
    private List<Purchase> purchase;

//--------------------------------------------------------------------------------------
//  경로3. /com/exmple/spring_week_2_test/entity/Purchase
//  다대일로 각각 설정 (id 제외
//--------------------------------------------------------------------------------------

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "purchase_id")
    private Book book;

 

 

 

완성된 다이어그램

 

1. 프로젝트 생성 

 

 

 

 


2. 스프링부트 버전과 dependency 추가

 

 

 


3. 빌드가 성공하면 먼저 src > main > resources > application.properties(스프링 환경 설정 파일)에 h2 설정 추가

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=

 

 

 

 


4. 스프링 구조에 맞게 패키지를 나눠서 5개 생성

 

 

 

 

 


5. DB로 생성할 Entity Class 작성

 

① 메모 Entity (작성한 글들이 저장될) : Class

package com.example.memopractice.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter             // Class 모든 필드의 Getter method를 생성
@Entity             // Entity임을 선언
@NoArgsConstructor  // @NoArgsConstructor : 파라미터가 없는 기본 생성자를 생성
public class Memo extends Timestamped {                 // 시간 값을 가져오기 위해 Timestamped 상속
    @Id                                                 // ID임을 선언
    @GeneratedValue(strategy = GenerationType.AUTO)     // 값 자동 생성 , 생성 전략 : 자동 증감
    private Long id;

    @Column(nullable = false)                           // 컬럼 설정 , null값 허용 선택 : 불가
    private String title;

    @Column(nullable = false)
    private String author;

    @Column(nullable = false)
    private String contents;

    @Column(nullable = false)
    private String password;
}

+ @Entity 어노테이션 위에 Entity값을 받을 수 있게 @Getter와 기본 생성자를 생성해주는 @NoArgsConstructor을 추가

 


 

② Timestamped Entity (작성된 시간, 수정된 시간을 추적할, 공용적인 Entity라 별도로 나누고 상속) : Class

package com.example.memopractice.entity;

import lombok.Getter;
import org.springframework.data.annotation.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;


                                                // @EntityListeners : 삽입, 삭제, 수정, 조회 등의 작업을 할 때 전, 후에 어떠한 작업을 하기 위해 이벤트 처리를 위한 어노테이션
@EntityListeners(AuditingEntityListener.class)  // AuditingEntityListener.class : Audit(감시하다) 옵션은 시간에 대해서 자동으로 값을 넣어 주는 기능
@MappedSuperclass                               // 공통 매핑 정보가 필요할 때, 부모클래스에 선언하고 속성만 상속 받아서 사용 하고 싶을때 사용
@Getter                                         // Class 모든 필드의 Getter method를 생성
public class Timestamped {

    @CreatedDate                        // 생성된 시간 정보
    private LocalDateTime createdAt;

    @LastModifiedDate                   // 수정된 시간 정보
    private LocalDateTime modifiedAt;
}

 

 

 

 

 


6. DB를 생성시켜주고 사용할 메소드를 정의하는 Repository를 생성 : Interface

package com.example.memopractice.repository;

import com.example.memopractice.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemoRepository extends JpaRepository <Memo, Long>{
}

 

 

 

 


7. 일단 여기까지 작성하고 실행해주면 h2-console을 이용해 테이블이 생성된 걸 확인할 수 있다.

 

 

 

 


8. DTO를 생성한다.

API 명세서를 생각하면서 RequestDto와 ResponseDto, 그리고 Result만 내보낼 ResultDto를 각각 생성

 

① RequestDto : 요청을 받아올 Dto 

package com.example.memopractice.dto;

import lombok.Getter;

@Getter
public class RequestDto {
    private String title;
    private String author;
    private String contents;
    private String password;
}

 


 

② ResponseDto : 응답으로 글 작성 정보를 내려줄 Dto, 생성시 값을 넣을 수 있게 생성자 작성

package com.example.memopractice.dto;

import com.example.memopractice.entity.Memo;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class ResponseDto {
    private Long id;
    private String title;
    private String author;
    private String contents;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    // password는 Reponse에 노출하지 않는다.


    // Entity -> DTO로 변환 : Entity를 그대로 밖으로 내보내면 안되기 때문에 DTO로 데이터를 필터링하고 필요한 부분만 정리하여 DTO로 내보낸다.
    public ResponseDto(Memo memo){
        this.id = memo.getId();
        this.title = memo.getTitle();
        this.author = memo.getAuthor();
        this.contents = memo.getContents();
        this.createdAt = memo.getCreatedAt();
        this.modifiedAt = memo.getModifiedAt();
    }
}

 


 

③ ResultDto :  응답으로 성공여부를 내려줄 Dto, 생성시 값을 넣을 수 있게 생성자 작성

package com.example.memopractice.dto;

import lombok.Getter;

@Getter
public class ResultDto {
    private boolean success;
    
    public ResultDto(boolean result){ // 외부에서 값을 받아와 적용시키기 위해 매개값 생성자
        this.success = result;
    }
}

 

 

 

 


9. 요청을 받을 Controller를 생성한다.

요청에 따른 URL을 각각 나누고 서비스를 연결해줄 준비를 한다.

URL등 RESTful 하게 만들수 있도록 신경쓰자

package com.example.memopractice.controller;

import com.example.memopractice.dto.*;
import com.example.memopractice.service.MemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController // @ResponseBody 어노이테이션을 따로 쓰지 않기 위해 @ResponseBody + @Controller인 @RestController 사용
@RequiredArgsConstructor // final변수나 Notnull 표시가된 변수등 필수적인 정보를 세팅하는 생성자를 생성 어노테티션
public class MemoController {

    // Controller은 Client에 가장 맞닿아 있어 DB랑 가까운 Entity가 나오지 않게 분리 , Service에서 리팩토링해서 controller로 내보내라
    
    private final MemoService service; // 서비스를 연결 = 주입한다고 표현 DI (dependency Injection)

    @GetMapping("/api/memos")   // Get방식
    public List<ResponseDto> getMemos(){    // 여러 Dto를 List로 Client에 보내기
        return service.getMemo();
    }

    @GetMapping("/api/memos/{id}")  // PUT방식
    public ResponseDto getMemo(@PathVariable Long id){    // 하나의 Dto를 Client에 보내기
        return service.getMemo(id);
    }

    @PostMapping("/api/memos")  // POST방식
    public ResponseDto createMemo(@RequestBody RequestDto dto){  // 들어오는건 Request 나가는건 Response로 각각 달리 할 수 있게 함
        return service.createMemo(dto);
    }

    @PutMapping("/api/memos/{id}") // PUT방식
    public ResponseDto updateMemo(@PathVariable Long id, @RequestBody RequestDto dto){   // 경로에서 id값 꺼내기. Body에서 값 꺼내기
        return service.updateMemo(id, dto);
    }

    @DeleteMapping("/api/memos/{id}")   // Delete방식
    public ResultDto deleteMemo(@PathVariable Long id, @RequestBody RequestDto dto){
        return service.deleteMemo(id, dto);
    }
}

 

 

 

 


10. Service를 생성한다.

package com.example.memopractice.service;

import com.example.memopractice.dto.*;
import com.example.memopractice.entity.Memo;
import com.example.memopractice.repository.MemoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@Service    // Service임을 선언
@RequiredArgsConstructor    // 필수적인 정보를 세팅하는 생성자를 생성 어노테이션
public class MemoService {

    private final MemoRepository repository; // Repository 주입

   
    public List<ResponseDto> getMemo() {
        List<Memo> memos = repository.findAllByOrderByModifiedAtDesc();     // 10-1 참조 : Repository에 검색 관련 메소드를 정의해야함
                                                                            // 10-2 참조 : Memo Entity에 Dto값을 받아 객체를 생성할 수 있도록 추가
        List<ResponseDto> exportDtoList = new ArrayList<>();
        for(Memo memo : memos){                                             // Entity -> Dto로 변환
            ResponseDto tempDto = new ResponseDto(memo);
            exportDtoList.add(tempDto);
        }
        return exportDtoList;
    }


    public ResponseDto getMemo(Long id) {
        Memo memo = repository.findById(id).orElseThrow(()-> new IllegalArgumentException("글이 없어요!"));  // 검색결과가 없으면 발생하는 예외를 처리
        return new ResponseDto(memo);                                       // Entity -> Dto로 변환
    }


    public ResponseDto createMemo(RequestDto dto) {
        Memo memo = new Memo(dto);
        repository.save(memo);
        return new ResponseDto(memo);
    }



    @Transactional
    public ResponseDto updateMemo(Long id, RequestDto dto) {
        Memo memo = repository.findById(id).orElseThrow(()-> new NullPointerException("글이 없습니다!"));
        if(memo.getPassword().equals(dto.getPassword())){
            memo.updateMemo(dto);              // 10-3 Entity 사항을 업데이트하는 메소드 추가
//            repository.save(memo);		// @Transactional 어노테이션을 쓰면 이건 생략, 어노테이션을 사용안하려면 이걸 추가
            return new ResponseDto(memo);
        } else {
            throw new IllegalArgumentException("비밀번호가 다릅니다");
        }
    }


    public ResultDto deleteMemo(Long id, RequestDto dto) {
        Memo memo = repository.findById(id).orElseThrow(()-> new IllegalArgumentException("글을 찾을 수 없습니다."));
        if(memo.getPassword().equals(dto.getPassword())){
            repository.delete(memo);
            return new ResultDto(true);
        } else {
            return new ResultDto(false);
        }
    }
}

 


 

10-1. Repository에 검색 관련 메소드 정의 

public interface MemoRepository extends JpaRepository <Memo, Long>{
    // 결과 필터링을 정의하고 출력할 타입을 정의하는 계층
 
    //추가부분
    List<Memo> findAllByOrderByModifiedAtDesc();        // findAllBy(모두찾는다)/OrderBy(-를 기준으로 정렬로)/ModifiedAt(ModifiedAt멤버변수를)/Desc(내림차순)
    
}

 


 

10-2. Memo Entity에 Dto값을 받아 객체를 생성할 수 있도록 "생성자" 추가

public Memo (RequestDto dto){   // 10-2 매개값 받는 생성자 추가
    this.title = dto.getTitle();
    this.author = dto.getAuthor();
    this.contents = dto.getContents();
    this.password = dto.getPassword();
}

 


 

10-3. Memo Entity에 Dto값을 받아 객체를 수정할 수 있도록 "메소드" 추가

public void updateMemo(RequestDto dto){
    this.title = dto.getTitle();
    this.author = dto.getAuthor();
    this.contents = dto.getContents();
//  this.password = dto.getPassword(); // password도 바꾼다면 포함
}

 

 

 

 


11. 시간 추적 기능을 사용하려면 실행 클래스에 @EnableJpaAuditing 어노테이션을 추가한다. 

package com.example.memopractice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing  // <--- 이걸 추가
@SpringBootApplication
public class MemoPracticeApplication {

    public static void main(String[] args) {
        SpringApplication.run(MemoPracticeApplication.class, args);
    }
}

 

 

 

 


12. 구현을 완료했다면 테스트는 Postman으로 진행

아래 6가지 사항을 맞춰 각각 요청을 테스트

 

① method 방식 선택

② URL

③ Request 방식

④ json 문자열 형태로 그대로 값을 전달하고 싶으면 raw선택

⑤ raw 중에도 방식 중 JSON을 선택

⑥ 필요한 정보를 key갑과 value값에 맞춰 기입

 

그리고 send하면 아래에 Response가 출력된다.

 

 

 

 

 


13. DB 변경도 확인

 

 

 

주어진 질문

  1. 수정, 삭제 API의 request를 어떤 방식으로 사용하셨나요? (param, query, body)
  2. 어떤 상황에 어떤 방식의 request를 써야하나요?
  3. RESTful한 API를 설계했나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요?
  4. 적절한 관심사 분리를 적용하였나요? (Controller, Repository, Service)
  5. 작성한 코드에서 빈(Bean)을 모두 찾아보세요!
  6. API 명세서 작성 가이드라인을 검색하여 직접 작성한 API 명세서와 비교해보세요!

 

 


수정, 삭제 API의 request를 어떤 방식으로 사용하셨나요? (param, query, body)  

body방식을 선택했다. 비록 프론트 부분이 없으나 게시판 기능을 하는 거였고, 건네 받을 데이터 중에는 url에 드러나지 말아야할 패스워드와 url로 들어가면 너무 길어지는 글 내용이 포함되어 있었다. 따라서 Request body에 데이터를 포함시켜 전달하는 body방식을 사용해 JSON으로 제출하는 것을 선택했다.

 

 

+

REST API의 parameter 4가지 타입

- header : Requet Header에 포함된 파라미터. 보통 인증 혹은 권한 부여에 관련되어 있다.

- path : 엔드포인트에서 쿼리문 이전의 파라미터. (ex.https://news.naver.com/main/list.naver?mode=LSD&mid=sec)

- query string : 쿼리문 내의 파라미터. 엔드포인트가 끝난 뒤 물음표 뒤에 온다. (ex.https://news.naver.com/main/list.naver?mode=LSD&mid=sec)

- request body : 리퀘스트 바디에 포함된 파라미터. 보통 JSON 형식으로 제출된다.

 

 

재미있는 REST API 파라미터의 종류와 개요

읽지 않아도 되는 서론;처음에 제목을 'REST API 파라미터의 종류와 개요'라 했다가 너무 재미없어 보여서 적절한 형용사를 추가했다.요즘 Udemy에서 Vue.js 강의를 들으며 간단한 펫프로젝트 사이트

yuda.dev

이진님이 알려주신 파라미터 설명 링크

 

@RequestParam, @PathVariable, @RequestBody

https://elfinlas.github.io/2018/02/18/spring-parameter/ Spring에서 @RequestParam과 @PathVariable Spring에서 Controller의 전달인자…Spring을 사용하다 보면 Controller 단에서 클라이언트에서 URL에 파라메터를 같이 전달하

u0hun.tistory.com


어떤 상황에 어떤 방식의 request를 써야하나요? 

상황별 Request) 

  • @RequestParam

URI의 QueryString으로 넘어오는 파라미터를 받는 Request 방식이다. 보통 정렬이나 필터링, 페이징을 적용하고자 할 때 사용된다. 

ex) http://localhost:8080/api/memo?name=littlezero

 

@RequestParam("name") String name  식으로 사용되며

RequestBody와 달리 받을 데이터 이름을 설정 해주어야 하며 이에 해당하는 데이터만을 받을 수 있다. 

해당하는 파라미터가 URI에 없으면 에러가 발생한다. 

 

+ URI의 파라미터 이름과 @RequestParam 정의시 변수명이 동일하면 "" 안의 이름은 생략해도 된다고 한다.

ex) @RequestParam("") String name

+ 가끔 어떤 파라미터는 @ReqeustParam을 쓰고 어떤 파라미터는 @RequestParam을 생략해서 요청을 받는다.

@RequestParam을 생략하면 내부적으로  String이나 Long 같은 타입은 @ReuqestParam으로 취급하고 그 이외에 파라미터는 @ModelAttribute로 취급

 

  • @PathVariable

URI의 경로의 일부를 파라미터로 받는 Request방식이다. 

REST API 호출시 많이 사용하는 방식으로 구체적으로 리소스를 식별하여 사용하고자 할때 사용한다. 

 

  • @RequestBody

HTTP Request의 Body에 담긴 값을 객체로 변환하여 받는 Request방식이다.

일단 URI에 해당 데이터가 노출되지 않아, 숨겨야할 데이터 또는 URI에 다 담을 수 없이 긴 데이터 등을 처리할 때 사용한다. xml이나 json기반의 메시지를 사용하는 경우 유용하다.

 

 

 

상황별 메소드) 

  • GET 

자원의 수정 없이 오로지 데이터를 조회할 목적일 때 사용하는 방식이다. CRUD에서는 R. Read 부분에 해당한다. DB로 치면 Select에 해당. 이번 과제에서는 메모글을 모두 조회하는 데에 사용했다.

  • POST

자원를 생성하여 추가할 때 사용하는 방식이다. CRUD에서 C. Create 부분에 해당하며, DB로 치면 Insert에 해당. 이번 과제에서 메모글을 새로 작성하여 저장하는 데에 사용했다.

  • PUT

자원를 변경할 때에 사용하는 방식이다. CRUD에서 U. Update에 해당하며, DB에서도 Update에 해당한다. 이번 과제에서 메모글을 수정하는데에 사용했다. 

참고로 PUT은 자원을 모두 대체(replace)하는 메소드이다. 요청한 URL 아래에 해당 자원이 존재하지 않으면 POST와 마찬가지로 새로운 자원으로써 저장한다. 만약 요청한 URL 아래에 해당 자원이 존재하면 기존에 존재하던 자원을 새롭게 요청으로 들어간 정보로 자원 전체를 대체한다. 만약 PUT을 사용하여 일부만 변경하고자 하여 자원의 전체 상태를 완전하지 못한 상태로 전송한다면 일부가 null값으로 변경될 수 있다. 

  • PATCH

PATCH 역시 데이터를 변경할 때 사용한다. 하지만 PUT과 PATCH는 서로 대체제 관계가 아니며, 다른 정의와 규약을 가지고 있다. PATCH 요청은 자원에 대한 부분적인 수정을 적용하기 위한 메소드이다. 따라서 필요한 정보에 대해서만 요청할 수 있다. 

  • DELETE

자원를 삭제할 때 사용하는 방식. CRUD에서 D. Delete에 해당하며, DB에서도 Delete에 해당한다. 이번 과제에서 메모글을 삭제하는 데에 사용했다.

 

+ 멱등성 (idempotent) 

멱등성의 멱(冪)은 거듭제곱이란 뜻이 있으며 등(等)은 같다는 뜻을 가지고 있다. 

거듭해도 같다는 뜻으로 수학에서는 연산을 여러번 적용하더라도 결과가 달라지지 않는 성질을 의미하며, 웹에서는 동일한 요청을 한 번 보내는 것과 여러번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태에도 동일하게 남을 때, 해당 HTTP 메소드가 멱등성을 가졌다고 한다.

 

 

REST API와 GET, POST, PUT, DELETE 통신에 대해

REST (Representational State Transfer) REST API는 웹에서 데이터를 전송 및 처리하는 방법을 정의한 인터페이스를 말한다. 모든 데이터 구조와 처리방식은 REST에서 URL을 통해 정의되며, 그래서 매우 직관적

memostack.tistory.com

 

자원을 수정하는 HTTP 메서드 - PUT vs PATCH

들어가며 웹 API를 설계할 때, 최대한 Http 표준을 따라서 용도에 맞는 Http Method를 사용해야 한다는 것은 아마 많은 개발자들이 인지하고 있을 것이다. 이번 글에서는 Http Method…

tecoble.techcourse.co.kr

 

 

 


RESTful한 API를 설계했나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요? 

REST라고 하면 HTTP라는 프로토콜을 이용해 웹에서 제공하는 모든 자원을 하나하나 식별할 있는 고유한 주소인 URI를 이용하여 HTTP 메소드를 통해 CRUD 처리하는 방식을 말한다.

그리고 RESTful API란 REST를 REST 답게 원리를 잘 따라 설계한 API를 말한다. 

 

일단 강의를 기준으로 해서 상황에 따른 적절한 CRUD를 사용했다고 생각하므로 RESTful한 설계가 되었다고 생각하지만, 아직 PUT이나 PATCH에 대한 차이를 정확하게 모른채로 사용해서 개인적으로 RESTful한가에 대한 의문이 있다.

 

API명세서 예시를 보면 URL이 동일하게 하고 Id가 필요한 경우만 뒤에 경로를 더 붙여 달리하여 Request의 요청 방식따라 달리 받게 했는 데, 내가 진행한 과제에서는 URL을 아예 다 다르게 써서 오히려 더 비효율적으로 한게 아닌가 생각이 든다.  
매니저님께 여쭤보니 RESTful한 API에는 URL에 동사를 사용하지 않는다고 하셔서 동사를 사용해 url을 다 다르게 만들어 RESTful하지 못했다.

 


적절한 관심사 분리를 적용하였나요? (Controller, Repository, Service) 

관심사 분리(SoC, Separation Of Concerns)란 객체 지향 프로그래밍(OOP, Object-Oriented Programming)의 5대 설계 원리인 SOLID 중 S에 해당하는  Single Responsiblity Principle (SRP, 단일 책임 원칙) 의 원리에 따르는 것으로 하나의 역할은 하나의 역할 수행에만 집중해야 함을 의미한다.

 

이번 과제에서 Controller와 Repository 그리고 Service를 구축하면서 해당 클래스가 그에 맞는 역할을 가지고 그 역할을 수행하는 데에만 집중할 수 있게 했느냐는 것이 관심사 분리를 제대로 적용한 것이라 볼 수 있다.

 

  • Controller는 사용자의 요청이 진입하는 시작점이자 중앙 제어자. 사용자의 요청과 데이터를 받아 적절한 Service 메소드로 연결해주고 Service를 통해 처리가 된 모델을 뷰와 함께 응답으로 보내주는 역할을 한다.
  • Service는 Controller를 통해 전달 받은 정보를 가공하는 과정 즉, 비즈니스 로직을 수행하는 역할이다. 이 과정에서 필요하면 Service는 데이터베이스에 접근하는 DAO(Data Access Object)를 이용해 데이터를 받아와 로직을 수행하기도 한다.
  • Repository는 Entity에 의해 생성된 DB에 접근하는 메소드 들을 사용하기 위한 인터페이스이다. 사용하고자 하는 CRUD를 어떻게 할 것인가 정의해주는 계층이다.

 

과제에서 Controller는 로직에 관여없이 철저하게 요청에 따른 연결을 해주는 역할 만을 수행했다.

Service는 데이터에 대한 모든 비즈니스 로직에 관련하여 수행하여 가장 큰 비중을 차지했다.

Repository는 Entity에 의해 생성된 DB로부터 어떤 방식으로 필터링하여 가져올 것인가 정의를 진행했기 때문에 전체적으로 적절한 관심사 분리가 이루어졌다고 생각한다. 

 

 

 

 

Controller, Service, Repository 가 무엇일까?

찾아본 결과 Controller가 무엇인지 알기 전에 MVC 패턴에 대하여 먼저 아는 것이 중요합니다!MVC패턴은 Model-View-Controller의 약자로서 개발을 할 때 3가지 형태로 역학을 나누어 개발하는 방법론입니

velog.io

 


작성한 코드에서 빈(Bean)을 모두 찾아보세요! 

스프링에서 빈으로 등록하기 위한 어노테이션으로는  @Configuration + @Bean,  @Component 어노테이션이 있으면 스프링 빈으로 자동 등록되는데 @Component를 포함하는 @Controller, @Service, @Repository 어노테이션들도 역시 빈으로 등록된다. @Configuration 에도 @Component가 포함되어있다.

 

@Service, @Repository 등이 이번 과제에 사용 되었으며 기본 @Controller이 아닌 @RestController를 사용했는데 이것도 기본적으로 @Controller + @ResponseBody를 결합한 것이라 @Component를 포함하므로 이역시 Bean으로 등록된다.

 

+

스프링 Bean이란? ) 

 

스프링의 특징에는 제어의 역전(IoC / Invention Of Control)이 있다.

제어의 역전이란, 간단히 말해서 객체의 생성 및 제어권을 사용자가 아닌 스프링에게 맡기는 것이다. 객체에 IoC가 적용된 경우에는 이러한 객체의 생성과 사용자의 제어권을 스프링에게 넘기게 되며 스프링의 DI(Dependency Injection) Container에 의하여 관리당하는 자바 객체를 사용자는 사용하게 된다. 이 객체를 '빈(bean)'이라 한다.

 

 

[Spring Boot] 스프링 빈(bean)이란? 스프링 빈 등록하는 방법

스프링(Spring) 빈의 개념과 빈을 등록하는 방법(컴포넌트 스캔과 자동 의존관계 설정, 자바 코드로 직접 스프링 빈 등록하기(Configuration))에 대해 공부하고 정리한 포스팅입니다.

velog.io

 

 


API 명세서 작성 가이드라인을 검색하여 직접 작성한 API 명세서와 비교해보세요! 

내가 작성한 API 명세서

기능 Method URL Request Response
모든 게시글
조회
GET /api/memos -
배열로, modifiedAt 내림차순
{
        "result"null,
        "message"null,
        "id"6,
        "title""제목1",
        "author""글쓴이1",
        "content""내용입니다",
        "createdAt""2022-11-30T15:02:36.775326",
        "modifiedAt""2022-11-30T15:02:36.775326"
  }
게시글 작성 POST /api/memoWrite
{
    "title" : "제목1",
    "author" : "글쓴이1",
    "password" : "12345",
    "content" : "내용입니다"
}
{
    "id"7,
    "title""제목1",
    "author""글쓴이1",
    "content""내용입니다",
    "createdAt""2022-11-30T15:04:52.0866289",
    "modifiedAt""2022-11-30T15:04:52.0866289"
}
선택한 게시글
조회
GET /api/memos/{id} id = 1
{
    "id": 1,
    "title""제목1",
    "author""글쓴이1",
    "content""내용입니다",
    "createdAt""2022-11-30T15:02:32.360081",
    "modifiedAt""2022-11-30T15:02:32.360081"
}
선택한 게시글
수정
PUT /api/memoModify/{id} id = 1,
{
    "title" : "제목1",
    "author" : "글쓴이1",
    "password" : "12345",
    "content" : "내용입니다"
}
{
    "id"1,
    "title""제목1234",
    "author""글쓴이1234",
    "content""수정해봅시다",
    "createdAt""2022-11-30T15:02:36.775326",
    "modifiedAt""2022-11-30T15:03:25.516594"
}
선택한 게시글
삭제
DELETE /api/memoDelete/{id} id = 1,
{
    "password" : "12345"
}
{
    "result""Success",
    "message""글을 성공적으로 지웠습니다"
}

 

예시와 비교했을 때 고쳐야 할 점) 

유사한 기능은 모아두기

여러 데이터를 Response을 할 때는 단 한개만 보여주지 말고 2-3개 보여주기

URL에 경로로 주어지는 path값의 id는 Request란에 포함 시키지 않고 URL에만 추가하기

 

API 명세서 작성 가이드라인을 검색해서 본 것과 비교했을 때 고쳐야 할 점) 

여러 명세서 중에는 Response로 돌아오는 각 데이터들에 대한 예시만 보여주는 게 아니라 데이터 파라미터에 대한 타입부터 이것의 의미하는 것까지 상세 설명을 적는 케이스도 있었다. 상세하게 적어놓은 것을 보니 이 API를 처음 접한 사람도 이 요청이 무엇을 위한 것이고 어떤 값을 받는지 이해하기 쉽게되어 있었다.

검색해보니 API는 이렇게 작성해! 라는 정해진 답은 없는 거 같았다. 여러 API 명세서를 읽어보면서 API 명세서를 작성하는 좋은 방법에 대해서도 생각해봐야 할 듯 하다. (심지어 API 명세서를 작성하기위한 프로그램도 존재한다.)

 

 

 

* 항해의 API 명세서 예시

 

 

+ Recent posts