본문 바로가기
SpringBoot

Spring Annotation 활용기

by se0nghyun2 2024. 5. 31.

 

내 프로젝트에선 유저 도메인의 다양한 필드들을 수정할 수 있다.

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

https://upperleaf.tistory.com/2

'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