인프런 수업 강의를 듣고 정리한 내용입니다.

 

📣 수업을 시작하기 전에
API 개발과 성능 최적화

✔️ REST API 개발

  • 등록, 수정, 조회 REST API 개발
  • API 개발 실무 노하우

무엇보다 성능 최적화가 중요하다!
좋지 않은 성능을 써보다가 → 좋은 성능으로 변경한다.

✔️ 학습 방법
스프링와 JPA 공부한 상태여야 한다.

 

📚 1. 회원 등록 API

프로젝트에서 디렉터리로 공통처리를 많이 한다.

📖 A. V1 엔티티를 Request Body에 직접 매핑

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;
    
    /**
     * 등록 V1: 요청 값으로 Member 엔티티를 직접 받는다.
     */
    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }
    
    @Data
    static class CreateMemberRequest {
        private String name;
    }
    
    @Data
    static class CreateMemberResponse{
        private Long id;

        public CreateMemberResponse(Long id) {
            this.id = id;
        }
    }
}

@RestController : @ResponseBody + @Controller 상속

  • @ResponseBody : 데이터 자체를 json이나 xml 보낼 때 주로 사용하는 애노테이션

 

✔️ 문제점

현재 Member 테이블에 아무런 입력 값 없이, PostMapping을 하여도 실행이 된다.
이때, @NotEmpty를 추가한 후, 아무 입력 값이 전송할 경우 오류가 발생한다. (400번 오류)

다만,

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
  • 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty 등등)
    스크린샷 2022-04-11 오후 5 26 55
  • 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모든 요청 요구사항을 담기는 어렵다.
  • 엔티티가 변경되면 API 스펙이 변한다.

 

✔️ 결론

  • API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다.
  • API를 만들 때는 절대로 파라미터로 엔티티를 받지 마라! (엔티티를 외부에 노출하면 안된다.)

 

📖 B. V2 엔티티 대신에 DTO를 RequestBody에 매핑

/**
* 등록 V2: 요청 값으로 Member 엔티티 대신에 별도의 DTO를 받는다.
*/
@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;
}
    
@Data
static class CreateMemberResponse{
	private Long id;
	public CreateMemberResponse(Long id) {
		this.id = id;
	}
}
  • CreateMemberResquestMember 엔티티 대신에 RequestBody와 매핑한다.
  • 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
  • 엔티티와 API 스펙을 명확하게 분리할 수 있다.
  • 엔티티가 변경되어도 API 스펙이 변하지 않는다.

 

📣 api 과정에서
(1) 필드 값이 변경되었을 때

  • V1번은 변경되었는지 알 수 없다.
  • V2번은 오류로 알 수 있다. (서비스를 안정적으로 유지할 수 있다.)

 

(2) 필드 값을 알고 싶을 때

  • V1은 어떤 것이 필드 데이터 입력되었는지 알 수 없다.
  • V2는 어떤 것이 필드 데이터로 입력되었는지 알 수 있다.

 

실무에서는 @RequestBody @Valid Member member 보다는 @RequestBody @Valid CreateMemberRequest request와 같이 많이 사용한다!

 

💡 참고
실무에서는 엔티티를 API 스펙에 노출하면 안된다!
DTO로 가는 객체들을 사용해서 등록이나 응답을 받는다!

 

✔️ DTO란?
DTO(Data Transfer Object) : 계층 간 데이터 교환을 하기 위해 사용하는 객체로, DTO는 로직을 가지지 않는 순수한 데이터 객체이다.(getter & setter 만 가진 클래스)

DTO 참고

 

✔️ DAO
DAO(Data Access Object) : 데이터베이스의 data에 접근하기 위한 객체이다. DataBase에 접근하기 위한 로직 & 비즈니스 로직을 분리하기 위해 사용한다.

  • 비즈니스 로직(Business logic)은 컴퓨터 프로그램에서 실세계의 규칙에 따라 데이터를 생성·표시·저장·변경하는 부분을 일컫는다. 이 용어는 특히 데이터베이스, 표시장치 등 프로그램의 다른 부분과 대조되는 개념으로 쓰인다. (참고)

 

✔️ VO

  • VO(Value Object) 값 오브젝트로써 값을 위해 쓰인다. read-Only 특징(사용하는 도중에 변경 불가능하며 오직 읽기만 가능)을 가진다.
  • DTO와 유사하지만, DTOsetter를 가지고 있어 값이 변경할 수 있다.
  • VO 읽기만 가능하여, setter를 가지고 있지 않아 값을 변경할 수 없다.

 

📚 2. 회원 수정 API

MemberApiController

/**
* 수정 API
*/
@PatchMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id,
					   @RequestBody @Valid UpdateMemberRequest request) {
                       
    memberService.update(id, request.getName());
    Member findMember = memberService.findOne(id);
    return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}

@Data
static class UpdateMemberRequest {
    private String name;
}

@Data
@AllArgsConstructor
static class UpdateMemberResponse {
    private Long id;
    private String name;
}
  • 회원 수정도 DTO를 요청 파라미터에 매핑한다.
  • DTO에서는 Lombok 애노테이션 많이 사용한다.
    memberService.update(id, request.getName()); // 트랜잭션 종료된다.
    Member findMember = memberService.findOne(id); // query를 통해 가져와서, 반환한다.
  • commandquery를 소스상에서 분리하는 것이 좋다.
  • 유지보수성이 증대한다.

 

MemberService

public class MemberService {
	private final MemberRepository memberRepository;
	
	/*
	* 회원 수정
	*/
	@Transactional
    public void update(Long id, String name) {
        Member member = memberRepository.findOne(id);
        member.setName(name);
    }
  • 변경 감지를 사용해서 데이터를 수정한다.

 

실행 결과
hello → new

스크린샷 2022-04-11 오후 6 03 15 스크린샷 2022-04-11 오후 6 05 12

 

💡 참고
PUT : 전체 업데이트할 때 사용한다.
PATCH : 부분 업데이트할 때 사용한다.

 

💡 참고
org.springframework.http.converter.HttpMessageNotReadableException 와 같은 예외가 발생했을 경우, HTTP 요청의 body 형식을 Text로 보냈는지 확인한다.
body 형식을 JSON으로 변경하여 해결할 수 있다.
참고

 

📚 3. 회원 조회 API

스크린샷 2022-04-11 오후 6 15 31
  • none으로 바꿀시 table drop이 일어나지 않는다.
  • 한 번 데이터 넣을 시, 재실행해도 계속 쓸 수 있다.

 

📖 A. 회원조회 V1: 응답 값으로 엔티티를 직접 외부에 노출

public class MemberApiController{
	
	private final MemberService memberService;

	/**
	* 조회 V1 : 응답 값으로 엔티티를 직접 외부에 노출한다.
	*/
	@GetMapping("/api/v1/members")
	public List<Member> membersV1() {
		return memberService.findMembers();
	}
}

 

실행 결과

스크린샷 2022-04-11 오후 6 39 24

✔️ 문제점

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
  • 기본적으로 엔티티의 모든 값이 노출된다.
  • 응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직 등등)
스크린샷 2022-04-18 오후 4 37 55

외부에 노출되는 것을 막는다.
api가 회원 데이터만 출력하고 싶을 때, Order를 제외한다.
(다만, 다른 api에서는 Order를 사용할 수도 있어 @JsonIgnore을 붙여주는 것은 좋지 않다.)
모든 상황에 대응할 수 없다.

  • 실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 어렵다.
  • 엔티티가 변경되면 API 스펙이 변한다.
  • 추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어렵다. (별도의 Result 클래스 생성으로 해결)

 

✔️ 결론

  • API 응답 스펙에 맞추어 별도의 DTO를 반환한다.

 

💡 참고
엔티티를 외부에 노출하지 말자!
실무에서는 member 엔티티의 데이터가 필요한 API가 계속 증가하게 된다.
어떤 API는 name 필드가 필요하지만, 어떤 API는 name 필드가 필요없을 수 있다. 결론적으로 엔티티 대신에 API 스펙에 맞는 별도의 DTO를 노출해야 한다.

 

📖 B. 회원조회 V2: 응답 값으로 엔티티가 아닌 별도의 DTO 사용

/**
 * 조회 V2: 응답 값으로 엔티티가 아닌 별도의 DTO를 반환한다.
 */
@GetMapping("/api/v2/members")
public Result membersV2() {
    List<Member> findMembers = memberService.findMembers();
    
    //엔티티 -> DTO 변환
    List<MemberDto> collect = findMembers.stream()
            .map(m -> new MemberDto(m.getName()))
            .collect(Collectors.toList());
	
    return new Result(collect);
}

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

 

실행 결과

스크린샷 2022-04-11 오후 6 31 23
  • 엔티티를 DTO로 변환해서 반환한다.
  • 엔티티가 변해도 API 스펙이 변경되지 않는다.
  • 추가로 Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.

Result에 count 필드 추가
스크린샷 2022-04-18 오후 4 54 05

 

💡 참고

  • api 만들 때는 엔티티를 노출하거나 받지 말라!
  • api에 맞는 Dto를 만들어 활용하라!

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글