IT기술/MSA (with. springboot)

Spring Boot REST API 완벽 구현 가이드: 스테레오타입 어노테이션을 활용한 호텔 관리 시스템

후스파 2025. 7. 10. 17:12
반응형

스프링 부트를 사용하여 간단한 REST API를 구현해보겠습니다. 이 예제에서는 스테레오타입 어노테이션을 활용하여 컨트롤러, 서비스, 리포지토리를 설정하고, JSON 형식으로 응답하는 REST API를 작성합니다.


프로젝트 구조

스프링 부트 프로젝트 구조는 다음과 같이 설정합니다:

src
└── main
    ├── java
    │   └── com
    │       └── example
    │           ├── MyApp.java
    │           ├── controller
    │           │   └── HotelController.java
    │           ├── service
    │           │   └── HotelService.java
    │           ├── repository
    │           │   └── HotelRepository.java
    │           └── model
    │               └── Hotel.java
    └── resources
        └── application.properties

클래스 설명

@SpringBootApplication

스프링 부트 애플리케이션의 시작점입니다.

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

모델 클래스: Hotel

호텔 정보를 담는 모델 클래스입니다.

package com.example.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class Hotel {
    @NotNull
    private Long id;

    @NotBlank
    private String name;

    @NotBlank
    private String location;

    private String description;
    private Double rating;

    // 기본 생성자
    public Hotel() {}

    // 매개변수 생성자
    public Hotel(Long id, String name, String location) {
        this.id = id;
        this.name = name;
        this.location = location;
    }

    // Getter 및 Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getLocation() { return location; }
    public void setLocation(String location) { this.location = location; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public Double getRating() { return rating; }
    public void setRating(Double rating) { this.rating = rating; }
}

리포지토리 클래스: HotelRepository

호텔 정보를 관리하는 리포지토리 클래스입니다. 간단한 메모리 기반 데이터 저장소를 사용합니다.

package com.example.repository;

import com.example.model.Hotel;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

@Repository
public class HotelRepository {
    private final List hotels = new ArrayList<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    // 초기 데이터 설정
    public HotelRepository() {
        hotels.add(new Hotel(idGenerator.getAndIncrement(), "Hilton Hotel", "New York"));
        hotels.add(new Hotel(idGenerator.getAndIncrement(), "Marriott Hotel", "Los Angeles"));
    }

    public List findAll() {
        return new ArrayList<>(hotels);
    }

    public Hotel save(Hotel hotel) {
        if (hotel.getId() == null) {
            hotel.setId(idGenerator.getAndIncrement());
        }

        // 기존 호텔 업데이트 또는 새 호텔 추가
        Optional existingHotel = findById(hotel.getId());
        if (existingHotel.isPresent()) {
            int index = hotels.indexOf(existingHotel.get());
            hotels.set(index, hotel);
        } else {
            hotels.add(hotel);
        }
        return hotel;
    }

    public Optional findById(Long id) {
        return hotels.stream()
                .filter(h -> h.getId().equals(id))
                .findFirst();
    }

    public boolean deleteById(Long id) {
        return hotels.removeIf(h -> h.getId().equals(id));
    }

    public List findByLocation(String location) {
        return hotels.stream()
                .filter(h -> h.getLocation().toLowerCase().contains(location.toLowerCase()))
                .toList();
    }
}

서비스 클래스: HotelService

비즈니스 로직을 처리하는 서비스 클래스입니다.

package com.example.service;

import com.example.model.Hotel;
import com.example.repository.HotelRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service
public class HotelService {

    @Autowired
    private HotelRepository hotelRepository;

    public List getAllHotels() {
        return hotelRepository.findAll();
    }

    public Hotel addHotel(Hotel hotel) {
        // 비즈니스 로직 검증
        if (hotel.getName() == null || hotel.getName().trim().isEmpty()) {
            throw new IllegalArgumentException("호텔 이름은 필수입니다.");
        }
        if (hotel.getLocation() == null || hotel.getLocation().trim().isEmpty()) {
            throw new IllegalArgumentException("호텔 위치는 필수입니다.");
        }

        return hotelRepository.save(hotel);
    }

    public Optional getHotelById(Long id) {
        return hotelRepository.findById(id);
    }

    public Hotel updateHotel(Long id, Hotel hotel) {
        Optional existingHotel = hotelRepository.findById(id);
        if (existingHotel.isPresent()) {
            hotel.setId(id);
            return hotelRepository.save(hotel);
        }
        throw new RuntimeException("호텔을 찾을 수 없습니다. ID: " + id);
    }

    public boolean deleteHotel(Long id) {
        if (hotelRepository.findById(id).isPresent()) {
            return hotelRepository.deleteById(id);
        }
        throw new RuntimeException("호텔을 찾을 수 없습니다. ID: " + id);
    }

    public List getHotelsByLocation(String location) {
        return hotelRepository.findByLocation(location);
    }
}

컨트롤러 클래스: HotelController

클라이언트의 요청을 처리하고 JSON 형식으로 응답하는 컨트롤러 클래스입니다.

package com.example.controller;

import com.example.model.Hotel;
import com.example.service.HotelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/v1/hotels")
@CrossOrigin(origins = "*")
public class HotelController {

    @Autowired
    private HotelService hotelService;

    // 모든 호텔 조회
    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity> getAllHotels(
            @RequestParam(required = false) String location) {

        List hotels;
        if (location != null && !location.trim().isEmpty()) {
            hotels = hotelService.getHotelsByLocation(location);
        } else {
            hotels = hotelService.getAllHotels();
        }

        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(hotels);
    }

    // 호텔 추가
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, 
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity addHotel(@Valid @RequestBody Hotel hotel) {
        try {
            Hotel savedHotel = hotelService.addHotel(hotel);
            return ResponseEntity.status(HttpStatus.CREATED)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(savedHotel);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().build();
        }
    }

    // 특정 호텔 조회
    @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity getHotelById(@PathVariable Long id) {
        Optional hotel = hotelService.getHotelById(id);

        return hotel.map(h -> ResponseEntity.ok()
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(h))
                .orElse(ResponseEntity.notFound().build());
    }

    // 호텔 정보 수정
    @PutMapping(value = "/{id}", 
                consumes = MediaType.APPLICATION_JSON_VALUE,
                produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity updateHotel(@PathVariable Long id, 
                                           @Valid @RequestBody Hotel hotel) {
        try {
            Hotel updatedHotel = hotelService.updateHotel(id, hotel);
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(updatedHotel);
        } catch (RuntimeException e) {
            return ResponseEntity.notFound().build();
        }
    }

    // 호텔 삭제
    @DeleteMapping("/{id}")
    public ResponseEntity deleteHotel(@PathVariable Long id) {
        try {
            boolean deleted = hotelService.deleteHotel(id);
            return deleted ? ResponseEntity.noContent().build() 
                          : ResponseEntity.notFound().build();
        } catch (RuntimeException e) {
            return ResponseEntity.notFound().build();
        }
    }
}

@ResponseBody와 HttpMessageConverter

@ResponseBody

  • 설명: @ResponseBody는 메소드가 반환하는 객체를 JSON으로 마샬링하여 HTTP 응답 본문에 직접 쓰는 역할을 합니다. 이 어노테이션이 선언된 메소드는 객체를 JSON 형식으로 변환하여 클라이언트에게 전달합니다
  • 적용 위치: 클래스 레벨에 선언하면 해당 클래스의 모든 메소드에 적용되며, 메소드 레벨에 선언하면 해당 메소드에만 적용됩니다

HttpMessageConverter

  • 역할: 스프링에서는 HttpMessageConverter를 사용하여 요청과 응답의 데이터를 변환합니다. 예를 들어, MappingJackson2HttpMessageConverter는 JSON 데이터를 처리하는 기본 변환기입니다
  • 작동 방식:
    • 클라이언트가 JSON 형식의 데이터를 보내면, HttpMessageConverter가 이를 Java 객체로 변환합니다
    • 서버가 Java 객체를 응답할 때, HttpMessageConverter가 이를 JSON 형식으로 변환하여 클라이언트에게 전송합니다

응답 메시지의 Content-Type

REST API는 JSON 형식으로 사용자에게 응답하며, 응답 메시지의 Content-Type은 application/json으로 설정됩니다. 이는 클라이언트가 응답을 올바르게 해석할 수 있도록 도와줍니다.


application.properties 설정

스프링 부트의 기본 설정 파일인 application.properties에서 필요한 기본 설정을 추가할 수 있습니다:

# 서버 포트 설정
server.port=8080

# 애플리케이션 이름
spring.application.name=hotel-management-api

# JSON 응답 포맷팅
spring.jackson.serialization.indent-output=true
spring.jackson.serialization.write-dates-as-timestamps=false

# 로깅 설정
logging.level.com.example=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n

# CORS 설정
management.endpoints.web.cors.allowed-origins=*
management.endpoints.web.cors.allowed-methods=GET,POST,PUT,DELETE

실행 및 테스트

이제 스프링 부트 애플리케이션을 실행하고, Postman이나 cURL을 사용하여 REST API를 테스트할 수 있습니다.

모든 호텔 조회

GET 요청: http://localhost:8080/api/v1/hotels

curl -X GET "http://localhost:8080/api/v1/hotels" \
     -H "Accept: application/json"

호텔 추가

POST 요청: http://localhost:8080/api/v1/hotels

curl -X POST "http://localhost:8080/api/v1/hotels" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{
       "name": "Grand Hotel",
       "location": "Paris",
       "description": "Luxury hotel in the heart of Paris",
       "rating": 4.8
     }'

Body (JSON):

{
    "name": "Hilton Hotel",
    "location": "New York",
    "description": "Premium business hotel",
    "rating": 4.5
}

특정 호텔 조회

GET 요청: http://localhost:8080/api/v1/hotels/1

curl -X GET "http://localhost:8080/api/v1/hotels/1" \
     -H "Accept: application/json"

호텔 정보 수정

PUT 요청: http://localhost:8080/api/v1/hotels/1

curl -X PUT "http://localhost:8080/api/v1/hotels/1" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{
       "name": "Updated Hilton Hotel",
       "location": "New York",
       "description": "Updated description",
       "rating": 4.7
     }'

호텔 삭제

DELETE 요청: http://localhost:8080/api/v1/hotels/1

curl -X DELETE "http://localhost:8080/api/v1/hotels/1"

위치별 호텔 검색

GET 요청: http://localhost:8080/api/v1/hotels?location=New York

curl -X GET "http://localhost:8080/api/v1/hotels?location=New%20York" \
     -H "Accept: application/json"

결론

이 예제에서는 스프링 부트를 사용하여 간단한 REST API를 구축하는 방법을 살펴보았습니다. 각 스테레오타입 어노테이션을 활용하여 컨트롤러, 서비스, 리포지토리 계층을 설정하고, JSON 형식으로 응답하는 REST API를 구현했습니다.
핵심 포인트:

  • @SpringBootApplication을 통한 자동 설정으로 빠른 개발 환경 구축
  • 스테레오타입 어노테이션(@Controller, @Service, @Repository)을 활용한 명확한 계층 분리
  • @RestController와 @ResponseBody를 통한 JSON 응답 자동화
  • HttpMessageConverter의 자동 변환 기능으로 객체-JSON 간 매핑
  • RESTful API 설계 원칙을 따른 명확한 엔드포인트 구조

이 구조를 기반으로 좀 더 복잡한 비즈니스 로직을 추가하여 확장할 수 있습니다. 데이터베이스 연동, 보안 설정, 예외 처리 등을 추가하여 실제 운영 환경에 적합한 API로 발전시킬 수 있습니다.

반응형