[JPA 활용 2편] 1. API 개발 기본

HJ·2024년 2월 14일
0

JPA 활용 2편

목록 보기
1/4
post-thumbnail
post-custom-banner

김영한 님의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의를 보고 작성한 내용입니다.


1. 회원 등록 API

1-1. Entity 를 RequestBody 에 직접 매핑

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    private final MemberService memberService;

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberResponse {
        private Long id;
        public CreateMemberResponse(Long id) {
            this.id = id;
        }
    }
}

API 요청을 보낼 때 Body 에 아무 값도 넣지 않아도 정상적으로 저장이 됩니다. 왜냐하면 Member 에 유효성 검사에 필요한 어노테이션이 없기 때문입니다.

Member 의 필드에 @NotEmpty 와 같은 어노테이션을 넣으면 오류를 반환됩니다.
이때 스프링이 만들어놓은 에러 메세지가 반환되기 때문에 원하는 형식으로 처리하기 위해 @ControllerAdvice 를 사용할 수 있습니다.

참고로 @RequestBody 는 JSON 으로 받아온 데이터를 객체에 매핑해서 넣어주고, @ResponseBody 는 메서드가 반환하는 객체를 JSON 형식으로 변환하여 전송합니다.


1-2. Entity 매핑 시 문제점

  1. Controller 까지를 presentation 계층이라고 하는데, 검증을 위해 Member Entity 에 @NotEmpty 와 같은 어노테이션을 사용하면, Entity 에 presentation 계층을 위한 로직이 추가가 되는 것입니다.

  2. Member Entity 를 위한 여러 API 가 만들어지는데 API 마다 @NotEmpty 와 같은 것들이 필요할 수도, 필요하지 않을 수도 있습니다.

  3. Member 의 name 을 userName 으로 변경한다고 했을 때, Entity 를 RequestBody 에 직접 매핑을 하게 되면 name 으로 보내지 않고 userName 으로 보내주어야 하기 때문에 API 스펙에 변화가 생기게 됩니다.

➜ 해결 : API 스펙을 위한 별도의 DTO 를 생성해서 파라미터로 받아야 합니다.


1-3. DTO를 RequestBody에 매핑

public class MemberApiController {
    ...
    @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 {
        private String name;
    }
}

CreateMemberRequest 라는 DTO 를 만들고, name 값을 전달 받을 수 있도록 합니다. 그 후 전달 받은 값으로 Entity 를 만들어 Service 에 넘겨줍니다.

DTO 를 사용함으로써 Entity 가 변화되어도 API 스펙은 변하지 않습니다. 또 DTO 를 보고 어떤 값이 전달되는지와 같은 API 스펙이 어떤지 알 수 있습니다.

정말 중요한 내용인데 API 는 항상 요청이 들어오는 것과 나가는 것에 절대 Entity 를 사용하지 않아야 합니다.




2. 회원 수정 API

2-1. Controller

public class MemberApiController {
    ...
    @PatchMapping("/api/v2/members/${id}")
    public UpdateMemberResponse updateMemberV2(@PathVariable Long id,
                                               @RequestBody @Valid UpdateMemberRequest request) {
        memberService.update(id, request.getName());    // Service 에서 트랜잭션이 끝나면 DB 에 업데이트된다
        Member member = memberService.find(id);
        return new UpdateMemberResponse(member.getId(), member.getName());
    }

    @Data
    static class UpdateMemberRequest {
        private String name;
    }

    @Data
    @AllArgsConstructor
    static class UpdateMemberResponse {
        private Long id;
        private String name;
    }
}

2-2. Service

public class MemberService {
    ...
    @Transactional
    public void update(Long id, String name) {
        Member member = memberRepository.findOne(id);
        member.setName(name);   // 변경감지
    }
}

Service 에서 memberRepository 를 통해 Member 를 찾으면 영속 상태인 Member 객체가 나오게 됩니다.

Member 객체의 이름을 변경하면 변경 감지에 의해 트랜잭션이 커밋되는 시점에 update 쿼리가 날라가 DB 의 값이 변경되게 됩니다.

그 후 반환할 때 Member 객체를 반환하면 영속 상태가 끊기는 Member 가 반환되기 때문에 Controller 에 전달해도 되지만 가급적이면 반환하지 않는 것이 좋습니다.

그렇기 때문에 Controller 에서 id 값으로 조회한 후에, 조회된 객체의 값으로 반환하는 로직이 있는 것입니다.




3. 회원 조회 API

3-1. 응답 값으로 엔티티를 직접 외부에 노출

public class MemberApiController {
    ...
    @GetMapping("/api/v1/members")
    public List<Member> memberV1() {
        return memberService.findMembers();
    }
}

엔티티를 직접 노출하게 되면 Order 와 같은 엔티티 내부의 정보들이 전부 다 외부에 노출되게 됩니다.

Member Entity 의 orders 에 @JsonIgnore 어노테이션을 추가하면 엔티티를 반환할 때 주문 정보를 제외한 나머지 정보들만 반환됩니다.


3-2. Entity 노출 시 문제점

하지만 이 역시 위에서 언급한 것처럼 여러 API 가 존재하는 경우에 대한 처리와 API 스펙 변경이라는 문제가 발생하기 때문에 좋지 않은 방법입니다.

[
    {
        "id": 1,
        "name": "kim",
        ...
    },
    {
        "id": 2,
        "name": "park",
        ...
    }
]

또 응답값을 확인해보면 Array 형태인 것을 확인할 수 있습니다. Array 를 반환하면 스펙이 굳어져버려서 확장할 수 없기 때문에 몇 명의 회원이 있는지를 나타내는 count 와 같은 값을 추가할 수 없습니다.

해결 : API 응답 스펙에 맞추어 별도의 DTO를 반환하도록 합니다


3-3. 응답 값으로 엔티티가 아닌 별도의 DTO 사용

public class MemberApiController {
    ...
    @GetMapping("/api/v2/members")
    public Result memberV2() {
        List<Member> findMembers = memberService.findMembers();
        // 1. Entity 를 DTO 로 변환
        List<MemberDTO> collect = findMembers.stream().map(member -> new MemberDTO(member.getName())).toList();
        return new Result(collect.size(), collect);
    }

    @Data
    @AllArgsConstructor
    static class MemberDTO {
        private String name;
    }

    @Data
    @AllArgsConstructor
    static class Result<T> {
        private int count;
        private T data;
    }
}   

기존과 동일하게 findMembers() 를 통해 Entity 가 담긴 List 를 반환받고, List 내부의 Entity 를 DTO 로 변환하는 과정을 거칩니다.

DTO 를 가진 List 를 이용해 Result 객체를 만들어서 반환하면 object 타입으로 응답이 나가게 됩니다.

{
    "count": 2,
    "data": [
        {
            "name": "kim"
        },
        {
            "name": "park"
        }
    ]
}
post-custom-banner

0개의 댓글