Create API Operation(1) - Search all

GEONNY·2024년 7월 26일
0

Building-API

목록 보기
7/28
post-thumbnail

이전에 생성했던 Member entity 의 CRUD 요청을 처리하는 Operaion 들을 생성하겠습니다.
우선 각각의 기능을 처리하기 위한 record 를 생성합니다.

📌create record

총 6개의 record를 생성했습니다. 추가, 수정은 요청/응답이 쌍으로 존재하고, 조회는 응답, 삭제는 없습니다. 조회는 응답만, 삭제는 요청만 생성한 이유는 조회는 전체 데이터를 조회, 삭제는 키 값만 받고 삭제된 개수만 리턴할 예정이기 때문입니다. 추가와 수정의 경우에는 실제 생성된 값을 응답하기 위해 요청과 응답을 쌍으로 생성합니다.
LocalDateTime type 은 @JsonFormat 을 사용해서 Json 변환 시점에 pattern 에 맞는 String 으로 변환하여 줍니다.
Response 객체는 @Builder 를 추가해서 응답객체 생성 시 편의성과 가독성을 높여줍니다.

public record MemberCreateRequest(
        String memberId,
        String password,
        String useYn,
        String memberName
) {
}
@Builder
public record MemberCreateResponse(
        String memberId,
        String memberName,
        String useYn,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime createDate,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime updateDate
) {
}
public record MemberModifyRequest(
        String memberId,
        String useYn,
        String memberName
) {
}
@Builder
public record MemberModifyResponse(
        String memberId,
        String memberName,
        Strig useYn,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime createDate,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime updateDate
) {
}
@Builder
public record MemberSearchResponse(
        String memberId,
        String memberName,
        Strig useYn,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime createDate,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime updateDate
) {
}

이제 요청에 대한 응답을 받을 MemberController 를 생성합니다.

📌create controller

@RestController
public class MemberController {

    @GetMapping(value = "/members"
            , produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<MemberSearchResponse>> getMembers() {
        return ResponseEntity.ok()
                .body(List.of(MemberSearchResponse.builder().build()));
    }

    @PostMapping(value = "/member"
            , consumes = MediaType.APPLICATION_JSON_VALUE
            , produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<MemberCreateResponse> createMember(
    		@RequestBody MemberCreateResponse parameter) {
        return ResponseEntity.ok()
                .body(MemberCreateResponse.builder().build());
    }

    @PutMapping(value = "/member"
            , consumes = MediaType.APPLICATION_JSON_VALUE
            , produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<MemberModifyResponse> modifyMember(
    		@RequestBody MemberModifyRequest parameter) {
        return ResponseEntity.ok()
                .body(MemberModifyResponse.builder().build());
    }

	@DeleteMapping(value = "/member/{memberId}"
    		, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Long> deleteMember(
            @PathVariable("memberId") String memberId) {
        return ResponseEntity.ok()
                .body(1L);
    }
}

Json 데이터를 리턴하기 위해 @Controller + @ResponseBody 인 @RestController 를 선언합니다.
RESTFul API 에서는 HTTP Method 를 사용해서 작업간 역할을 구분합니다. 예를들어 GET - 조회, POST - 생성, PUT - 수정, DELETE - 삭제 와 같습니다. 그리고 URI 에는 고유한 식별자가 사용되어야 하고 (Member) 명사, 혹은 동명사 (동사 X)가 권장됩니다. 복수 건 조회의 경우 복수명사(Members)를 사용합니다.
우선 오류 방지를 위해 각 리턴 타입에 맞는 객체를 초기화 하여 추가해 두었습니다.

이제 실제 로직 처리를 위한 서비스를 구현하겠습니다. Interface를 작성하고 이를 implement 하도록 합니다.

📌create service

public interface MemberService {
    List<MemberSearchResponse> getMembers();
	
    @Transactional
    MemberCreateResponse createMember(MemberCreateRequest parameter);

	@Transactional
    MemberModifyResponse modifyMember(MemberModifyRequest parameter);

	@Transactional
    Long deleteMember(String memberId);
}
@Service
public class MemberServiceImpl implements MemberService {
    @Override
    public List<MemberSearchResponse> getMembers() {
        return null;
    }

    @Override
    public MemberCreateResponse createMember(MemberCreateRequest parameter) {
        return null;
    }

    @Override
    public MemberModifyResponse modifyMember(MemberModifyRequest parameter) {
        return null;
    }

    @Override
    public Long deleteMember(String memberId) {
        return null;
    }
}

springframework 의 @Transactional import 합니다. @Transactional 에 대해서는 나중에 따로 정리하도록 하겠습니다.

Database 와 연동 처리를 위한 Repository 를 생성합니다. 기본적인 CRUD 처리를 위해 JpaRepository를 상속 받습니다.

📌create repository

@Repository
public interface MemberRepository extends JpaRepository<Member, String> { //entity type, key type
}

📌Service Implementation

서비스 로직을 구현합니다. memberRepository 를 DI 하고 JpaRepository 에서 제공하는 method 를 사용합니다.

@Service
@RequiredArgsConstructor //lombok annotation, final class 객체를 생성자에서 의존성 주입 함
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository; // Dependency injection

JpaRepository 의 findAll method 를 사용하여 전체 member 를 조회하고, entity를 record로 변경하여 리턴합니다. entity 데이터를 직접 리턴하지 않고 record 로 리턴하는 이유는 entity의 경우 Database table 과 직접 매핑 되는 객체이기 때문에 테이블 구조나 비지니스 로직이 외부로 노출될 위험이 있고, 불필요한 정보까지 제공될 수 있기 때문입니다. 그리고 외부로 노출되는 record 와 내부 서비스에서 사용되는 entity를 분리하면 presentation layer 와 service layer 를 분리하여 결합도를 낮출 수 있습니다. 기타 장점은 하단 정리 부분을 참고하세요.

@Override
public List<MemberSearchResponse> getMembers() {
    return memberRepository.findAll().stream()
            .map(memberEntity -> MemberSearchResponse.builder()
                    .memberId(memberEntity.getMemberId())
                    .memberName(memberEntity.getMemberName())
                    .createDate(memberEntity.getCreateDate())
                    .updateDate(memberEntity.getUpdateDate())
                    .build())
            .toList();
}

Controller 에서도 구현된 memberService 를 DI 하고 조회 Method를 호출하도록 변경합니다.

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @GetMapping(value = "/members", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<MemberSearchResponse>> getMembers() {
        return ResponseEntity.ok()
                .body(memberService.getMembers());
    }

테스트로 데이터를 추가하고 조회해 보겠습니다.

insert into "member" (
	member_id ,
	member_pw ,
	member_nm ,
    use_yn,
	create_dt ,
	update_dt 
) values (
	'member1',
	'1234' ,
	'회원 명',
    'Y',
	now() ,
	now() ,
	now()
);
commit;

📌 Data request / response

브라우저에 조회 요청 주소를 입력합니다.
http://localhost:13713/my-api/members

[
  {
    memberId: "member1",
    memberName: "회원 명",
    createDate: "2024-07-25 17:43:42",
    updateDate: "2024-07-25 17:43:42"
  }
]

위와 같은 데이터가 리턴되는 것을 확인 하실 수 있습니다.

📚참고

📕Entity 와 record를 분리하는 이유

  1. Entity 를 직접 전달 할 경우 database 구조나 비지니스 로직이 외부로 노출될 위험이 있음. (보안적 측면, 캡슐화)
  2. 불필요한 데이터를 제외하고 전송 할 수 있음. (성능적 측면)
  3. Presentation layer 와 service layer 의 분리 (계측 간 분리, 결합도 감소)
  4. Entity 구조 변경에 의한 응답 인터페이스 유지 (유지보수 측면)
  5. 속성 타입 변경이 용이
  6. Entity 지연로딩에 의한 예외 발생 감소

이런 이유로 Entity to Dto, Dto to Entity 기능을 제공해주는 MapStruct 나 ModelMapper 와 같은 라이브러리들이 제공됩니다. 추후 extra edition 을 통해 알아보도록 하겠습니다.

📙Spring DI 란?

Spring Framework 특징 중에 하나인 DI (Dependency Injection) 는 객체간 의존 관계를 설정하는 개념입니다. DI는 객체지향 프로그래밍에서 객체간 결합도를 낮추고 코드의 재사용성을 높여 줍니다.
간단히 설명하면 Spring Framework 는 객체를 Spring container 에 등록해서 객체의 생명주기를 개발자 대신 관리해주며 필요 시 spring container 에 등록된 객체를 가져다 쓸수 있는데, 이 가져다 쓰는 행위를 DI 라고 합니다.
앞에서 사용된 @RestController, @Service, @Repository 등이 Spring container 에 객체를 등록 해주는 Annotaion 이고 @RequiredArgsConstructor 가 생성자 주입 방식으로 DI 를 하는 lombok 의 Annotation 입니다.

Spring 에서 제공하는 DI Annotation 에는 @Autowired, @Resource 등이 있지만, 생성자 주입 방식을 권장하며 이 방식의 구현을 지원하는 @RequiredArgsConstructor 사용하도록 합시다.

📗Service layer를 interface로 구현하는 이유

Java 에서 Interface 라는 개념을 만든 이유가 뭘까요? 굳이 파일을 하나 더 만들어서 귀찮게 하는 이유가 뭘까요?
저도 처음에는 습관처럼 반복해서 Interface를 만들어 service 로직을 구현했습니다. 불필요하다는 생각이 들어 빼버린적도 있죠. 하지만 Interface 로 service 로직을 구현하는 이유는 분명히 있습니다.
현업에서는 설계자와 구현자가 분리되는 경우가 있습니다. 설계자는 필요하다고 생각하는 기능의Interface 까지만 작성을 하고 구현자에게 구현 작업을 내리죠. 이처럼 service 로직의 설계도 역할을 하기도 합니다.
서비스 로직이 대대적으로 변경되어야 할 경우 Interface 로 구현되어 있다면 해당 Interface 의 구현체만 새로 만들어 변경하면 됩니다. 만약 변경한 코드에 문제가 있다면 구현체만 다시 변경하면 되죠. 이처럼 클래스 간 결합도를 낮출 수 있어 유지보수나 변경에 유연하게 대처할 수 있습니다.
구현의 세부 사항을 숨겨 캡슐화 할 수도 있습니다.

📘Override, Overload

Override 는 상속받은 부모나 Interface 의 메서드를 재정의 해서 사용하는 방식을 말합니다. @Override 를 생략해도 되지만, 명시적으로 꼭 추가해서 사용하도록 합니다.
Overload 는 같은 이름의 메서드를 파라미터의 타입, 개수만 다르게 해서 사용하는 방식을 말합니다. 둘 다 개발 시 많이 사용되니, 개념적인 차이는 알고 넘어갑시다.

profile
Back-end developer

0개의 댓글