하위 태스크 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.javaMemberController 클래스를 생성한다. @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.javaMemberRequest 클래스를 생성한다.

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 상태 코드 적용

적절한 상태 코드 반환

MemberControllerApiResponse로 감싼 내용을 응답하고 적절한 상태 코드를 반환하도록 수정한 결과는 다음과 같다.

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: