Create API Operation(3) - Create

GEONNY·2024년 8월 3일
0

Building-API

목록 보기
14/28
post-thumbnail

이전에 작성했던 Member Service 의 추가, 수정, 삭제 상세 로직을 구현하고, Controller 에 서비스를 연결 하겠습니다. 첫번 째 추가 로직 입니다.

📌Controller

Item 에 서비스의 createMember method 리턴 값을 담은 ItemResponse 를 리턴하게 변경합니다.

    @PostMapping(value = "/member"
            , consumes = MediaType.APPLICATION_JSON_VALUE
            , produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ItemResponse<MemberCreateResponse>> createMember(
    		@RequestBody MemberCreateRequest parameter) {
        return ResponseEntity.ok()
                .body(ItemResponse.<MemberCreateResponse>builder()
                        .status(messageConfig.getCode(NormalCode.CREATE_SUCCESS))
                        .message(messageConfig.getMessage(NormalCode.CREATE_SUCCESS))
                        .item(memberService.createMember(parameter))
                        .build());
    }

📌service create method

MemberServiceImpl > createMember

@Override
@Transactional
public MemberCreateResponse createMember(MemberCreateRequest parameter) {
    if (memberRepository.existsById(parameter.memberId())) {
        throw new EntityExistsException(
        	"이미 존재하는 ID 입니다. -> " + parameter.memberId()
        );
    }
    Member newMember = Member.builder()
            .memberId(parameter.memberId())
            .password(parameter.password())
            .memberName(parameter.memberName())
            .useYn(parameter.useYn())
            .build(); //비영속 Member 생성
    Member savedMember = memberRepository.save(newMember); // 영속 상태로 변경
    return MemberCreateResponse.builder()
            .memberId(savedMember.getMemberId())
            .memberName(savedMember.getMemberName())
            .useYn(savedMember.getUseYn())
            .createDate(savedMember.getCreateDate())
            .updateDate(savedMember.getUpdateDate())
            .build(); // record 로 리턴
}

Id 유효성 체크 후 비영속 Entity 를 생성하고, memberRepository 의 save method를 호출 합니다. save 메서드에서는 리턴되는 entity를 받아 record 로 변경하여 리턴합니다.

📌Install Talend API Tester

테스트를 위해 크롬의 확장 프로그램인 Talend API Tester 를 설치해줍니다.

크롬 우측 상단 맞춤설정 및 제어에서 확장프로그램 > 확장 프로그램 관리 를 선택합니다.

talend api 를 검색합니다.

Talend API Tester - Free Edition 이 나오면 클릭하고 Chrome에 추가 를 클릭합니다.

설치가 완료되면 Talend API Tester 를 실행합니다.

📌Test

METHOD 는 POST, 주소에는 http://localhost:13713/my-api/member 를 입력합니다.
Content-Type은 application/json 으로 두고 BODY 에 API 로 전달할 데이터를 입력합니다.

{
    "memberId": "member2",
    "password": "1234",
    "useYn": "Y",
    "memberName": "회원2",
    "authorityCode": "ROLE_ADMIN"
}

Send 버튼을 클릭하면 (or Alt + Enter) API 로 데이터가 전송되고 응답 결과가 아래 표출 됩니다.
createDate 와 updateDate 를 전송하지 않았지만 Auditing 에 의해 추가되어 전송된 것을 확인하실 수 있습니다.
Send 버튼을 다시 눌러 중복 ID 체크 로직을 테스트 합니다.

console

jakarta.persistence.EntityExistsException: 이미 존재하는 ID 입니다. -> member2
이하 생략...

정상 동작 했지만 500 에러가 전달되었네요. ErrorCode 에 EXISTS_DATA 를 추가하고 GlobalExceptionHandler 에 EntityExistsException 처리를 추가해 줍니다.

📌Error Code, @ExceptionHandler 추가

common.code.ErrorCode

EXISTS_DATA("ERR_DT_02") // 에러 코드 추가

common.exception.GlobalExceptionHandler

@ExceptionHandler(EntityExistsException.class) // ExceptionHandler 추가
public ResponseEntity<ErrorResponse> handleEntityExist(EntityExistsException e) {
    return generateErrorResponse(ErrorCode.EXISTS_DATA, e);
}

다국어 처리를 위해 message.properties 에도 추가합니다.

프로젝트를 재실행 하고 다시 추가요청을 합니다.

정상적으로 Response 가 전달되었습니다.

📚참고

📕save method in SimpleJpaRepository

비영속 상태의 Entity 를 영속상태로 변경할 때 SimpleJpaRepository 의 save method 를 호출했습니다.

Member savedMember = memberRepository.save(newMember);

save method 가 어떻게 entity 를 영속 상태로 만드는지 보도록 하죠.
SimpleJpaRepository > save

@Override
@Transactional
public <S extends T> S save(S entity) {

	Assert.notNull(entity, "Entity must not be null");

	if (entityInformation.isNew(entity)) {
		entityManager.persist(entity);
		return entity;
	} else {
		return entityManager.merge(entity);
	}
}

entity 를 parameter 로 받아 null 체크 후 새로운 entity 이면 persist method를, 아니라면 merge method 를 호출합니다. 근데 좀 의아한 점이 있습니다. save method는 새로운 객체인 경우 persist method 만 호출 후에 전달받은 entity 를 그대로 리턴하는데, 왜 MemberService 에서는 return 받은 객체를 사용했을까요?
newMember 와 savedMember 가 같은 객체인지 의문이 듭니다. 확인을 해보도록 하죠.
save method 호출 라인에 디버깅 포인트를 설정하고 id 를 member3 으로 변경 후 재요청 해봅니다.


예상과는 달리 newMember 와 savedMember의 주소가 다른것을 확인할 수 있습니다. 여기서 유추해볼 수 있는건
isNew method 결과가 false인가? merge method 에서 새로운 entity 로 변경하나?
persist method 에서 새로운 객체를 생성하여 parameter 로 받은 entity 를 변경하나?

일 것입니다.
먼저 isNew method 를 확인해보도록 하죠.

📖isNew?

isNew method 는 AbstractEntityInformation 추상 클래스에 있습니다.

public boolean isNew(T entity) {

		ID id = getId(entity);
		Class<ID> idType = getIdType();

		if (!idType.isPrimitive()) {
			return id == null;
		}

		if (id instanceof Number) {
			return ((Number) id).longValue() == 0L;
		}

		throw new IllegalArgumentException(
        	String.format("Unsupported primitive id type %s", idType)
        );
	}

entity 에서 id 를 추출하여 해당 id type 이 참조타입이라면 (원시타입이 아니라면, Wrapper class 포함) null 과 비교하고, 원시타입이라면 OL 과 비교한 값을 리턴하고 있습니다.
우리가 전달했던 Member entity 객체의 id 는 String type 이고 "member2" 라는 값을 넘겼기 때문에 false 가 리턴되었던 것입니다. 오, 그럼 merge method 탄거네요? 맞습니다. 비영속 상태의 새로운 Entity 를 넘겼는데 왜 isNew 라는 메서드의 결과를 false 가 되게 했을까요?
코드를 보면 JPA에선 ID 값이 있을 경우, 신규 영속화 대상이 아니라고 판단합니다. 그럼 언제 신규 영속화 대상이라고 판단할까요? 아직 설명하진 않았지만 @SequenceGenerator@GenericGenerator 를 사용하여 영속 상태가 될 때 키를 생성하는 방식에서는 ID 값이 isNew method 호출 시에는 없기 때문에 신규 영속화 대상이라고 판단하게 됩니다.

그럼 첫번 째 의문은 풀렸습니다. isNew method 가 false 를 리턴했고, merge 메서드가 실행되었습니다.
merge method 에서는 실제 Id 에 해당하는 데이터가 있는지 Select query를 실행하게 되고, DB에 데이터가 없다면 Insert문을 생성하여 DB로 전송, 성공하면 영속화 후 영속화된 Entity 를 리턴합니다.

📖Override isNew()

위에서 설명한 것과 같이 isNew method 의 결과가 false 이면 merge method 를 실행하게 되고, 여기서는 select query와 insert query, 2번의 DB connection 이 일어나게 됩니다.
createMember method 가장 처음에 중복 체크를 하기 때문에 select query 는 불필요한 로직입니다.
중복체크 확인 코드를 제거하고 구현해도 중복일 경우 Exception 이 발생하게 됩니다.

org.postgresql.util.PSQLException: 오류: 중복된 키 값이 "user_pk" 고유 제약 조건을 위반함
  Detail: (member_id)=(member5) 키가 이미 있습니다.

개발을 진행하면서 발생할 가능성이 있는 unchecked Exception 은 상위 Exception 으로 GlobalExceptionHandler에 추가해두도록 합니다.
ErrorCode

SQL_ERROR("ERR_SQ_01")

GlobalExceptionHandler

@ExceptionHandler(SQLException.class)
public ResponseEntity<ErrorResponse> handleSQL(SQLException e) {
    return generateErrorResponse(ErrorCode.SQL_ERROR, e);
}

📖Persistable interface

이를 해결하는 방법으로는 org.springframework.data.domain.Persistable interface를 구현하는 방법이 있습니다. Persistable interface 는 getId 와 isNew method 를 구현해야 하는데, 여기서 isNew method가 override되면서 save method 의 isNew method를 대체하게 됩니다.
entity.Member

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table
@Entity(name = "member")
public class Member extends BaseEntity implements Persistable<String> { //Persistable 구현

    @Id
    @Column(name = "member_id")
    private String memberId;

    @Column(name = "member_pw")
    private String password;

    @Column(name = "member_nm")
    private String memberName;

    @Column(name = "use_yn")
    private String useYn;

    @Column(name = "authority_cd")
    private String authorityCode;

    @Override
    public String getId() {
        return this.memberId;
    }

    @Override
    public boolean isNew() {
        return getCreateDate() == null; //영속화 시 생성
    }
}

isNew method 를 JPA aditing 에 의해 영속화 시 추가되는 createDate와 비교하도록 구현하여 persist method가 동작하게 하고 불필요한 select query 요청을 줄여줍니다.
수정 후 memberId 를 member4 로 변경 후 다시 요청해 봅시다.

newMember 와 savedMember 가 같은 Entity 인 것을 확인할 수 있습니다.
이제 save method 의 리턴 값을 받지않고 newMember 값을 전달하여도 올바르게 동작하게 됩니다.😆

@Override
public MemberCreateResponse createMember(MemberCreateRequest parameter) {
    if (memberRepository.existsById(parameter.memberId())) {
        throw new EntityExistsException("이미 존재하는 ID 입니다. -> " + parameter.memberId());
    }
    Member newMember = Member.builder()
            .memberId(parameter.memberId())
            .password(parameter.password())
            .memberName(parameter.memberName())
            .useYn(parameter.useYn())
            .build();
    memberRepository.save(newMember);
    return MemberCreateResponse.builder()
            .memberId(newMember.getMemberId())
            .memberName(newMember.getMemberName())
            .useYn(newMember.getUseYn())
            .createDate(newMember.getCreateDate())
            .updateDate(newMember.getUpdateDate())
            .build();
}
profile
Back-end developer

0개의 댓글