[Java]/Spring Boot

[Java] Spring Boot: 방명록 Rest API 구현(feat. MySQL 연동)

기록자_Recordian 2025. 4. 17. 23:06
728x90
반응형
이전 내용
 

[Java] Spring Boot: 코드, 요청&응답 과정 이해하기

이전 내용 [Java] Spring Boot: Maven Repository이전 내용 [Java] 스프링부트: Spring Initializr - Dependencies이전 내용 [Java] Spring Boot: 제어 역전, 의존성 주입이전 내용 [Java] Spring Boot - 기본 구조, "Hello World" 띄워

puppy-foot-it.tistory.com


Rest API

 

◆ REST API(Representational State Transfer API)

REST API는 HTTP 프로토콜을 기반으로 하는 API로, 웹 서비스를 설계하기 위한 아키텍처 스타일로, 클라이언트와 서버 간의 상호작용을 효율적으로 관리한다. 


[REST 와 API]

  • REST: REST는 자원(리소스)의 상태 표현을 사용하여 클라이언트와 서버 간의 통신을 정의하는 아키텍처 스타일이다. 각 리소스는 URI(Uniform Resource Identifier)를 통해 고유하게 식별된다.
  • API: 응용 프로그램 프로그래밍 인터페이스(Application Programming Interface)로, 소프트웨어 간의 통신을 위한 규약을 의미한다. 

[REST의 특징]

  • 무상태성(Stateless): 서버는 클라이언트의 상태를 저장하고, 각 요청은 독립적이며 필요한 모든 정보를 포함해야 한다.
  • 캐시 가능(Cacheable): 클라이언트와 서버 간의 데이터 전송 비용을 줄이기 위해 응답이 캐시될 수 있다. 이는 성능을 향상시킨다.
  • 계층화: REST 아키텍처는 여러 계층으로 구성될 수 있으며, 클라이언트는 최상위 레벨의 서버와 통신하지만, 실제 리소스는 여러 중간 서버를 통해 제공될 수 있다.
  • 일관된 인터페이스: 모든 요청은 특정 HTTP 메서드(예: GET, POST, PUT, DELETE)를 사용하여 수행된다.

[HTTP 메서드와 REST API]

  • GET: 리소스 조회. 서버에서 데이터를 요청할 때 사용.
  • POST: 새로운 리소스 생성. 서버에 새 데이터를 전송할 때 사용.
  • PUT: 기존 리소스 업데이트. 특정 리소스를 완전히 교체할 때 사용.
  • PATCH: 기존 리소스를 부분적으로 업데이트. 필요한 수정만 반영.
  • DELETE: 특정 리소스 삭제. 서버에서 해당 리소스를 제거.

[REST API 설계 원칙]

- 자원기반: URI는 리소스를 표현해야 하며, 명사 형식으로 설계.

  • /users: 사용자 목록을 조회
  • /users/1: 특정 사용자의 상세 정보를 조회

- HTTP 상태 코드 사용: 요청의 성공 여부를 나타내기 위해 적절한 HTTP 상태 코드 사용.

  • 200 OK: 요청 성공
  • 201 Created: 리소스 생성 성공
  • 204 No Content: 삭제 성공
  • 404 Not Found: 요청한 리소스가 없음
  • 500 Internal Server Error: 서버 오류

[간단한 REST API 예시]
간단한 사용자 관리 REST API의 예.

  • GET /users: 모든 사용자 목록 조회
  • GET /users/{id}: 특정 사용자 정보 조회
  • POST /users: 새로운 사용자 추가
  • PUT /users/{id}: 특정 사용자 정보 업데이트
  • DELETE /users/{id}: 특정 사용자 삭제

방명록 Rest API 구현하기
(feat.MySQL 연동)

 

다음의 요구사항에 알맞은 방명록 REST API를 구현하라.

1. 방명록 테이블은 아래와 같은 구조로 설계

  • id: Primary Key (자동 증가)
  • author: 작성자 이름
  • content: 방명록 내용
  • createdAt: 작성 시간
  • updatedAt: 수정 시간

이를 SQL 로 작성하면 아래와 같다. 

CREATE TABLE guestbook (
	id BIGINT AUTO_INCREMENT PRIMARY KEY,
    author VARCHAR(50) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP
);

 

2. 해당 데이터를 로컬 프로그램에 설치된 MySQL과 연동되도록 하라.

단, MySQL에 접속해서 해당 테이블을 직접 생성, 삽입, 수정, 삭제 하지말고 스프링 부트에서 모든 작업이 진행될 수 있도록 할 것.

 

3. API 엔드포인트

HTTP Method 엔드포인트 설명
POST /api/guestbook 방명록 생성
GET /api/guestbook 모든 방명록 조회
GET /api/guestbook{id} 특정 방명록 조회
PUT /api/guestbook{id} 특정 방명록 수정
DELETE /api/guestbook{id} 특정 방명록 삭제

풀이

 

[순서 요약 1] ※ 세부 과정
의존성 관리 → properties 설정 추가 → 엔터티 구성(Article.java) → 리포지토리(인터페이스) 생성 →  서비스 클래스 생성 및 메소드 구현 컨트롤러 생성 및 메소드 구현  → API 테스트 → 태스트 코드 작성

[순서 요약 2] ※ 전체 과정

방명록 개발을 위한 엔티티 구성 → 방명록 작성을 위한 API 구현 → 방명록 목록 조회를 위한 API 구현  → 방명록 삭제 API 구현 → 방명록 수정 API 구현 (내용 상 한 번에 다 소개)

 

1. 의존성 관리

Initializr 에서 필요한 모듈을 dependencies를 통해 넣어준다. 특히, 로컬 컴퓨터에 설치된 MySQL을 사용할 것이므로, mysql-connector-j를 넣어줘야 한다. (또는, build.gradle에서 넣어줘도 된다.)

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-mustache'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	runtimeOnly 'com.mysql:mysql-connector-j'
	implementation 'com.mysql:mysql-connector-j'
}

상단의 내용은 build.gradle의 내용 중 dependencies 부분이다.


2. properties 추가해주기

그리고나서 src - main - resources 의 applcation.properties 로 들어와서 포트를 변경하고 (8080 포트와 충돌을 피하기 위함)

mysql을 사용하기 위한 설정을 추가해 준다.

spring.application.name=guestBook

server.port = 8081

spring.datasource.url=jdbc:mysql://localhost:3306/tabledb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=사용자명
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
  • spring.datasource.username=사용자명:
    이 항목은 데이터베이스에 연결하기 위해 사용할 사용자 계정 설정. 실제 사용자명으로 변경 요망.
  • spring.datasource.password=비밀번호:
    데이터베이스 사용자 계정의 비밀번호를 설정하는 항목. 실제 비밀번호로 변경하여 입력 요망.
  • spring.datasource.driver-class-name=cohttp://m.mysql.cj.jdbc.Driver:
    MySQL 데이터베이스를 사용하기 위해 필요한 JDBC 드라이버 클래스 지정. cohttp://m.mysql.cj.jdbc.Driver는 MySQL Connector/J 8.0 이상 버전에 사용되는 드라이버 클래스.

JPA 설정

  • spring.jpa.show-sql=true:
    애플리케이션이 실행될 때 Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력하도록 설정. 이를 통해 데이터베이스와의 상호작용을 쉽게 디버깅할 수 있다.
  • spring.jpa.hibernate.ddl-auto=update:
    Hibernate의 DDL(데이터 정의 언어) 자동 생성 방법을 설정. update 값은 애플리케이션이 실행될 때 데이터베이스 스키마를 자동으로 업데이트. 즉, 기존의 데이터베이스 구조를 변경하거나 새로운 엔티티가 추가될 경우 이를 반영.
  • spring.jpa.properties.hibernate.format_sql=true:
    출력되는 SQL 쿼리가 보기 쉽게 포맷되어 출력되도록 설정. 이를 통해 SQL 쿼리를 보다 쉽게 이해하고 확인할 수 있다.

[hibernate.ddl-auto의 다른 설정 값]

  • none: DDL 자동 생성이 이루어지지 않는다.
  • validate: 엔티티와 데이터베이스 스키마를 비교하여 문제가 있을 경우 오류를 발생시킨다.
  • create: 매번 애플리케이션이 시작될 때마다 기존 데이터를 삭제하고 새로운 데이터베이스 스키마를 생성한다.
  • create-drop: create와 동일하지만 애플리케이션 종료 시 데이터베이스를 삭제한다.

3. 엔티티 구성하기 (JPA)

domain 패키지에 Article.java 파일을 만든 다음 해당 코드 입력

package com.example.guestBook.domain;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity // 엔티티 지정
@Table(name = "guestbook")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder // 빌터 패턴으로 객체 생성
@EntityListeners(AuditingEntityListener.class) // 최초 입력 시간, 수정 시간 자동 관리 위함
public class Article {

    @Id // id 필드ㅣ 기본키
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 자동으로 1씩 증가
    private Long id;

    @Column(nullable = false, length=50) // 작성자, Not Null
    private String author;

    @Column(nullable = false, columnDefinition = "TEXT") // 내용, Not Null
    private String content;

    @CreatedDate // 작성 시간
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate // 수정 시간
    @Column(nullable = true)
    private LocalDateTime updatedAt;

    public Article(String author, String content) {
        this.author = author;
        this.content = content;
    }

    @PrePersist // 엔티티가 데이터베이스에 저장되기 전에 createdAT을 현재 시간으로 설정
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
    }

    @PreUpdate // 엔티티가 업데이트할 때 updatedAT을 현재 시간으로 설정
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    // 엔티티에 요청받은 내용으로 값 수정 메소드
    public void update(String author, String content) {
        this.author = author;
        this.content = content;
    }

}

[주요 애너테이션]

  • @Entity: 이 클래스가 JPA의 엔티티임을 나타냄.
  • @Table(name = "guestbook"): 이 엔티티가 guestbook이라는 테이블과 매핑된다는 것을 나타냄.
  • @EntityListeners(AuditingEntityListener.class): 기록의 생성 및 수정 시간을 자동으로 관리하도록 지정.
  • @Getter: Lombok 라이브러리의 애너테이션으로, 클래스의 모든 필드에 대한 getter 메서드를 자동으로 생성.
  • @Setter: Lombok 라이브러리의 애너테이션으로, 클래스의 모든 필드에 대한 setter 메서드를 자동으로 생성.
  • @NoArgsConstructor: Lombok 애너테이션으로, 매개변수가 없는 기본 생성자를 자동으로 생성.
  • @AllArgsConstructor: Lombok 애너테이션으로, 모든 필드를 인자로 받는 생성자를 자동으로 생성.
  • @Builder: Lombok 애너테이션으로, 빌더 패턴을 사용하여 객체를 생성할 수 있는 메서드를 자동으로 생성.
  • @EntityListeners(AuditingEntityListener.class): 이 엔티티의 생명주기 이벤트에 대해 지정된 리스너(여기서는 AuditingEntityListener)를 등록. 이를 통해 자동으로 생성 시간과 수정 시간을 관리.

※ 빌더 패턴(Builder Pattern): 복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴. 생성자에 들어갈 매개 변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식

[필드 설명]
- ID 필드:

  • @Id: 기본 키를 지정. (Primary Key를 가지는 변수 선언)
  • @GeneratedValue(strategy = GenerationType.IDENTITY): 기본 키 값을 증가시키도록 설정.

- 작성자 필드:

  • @Column(nullable = false, length=50): 작성자의 이름을 저장하며, NULL이 허용되지 않고 최대 길이는 50자.

- 내용 필드:

  • @Column(nullable = false, columnDefinition = "TEXT"): 내용이 저장되며, NULL이 허용되지 않고 긴 텍스트 형식으로 저장.

- 작성 시간:

  • @CreatedDate: 글이 작성된 시간을 자동으로 관리.
  • @Column(nullable = false, updatable = false): NULL이 허용되지 않으며, 수정 시 업데이트되지 않음.

- 수정 시간:

  • @LastModifiedDate: 글이 수정된 시간을 기록.
  • @Column(nullable = true): NULL 허용.

- 메서드 설명

  • 생성자: 기본 생성자와 작성자, 내용을 인자로 받는 생성자가 제공 됨.

- @PrePersist / @PreUpdate:

  • onCreate(): 엔티티가 데이터베이스에 저장되기 전에 생성 시간을 현재 시간으로 설정.
  • onUpdate(): 엔티티가 업데이트될 때 수정 시간을 현재 시간으로 설정.
  • update(): 작성자와 내용을 수정하는 메서드. 이 메서드를 통해 기존 엔티티의 데이터를 갱신할 수 있다.

4. 리포지토리 생성

repository 패키지를 생성하고, GuestBookRepository.java 파일을 생성 후 GuestBookRepository 인터페이스 생성

package com.example.guestBook.repository;

import com.example.guestBook.domain.Article;
import org.springframework.data.jpa.repository.JpaRepository;

public interface GuestBookRepository extends JpaRepository<Article, Long> {
}
  • JpaRepository: Spring Data JPA에서 제공하는 인터페이스 중 하나로, JPA를 사용하여 데이터베이스를 조작하기 위한 메서드들을 제공
  • JPARepository 인터페이스를 상속받는 인터페이스를 정의하면, 해당 인터페이스를 구현하는 클래스는 JPA에서 제공하는 메서드 사용 가능
  • JpaRepository를 사용하면, 복잡한 JDBC(Java DataBase Connectivity) 코드를 작성하지 않아도 간단하게 DB와의 데이터 접근 작업 처리 가능
  • JPARepository 인터페이스는 제네릭 타입을 사용하여 Entity클래스와 복합키를 사용하고 있다면 해당 Entity의 ID클래스를 명시 ▶ JpaRepository<T, ID> *T: Entity 클래스

[JPA 메소드]

메소드 기능
save() 레코드 저장 (insert, update)
findOne() primary key로 레코드 한건 찾기
findAll() 전체 레코드 불러오기. 정렬(sort), 페이징(pageable) 가능
count() 레코드 갯수 세기
delete() 레코드 삭제

 


5. 방명록 작성, 조회, 수정, 삭제 등의 기능을 위한 API 구현

5-1. dto 패키지 생성 및 코드 작성

먼저 dto 패키지를 생성하고, dto 패키지를 서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체 생성 

※ DTO는 단순하게 데이터를 옮기기 위한 전달자 역할을 하므로, 별도의 비즈니스 로직을 포함하지 않음.

package com.example.guestBook.dto;

import com.example.guestBook.domain.Article;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자
@Getter
public class AddArticleRequest {

    private String author;
    private String content;

    public Article toEntity() {
        return Article.builder()
                .author(author)
                .content(content)
                .build();
    }
}
  • toEntity(): 빌더 패턴을 사용해 DTO를 엔터티로 만들어주는 메소드. 추후 방명록에 글 추가 시 저장할 엔터티로 변환하는 용도로 사용.
  • Article.builder(): Article 클래스에 정의된 정적 메소드를 호출하여, 빌더 객체를 생성.
  • 각각 Article 객체의 author와 content 속성을 설정. author와 content는 이 메소드 내에서 정의된 변수들로, Article 객체의 속성을 나타냄.
  • .build() 메소드를 호출하여 설정된 값을 기반으로 Article 객체를 생성

5-2. 서비스 메소드 코드 작성

service 패키지를 생성한 뒤, GuestBookService.java를 생성 후  GuestBookService 클래스 생성하고, 방명록(Guestbook)과 관련된 다양한 기능을 제공하는 메소드 작성

package com.example.guestBook.service;

import com.example.guestBook.domain.Article;
import com.example.guestBook.dto.AddArticleRequest;
import com.example.guestBook.dto.UpdateArticleRequest;
import com.example.guestBook.repository.GuestBookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@RequiredArgsConstructor // final이 붙거나 @NotNull 붙은 필드의 생성자 추가
@Service // 빈으로 등록
public class GuestBookService {
    private final GuestBookRepository guestBookRepository;

    // 블로그 글 추가 메소드
    public Article save(AddArticleRequest request) {
        return guestBookRepository.save(request.toEntity());
    }

    public List<Article> findAll() {
        return guestBookRepository.findAll();
    }

    // 방명록 하나 조회하는 메소드 추가
    public Article findById(Long id) {
        return guestBookRepository.findById(id).
                orElseThrow(() -> new IllegalArgumentException("not found: " + id));
    }

    // 방명록의 ID 받은 뒤 db에서 삭제
    public void delete(Long id) {
        guestBookRepository.deleteById(id);
    }

    // 방명록 수정하는 메소드
    @Transactional
    public Article update(Long id, UpdateArticleRequest request) {
        Article article = guestBookRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + id));

        article.update(request.getAuthor(), request.getContent());

        return article;
    }
}
  • @RequestArgsConstructor: 빈을 생성자로 생성하는 롬복에서 지원하는 애너테이션으로, final 키워드나 @NotNull이 붙은 필드로 생성자를 만들어 줌.
  • @ Service: 해당 클래스를 빈으로 서블릿 컨테이너에 등록해줌.
  • save(): JpaRepository에서 지원하는 저장 메서드. AddArtcicleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장.
  • findAll(): 데이터베이스에 저장된 모든 방명록 글을 조회하여 반환
  • findById(): 주어진 ID로 방명록 글을 조회하며, 해당 ID의 글이 존재하지 않을 경우 IllegalArgumentException을 발생
  • delete(): 방명록 글의 ID를 받은 뒤 JPA에서 제공하는 deleteById() 메소드를 이용해 데이터베이스에서 데이터 삭제 
  • update(): 주어진 ID의 방명록 글을 조회한 후 존재하지 않으면 예외를 발생시킨다. 그 후, 요청에서 전달받은 데이터로 글을 수정하고 해당 객체를 반환.
  • @Transactional 애너테이션: 이 메소드가 데이터베이스 트랜잭션을 지원하도록 함.

IllegalArgumentException: 오류 발생 지점에서 잘못 된 파라미터(인자)가 넘어갔을 때 발생하는 오류

 

5-3. 컨트롤러 메소드 코드 작성

URL에 매핑하기 위한 컨트롤러 메소드 추가.

컨트롤러 메소드에는 URL 매핑 애너테이션을 사용할 수 있다.

[주요 URL 매핑 애너테이션]

  • @GetMapping : HTTP 메소드가 GET일 때 요청받은 URL과 동일한 메서드와 매핑 ▶ 조회
  • @PostMapping: HTTP 메소드가 POST일 때 요청받은 URL과 동일한 메서드와 매핑 ▶ 입력
  • @PutMapping : HTTP 메소드가 PUT일 때 요청받은 URL과 동일한 메서드와 매핑 ▶ 수정
  • @DeleteMapping : HTTP 메소드가 DELETE일 때 요청받은 URL과 동일한 메서드와 매핑 ▶ 삭제

controller 패키지를 생성한 뒤, controller 패키지에서 GuestBookApiController.java 파일을 생성 후 컨트롤러 메소드 작성

package com.example.guestBook.controller;

import com.example.guestBook.domain.Article;
import com.example.guestBook.dto.AddArticleRequest;
import com.example.guestBook.dto.ArticleResponse;
import com.example.guestBook.dto.UpdateArticleRequest;
import com.example.guestBook.service.GuestBookService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController// HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class GuestBookApiController {

    private final GuestBookService guestBookService;

    // HTTP 메서드가 POST일 때 전달받은 URL 동일하면 메소드로 매핑
    @PostMapping("/api/guestbook")
    // 요청 본문 값 매핑
    public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request) {
        Article savedArticle = guestBookService.save(request);
        // 요청 자원이 성공적으로 생성되었으며 저장된 블로그 글 정보를 응답 객체에 담아 전송
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(savedArticle);
    }

    // 방명록 글 조회 후 반환
    @GetMapping("/api/guestbook")
    public ResponseEntity<List<ArticleResponse>> findAllArticles() {
        List<ArticleResponse> articles = guestBookService.findAll().stream()
                .map(ArticleResponse::new)
                .toList();

        return ResponseEntity.ok()
                .body(articles);
    }

    // GET 요청올 때 방명록 조회하기 위해 매핑할 메소드 작성
    @GetMapping("/api/guestbook/{id}")
    public ResponseEntity<ArticleResponse> findById(@PathVariable Long id) {
        Article article = guestBookService.findById(id);
        return ResponseEntity.ok()
                .body(new ArticleResponse(article));
    }

    // DELETE 요청이 오면 글 삭제 위한 메소드 작성
    @DeleteMapping("/api/guestbook/{id}")
    public ResponseEntity<Void> deleteArticle(@PathVariable Long id) {
        guestBookService.deleteArticle(id);
        return ResponseEntity.ok().build();
    }

    // PUT 요청이 오면 글을 수정하기 위한 UpdateArticle() 메소드
    @PutMapping("/api/guestbook/{id}")
    public ResponseEntity<Article> updateArticle(@PathVariable Long id, @RequestBody UpdateArticleRequest request) {
        Article updatedArticle = guestBookService.update(id, request);

        return ResponseEntity.ok()
                .body(updatedArticle);
    }
}
  • @RestController 애너테이션: HTTP 응답으로 객체 데이터를 JSON 형식으로 반환
  • @RequestBody 애너테이션: HTTP를 요청할 때 응답에 해당하는 값을 @RequestBody 애너테이션이 붙은 대상 객체인 AddArticleRequest에 매핑
  • @ResponseEntity.status().body(): 응답 코드로 201 (Created)를 응답하고 테이블에 저장된 객체 반환

6. 테스트 (포스트맨)

스프링 부트 서버(GuestBookApplication.java)를 실행한 후, 포스트맨을 켜서 기능이 정상적으로 작동되는지 확인한다.

package com.example.guestBook;

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

@SpringBootApplication
@EnableJpaAuditing // 수정된 날짜가 자동으로 업데이트하도록 설정
public class GuestBookApplication {

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

}

 

[테이블 생성]

서버를 실행한 뒤, MySQL Workbench를 실행하여 테이블이 잘 실행되었는지 확인

guestbook 테이블이 잘 생성되었고, SQL 문으로 데이터를 조회해보면 데이터가 잘 뜨는 것을 확인할 수 있다.

SELECT * FROM guestbook;

 

[조회]

HTTP 메소드를 GET으로 선택하고, url에 엔드포인트를 입력한 뒤 탭에서 Params를 선택하고 SEND를 누르면

데이터가 조회되는 것을 확인할 수 있다.

 

[특정 방명록 조회]

포스트맨에서 HTTP 메소드를 GET으로 설정하고, 엔드포인트를 입력(api/guestbook/특정id)하고 나서 Params에 두고 SEND 클릭

ID 5번의 방명록이 잘 조회되는 것을 확인할 수 있다.

 

[데이터 생성]

포스트맨에서 HTTP 메소드를 POST로 설정하고, 엔드포인트를 입력한 뒤(URL), Body - Raw - JSON으로 설정 후, 새로운 값을 입력하여 SEND를 누르면

새로운 값이 잘 저장되는 것을 확인할 수 있다.

 

[데이터 삭제]

방금 생성한 id 7의 방명록을 삭제하려면

HTTP 메소드: DELETE, 엔드포인트는 api/guestbook/7 로 지정하고, Params로 설정한 뒤 SEND를 누르면

방명록이 잘 삭제된 것을 확인할 수 있다.

 

[데이터 수정]

데이터 수정을 위해 방명록을 하나 생성한다 (엔드포인트는 api/guestbook)

id가 8로 생성된 것을 확인할 수 있는데,

 

수정을 위해 엔드포인트는 api/guestbook/8로 지정하고, HTTP 메소드는 PUT으로, Body - raw - JSON 으로 바꾼 뒤, 변경할 값을 입력해주면

작성자가 홍길순에서 홍길동으로 변경된 것을 확인할 수 있다.

 

이것으로 방명록 테이블의 생성, 데이터 삽입, 조회, 삭제, 수정 기능이 잘 구현된 것을 확인할 수 있다.


7. 테스트 코드 작성

보통 코드를 작성한 후, 해당 코드에 이상이 없는지 테스트 작업을 반복적으로 수행해야 하는데, 이를 줄여줄 테스트 코드를 작성한다.

GuestBookApiController 클래스에 alt+enter를 누르고 [테스트 생성]을 클릭하여 테스트 코드 파일을 생성한다.

이 파일은 /test/java/패키지 아래에 생성되는데, 이 파일의 코드를 입력해 준다.

이 테스트는 방명록 기능에 대한 다양한 시나리오를 검증하며, 각 테스트 메소드는 방명록의 CRUD(Create, Read, Update, Delete) 기능을 확인한다.

package com.example.guestBook.controller;

import com.example.guestBook.domain.Article;
import com.example.guestBook.dto.AddArticleRequest;
import com.example.guestBook.dto.UpdateArticleRequest;
import com.example.guestBook.repository.GuestBookRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMvc 생성
class GuestBookApiControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    GuestBookRepository guestBookRepository;

    @BeforeEach // 테스트 실행 전 실행 메소드
    public void mockMvcSetup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
        guestBookRepository.deleteAll();
    }

    @DisplayName("addArticle: 방명록 추가 성공")
    @Test
    public void addArticle() throws Exception {
        //given
        final String url = "/api/guestbook";
        final String author = "author";
        final String content = "content";
        final AddArticleRequest userRequest = new AddArticleRequest(author, content);

        // 객체 JSON으로 직렬화
        final String requestBody = objectMapper.writeValueAsString(userRequest);

        // when
        // 설정한 내용 바탕으로 요청 전송
        ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(requestBody));

        //then
        result.andExpect(status().isCreated());

        List<Article> articles = guestBookRepository.findAll();

        assertThat(articles.size()).isEqualTo(1); // 크기가 1인지 검증
        assertThat(articles.get(0).getAuthor()).isEqualTo(author);
        assertThat(articles.get(0).getContent()).isEqualTo(content);
    }

    @DisplayName("findAllArticles: 방명록 글 목록 조회 성공")
    @Test
    public void findAllArticles() throws Exception {
        //given
        final String url = "/api/guestbook";
        final String author = "author";
        final String content = "content";

        guestBookRepository.save(Article.builder()
                .author(author)
                .content(content)
                .build());

        //when
        final ResultActions resultActions = mockMvc.perform(get(url)
                .accept(MediaType.APPLICATION_JSON_VALUE));

        //then
        resultActions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].content").value(content))
                .andExpect(jsonPath("$[0].author").value(author));
    }

    @DisplayName("findArticle: 방명록 글 조회 성공")
    @Test
    public void findArticle() throws Exception {
        //given
        final String url = "/api/guestbook/{id}";
        final String author = "author";
        final String content = "content";

        Article savedArticle = guestBookRepository.save(Article.builder()
                .author(author)
                .content(content)
                .build());

        // when
        final ResultActions resultActions = mockMvc.perform(get(url, savedArticle.getId()));

        // then
        resultActions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").value(content))
                .andExpect(jsonPath("$.author").value(author));
    }

    @DisplayName("deleteArticle: 방명록 글 삭제 성공")
    @Test
    public void deleteArticle() throws Exception {
        //given
        final String url = "/api/guestbook/{id}";
        final String author = "author";
        final String content = "content";

        Article savedArticle = guestBookRepository.save(Article.builder()
                .author(author)
                .content(content)
                .build());

        //when
        mockMvc.perform(delete(url, savedArticle.getId()))
                .andExpect(status().isOk());

        //then
        List<Article> articles = guestBookRepository.findAll();
        assertThat(articles).isEmpty();
    }

    @DisplayName("updateArticle: 블로그 글 수정 성공")
    @Test
    public void updateArticle() throws Exception {
        //given
        final String url = "/api/guestbook/{id}";
        final String author = "author";
        final String content = "content";

        Article savedArticle = guestBookRepository.save(Article.builder()
                .author(author)
                .content(content)
                .build());

        final String newAuthor = "new Author";
        final String newContent = "new Content";

        UpdateArticleRequest request = new UpdateArticleRequest(newAuthor, newContent);

        // when
        ResultActions result = mockMvc.perform(put(url, savedArticle.getId())
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(request)));

        //then
        result.andExpect(status().isOk());

        Article article = guestBookRepository.findById(savedArticle.getId()).get();

        assertThat(article.getAuthor()).isEqualTo(newAuthor);
        assertThat(article.getContent()).isEqualTo(newContent);
    }

}

 

- 애너테이션

  • @SpringBootTest: Spring Boot 애플리케이션의 컨텍스트를 로드하여 테스트 환경 설정.
  • @AutoConfigureMockMvc: MockMvc를 자동으로 설정하여 HTTP 요청을 모방할 수 있게 함.
  • @DisplayName: 테스트 메소드에 설명을 추가하여 가독성을 높임.
  • @Test: 이 메소드가 JUnit 테스트 메소드임을 표시.

- 필드 정의

  • MockMvc: HTTP 요청을 테스트하기 위한 객체.
  • ObjectMapper: Java 객체를 JSON 형태로 변환하기 위한 도구. 요청 및 응답 본문의 직렬화와 역직렬화를 처리.
  • WebApplicationContext: Spring 애플리케이션의 웹 환경을 제공하며, MockMvc를 설정하는 데 사용.
  • GuestBookRepository: 데이터베이스와의 상호작용을 위한 리포지토리입니다. 방명록 데이터를 저장하고 조회하는 데 사용.

- 테스트 준비

  • @BeforeEach: 각 테스트 실행 전에 호출되며, MockMvc를 WebApplicationContext를 사용하여 설정하고, 테스트 데이터베이스를 초기화

 

- 방명록 추가 테스트

  • addArticle 메소드는 방명록을 추가하는 API가 제대로 작동하는지 확인. 요청 본문을 JSON으로 직렬화한 후, MockMvc를 사용해 POST 요청을 보낸다. 결과적으로 상태 코드가 201 Created인지, 데이터베이스에 추가된 방명록이 존재하는지를 검증.

- 모든 방명록 조회 테스트

  • findAllArticles 메소드는 모든 방명록을 조회하는 API가 제대로 작동하는지 테스트. 데이터베이스에 방명록을 추가한 후, GET 요청을 보내고, 반환된 데이터의 내용을 검증.

- 특정 방명록 조회 테스트

  • findArticle 메소드는 특정 ID의 방명록을 조회하는 API를 테스트. 방명록을 추가한 후, 해당 ID로 GET 요청을 보내고, 응답 데이터가 예상한 값과 일치하는지 검증

- 방명록 삭제 테스트

  • deleteArticle 메소드는 방명록을 삭제하는 API를 테스트. 방명록을 추가한 후, DELETE 요청을 보내고, 데이터베이스에서 방명록이 삭제되었는지 확인

- 방명록 수정 테스트

  • updateArticle 메소드는 특정 방명록 글을 수정하는 API의 기능이 제대로 작동하는지를 검증.

그리고 테스트 컨트롤러를 실행해보면 테스트 통과 여부를 안내해 준다.

 

5개의 메소드 테스트가 모두 통과한 것을 확인할 수 있다.


[참고]

스프링 부트 3 백엔드 개발자 되기

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%B9%8C%EB%8D%94Builder-%ED%8C%A8%ED%84%B4-%EB%81%9D%ED%8C%90%EC%99%95-%EC%A0%95%EB%A6%AC

https://velog.io/@minju0426/JPARepository%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90%EC%82%AC%EC%9A%A9%EB%B2%95-Method

https://jobc.tistory.com/120


다음 내용

 

[Java] Spring Boot: 테스트 코드

이전 내용 [Java] Spring Boot: 방명록 Rest API 구현(feat. MySQL 연동)이전 내용 [Java] Spring Boot: 코드, 요청&응답 과정 이해하기이전 내용 [Java] Spring Boot: Maven Repository이전 내용 [Java] 스프링부트: Spring Initial

puppy-foot-it.tistory.com

728x90
반응형