내 프로젝트에선 유저 도메인의 다양한 필드들을 수정할 수 있다.
Restful API에선 보통 PUT /도메인이름/{no} 를 통해 수정이 이루어진다.
이 때, 수정요청은 크게 아래 두가지 케이스 존재한다.
그 중 case2와 추가조건(조건1,2) 에 해당하는 개발을 진행해본다.
case1) 엔티티의 모든 필드에 데이터 넣어 요청
case2) 모든 필드 중 특정 필드만 데이터 넣어서 요청
더하여 특정 필드엔 두 가지 조건이 존재한다.
조건 1. 특정 대상 필드만 업데이트 대상
조건 2. 특정 대상 필드에 null 인입 시 경우에 따라 null로 업데이트 및 기존 데이터 유지
UserEntity
우선 userPwd, gender, tel 필드들은 모두 업데이트 대상이다.
userPwd, gender 필드들은 null 인입 시 기존 데이터를 유지해야 하나,
tel 필드의 경우는 null 인입 시 null 그대로 업데이트되어야 한다.
그 외 필드들은 어떠한 경우에도 업데이트되어선 안된다.
@Entity
public class UserEntity{
...
private String userId;
private String userPwd;
private String userName;
private String tel;
private String userEmail;
private String gender;
private Integer useFlg;
}
각 조건의 해결책을 생각해본다.
조건1. 특정 필드만 업데이트 대상 ?????
각 필드들에 @Merge라는 커스텀 어노테이션을 만들어 선언해주면 된다.
Merge annotation v1.0
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Merge {
}
UserEntity v1.0
@Merge어노테이션 선언함에 따라 업데이트가 필요한 필드들이 특정되었다.
선언되지 않은 필드들은 업데이트 대상에서 제외가 된다.
@Entity
public class UserEntity{
...
private String userId;
@Merge //업데이트 대상
private String userPwd;
private String userName;
@Merge(ignoreNull = false) //업데이트 대상
private String tel;
private String userEmail;
@Merge //업데이트 대상
private String gender;
private Integer useFlg;
}
UserService
참고로 update는 jpa의 더티체킹을 통하여 진행한다.
기존 엔티티와 업데이트 데이터 정보가 담긴 target 엔티티를 생성하여
MergeUtil.merge 메소드를 통하여 기존 엔티티의 필드들을 조건2)에 따라 덮어씌워주거나 유지해주어야 한다.
@Override
@Transactional
public UserSearchResDto update(Long userNo, UserUpdateReqDto userUpdateReqDto) {
UserEntity sourceUser = getUserEntityByUserNo(userNo);
UserEntity targetUser= UserEntity.builder()
.userPwd(userUpdateReqDto.getUserPwd)
.tel(userUpateReqDto.getTel())
.gender(userUpdateReqDto.getGender())
.build();
MergeUtil.merge(selectedUser,targetUser);
return UserSearchResDto.from(selectedUser);
}
조건2. 특정 대상 필드에 null 인입 시 경우에 따라 null로 업데이트 및 기존 데이터 유지 ??
Merge annotation v1.1
null 처리여부에 대한 조건은 @Merge 어노테이션에 속성을 추가하여 구별하면 된다.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Merge {
/* null인 필드 무시
* true) null인입 시 이전 소스필드 유지
* false) null인입 시 null 입력-> 들어온 그대로 입력
* */
boolean ignoreNull() default true;
}
현재 조건1,조건2는 Merge어노테이션과 어노테이션의 속성값을 추가해주므로써 해결하였다.
이젠 MergeUtil.merge 함수를 구현해본다.
(Reflection에 대한 이해도 필요)
조건 검증
1. null 처리 안되는 경우 기존 데이터 유지
private static boolean canUpdate(Object oldValue,Object newValue){
/*
case1) oldValue="s", newValue= "s" -> 유지
case2) oldValue=null, newValue= "s" -> 업데이트
case3) oldValue=null, newValue=null -> 유지
case4) oldValue="s", newValue=null -> 유지
*/
if(newValue!=null && !newValue.equals(oldValue)){
//newValue가 null이 아니면서 newValue!=Object 다르면 true리턴
return true;
}
return false;
}
2.null 처리 되는 경우 들어오는 대로 데이터 업데이트
private static boolean canUpdateNull(Object oldValue,Object newValue){
/*
case1) oldValue="s", newValue="s" -> 유지
case2) oldValue=null, newValue= "s" -> 업데이트
case3) oldValue=null, newValue=null -> 유지
case4) oldValue="s", newValue=null -> 업데이트
*/
if(newValue==null){
return oldValue!=null;
}
return !newValue.equals(oldValue);
}
MergeUtil
public static boolean merge(Object source, Object target) {
if(source.getClass()!=target.getClass()){
throw new IllegalStateException("The two parameter objects should be same.");
}
boolean updated =false;
List<String> mergedList = new LinkedList<>();
try{
for(Field field:source.getClass().getDeclaredFields()){ //각 필드 접근
//Merge 어노테이션 get
Annotation annotation = field.getAnnotation(Merge.class);
//Merge어노테이션 존재하는 경우만 처리 => ★조건1 만족★
if(annotation==null){
log.info("Merge 대상이 아닙니다 => "+field.getName());
continue;
}
//필드 접근 가능
field.setAccessible(true);
Object oldValue = field.get(source);
Object newValue = field.get(target);
boolean canMerge = false;
//어노테이션 조건 분기 처리 => ★조건2 만족★
if(((Merge)annotation).ignoreNull()){
//
if(canUpdate(oldValue,newValue)){
canMerge=true;
}
}
else{
//
if(canUpdateNull(oldValue,newValue) ){
canMerge=true;
}
}
//머지 가능한 경우
if(canMerge){
field.set(source,newValue); //소스엔티티에 newValue세팅
updated=true;
mergedList.add(String.format("%s : %s -> %s", field.getName(), oldValue, newValue));
}
}
}catch (Exception e){
log.error("error occurs during Merge fields of "+target.getClass().toString() + e);
}
if(updated) {
log.info(String.join("\n", mergedList));
}
return updated;
}
테스트 코드 검증
public class TestDomain {
@Merge(ignoreNull = false)
public String a;
@Merge(ignoreNull = false)
public String b;
@Merge
public Long c;
@Merge
public Long d;
public boolean e;
}
public class MergeTest {
@Test
public void mergeUtilTest(){
TestDomain source =new TestDomain("oldA","oldB",1L,1L,true);
TestDomain source =new TestDomain(null,"newB",5L,null,false);
MergeUtil.merge(source,target);
Assertions.assertEquals(source.getA(),null);
Assertions.assertEquals(source.getB(),"newB");
Assertions.assertEquals(source.getC(),5L);
Assertions.assertEquals(source.getD(),1L);
Assertions.assertEquals(source.isE(),true);
}
}
필드 a : 업데이트 대상 + 들어온 그대로 입력 => 예상 null
필드 b: 업데이트 대상 + 들어온 그대로 입력 => newB
필드 c: 업데이트 대상 => 5L
필드 d: 업데이트 대상 + null 인입 시 이전 데이터 유지 => 예상 1L
필드 e: 업데이트 대상 아님 => 예상 true
테스트 케이스 성공적으로 통과!!
이로써
업데이트가 되어야 하지 않은 필드도 만약 들어오게 되어도 Merge어노테이션과 merge메소드를 통하여 불필요한 업데이트를 방지할 수 있게 되었다.
참고
https://blog.gangnamunni.com/post/Annotation-Reflection-Entity-update/
Spring Annotation 과 Reflection 을 활용해서 Entity의 여러 필드 한번에 수정하기
Custom Annotation 과 Spring Reflection Util 활용기 by 강남언니 블로그
blog.gangnamunni.com
'SpringBoot' 카테고리의 다른 글
@OneToOne (2) | 2024.04.22 |
---|---|
페이징 개선(첫 페이지 조회결과 cache) (1) | 2024.04.18 |
OpenFeign 사용 시 헤더 값 넘기기 (0) | 2024.03.27 |
DB-> Entity, Entity->DB 자동 변환 (@Convert) (0) | 2023.05.14 |
Lombok 어노테이션 (0) | 2022.12.06 |