Update 로직을 구현하겠습니다.
Create 로직과 같이 서비스의 modifyMember method 의 리턴 값을 item 에 담아 ItemsResponse를 리턴하게 변경합니다.
@PutMapping(value = "/member"
, consumes = MediaType.APPLICATION_JSON_VALUE
, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ItemResponse<MemberModifyResponse>> modifyMember(
@RequestBody MemberModifyRequest parameter) {
return ResponseEntity.ok()
.body(ItemResponse.<MemberModifyResponse>builder()
.status(messageConfig.getCode(NormalCode.MODIFY_SUCCESS))
.message(messageConfig.getMessage(NormalCode.MODIFY_SUCCESS))
.item(memberService.modifyMember(parameter))
.build());
전달 받은 MemberModifyRequest record 데이터로 entity를 update 하기 위해 Member Entity 내에 UpdateFromRecord method 를 구현합니다. memberName 과 useYn 두 필드 값을 변경합니다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table
@Entity(name = "member")
public class Member extends BaseEntity implements Persistable<String> {
@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;
}
public void updateFromRecord(MemberModifyRequest parameter) {
this.memberName = parameter.memberName();
this.useYn = parameter.useYn();
}
}
이제 Service 로직을 구현합니다.
@Override
@Transactional
public MemberModifyResponse modifyMember(MemberModifyRequest parameter) {
Member memberEntity = memberRepository.findById(parameter.memberId())
.orElseThrow(() -> new EntityNotFoundException(
"수정할 회원 ID 가 존재하지 않습니다. -> " + parameter.memberId())
);
memberEntity.updateFromRecord(parameter);
memberRepository.saveAndFlush(memberEntity);
return MemberModifyResponse.builder()
.memberId(memberEntity.getMemberId())
.memberName(memberEntity.getMemberName())
.useYn(memberEntity.getUseYn())
.createDate(memberEntity.getCreateDate())
.updateDate(memberEntity.getUpdateDate())
.build();
}
한 트랜젝션 내에서 처리하기 위해 @Transactional
이 추가하였습니다. create 로직과 다르게 save 가 아닌 saveAndFlush method 를 호출하였습니다.
member update 를 요청해 보도록 하죠.
정상적인 응답을 받았습니다.
JPA 에서는 Transaction 이 끝나면 영속 상태의 Entity 변경사항을 자동으로 검지하여 Database 에 반영합니다. 이를 Dirty Checking
이라 하고 위 코드에서 Dirty Checking
을 확인하기 위해 saveAndFlush method 를 주석처리 해줍니다.
@Override
@Transactional
public MemberModifyResponse modifyMember(MemberModifyRequest parameter) {
Member memberEntity = memberRepository.findById(parameter.memberId())
.orElseThrow(() -> new EntityNotFoundException(
"수정할 회원 ID 가 존재하지 않습니다. -> " + parameter.memberId())
);
memberEntity.updateFromRecord(parameter);
//memberRepository.saveAndFlush(memberEntity);
return MemberModifyResponse.builder()
.memberId(memberEntity.getMemberId())
.memberName(memberEntity.getMemberName())
.useYn(memberEntity.getUseYn())
.createDate(memberEntity.getCreateDate())
.updateDate(memberEntity.getUpdateDate())
.build();
}
SimpleJpaRepository 의 saveAndFlush method 는 단순히 save method 호출 후 flush를 호출하는 차이입니다.
@Override
@Transactional
public <S extends T> S saveAndFlush(S entity) {
S result = save(entity);
flush();
return result;
}
이전 Create API Operation(3) - Create 에서 봤듯이 EntityManager의 merge method 는 select query 로 조회한 데이터와 entity 간의 변경사항이 있을 경우 update query 를 요청합니다. update query 를 발생시키기 위해 useYn 값을 'Y' 로 변경한 후 다시 update 요청을 합니다. useYn 값은 정상적으로 변경되지만 updateDate (Auditing이 적용된 필드) 는 변경되지 않고 이전의 값이 전달 됩니다. Database를 확인해보면 update_dt 는 정상적으로 반영이 됐습니다.
saveAndFlush 삭제 시수정한 값 반영 O
, Database 반영 O
, Auditing 설정 값 반영 X
근데 왜 응답 객체의 값들 중 Auditing이 설정된 값은 정상적으로 전달되지 않은 걸까요? 이는 Transaction 과 밀접한 관련이 있습니다.
JPA 에서는 Transaction 이 열리면 Database connection pool 을 로드합니다. 그리고Transaction 이 닫히기 전까지 변경된 모든 entity 의 수정 query 를 작성하여(Dirty Checking
) 영속성 컨텍스트에 누적하고, 닫히기 직전 Database로 전송 후 Commit 합니다. 이 commit 되는 시점에 flush 가 발생하여 영속성 컨텍스트에도 update 되게 됩니다.
정리하면, modifyMember method 가 실행 될 때 Transaction 이 열리고, 리턴 후 method가 종료될 때 Transaction이 닫히면서 flush 가 일어나 Auditing 이 설정된 값이 update 되니, 응답에서는 이전의 반영한 값이 전달됩니다. 이런 문제를 해결하기 위해 saveAndFlush method를 호출하여 변경 후의 값을 영속성 컨텍스에 즉시 반영하고 해당 값을 리턴하는 것 입니다.
modifyMember method 호출
-> Transaction open
-> Dirty Checking 중 flush 실행
->
return response
-> commit and flush
-> Transaction close
@Transactional 은 Database 의 Transaction 을 관리하기 위한 Annotation 입니다. Database 의 Transaction 은 하나의 논리적 작업 단위로, 여러개의 연산이 포함될 수 있고 모두 성공하여 완료되거나, 하나라도 실패 시 모두 취소 되어야 합니다. 이는 ACID 원칙을 보장하기 위함입니다.
원자성 (Atomicity)
: 트랜잭션 내의 모든 연산이 완전히 실행되거나, 하나도 실행되지 않아야 합니다. 즉, 트랜잭션은 '모두 또는 아무것도 없음'을 보장합니다.
일관성 (Consistency)
: 트랜잭션이 성공적으로 완료되면 데이터베이스는 일관된 상태를 유지해야 합니다. 트랜잭션 전과 후의 데이터베이스 상태는 일관성 있어야 합니다.
고립성 (Isolation)
: 트랜잭션은 다른 트랜잭션의 영향을 받지 않고 독립적으로 실행되어야 합니다. 각 트랜잭션은 다른 트랜잭션이 완료되기 전까지는 자신의 연산 결과를 외부에 노출하지 않아야 합니다.
지속성 (Durability)
: 트랜잭션이 완료되면, 그 결과는 시스템 오류가 발생하더라도 영구적으로 저장되어야 합니다.
@Transactional
을 import 하면 2개의 import 중 선택하게 됩니다. java 의 Transactional(jakarta.transaction
) 과 spring의 Transactional(org.springframework.transaction.annotation
).
결론적으로 둘의 기능은 같습니다. Transaction 관리라는 같은 목적으로 설계되었기 때문에 기본 기능은 같지만, 세부적인 설정 범위나 사용 환경의 차이를 보입니다.
Spring Framwork 에서는 보다 다양한 설정이 가능한 Springframework 의 @Transactional 을 쓰는게 확장성 측면에서는 좋다고 생각합니다.
항목 | Spring @Transactional | Java @Transactional |
---|---|---|
프레임 워크 | Spring Framework | Java EE (Jakarta EE) |
용도 | 다양한 데이터 접근 기술 지원 (JPA, Hibernate, JDBC 등) | EJB와 CDI 기반 애플리케이션 |
구성 및 사용 | AOP를 사용하여 트랜잭션 경계 관리 | JTA를 통해 트랜잭션 관리 |
전파 옵션 | 세분화된 전파 옵션 제공 (REQUIRED, REQUIRES_NEW 등) | 제한된 전파 옵션 제공 (MANDATORY, REQUIRED 등) |
격리 수준 | 세밀한 격리 수준 설정 가능 (DEFAULT, READ_COMMITTED 등) | 기본적인 격리 수준 설정 가능 |
기타 기능 | 트랜잭션 매니저 통합 (JdbcTransactionManager, JpaTransactionManager 등) | 주로 EJB와의 통합 |
기반 | Spring 트랜잭션 관리 서비스 | Java EE 컨테이너의 트랜잭션 관리 서비스 |
Member interface 의 create, modify, delete method 에는 @Transactional 이 붙어있습니다. interface 에 @Transactional 을 붙이게 되면 자동으로 구현체에서도 @Transactional 설정이 반영 됩니다. Trancation 은 상위의 설정을 따르기 때문에 interface 나 구현체에서 설정한 속성이 하위 save, delete method 로 전달되게 되는 것이죠.
상위에서 Transaction의 전파 속성, 격리 수준을 변경해야 할 경우나 상위 method 내에서 여러번 save, delete method 를 호출하는 경우 오류 발생 시 Rollback 처리를 위해 @Transactional 을 추가하게 됩니다.