ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringBoot API(등록/수정/조회) 만들기
    DEV/SpringBoot 2025. 5. 12. 09:21

     

     

    Spring Boot는 Java 기반 애플리케이션을 쉽게 개발하고 배포할 수 있도록 도와주는 오픈소스 프레임워크예요.

    최소한의 설정으로 실행 가능한 독립적인 애플리케이션을 빠르게 만들 수 있도록 도와줍니다 🙌

     

     

     

     

     

    📌 등록/수정/조회 API 만들기 - 3계층 구조와 테스트까지

    Spring Boot로 웹 서비스를 개발하면서, REST API를 만드는 것은 매우 기본이지만 중요한 과정입니다.
    이번 포스팅에서는 Web Layer, Service Layer, Repository Layer로 나누고, DTO와 Domain Model을 구분하여

    API를 구현한 과정을 정리해보았습니다.


    ✅ API 구현을 위한 구조 설계

    📂 3계층 구조

    Layer 역할
    Web Layer API 요청을 받고 응답을 처리
    Service Layer 트랜잭션 흐름과 도메인 객체 간 로직 순서 제어
    Repository Layer DB 접근 및 CRUD 처리

    📌 DTO vs Domain Model

    • DTO (Data Transfer Object) : 외부 요청/응답에 사용하는 데이터 전용 객체
    • Domain Model (Entity) : DB 테이블과 매핑되는 핵심 비즈니스 객체

    ✅ 구현 클래스

    • PostsApiController (Web Layer)
    • PostsService (Service Layer)
    • PostsSaveRequestDto (DTO)
    • Posts (Domain Model / Entity)

    ✅ 테스트 기반 개발 흐름

    1. 처음에는 PostsApiController, PostsService, PostsSaveRequestDto를 작성
    2. PostsApiControllerTest를 작성하여 등록 API 정상 동작 확인
    3. 이후 PostsResponseDto, PostsUpdateRequestDto, Posts까지 확장하며 전체 등록/수정/조회 구조 구현
    4. 테스트도 PostsApiControllerTest를 수정하여 수정 API까지 확인

    ✅ 코드

    아래는 실제 구현한 클래스 및 테스트 코드입니다. 각 영역의 역할을 명확히 분리하고 DTO를 통해 안전하게 데이터 전송이 이루어집니다.

    🧩 PostsApiController.java

    PATH: /src/main/com.kior.blog.springboot/web/PostsApicontroller

    package com.kior.blog.springboot.web;
    
    import com.kior.blog.springboot.service.PostsService;
    import com.kior.blog.springboot.web.dto.PostsSaveRequestDto;
    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.*;
    
    @RequiredArgsConstructor
    @RestController
    public class PostsApiController {
    
        private final PostsService postsService;
    
        @PostMapping("/api/v1/posts")
        public Long save(@RequestBody PostsSaveRequestDto requestDto) {
            return postsService.save(requestDto);
        }
    }

    🧩 PostsService.java

    PATH: /src/main/com.kior.blog.springboot/service/PostsService

    package com.kior.blog.springboot.service;
    
    
    import com.kior.blog.springboot.domain.posts.PostsRepository;
    import com.kior.blog.springboot.web.dto.PostsSaveRequestDto;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @RequiredArgsConstructor
    @Service
    public class PostsService {
        private final PostsRepository postsRepository;
    
        @Transactional
        public Long save(PostsSaveRequestDto requestDto) {
            return postsRepository.save(requestDto.toEntity()).getId();
        }
    }

    🧩 PostsSaveRequestDto.java

    PATH: /src/main/com.kior.blog.springboot/web/dto/PostsSaveRequestDto

    package com.kior.blog.springboot.web.dto;
    
    import com.kior.blog.springboot.domain.posts.Posts;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    @Getter
    @NoArgsConstructor
    public class PostsSaveRequestDto {
        private String title;
        private String content;
        private String author;
    
        @Builder
        public PostsSaveRequestDto(String title, String content, String author) {
            this.title = title;
            this.content = content;
            this.author = author;
        }
    
        public Posts toEntity() {
            return Posts.builder()
                    .title(title)
                    .content(content)
                    .author(author)
                    .build();
        }
    
    }

    🧪 PostsApiControllerTest.java

    PATH: /src/test/com.kior.blog.springboot/web/PostsApiControllerTest

    package com.kior.blog.springboot.web;
    
    import com.kior.blog.springboot.domain.posts.Posts;
    import com.kior.blog.springboot.domain.posts.PostsRepository;
    import com.kior.blog.springboot.web.dto.PostsSaveRequestDto;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.web.client.TestRestTemplate;
    import org.springframework.boot.web.server.LocalServerPort;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    
    import java.util.List;
    
    import static org.assertj.core.api.Assertions.assertThat;
    
    
    @ExtendWith(SpringExtension.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class PostsApiControllerTest {
    
        @LocalServerPort
        private int port;
    
        @Autowired
        private TestRestTemplate restTemplate;
    
        @Autowired
        private PostsRepository postsRepository;
    
        @AfterEach
        public void tearDown() {
            postsRepository.deleteAll();
        }
    
        @Test
        public void Posts_등록된다() throws Exception {
            // given
            String title = "title";
            String content = "content";
            PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                    .title(title)
                    .content(content)
                    .author("author")
                    .build();
    
            String url = "http://localhost:" + port + "/api/v1/posts";
    
            // when
            ResponseEntity<Long> responseEntity = responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
    
            // then
            assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
            assertThat(responseEntity.getBody()).isGreaterThan(0L);
    
            List<Posts> all = postsRepository.findAll();
            assertThat(all.get(0).getTitle()).isEqualTo(title);
            assertThat(all.get(0).getContent()).isEqualTo(content);
        }
    }

    ✅ Test 컴파일 확인.

    1. 기존 HelloController 다르게 @WebMvcTest를 사용하지 않은 것은 JPA 동작하지 않기 때문입니다.
       그러므로 JPA 테스트할 때는 @SpringBootTest, TestRestTemplate를 사용하면 됩니다.

    2. RANDOM_PORT 실행과 insert 쿼리 동작을 확인할 수 있습니다.

     


     

    🧩 PostsUpdateRequestDto.java

    PATH: /src/main/com.kior.blog.springboot/web/dto/PostsUpdateRequestDto

    package com.kior.blog.springboot.web.dto;
    
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    @Getter
    @NoArgsConstructor
    public class PostsUpdateRequestDto {
        private String title;
        private String content;
    
        @Builder
        public PostsUpdateRequestDto(String title, String content) {
            this.title = title;
            this.content = content;
        }
    }

    🧩 PostsResponseDto.java

    PATH: /src/main/com.kior.blog.springboot/web/dto/PostsReponseDto

    package com.kior.blog.springboot.web.dto;
    
    import com.kior.blog.springboot.domain.posts.Posts;
    import lombok.Getter;
    
    @Getter
    public class PostsResponseDto {
    
        private Long id;
        private String title;
        private String content;
        private String author;
    
        public PostsResponseDto(Posts entity) {
            this.id = entity.getId();
            this.title = entity.getTitle();
            this.content = entity.getContent();
            this.author = entity.getAuthor();
        }
    }

    🧩 Posts.java (Entity)

    PATH: /src/main/com.kior.blog.springboot/domain.posts/Posts

    package com.kior.blog.springboot.domain.posts;
    
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    @Getter
    @NoArgsConstructor
    @Entity
    public class Posts {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(length = 500, nullable = false)
        private String title;
    
        @Column(columnDefinition = "TEXT", nullable = false)
        private String content;
    
        private String author;
    
        @Builder
        public Posts(String title, String content, String author) {
            this.title = title;
            this.content = content;
            this.author = author;
        }
    
        public void update(String title, String content) {
            this.title = title;
            this.content = content;
        }
    
    }

    🧩 (추가) PostsApiController.java

    (생략)...
    
    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
        return postsService.update(id, requestDto);
    }
    
    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDto findById(@PathVariable Long id) {
        return postsService.findById(id);
    }
    
    ...(생략)

    🧪 PostsApiControllerTest.java

    (생략)...
    
        @Test
        public void Posts_수정된다() throws Exception {
            //given
            Posts savedPosts = postsRepository.save(Posts.builder()
                    .title("title")
                    .content("content")
                    .author("author")
                    .build());
    
            Long updateId = savedPosts.getId();
            String expectedTitle = "title2";
            String expectedContent = "content2";
    
            PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                    .title(expectedTitle)
                    .content(expectedContent)
                    .build();
    
            String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
            HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
    
    
            //when
            ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);
    
            //then
            assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
            assertThat(responseEntity.getBody()).isGreaterThan(0L);
    
            List<Posts> all = postsRepository.findAll();
            assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
            assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
        }
    
    ...(생략)

     

    ✅ Test 컴파일 확인.

    1.  update 쿼리가 수행됨을 확인 가능.

     


    🖥️ 웹 콘솔로 결과 확인하기

    API 호출 후, 실제로 데이터가 잘 저장되었는지 확인하려면 H2 Database Console을 이용하면 아주 편리합니다.

    ✅ 1. application.properties에 설정 추가

    spring.jpa.show_sql=true
    spring.h2.console.enabled=true
    
    # H2 메모리 DB 설정
    spring.datasource.url=jdbc:h2:mem:testdb
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa
    spring.datasource.password=
    
    # JPA가 테이블 자동 생성하도록 설정
    spring.jpa.hibernate.ddl-auto=create-drop

    만약... 메모리 DB 설정이 없을 경우에는...

    TestConnection 할때 아래의 Error 메세지를 확인하게 됩니다. 그러니 당황하지 않고 application.properties에 DB 설정을 추가합니다.

     

    ✅ 2. 브라우저에서 H2 콘솔 접속

    Spring Boot 애플리케이션을 실행한 후, [ http://localhost:8080/h2-console ]로 접속합니다:

    접속하면 다음과 같은 내용으로 변경합니다.

    ^ Test Connection Success ^

     

     

     

    Connect(연결) 클릭하면 SQL 콘솔 화면이 나오며, 다음과 같은 쿼리를 직접 실행해볼 수 있습니다

     

     

    간단하게 insert쿼리를 통해서 데이터를 추가하고 조회해보겠습니다.

     

    ---

    TIP: H2는 인메모리 DB이므로 애플리케이션을 재시작하면 데이터가 초기화됩니다.
    테스트 확인 용도로만 사용하는 것이 좋습니다.

     

     

     

     

     

    ✅ 마무리하며

    브라우저에서 API 조회 기능을 확인하며 마무리 합니다.

     

     

    Spring Boot에서 API를 설계할 때 단순히 동작하는 코드가 아니었습니다.

     계층 간 역할을 명확히 나누고 테스트로 검증하는 개발 방식은 유지보수성과 안정성을 크게 높여준다고 합니다.

     

    특히 Service Layer는 단순히 로직을 넣는 곳이 아니라,

    트랜잭션을 묶고 도메인 로직의 흐름을 제어하는 역할이라는 점을 확인할 수 있었습니다.

     

     

     

    'DEV > SpringBoot' 카테고리의 다른 글

    SpringBoot JPA Auditing 생성/수정시간 자동화하기  (0) 2025.05.28
    SpringBoot Use JPA  (0) 2025.04.29
    SpringBoot Use Lombok  (0) 2025.03.26
    SpringBoot Hello Controller  (0) 2025.03.11
    SpringBoot used Git for Intellij  (0) 2025.03.07
Designed by Tistory.