Spring Boot에서의 REST API 작성 예시 (2/4)

2025. 4. 26. 21:55·Study

본 문서는 REST API 시리즈 중 2편입니다. 1편에서는 REST API의 기본 개념과 설계 원칙을 다루었으며, 이번 편에서는 Spring Boot를 활용한 구현 방법을 설명합니다.

1. Spring Boot 소개

Spring Boot는 Java 기반의 애플리케이션 개발을 단순화하는 Spring Framework의 확장으로, 복잡한 설정 없이도 빠르게 프로덕션 수준의 애플리케이션을 개발할 수 있게 해줍니다.

 

특징 설명

자동 설정 의존성을 기반으로 애플리케이션 설정을 자동화
내장 서버 Tomcat, Jetty, Undertow 등의 서버를 내장
스타터 의존성 목적별로 필요한 의존성을 묶어 제공
모니터링 Actuator를 통한 애플리케이션 모니터링 지원
프로덕션 준비 메트릭, 상태 확인, 외부화된 설정 등 지원

 

2. 개발 환경 설정

2.1 필수 도구

  • JDK 17 이상
  • Maven 3.6+ 또는 Gradle 7.0+
  • IDE (IntelliJ IDEA, Eclipse, VS Code 등)

2.2 프로젝트 생성

Spring Initializr(https://start.spring.io/)를 통해 프로젝트를 생성할 수 있습니다:

설정 항목 권장 설정

Project Maven 또는 Gradle
Language Java
Spring Boot 3.2.x (최신 안정 버전)
Packaging Jar
Dependencies Spring Web, Spring Data JPA, H2 Database, Lombok, Validation

 

3. REST API 프로젝트 구조

 

Spring Boot 프로젝트의 표준 구조:

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           └── demo/
│   │               ├── DemoApplication.java
│   │               ├── controller/
│   │               │   └── UserController.java
│   │               ├── service/
│   │               │   └── UserService.java
│   │               ├── repository/
│   │               │   └── UserRepository.java
│   │               ├── entity/
│   │               │   └── User.java
│   │               ├── dto/
│   │               │   ├── UserRequestDto.java
│   │               │   └── UserResponseDto.java
│   │               ├── exception/
│   │               │   ├── GlobalExceptionHandler.java
│   │               │   ├── ResourceNotFoundException.java
│   │               │   └── ErrorResponse.java
│   │               └── config/
│   │                   └── SwaggerConfig.java
│   └── resources/
│       ├── application.properties
│       ├── application-dev.properties
│       └── application-prod.properties
└── test/
    └── java/
        └── com/
            └── example/
                └── demo/
                    ├── controller/
                    │   └── UserControllerTest.java
                    └── service/
                        └── UserServiceTest.java

 

4. 기본 의존성 설정

4.1 Maven (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>rest-api-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rest-api-demo</name>
    <description>Demo REST API project with Spring Boot</description>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- SpringDoc OpenAPI (Swagger) -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.2.0</version>
        </dependency>
        
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

4.2 Gradle (build.gradle)

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

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

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

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

 

5. 애플리케이션 설정

5.1 application.properties

properties
# 서버 설정
server.port=8080
server.servlet.context-path=/api

# 데이터베이스 설정
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 콘솔 설정
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# 로깅 설정
logging.level.org.springframework=INFO
logging.level.com.example=DEBUG

# OpenAPI 설정
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.operationsSorter=method

 

6. 모델 및 엔티티 정의

6.1 User 엔티티

package com.example.demo.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 50)
    private String name;
    
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    @Column(length = 20)
    private String phone;
    
    @Column(nullable = false)
    private LocalDateTime createdAt;
    
    @Column
    private LocalDateTime updatedAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

 

6.2 DTO (Data Transfer Objects)

 

UserRequestDto.java

package com.example.demo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserRequestDto {
    
    @NotBlank(message = "이름은 필수 입력 항목입니다")
    @Size(min = 2, max = 50, message = "이름은 2~50자 사이여야 합니다")
    private String name;
    
    @NotBlank(message = "이메일은 필수 입력 항목입니다")
    @Email(message = "유효한 이메일 형식이 아닙니다")
    private String email;
    
    @Pattern(regexp = "^[0-9]{10,15}$", message = "전화번호는 10~15자의 숫자여야 합니다")
    private String phone;
}

 

UserResponseDto.java

package com.example.demo.dto;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserResponseDto {
    
    private Long id;
    private String name;
    private String email;
    private String phone;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

 

7. Repository 계층

7.1 UserRepository.java

package com.example.demo.repository;

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

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    Optional<User> findByEmail(String email);
    
    List<User> findByNameContainingIgnoreCase(String name);
    
    boolean existsByEmail(String email);
}

 

8. Service 계층

8.1 UserService 인터페이스

package com.example.demo.service;

import java.util.List;

import com.example.demo.dto.UserRequestDto;
import com.example.demo.dto.UserResponseDto;

public interface UserService {
    
    UserResponseDto createUser(UserRequestDto userRequestDto);
    
    UserResponseDto getUserById(Long id);
    
    List<UserResponseDto> getAllUsers();
    
    List<UserResponseDto> searchUsersByName(String name);
    
    UserResponseDto updateUser(Long id, UserRequestDto userRequestDto);
    
    void deleteUser(Long id);
}

8.2 UserServiceImpl.java

package com.example.demo.service.impl;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.dto.UserRequestDto;
import com.example.demo.dto.UserResponseDto;
import com.example.demo.entity.User;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.exception.ResourceAlreadyExistsException;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
    
    private final UserRepository userRepository;
    
    @Override
    @Transactional
    public UserResponseDto createUser(UserRequestDto userRequestDto) {
        // 이메일 중복 검사
        if (userRepository.existsByEmail(userRequestDto.getEmail())) {
            throw new ResourceAlreadyExistsException("이미 등록된 이메일입니다: " + userRequestDto.getEmail());
        }
        
        User user = User.builder()
                .name(userRequestDto.getName())
                .email(userRequestDto.getEmail())
                .phone(userRequestDto.getPhone())
                .build();
        
        User savedUser = userRepository.save(user);
        
        return mapToResponseDto(savedUser);
    }
    
    @Override
    @Transactional(readOnly = true)
    public UserResponseDto getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("ID에 해당하는 사용자를 찾을 수 없습니다: " + id));
        
        return mapToResponseDto(user);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserResponseDto> getAllUsers() {
        return userRepository.findAll().stream()
                .map(this::mapToResponseDto)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserResponseDto> searchUsersByName(String name) {
        return userRepository.findByNameContainingIgnoreCase(name).stream()
                .map(this::mapToResponseDto)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional
    public UserResponseDto updateUser(Long id, UserRequestDto userRequestDto) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("ID에 해당하는 사용자를 찾을 수 없습니다: " + id));
        
        // 이메일이 변경되었고, 그 이메일이 이미 존재하는 경우
        if (!user.getEmail().equals(userRequestDto.getEmail()) &&
                userRepository.existsByEmail(userRequestDto.getEmail())) {
            throw new ResourceAlreadyExistsException("이미 등록된 이메일입니다: " + userRequestDto.getEmail());
        }
        
        user.setName(userRequestDto.getName());
        user.setEmail(userRequestDto.getEmail());
        user.setPhone(userRequestDto.getPhone());
        
        User updatedUser = userRepository.save(user);
        
        return mapToResponseDto(updatedUser);
    }
    
    @Override
    @Transactional
    public void deleteUser(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("ID에 해당하는 사용자를 찾을 수 없습니다: " + id));
        
        userRepository.delete(user);
    }
    
    private UserResponseDto mapToResponseDto(User user) {
        return UserResponseDto.builder()
                .id(user.getId())
                .name(user.getName())
                .email(user.getEmail())
                .phone(user.getPhone())
                .createdAt(user.getCreatedAt())
                .updatedAt(user.getUpdatedAt())
                .build();
    }
}

 

9. Controller 계층

9.1 UserController.java

package com.example.demo.controller;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.example.demo.dto.UserRequestDto;
import com.example.demo.dto.UserResponseDto;
import com.example.demo.service.UserService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
@Tag(name = "User Controller", description = "사용자 관리 API")
public class UserController {
    
    private final UserService userService;
    
    @PostMapping
    @Operation(summary = "사용자 생성", description = "새로운 사용자를 생성합니다.")
    @ApiResponse(responseCode = "201", description = "사용자 생성 성공")
    public ResponseEntity<UserResponseDto> createUser(@Valid @RequestBody UserRequestDto userRequestDto) {
        UserResponseDto createdUser = userService.createUser(userRequestDto);
        return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
    }
    
    @GetMapping("/{id}")
    @Operation(summary = "사용자 조회", description = "ID로 사용자를 조회합니다.")
    @ApiResponse(responseCode = "200", description = "조회 성공")
    @ApiResponse(responseCode = "404", description = "사용자 없음", content = @Content)
    public ResponseEntity<UserResponseDto> getUserById(
            @Parameter(description = "사용자 ID") @PathVariable Long id) {
        UserResponseDto user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping
    @Operation(summary = "사용자 목록 조회", description = "모든 사용자 목록을 조회하거나 이름으로 검색합니다.")
    @ApiResponse(responseCode = "200", description = "조회 성공")
    public ResponseEntity<List<UserResponseDto>> getUsers(
            @Parameter(description = "검색할 사용자 이름 (선택사항)") 
            @RequestParam(required = false) String name) {
        List<UserResponseDto> users = (name != null) 
                ? userService.searchUsersByName(name)
                : userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    @PutMapping("/{id}")
    @Operation(summary = "사용자 정보 수정", description = "ID로 사용자 정보를 수정합니다.")
    @ApiResponse(responseCode = "200", description = "수정 성공")
    @ApiResponse(responseCode = "404", description = "사용자 없음", content = @Content)
    public ResponseEntity<UserResponseDto> updateUser(
            @Parameter(description = "사용자 ID") @PathVariable Long id,
            @Valid @RequestBody UserRequestDto userRequestDto) {
        UserResponseDto updatedUser = userService.updateUser(id, userRequestDto);
        return ResponseEntity.ok(updatedUser);
    }
    
    @DeleteMapping("/{id}")
    @Operation(summary = "사용자 삭제", description = "ID로 사용자를 삭제합니다.")
    @ApiResponse(responseCode = "204", description = "삭제 성공")
    @ApiResponse(responseCode = "404", description = "사용자 없음", content = @Content)
    public ResponseEntity<Void> deleteUser(
            @Parameter(description = "사용자 ID") @PathVariable Long id) {
        userService.deleteUser(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

 

10. 예외 처리

10.1 사용자 정의 예외 클래스

ResourceNotFoundException.java

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

 

ResourceAlreadyExistsException.java

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.CONFLICT)
public class ResourceAlreadyExistsException extends RuntimeException {
    
    public ResourceAlreadyExistsException(String message) {
        super(message);
    }
}

 

10.2 글로벌 예외 핸들러

ErrorResponse.java

package com.example.demo.exception;

import java.time.LocalDateTime;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
    
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
    private List<ValidationError> validationErrors;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ValidationError {
        private String field;
        private String message;
    }
}

 

GlobalExceptionHandler.java

package com.example.demo.exception;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import lombok.extern.slf4j.Slf4j;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {
        
        ErrorResponse errorResponse = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.NOT_FOUND.value())
                .error(HttpStatus.NOT_FOUND.getReasonPhrase())
                .message(ex.getMessage())
                .path(request.getDescription(false).replace("uri=", ""))
                .build();
        
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(ResourceAlreadyExistsException.class)
    public ResponseEntity<ErrorResponse> handleResourceAlreadyExistsException(
            ResourceAlreadyExistsException ex, WebRequest request) {
        
        ErrorResponse errorResponse = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.CONFLICT.value())
                .error(HttpStatus.CONFLICT.getReasonPhrase())
                .message(ex.getMessage())
                .path(request.getDescription(false).replace("uri=", ""))
                .build();
        
        return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex, WebRequest request) {
        
        List<ErrorResponse.ValidationError> validationErrors = new ArrayList<>();
        
        for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
            ErrorResponse.ValidationError validationError = ErrorResponse.ValidationError.builder()
                    .field(fieldError.getField())
                    .message(fieldError.getDefaultMessage())
                    .build();
            
            validationErrors.add(validationError);
        }
        
        ErrorResponse errorResponse = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.BAD_REQUEST.value())
                .error(HttpStatus.BAD_REQUEST.getReasonPhrase())
                .message("입력값 검증에 실패했습니다")
                .path(request.getDescription(false).replace("uri=", ""))
                .validationErrors(validationErrors)
                .build();
        
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(
            Exception ex, WebRequest request) {
        
        log.error("Unexpected error occurred", ex);
        
        ErrorResponse errorResponse = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
                .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
                .message("서버 내부 오류가 발생했습니다. 관리자에게 문의하세요.")
                .path(request.getDescription(false).replace("uri=", ""))
                .build();
        
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

11. API 문서화 설정 (Swagger/OpenAPI)

11.1 OpenAPI 설정

package com.example.demo.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("Spring Boot REST API")
                .description("Spring Boot을 이용한 REST API 예제")
                .version("v1.0.0")
                .contact(new Contact()
                    .name("API Support")
                    .url("https://www.example.com")
                    .email("support@example.com"))
                .license(new License()
                    .name("Apache 2.0")
                    .url("https://www.apache.org/licenses/LICENSE-2.0")))
            .servers(List.of(
                new Server()
                    .url("http://localhost:8080/api")
                    .description("Local Development Server")
            ));
    }
}

 

12. 단위 테스트

12.1 Service 계층 테스트 (UserServiceTest.java)

package com.example.demo.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

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

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.example.demo.dto.UserRequestDto;
import com.example.demo.dto.UserResponseDto;
import com.example.demo.entity.User;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.exception.ResourceAlreadyExistsException;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.impl.UserServiceImpl;

import java.time.LocalDateTime;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserServiceImpl userService;
    
    private User user;
    private UserRequestDto userRequestDto;
    
    @BeforeEach
    void setUp() {
        LocalDateTime now = LocalDateTime.now();
        
        user = User.builder()
                .id(1L)
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .createdAt(now)
                .build();
        
        userRequestDto = UserRequestDto.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
    }
    
    @Test
    @DisplayName("사용자 생성 성공 테스트")
    void createUserSuccess() {
        // Given
        when(userRepository.existsByEmail(anyString())).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(user);
        
        // When
        UserResponseDto result = userService.createUser(userRequestDto);
        
        // Then
        assertNotNull(result);
        assertEquals(user.getId(), result.getId());
        assertEquals(user.getName(), result.getName());
        assertEquals(user.getEmail(), result.getEmail());
        verify(userRepository, times(1)).existsByEmail(anyString());
        verify(userRepository, times(1)).save(any(User.class));
    }
    
    @Test
    @DisplayName("중복 이메일로 사용자 생성 실패 테스트")
    void createUserFailDuplicateEmail() {
        // Given
        when(userRepository.existsByEmail(anyString())).thenReturn(true);
        
        // When & Then
        assertThrows(ResourceAlreadyExistsException.class, () -> {
            userService.createUser(userRequestDto);
        });
        verify(userRepository, times(1)).existsByEmail(anyString());
        verify(userRepository, never()).save(any(User.class));
    }
    
    @Test
    @DisplayName("ID로 사용자 조회 성공 테스트")
    void getUserByIdSuccess() {
        // Given
        when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
        
        // When
        UserResponseDto result = userService.getUserById(1L);
        
        // Then
        assertNotNull(result);
        assertEquals(user.getId(), result.getId());
        assertEquals(user.getName(), result.getName());
        verify(userRepository, times(1)).findById(anyLong());
    }
    
    @Test
    @DisplayName("존재하지 않는 ID로 사용자 조회 실패 테스트")
    void getUserByIdNotFound() {
        // Given
        when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
        
        // When & Then
        assertThrows(ResourceNotFoundException.class, () -> {
            userService.getUserById(1L);
        });
        verify(userRepository, times(1)).findById(anyLong());
    }
    
    @Test
    @DisplayName("모든 사용자 조회 테스트")
    void getAllUsers() {
        // Given
        User user2 = User.builder()
                .id(2L)
                .name("Jane Doe")
                .email("jane@example.com")
                .phone("9876543210")
                .createdAt(LocalDateTime.now())
                .build();
        
        when(userRepository.findAll()).thenReturn(Arrays.asList(user, user2));
        
        // When
        List<UserResponseDto> result = userService.getAllUsers();
        
        // Then
        assertNotNull(result);
        assertEquals(2, result.size());
        verify(userRepository, times(1)).findAll();
    }
}

 

12.2 Controller 계층 테스트 (UserControllerTest.java)

package com.example.demo.controller;

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

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.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import com.example.demo.dto.UserRequestDto;
import com.example.demo.dto.UserResponseDto;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebMvcTest(UserController.class)
public class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private UserService userService;
    
    private UserRequestDto userRequestDto;
    private UserResponseDto userResponseDto;
    
    @BeforeEach
    void setUp() {
        LocalDateTime now = LocalDateTime.now();
        
        userRequestDto = UserRequestDto.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        userResponseDto = UserResponseDto.builder()
                .id(1L)
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .createdAt(now)
                .build();
    }
    
    @Test
    @DisplayName("사용자 생성 API 테스트")
    void createUserTest() throws Exception {
        when(userService.createUser(any(UserRequestDto.class))).thenReturn(userResponseDto);
        
        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userRequestDto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.name", is("John Doe")))
                .andExpect(jsonPath("$.email", is("john@example.com")));
    }
    
    @Test
    @DisplayName("ID로 사용자 조회 API 테스트")
    void getUserByIdTest() throws Exception {
        when(userService.getUserById(anyLong())).thenReturn(userResponseDto);
        
        mockMvc.perform(get("/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.name", is("John Doe")))
                .andExpect(jsonPath("$.email", is("john@example.com")));
    }
    
    @Test
    @DisplayName("존재하지 않는 ID로 사용자 조회 API 테스트")
    void getUserByIdNotFoundTest() throws Exception {
        when(userService.getUserById(anyLong())).thenThrow(new ResourceNotFoundException("ID에 해당하는 사용자를 찾을 수 없습니다: 1"));
        
        mockMvc.perform(get("/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound());
    }
    
    @Test
    @DisplayName("모든 사용자 조회 API 테스트")
    void getAllUsersTest() throws Exception {
        UserResponseDto user2 = UserResponseDto.builder()
                .id(2L)
                .name("Jane Doe")
                .email("jane@example.com")
                .phone("9876543210")
                .createdAt(LocalDateTime.now())
                .build();
        
        List<UserResponseDto> users = Arrays.asList(userResponseDto, user2);
        
        when(userService.getAllUsers()).thenReturn(users);
        
        mockMvc.perform(get("/users")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()", is(2)))
                .andExpect(jsonPath("$[0].id", is(1)))
                .andExpect(jsonPath("$[0].name", is("John Doe")))
                .andExpect(jsonPath("$[1].id", is(2)))
                .andExpect(jsonPath("$[1].name", is("Jane Doe")));
    }
    
    @Test
    @DisplayName("사용자 수정 API 테스트")
    void updateUserTest() throws Exception {
        when(userService.updateUser(anyLong(), any(UserRequestDto.class))).thenReturn(userResponseDto);
        
        mockMvc.perform(put("/users/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userRequestDto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.name", is("John Doe")))
                .andExpect(jsonPath("$.email", is("john@example.com")));
    }
    
    @Test
    @DisplayName("사용자 삭제 API 테스트")
    void deleteUserTest() throws Exception {
        doNothing().when(userService).deleteUser(anyLong());
        
        mockMvc.perform(delete("/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNoContent());
    }
    
    @Test
    @DisplayName("이름으로 사용자 검색 API 테스트")
    void searchUsersByNameTest() throws Exception {
        List<UserResponseDto> users = Arrays.asList(userResponseDto);
        
        when(userService.searchUsersByName(anyString())).thenReturn(users);
        
        mockMvc.perform(get("/users?name=John")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()", is(1)))
                .andExpect(jsonPath("$[0].id", is(1)))
                .andExpect(jsonPath("$[0].name", is("John Doe")));
    }
}

 

13. 통합 테스트

13.1 리포지토리 통합 테스트 (UserRepositoryIntegrationTest.java)

package com.example.demo.repository;

import static org.junit.jupiter.api.Assertions.*;

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

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.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;

import com.example.demo.entity.User;

@DataJpaTest
public class UserRepositoryIntegrationTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @DisplayName("사용자 저장 테스트")
    void saveUserTest() {
        // Given
        User user = User.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        // When
        User savedUser = userRepository.save(user);
        
        // Then
        assertNotNull(savedUser);
        assertNotNull(savedUser.getId());
        assertEquals(user.getName(), savedUser.getName());
        assertEquals(user.getEmail(), savedUser.getEmail());
        assertNotNull(savedUser.getCreatedAt());
    }
    
    @Test
    @DisplayName("이메일로 사용자 조회 테스트")
    void findByEmailTest() {
        // Given
        User user = User.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        userRepository.save(user);
        
        // When
        Optional<User> foundUser = userRepository.findByEmail("john@example.com");
        
        // Then
        assertTrue(foundUser.isPresent());
        assertEquals(user.getName(), foundUser.get().getName());
    }
    
    @Test
    @DisplayName("이름으로 사용자 검색 테스트")
    void findByNameContainingIgnoreCaseTest() {
        // Given
        User user1 = User.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        User user2 = User.builder()
                .name("Jane Doe")
                .email("jane@example.com")
                .phone("9876543210")
                .build();
        
        User user3 = User.builder()
                .name("Bob Smith")
                .email("bob@example.com")
                .phone("5555555555")
                .build();
        
        userRepository.saveAll(List.of(user1, user2, user3));
        
        // When
        List<User> result = userRepository.findByNameContainingIgnoreCase("doe");
        
        // Then
        assertEquals(2, result.size());
        assertTrue(result.stream().anyMatch(user -> user.getName().equals("John Doe")));
        assertTrue(result.stream().anyMatch(user -> user.getName().equals("Jane Doe")));
    }
    
    @Test
    @DisplayName("이메일 존재 여부 확인 테스트")
    void existsByEmailTest() {
        // Given
        User user = User.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        userRepository.save(user);
        
        // When & Then
        assertTrue(userRepository.existsByEmail("john@example.com"));
        assertFalse(userRepository.existsByEmail("nonexistent@example.com"));
    }
}

 

13.2 컨트롤러 통합 테스트 (UserControllerIntegrationTest.java)

package com.example.demo.controller;

import static org.hamcrest.CoreMatchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.dto.UserRequestDto;
import com.example.demo.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class UserControllerIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }
    
    @Test
    @DisplayName("사용자 생성 통합 테스트")
    void createUserIntegrationTest() throws Exception {
        UserRequestDto userRequestDto = UserRequestDto.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userRequestDto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.name", is("John Doe")))
                .andExpect(jsonPath("$.email", is("john@example.com")));
    }
    
    @Test
    @DisplayName("중복 이메일로 사용자 생성 실패 통합 테스트")
    void createUserWithDuplicateEmailIntegrationTest() throws Exception {
        // 첫 번째 사용자 생성
        UserRequestDto user1 = UserRequestDto.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user1)))
                .andExpect(status().isCreated());
        
        // 동일한 이메일로 두 번째 사용자 생성 시도
        UserRequestDto user2 = UserRequestDto.builder()
                .name("Jane Doe")
                .email("john@example.com")  // 동일한 이메일
                .phone("9876543210")
                .build();
        
        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user2)))
                .andExpect(status().isConflict());
    }
    
    @Test
    @DisplayName("사용자 조회 및 수정 통합 테스트")
    void getUserAndUpdateIntegrationTest() throws Exception {
        // 사용자 생성
        UserRequestDto createDto = UserRequestDto.builder()
                .name("John Doe")
                .email("john@example.com")
                .phone("1234567890")
                .build();
        
        String createResponse = mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDto)))
                .andExpect(status().isCreated())
                .andReturn().getResponse().getContentAsString();
        
        int userId = objectMapper.readTree(createResponse).get("id").asInt();
        
        // 사용자 조회
        mockMvc.perform(get("/users/" + userId)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("John Doe")));
        
        // 사용자 정보 수정
        UserRequestDto updateDto = UserRequestDto.builder()
                .name("John Updated")
                .email("john.updated@example.com")
                .phone("5555555555")
                .build();
        
        mockMvc.perform(put("/users/" + userId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateDto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("John Updated")))
                .andExpect(jsonPath("$.email", is("john.updated@example.com")));
        
        // 수정된 사용자 조회
        mockMvc.perform(get("/users/" + userId)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("John Updated")));
    }
}

 

14. API 배포 및 모니터링

14.1 Docker를 이용한 애플리케이션 컨테이너화

Dockerfile

FROM eclipse-temurin:17-jdk-alpine

WORKDIR /app

COPY target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/userdb
      - SPRING_DATASOURCE_USERNAME=postgres
      - SPRING_DATASOURCE_PASSWORD=postgres
    depends_on:
      - db
      
  db:
    image: postgres:14.1-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=userdb
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

 

14.2 애플리케이션 모니터링

Spring Boot Actuator를 사용하여 모니터링 구성:

의존성 추가 (pom.xml에 추가)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Prometheus 메트릭 내보내기 (선택 사항) -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

application.properties 설정

# Actuator 설정
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always
management.health.diskspace.enabled=true
management.health.db.enabled=true

# 애플리케이션 정보
info.app.name=REST API Demo
info.app.description=Spring Boot으로 구현한 REST API
info.app.version=1.0.0

 

15. 보안 구현

15.1 Spring Security 설정

의존성 추가 (pom.xml에 추가)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT 지원 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

SecurityConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/auth/**", "/h2-console/**", "/swagger-ui/**", "/api-docs/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .headers().frameOptions().sameOrigin(); // H2 콘솔 사용을 위한 설정
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

16. 결론

Spring Boot를 사용하여 RESTful API를 개발하는 방법에 대해 알아보았습니다. 이 문서에서는 프로젝트 설정부터 구현, 테스트, 배포까지의 전체 과정을 다루었습니다.

Spring Boot는 다음과 같은 장점을 제공합니다:

  • 자동 설정을 통한 빠른 개발 환경 구성
  • 다양한 스타터 패키지를 통한 손쉬운 의존성 관리
  • 내장 서버를 통한 간편한 배포
  • 풍부한 테스트 지원
  • 다양한 모니터링 및 보안 기능

REST API를 구현할 때는 다음 사항을 고려해야 합니다:

  • 적절한 계층 구조 설계 (Controller, Service, Repository)
  • DTO 패턴을 통한 데이터 전송 객체 분리
  • 일관된 예외 처리 및 응답 포맷 정의
  • 단위 테스트 및 통합 테스트를 통한 품질 보증
  • API 문서화를 통한 개발자 경험 향상

 

17. 후속 시리즈 안내

본 문서는 REST API 구현 시리즈의 두 번째 편입니다. 시리즈의 다음 주제는 다음과 같습니다:

시리즈 제목 주요 내용

1편 REST API의 기본 개념과 설계 원칙 REST 아키텍처, HTTP 메서드, 상태 코드, 설계 모범 사례 등
3편 Nest.JS에서의 REST API 작성 예시 Node.js 기반 Nest.JS 프레임워크를 이용한 REST API 구현
4편 그 외 플랫폼에서의 REST API 작성 예시 Django, Flask, Express.js 등 다양한 프레임워크에서의 구현

각 편에서는 해당 프레임워크의 특성을 살린 REST API 설계 및 구현 방법, 보안 처리, 테스트 방법 등을 실제 코드 예제와 함께 제공할 예정입니다.

 

18. 참고 자료

  • Spring Boot 공식 문서
  • Spring Data JPA 공식 문서
  • Spring Security 공식 문서
  • Spring REST API 모범 사례
  • SpringDoc OpenAPI
  • Docker 공식 문서
  • JUnit 5 사용자 가이드

 

19. 마무리

Spring Boot는 Java 기반의 백엔드 개발에서 가장 인기 있는 프레임워크 중 하나로, RESTful API 개발에 특히 적합합니다. 이 문서에서 살펴본 것처럼 Spring Boot는 강력한 기능과 유연성을 제공하면서도, 개발자가 비즈니스 로직에 집중할 수 있도록 많은 보일러플레이트 코드를 제거해 줍니다.

REST API 개발에 있어서 Spring Boot의 주요 강점은 다음과 같습니다:

  1. 개발 생산성 - 자동 설정과 스타터 의존성으로 개발 속도를 높입니다.
  2. 확장성 - 마이크로서비스 아키텍처에 적합한 독립적인 서비스 구성이 가능합니다.
  3. 통합 용이성 - 다양한 데이터베이스, 캐시, 메시징 시스템 등과 쉽게 통합됩니다.
  4. 보안 - Spring Security를 통한 강력한 보안 기능을 제공합니다.
  5. 테스트 지원 - 단위 테스트와 통합 테스트를 위한 다양한 도구를 제공합니다.
  6. 모니터링 - Actuator를 통한 애플리케이션 상태 모니터링이 가능합니다.

이 문서에서 제공한 예제 코드와 설명을 통해 Spring Boot로 견고하고 확장 가능한 REST API를 개발하는 데 필요한 기본 지식을 얻으셨기를 바랍니다. REST API 개발은 단순히 기술적 구현을 넘어 좋은 설계와 사용자 경험을 고려하는 것이 중요합니다. 1편에서 다룬 REST 설계 원칙과 모범 사례를 함께 적용하면, 더욱 효과적인 API를 구축할 수 있을 것입니다.

다음 편에서는 Node.js 생태계의 Nest.JS 프레임워크를 활용한 REST API 개발 방법을 살펴보겠습니다. 이를 통해 다양한 백엔드 기술 스택에서의 REST API 구현 방식을 비교하고, 각 프레임워크의 장단점을 이해할 수 있을 것입니다.

감사합니다.

반응형
저작자표시 비영리 변경금지 (새창열림)

'Study' 카테고리의 다른 글

그 외 플랫폼에서의 REST API 작성 예시 (4/4)  (0) 2025.04.26
Nest.JS에서의 REST API 작성 예시 (3/4)  (0) 2025.04.26
REST API 란 무엇인가? (1/4)  (0) 2025.04.26
Spring JPA에서 PostgreSQL JSONB 타입 활용하기  (1) 2025.04.21
'Study' 카테고리의 다른 글
  • 그 외 플랫폼에서의 REST API 작성 예시 (4/4)
  • Nest.JS에서의 REST API 작성 예시 (3/4)
  • REST API 란 무엇인가? (1/4)
  • Spring JPA에서 PostgreSQL JSONB 타입 활용하기
모리군
모리군
    반응형
  • 모리군
    나의 일상 그리고 취미
    모리군
  • 전체
    오늘
    어제
    • 분류 전체보기 (23)
      • 독백 (0)
      • Study (11)
        • Authentication (4)
        • Supabase (2)
      • Javascript (3)
        • node.js (0)
        • react.js (0)
        • vue.js (0)
        • LeetCode (3)
      • AI (0)
      • PHP (0)
      • HTML, CSS (0)
      • 툴, 플러그인 (0)
      • 취미 (1)
        • 보드게임 (1)
      • 교통 (0)
        • 철도 (0)
        • 도로 (0)
      • 부동산 (2)
        • 서울 (0)
        • 경기 성남 (0)
        • 경기 수원 (0)
        • 경기 화성 (2)
        • 경기 남양주 (0)
        • 광주 (0)
      • 역사 (4)
        • 이 주의 역사 (1)
      • 영어기사 읽기 (1주일 1번) (0)
        • 스포츠 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ChatGPT
    한국발명
    express
    토큰관리
    java
    프롬프트엔지니어링
    PostgreSQL
    JWT
    임베딩
    카카오T 대리
    Spring Boot
    윌리엄 딕슨
    FastAPI
    node.js
    백엔드
    광주민주화운동
    OpenAI
    algorithm
    웹 서비스
    벡터데이터베이스
    typescript
    supabase
    백엔드개발
    슈파베이스
    REST API
    초기 영화
    LeetCode
    Rag
    대리운전
    javascript
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
모리군
Spring Boot에서의 REST API 작성 예시 (2/4)
상단으로

티스토리툴바