[Spring 실습] REST API 설계

Jiwoo Jung·2024년 11월 16일
0

GDGoC Spring 스터디

목록 보기
10/15

HATEOAS와 커스텀 예외 처리를 포함한 REST API 구축

프로젝트 구조

📂 controller      
│  └─ MemberController.java
📂 domain         
│  └─ Member.java
 📂 exception        
│  └─ MemberNotFoundException.java
│  └─ MemberNotFoundExceptionHandler.java
📂 repository     
│  └─ MemberRepository.java
📂 service          
│  └─ MemberService.java
└─ Week5Application.java

HATEOAS 없이 구현

h2 databasespring jpa 사용

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
}

application.properties

// h2 database
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

// jpa
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=create
// jpa log
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

spring.jpa.hibernate.ddl-auto=create: 엔티티를 보고 자동으로 데이터베이스 생성

Member

package com.example.demo.domain;

import jakarta.persistence.*;
import org.springframework.hateoas.RepresentationModel;

@Entity
@Table(name = "Members")
public class Member extends RepresentationModel<Member> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    // Constructor
    public Member() {}

    public Member(String username) {
        this.username = username;
    }

    // Getters and Setters
}
  • @Entity
    이 클래스가 JPA 엔티티임을 선언
    데이터베이스 테이블과 매핑되며, JPA가 이 클래스를 사용하여 데이터베이스 작업(저장, 조회, 수정, 삭제)을 수행
  • @Table(name = "Members")
    이 엔티티 클래스가 데이터베이스의 어떤 테이블과 매핑될지 지정.
  • @Id
    이 필드가 Primary Key(기본 키)임을 지정.
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    id 필드가 자동으로 생성되도록 설정.
  • private Long id;
    엔티티의 Primary Key로, 데이터베이스에서 자동으로 생성되는 필드.
  • private String username;
    엔티티의 username 속성으로, 데이터베이스 테이블의 열(column)에 매핑.

MemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> { }
  • JpaRepository<Entity 클래스, PK 타입>

  • JpaRepository는 PagingAndSortingRepository, QueryByExampleExecutor 인터페이스를 상속

  • PagingAndSortingRepository는 CrudRepository 인터페이스를 상속

  • CrudRepository 인터페이스는 기본적인 CRUD 메소드 제공
    : save(), findById(), existsById(), count(), deleteById(), delete(), deleteAll()

  • QueryByExampleExecutor 인터페이스는 더 다양한 CRUD 메소드 제공
    : findOne(), findAll(), count(), exists()

MemberService

package com.example.demo.service;

import com.example.demo.exception.MemberNotFoundException;
import org.springframework.stereotype.Service;
import com.example.demo.domain.Member;
import com.example.demo.repository.MemberRepository;

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

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public List<Member> getAllMembers() {
        return memberRepository.findAll();
    }

    public Member getMemberById(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(() -> new MemberNotFoundException("No member found with ID " + id));
    }

    public Member createMember(Member member) {
        return memberRepository.save(member);
    }

    public Member updateMember(Long id, Member updatedMember) {
        Member member = getMemberById(id);
        member.setUsername(updatedMember.getUsername());
        return memberRepository.save(member);
    }

    public void deleteMember(Long id) {
        getMemberById(id); // id 존재 여부 확인하면서 예외 처리
        memberRepository.deleteById(id);
    }
}
  • getMemberById()에서 id에 해당하는 멤버가 존재하지 않을 때, 커스텀 예외 처리

MemberController

package com.example.demo.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.demo.domain.Member;
import com.example.demo.service.MemberService;

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

@RestController
@RequestMapping("/api/members")
public class MemberController {

    private final MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    // GET /api/members
    @GetMapping
    public ResponseEntity<List<Member>> getAllMembers(){
        return ResponseEntity.ok(memberService.getAllMembers());
    }

    // GET /api/members/{id}
    @GetMapping("/{id}")
    public ResponseEntity<Member> getMemberById(@PathVariable Long id) {
        return ResponseEntity.ok(memberService.getMemberById(id));
    }

    // POST /api/members
    @PostMapping
    public ResponseEntity<Member> createMember(@RequestBody Member member) {
        Member createdMember = memberService.createMember(member);
        return ResponseEntity.status(201).body(createdMember);
    }

    // PUT /api/members/{id}
    @PutMapping("/{id}")
    public ResponseEntity<Member> updateMember(@PathVariable Long id, @RequestBody Member member) {
        return ResponseEntity.ok(memberService.updateMember(id, member));
    }

    // DELETE /api/members/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<String> deleteMember(@PathVariable Long id) {
        memberService.deleteMember(id);
        return ResponseEntity.noContent().build();
    }
}
  • ResponseEntity를 이용하여 반환

MemberNotFoundException

package com.example.demo.exception;

public class MemberNotFoundException extends RuntimeException {
    public MemberNotFoundException(String message) {
        super(message);
    }
}

MemberNotFoundExceptionHandler

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class MemberNotFoundExceptionHandler {

    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity<Object> handleMemberNotFound(MemberNotFoundException ex) {
        Map<String, Object> response = new HashMap<>();
        response.put("error", "Member Not Found");
        response.put("message", ex.getMessage());
        response.put("timestamp", LocalDateTime.now());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }
}
  • Object를 반환해서 에러 종류, 메세지, 타임스탬프를 반환해준다.

TroubleShooting

  • h2 콘솔 잘못 들어감 -> localhost:8080/h2-console/로 들어가야 한다!
  • id를 넘겨줘야하는 메소드들에서 오류가 났다. 찾아보니, (@PathVariable("id") Long id) 이렇게 "id"까지 명시해주면 해결된다. Gradle로 빌드하면 안그래도 된다고 한다. velog.io/@ghwns9991

실행 결과

POST

GET

PUT

DELETE

Custom Exception

  • 에러 종류, 메세지, 타임스탬프가 잘 반환되었다.

HATEOAS 구현

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-hateoas'
}

MemberController

1. HATEOAS 모델 적용
EntityModel 또는 CollectionModel을 사용하여 링크 추가.
2. 링크 생성
WebMvcLinkBuilder를 사용해 관련 메서드와 리소스를 연결.

    // GET /api/members/{id}
    @GetMapping("/{id}")
    public ResponseEntity<EntityModel<Member>> getMemberById(@PathVariable("id") Long id) {
        Member member = memberService.getMemberById(id);

        // HATEOAS 링크 추가
        EntityModel<Member> memberModel = EntityModel.of(member,
                WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(MemberController.class).getMemberById(id)).withSelfRel(),
                WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(MemberController.class).getAllMembers()).withRel("all-members")
        );

        return ResponseEntity.ok(memberModel);
    }

HATEOAS 구현

  1. EntityModel 또는 CollectionModel 사용
  2. RepresentationModel 사용

실행 결과

0개의 댓글