00. 컨트롤러 작성
요청을 받아 처리할 컨트롤러를 작성하자.
@RestController
로 Json 형태로 객체 데이터를 반환함을 명시한다. @RequiredArgsConstructor
로 final 필드인 memberService를 주입받는다.
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
}
01. 회원 등록 API
가장 간단한 형태의 API를 만들어 보자.
MemberApiController 클래스 내에 saveMemberV1
를 다음과 같이 작성하였다.
@PostMapping("/api/v1/members")
public MemberApiController.CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
Long id = memberService.join(member);
return new MemberApiController.CreateMemberResponse(id);
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
saveMemberV1
를 살펴보자.
매개변수로 Member 엔티티를 직접 받고, 해당 엔티티를 @Valid
로 검증한다. 엔티티가 직접 노출된다. 엔티티를 건들면 API 스펙 자체가 바뀐다. API 스펙을 위한 별도의 Dto를 사용해야 한다. 회원가입 같은 경우를 보면, 간편가입, 자체가입 등 여러 방식이 있기 때문에 Member 엔티티 하나만으로 모든 요청에 대한 요구사항을 반영할 수 없다. API를 만들 때는 파라미터로 엔티티를 받으면 안된다. 엔티티를 외부에 노출하면 안된다. @Valid
도 엔티티에 작성하면 여러 요청에 대해 유연하게 대처할 수 없으므로 Dto에 적용시켜야 한다. 응답 형식은 id
필드만 반환하기 위해 CreateMemberResponse
클래스를 만들었다. 실행하면 아래와 같이 결과를 볼 수 있다.
V1에서의 문제점을 개선하여 saveMemberV2
를 다음과 같이 작성한다.
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
@NotEmpty
private String name;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
saveMemberV2
를 살펴보자.
매개변수로 CreateMemberRequest 클래스를 받는다. 간단히 name 필드만 갖고 있다. 검증을 위해 name 필드에 @NotEmpty
가 작성되어 있는 것을 볼 수 있다. 응답은 V1과 동일한 형태로 받는다. 실행하면 다음과 같은 결과를 볼 수 있다.
결론
API를 만들 때는 파라미터로 엔티티를 받으면 안된다.
엔티티를 외부에 노출해서는 안된다.
02. 회원 수정 API
MemberApiController 클래스 내에 updateMemberV2
를 다음과 같이 작성하였다.
@PutMapping("/api/v2/members/{id}")
public MemberApiController.UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id, @RequestBody @Valid MemberApiController.UpdateMemberRequest request) {
memberService.update(id, request.getName());
Member findMember = memberService.findOne(id);
return new MemberApiController.UpdateMemberResponse(findMember.getId(), findMember.getName());
}
@Data
static class UpdateMemberRequest {
private String name;
}
@Data
@AllArgsConstructor
static class UpdateMemberResponse {
private Long id;
private String name;
}
다음은 서비스 단 코드이다. (서비스 단의 전체 코드는 이 글 하단에 있다.)
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
}
컨트롤러에서는 pathVariable로 id를 받고, @RequestBody로 수정할 값을 넣는다.
엔티티를 수정할 때는 변경감지
를 사용한다. 영속 상태의 member를 setName으로 이름을 바꾸면 스프링 AOP가 동작하면서 @Transactional
어노테이션에 의해서 트랜잭션 관련 AOP가 끝나는 시점에 JPA가 flush를 하고 commit한다.
서비스의 update 메소드에서 Member를 반환해도 되는데, 그렇게 되면 영속 상태가 끊긴 것이 반환되는 것이다. 커맨드와 쿼리를 철저하게 분리한다는 원칙을 지키자. update의 반환타입은 void로 하고 컨트롤러에서 해당 id로 Member를 찾아서 그 id 값을 반환하면 된다. 이렇게 하면 유지보수 측면에서 좋다.
테스트를 하기 위해서 Member가 최소한 1개는 있어야 하기 때문에 회원 등록 API로 데이터를 넣은 후 진행했다.
03. 회원 조회 API
MemberApiController 클래스 내에 membersV1
를 다음과 같이 작성하였다.
@GetMapping("/api/v1/members")
public List<Member> membersV1() {
return memberService.findMembers();
}
다음은 서비스 단 코드이다. (서비스 단의 전체 코드는 이 글 하단에 있다.)
public List<Member> findMembers() {
return memberRepository.findAll();
}
membersV1
를 살펴보자.
지금 코드는 Member 엔티티와 관련된 모든 정보들을 반환하고 있다. 응답에 엔티티를 직접 노출하고 있는 것이다. 엔티티에서 @JsonIgnore
를 사용하면 응답에서 제외된다. 하지만, 클라이언트는 다양한 API 스타일을 원할 것이다. 어떤 응답에서는 특정 필드를 포함하고 싶을 것이고, 어떤 응답에서는 특정 필드가 필요없어서 포함하지 않아도 되는 경우가 있을 것이다.
테스트를 하기 위해서 회원 등록 API로 3개의 Member 엔티티를 넣어놨다. 다음은 실행 결과이다. Member 엔티티와 관련된 모든 정보들을 반환하고 있는 것을 볼 수 있다. 또한, 응답으로 바로 Array가 반환되고 있다.
위 사항을 고려하여 membersV2
를 다음과 같이 작성한다.
@GetMapping("/api/v2/members")
public MemberApiController.Result memberV2() {
List<Member> findMembers = memberService.findMembers();
List<MemberApiController.MemberDto> collect = findMembers.stream()
.map(m -> new MemberApiController.MemberDto(m.getName()))
.collect(Collectors.toList());
return new MemberApiController.Result(collect.size(), collect);
}
@Data
@AllArgsConstructor
static class Result<T> {
private int count;
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private String name;
}
V2에서는 응답을 위한 MemberDto 클래스를 정의했다. 그리고 응답 형태의 알관성을 주기 위해 Result 클래스도 작성하였다. 응답 스펙으로는 Object가 오는 것이 좋다. 바로 Array를 반환하게 되면 유연성이 확 줄어든다.
테스트 결과는 아래와 같다. 응답이 Object로 오는 것을 볼 수 있다.
04. MeberService 코드
@Service
@Transactional(readOnly = true) // 기본적으로 데이터의 변경은 트랜잭션 내에서 진행되어야 함
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 회원 가입
@Transactional
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId(); // 항상 값이 있다는 보장이 있음
}
// 중복 회원 검증, 최후의 검증으로 DB에서 unique 제약 조건 권장
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
// 회원 전체 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// 회원 한 명 조회
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
}
}
'API' 카테고리의 다른 글
API 개발 고급 - 간단한 주문 조회 V2: 엔티티를 DTO로 변환 (0) | 2024.02.05 |
---|---|
API 개발 고급 - 간단한 주문 조회 V1: 엔티티를 직접 노출 (0) | 2024.02.05 |