HATEOAS와 커스텀 예외 처리를 포함한 REST API 구축
📂 controller
│ └─ MemberController.java
📂 domain
│ └─ Member.java
📂 exception
│ └─ MemberNotFoundException.java
│ └─ MemberNotFoundExceptionHandler.java
📂 repository
│ └─ MemberRepository.java
📂 service
│ └─ MemberService.java
└─ Week5Application.java
h2 database와 spring 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
@Table(name = "Members")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
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에 해당하는 멤버가 존재하지 않을 때, 커스텀 예외 처리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
를 이용하여 반환package com.example.demo.exception;
public class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException(String message) {
super(message);
}
}
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);
}
}
localhost:8080/h2-console/
로 들어가야 한다!(@PathVariable("id") Long id)
이렇게 "id"까지 명시해주면 해결된다. Gradle로 빌드하면 안그래도 된다고 한다. velog.io/@ghwns9991build.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 구현
EntityModel
또는CollectionModel
사용RepresentationModel
사용