[실습] test CRUD controller 만들기

목적

Spring Boot와 JPA를 활용한 CRUD(Create, Read, Update, Delete) RESTful API 개발 과정을 실습을 통해 익힐 수 있습니다.

  • Spring Boot 아키텍처 이해 (Entity, repository, service, controller 계층)
  • RESTful API 설계 원칙 습득 (Http 메서드, 상태코드)
  • H2 인메모리 데이터베이스를 활용한 개발 환경 최적화

Prerequisite

  • 02-springboot에서 진행한 세팅 완료

folder structure

.
├── build.gradle
├── compose.yaml
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── ddcn41
    │   │           └── ticketing_system
    │   │               ├── config
    │   │               │   └── SecurityConfig.java
    │   │               ├── controller
    │   │               │   └── VenueController.java
    │   │               ├── dto
    │   │               │   └── VenueDto.java
    │   │               ├── entity
    │   │               │   ├── Booking.java
    │   │               │   ├── BookingSeat.java
    │   │               │   ├── Payment.java
    │   │               │   ├── Performance.java
    │   │               │   ├── PerformanceSchedule.java
    │   │               │   ├── Refund.java
    │   │               │   ├── ScheduleSeat.java
    │   │               │   ├── SeatLock.java
    │   │               │   ├── SystemMetric.java
    │   │               │   ├── User.java
    │   │               │   ├── Venue.java
    │   │               │   └── VenueSeat.java
    │   │               ├── repository
    │   │               │   └── VenueRepository.java
    │   │               ├── service
    │   │               │   └── VenueService.java
    │   │               └── TicketingSystemApplication.java
    │   └── resources
    │       ├── application.yml
    │       ├── data.sql

API 작성

Entity 작성

  • @Entity: JPA 엔티티임을 명시
  • @Table(name = "venues"): 테이블 명을 실세 데이터베이스와 동일하게 명시적으로 지정
  • Lombok 어노테이션: 보일러플레이트 코드를 자동 생성
  • @CreationTimestamp/@UpdateTimestamp: 생성/수정 시간을 자동으로 관리
  • 제약조건: nullable, length 등으로 데이터 무결성을 보장
package org.ddcn41.ticketing_system.entity;

@Entity
@Table(name = "venues")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Venue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "venue_id")
    private Long venueId;

    @Column(name = "venue_name", nullable = false)
    private String venueName;

    @Column(columnDefinition = "TEXT")
    private String address;

    @Column(columnDefinition = "TEXT")
    private String description;

    @Column(name = "total_capacity", nullable = false)
    @Builder.Default
    private Integer totalCapacity = 0;

    @Column(length = 100)
    private String contact;

    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "venue", cascade = CascadeType.ALL)
    private List<Performance> performances;

    @OneToMany(mappedBy = "venue", cascade = CascadeType.ALL)
    private List<VenueSeat> venueSeats;
}

Repository & DTO 작성

  • VenueRepository.java
    • Spring Data JPA의 기능을 활용하여 데이터 접근 계층을 구현
    • @Repository: Spring Framework에서 제공하는 데이터 접근 계층(DAO - Data Access Object)을 나타냄
package org.ddcn41.ticketing_system.repository;

@Repository
public interface VenueRepository extends JpaRepository<Venue, Long> {
    // 추가적인 쿼리 메소드들이 필요하면 여기에 추가
}
  • VenueDto.java
    • 클라이언트와 서버 간 데이터 전송을 위한 DTO 클래스를 정의
package org.ddcn41.ticketing_system.dto;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VenueDto {
    private Long venueId;
    private String venueName;
    private String address;
    private String description;
    private Integer totalCapacity;
    private String contact;
}

Service 작성

비즈니스 로직을 담당하는 Service 계층

package org.ddcn41.ticketing_system.service;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class VenueService {

    private final VenueRepository venueRepository;

    // 모든 공연장 조회
    public List<VenueDto> getAllVenues() {
        return venueRepository.findAll().stream()
                .map(this::convertToDto)
                .collect(Collectors.toList());
    }

    // 공연장 ID로 조회
    public VenueDto getVenueById(Long venueId) {
        Venue venue = venueRepository.findById(venueId)
                .orElseThrow(() -> new RuntimeException("Venue not found with id: " + venueId));
        return convertToDto(venue);
    }

    // 공연장 생성
    @Transactional
    public VenueDto createVenue(VenueDto venueDto) {
        Venue venue = Venue.builder()
                .venueName(venueDto.getVenueName())
                .address(venueDto.getAddress())
                .description(venueDto.getDescription())
                .totalCapacity(venueDto.getTotalCapacity())
                .contact(venueDto.getContact())
                .build();

        Venue savedVenue = venueRepository.save(venue);
        return convertToDto(savedVenue);
    }

    // 공연장 수정
    @Transactional
    public VenueDto updateVenue(Long venueId, VenueDto venueDto) {
        Venue venue = venueRepository.findById(venueId)
                .orElseThrow(() -> new RuntimeException("Venue not found with id: " + venueId));

        venue.setVenueName(venueDto.getVenueName());
        venue.setAddress(venueDto.getAddress());
        venue.setDescription(venueDto.getDescription());
        venue.setTotalCapacity(venueDto.getTotalCapacity());
        venue.setContact(venueDto.getContact());

        Venue updatedVenue = venueRepository.save(venue);
        return convertToDto(updatedVenue);
    }

    // 공연장 삭제
    @Transactional
    public void deleteVenue(Long venueId) {
        if (!venueRepository.existsById(venueId)) {
            throw new RuntimeException("Venue not found with id: " + venueId);
        }
        venueRepository.deleteById(venueId);
    }

    // Entity를 DTO로 변환
    private VenueDto convertToDto(Venue venue) {
        return VenueDto.builder()
                .venueId(venue.getVenueId())
                .venueName(venue.getVenueName())
                .address(venue.getAddress())
                .description(venue.getDescription())
                .totalCapacity(venue.getTotalCapacity())
                .contact(venue.getContact())
                .build();
    }
}

Controller 작성

RESTful API 엔드포인트를 제공하는 Controller 계층

package org.ddcn41.ticketing_system.controller;

@RestController
@RequestMapping("/api/venues")
@RequiredArgsConstructor
public class VenueController {

    private final VenueService venueService;

    // 모든 공연장 조회
    @GetMapping
    public ResponseEntity<List<VenueDto>> getAllVenues() {
        List<VenueDto> venues = venueService.getAllVenues();
        return ResponseEntity.ok(venues);
    }

    // 특정 공연장 조회
    @GetMapping("/{venueId}")
    public ResponseEntity<VenueDto> getVenueById(@PathVariable Long venueId) {
        VenueDto venue = venueService.getVenueById(venueId);
        return ResponseEntity.ok(venue);
    }

    // 공연장 생성
    @PostMapping
    public ResponseEntity<VenueDto> createVenue(@RequestBody VenueDto venueDto) {
        VenueDto createdVenue = venueService.createVenue(venueDto);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdVenue);
    }

    // 공연장 수정
    @PutMapping("/{venueId}")
    public ResponseEntity<VenueDto> updateVenue(@PathVariable Long venueId, @RequestBody VenueDto venueDto) {
        VenueDto updatedVenue = venueService.updateVenue(venueId, venueDto);
        return ResponseEntity.ok(updatedVenue);
    }

    // 공연장 삭제
    @DeleteMapping("/{venueId}")
    public ResponseEntity<Void> deleteVenue(@PathVariable Long venueId) {
        venueService.deleteVenue(venueId);
        return ResponseEntity.noContent().build();
    }
}

API 호출

curl -X POST http://localhost:8080/api/venues \
  -H "Content-Type: application/json" \
  -d '{
    "venueName": "서울 예술의 전당",
    "address": "서울특별시 서초구 남부순환로 2406",
    "description": "대한민국 최고의 공연장",
    "totalCapacity": 2000,
    "contact": "02-580-1300"
  }'
curl -X GET http://localhost:8080/api/venues
curl -X PUT http://localhost:8080/api/venues/1 \
  -H "Content-Type: application/json" \
  -d '{
    "venueName": "서울 예술의 전당 (수정됨)",
    "address": "서울특별시 서초구 남부순환로 2406",
    "description": "대한민국 최고의 공연장 (업데이트)",
    "totalCapacity": 2100,
    "contact": "02-580-1300"
  }'
curl -X DELETE http://localhost:8080/api/venues/1

실행 화면

test1 test2

Trouble Shooting

H2 데이터베이스에서 테이블이 자동 생성되지 않아 빌드마다 입력이 필요함

  • 애플리케이션 재시작 시마다 H2 인메모리 데이터베이스의 테이블이 사라짐
  • JPA의 ddl-auto 설정만으로는 복잡한 테이블 구조와 인덱스가 제대로 생성되지 않음
  • 매번 수동으로 테이블을 생성해야 하는 번거로움 발생

해결

application.yaml에 h2 설정을 수정하고, resources에 sql 문을 미리 정의하여 둔다. 이렇게 하면 로컬에서 개발을 빠르게 진행하고, 추후 클라우드로 옮겨 같은 코드를 그대로 유지/개발할 수 있다.

References

Restful API guide

Spring Data JPA

Spring Web MVC

Lombok

HTTP Status Codes

results matching ""

    No results matching ""