하위 태스크 1
프로젝트 구조 파악
restful 프로젝트의 구조와 기존 코드 확인
Member 엔티티와 MemberRepository 인터페이스를 확인한다.
model/Member.java:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@Column(unique = true)
private String email;
private String password;
private Integer age;
private Boolean enabled;
@ToString.Exclude
@JsonIgnore
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Article> articles;
}repository/MemberRepository.java:
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByName(String name);
List<Member> findByNameAndEmail(String name, String email);
List<Member> findByNameOrEmail(String name, String email);
List<Member> findByNameContaining(String name);
List<Member> findByNameLike(String name);
List<Member> findByAgeIs(Integer age);
List<Member> findByAgeIsNull();
List<Member> findByAgeIsNotNull();
List<Member> findByAgeGreaterThan(Integer age);
List<Member> findByAgeGreaterThanEqual(Integer age);
List<Member> findByAgeLessThan(Integer age);
List<Member> findByAgeLessThanEqual(Integer age);
// JPA Query Methods for ORDER BY
List<Member> findAllByOrderByNameAsc();
List<Member> findAllByOrderByNameDesc();
List<Member> findAllByOrderByNameAscAgeDesc();
// JPA Query Methods for WHERE ORDER BY
List<Member> findByNameContainingOrderByNameAsc(String name);
// JPQL (Java Persistence Query Language)
@Query("select m from Member m where m.enabled = :active and m.age >= 19 and m.email is not null order by m.name")
List<Member> getActiveAdultWithEmail(@Param("active") boolean active);
@Query(value = "select * from member where enabled = :active and age >= 19 and email is not null order by name", nativeQuery = true)
List<Member> getActiveAdultWithEmailByNative(@Param("active") boolean active);
}하위 태스크 2
MemberController 생성
@RestController와 @RequestMapping 적용
controller/MemberController.java에 MemberController 클래스를 생성한다. @RestController와 @RequestMapping 어노테이션을 적용한다.
@RestController
@RequestMapping("/api/members")
public class MemberController {
@Autowired
private MemberRepository memberRepository;
}하위 태스크 3
GET 전체 조회 구현
@GetMapping으로 모든 멤버 조회
모든 멤버를 조회하는 getAllMembers 메서드를 정의한다. @GetMapping 어노테이션을 적용해 GET /api/members 엔드포인트에 매핑한다.
// ...
public class MemberController {
// ...
@GetMapping
public List<Member> getAllMembers() {
return memberRepository.findAll();
}
}하위 태스크 4
GET 단일 조회 구현
@PathVariable을 사용한 단일 멤버 조회
단일 멤버를 조회하는 getMember 메서드를 정의한다. 메서드에 @GetMapping 어노테이션을 적용하고 매개변수에 @PathVariable 어노테이션을 적용해 GET /api/members/:id 엔드포인트에 매핑한다.
// ...
public class MemberController {
// ...
@GetMapping("/{id}")
public Member getMember(@PathVariable Long id) {
return memberRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Member not found"));
}
}하위 태스크 5
POST 데이터 생성 구현
@PostMapping과 @RequestBody 사용
멤버를 생성하는 createMember 메서드를 정의한다. 메서드에 @PostMapping 어노테이션, 매개변수에 @PostMapping 어노테이션을 적용해 POST /api/members/:id 엔드포인트에 매핑한다.
// ...
public class MemberController {
// ...
@PostMapping
public ResponseEntity<Member> createMember(@RequestBody Member member) {
Member savedMember = memberRepository.save(member);
return ResponseEntity.status(HttpStatus.CREATED).body(savedMember);
}
}하위 태스크 6
DTO 클래스 생성
MemberRequest DTO 생성 (선택사항)
dto/MemberRequest.java에 MemberRequest 클래스를 생성한다.
public class MemberRequest {
private String name;
private String email;
}하위 태스크 7
유효성 검증 추가
@Valid를 사용한 입력 데이터 검증
build.gradle에 유효성 검사 의존성을 추가한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
// ...
}Gradle 프로젝트를 동기화 한다.
DTO에 구체적인 제약 조건을 명시한다.
dto/MemberRequest.java:
@Data
public class MemberRequest {
@NotBlank
private String name;
@NotBlank
@Email
private String email;
}MemberController 클래스의 메서드 매개변수에 @Valid 어노테이션을 적용한다.
controller/MemberController.java:
// ...
public class MemberController {
// ...
@PostMapping
public ResponseEntity<Member> createMember(@Valid @RequestBody Member member) {
// ...
}
}하위 태스크 8
PUT 데이터 수정 구현
@PutMapping을 사용한 전체 수정
멤버를 수정(전체)하는 updateMember 메서드를 정의한다. 메서드에 @PutMapping 어노테이션을 적용해 PUT /api/members/:id 엔드포인트에 매핑한다.
// ...
public class MemberController {
// ...
@PutMapping("/{id}")
public ResponseEntity<Member> updateMember(
@PathVariable Long id,
@RequestBody Member member) {
Member existingMember = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Member not found"));
existingMember.setName(member.getName());
existingMember.setEmail(member.getEmail());
Member updatedMember = memberRepository.save(existingMember);
return ResponseEntity.ok(updatedMember);
}
}하위 태스크 9
PATCH 부분 수정 구현
@PatchMapping을 사용한 부분 수정 (선택사항)
멤버를 수정(부분)하는 patchMember 메서드를 정의한다. 메서드에 @PatchMapping 어노테이션을 적용해 PATCH /api/members/:id 엔드포인트에 매핑한다.
// ...
public class MemberController {
// ...
@PatchMapping("/{id}")
public ResponseEntity<Member> patchMember(
@PathVariable Long id,
@RequestBody Member member) {
Member existingMember = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Member not found"));
if (member.getName() != null) {
existingMember.setName(member.getName());
}
if (member.getEmail() != null) {
existingMember.setEmail(member.getEmail());
}
Member updatedMember = memberRepository.save(existingMember);
return ResponseEntity.ok(updatedMember);
}
}하위 태스크 10
DELETE 데이터 삭제 구현
@DeleteMapping을 사용한 삭제
멤버를 삭제하는 deleteMember 메서드를 정의한다. 메서드에 @DeleteMapping 어노테이션을 적용해 DELETE /api/members/:id 엔드포인트에 매핑한다.
// ...
public class MemberController {
// ...
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMember(@PathVariable Long id) {
memberRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
}하위 태스크 11
공통 응답 DTO 생성
ApiResponse 클래스 생성
공통 응답 DTO인 ApiResponse 클래스를 작성한다.
dto/ApiResponse.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
}하위 태스크 12
전역 예외 처리 구현
@ControllerAdvice를 사용한 예외 처리
GlobalExceptionHandler 클래스를 생성해 전역 예외 처리 핸들러를 작성한다.
exception/GlobalExceptionHandler.java:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ApiResponse<Void>> handleException(RuntimeException e) {
ApiResponse<Void> response = new ApiResponse<>();
response.setSuccess(false);
response.setMessage(e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
}하위 태스크 13
HTTP 상태 코드 적용
적절한 상태 코드 반환
MemberController가 ApiResponse로 감싼 내용을 응답하고 적절한 상태 코드를 반환하도록 수정한 결과는 다음과 같다.
controller/MemberController.java:
@RestController
@RequestMapping("/api/members")
public class MemberController {
@Autowired
private MemberRepository memberRepository;
@GetMapping
public ResponseEntity<ApiResponse<List<Member>>> getAllMembers() {
ApiResponse<List<Member>> response = new ApiResponse<>(true, "Success", memberRepository.findAll());
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Member>> getMember(@PathVariable Long id) {
Member existingMember = memberRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Member not found"));
ApiResponse<Member> response = new ApiResponse<>(true, "Success", existingMember);
return ResponseEntity.ok(response);
}
@PostMapping
public ResponseEntity<ApiResponse<Member>> createMember(@Valid @RequestBody Member member) {
Member savedMember = memberRepository.save(member);
ApiResponse<Member> response = new ApiResponse<>(true, "Success", savedMember);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Member>> updateMember(
@PathVariable Long id,
@RequestBody Member member) {
Member existingMember = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Member not found"));
existingMember.setName(member.getName());
existingMember.setEmail(member.getEmail());
Member updatedMember = memberRepository.save(existingMember);
ApiResponse<Member> response = new ApiResponse<>(true, "Success", updatedMember);
return ResponseEntity.ok(response);
}
@PatchMapping("/{id}")
public ResponseEntity<ApiResponse<Member>> patchMember(
@PathVariable Long id,
@RequestBody Member member) {
Member existingMember = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Member not found"));
if (member.getName() != null) {
existingMember.setName(member.getName());
}
if (member.getEmail() != null) {
existingMember.setEmail(member.getEmail());
}
Member updatedMember = memberRepository.save(existingMember);
ApiResponse<Member> response = new ApiResponse<>(true, "Success", updatedMember);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMember(@PathVariable Long id) {
memberRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
}하위 태스크 14
API 테스트
Postman을 사용한 모든 엔드포인트 테스트
Postman의 대안인 Insomnia를 사용해 모든 엔드포인트를 테스트한다.
GET /api/members:

GET /api/members/:id:

POST /api/members:

PUT /api/members/:id:

PATCH /api/members/:id:

DELETE /api/members/:id:
