이전에 작성했던 Member Service 의 추가, 수정, 삭제 상세 로직을 구현하고, 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());
}
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 로 변경하여 리턴합니다.
테스트를 위해 크롬의 확장 프로그램인 Talend API Tester
를 설치해줍니다.
크롬 우측 상단 맞춤설정 및 제어에서 확장프로그램 > 확장 프로그램 관리
를 선택합니다.
talend api
를 검색합니다.
Talend API Tester - Free Edition
이 나오면 클릭하고 Chrome에 추가
를 클릭합니다.
설치가 완료되면 Talend API Tester
를 실행합니다.
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
처리를 추가해 줍니다.
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 가 전달되었습니다.
비영속 상태의 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 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 를 리턴합니다.
위에서 설명한 것과 같이 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);
}
이를 해결하는 방법으로는 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();
}