고민거리

[SpringBoot] 수정 기능에 대한 고찰

스프링피바라기 2021. 12. 21. 01:57

어떤 프로젝트가 되었든지 간에,

우리는 "수정하기"라는 기능은 필수적이다.

 

회원 정보 수정

게시글 수정

상품 수정

업체 수정

등등..

 

사실 매우 간단한 기능이다.

@PutMapping
void modify(..) {...}

혹은

@PatchMapping
void modify(..) {...}

이렇게 Controller에서 매핑만 해주면 금방 구현이 가능한 기능이다.

 

하지만 이렇게 고민 목록에 넣은 이유는

수정하기가 쉬우면서 은근 복잡한 로직이 된다는 이유이기 때문이다.

 

왜 복잡?

지금부터 2가지 문제에 대해 이야기하고, 내가 찾은 해결방법을 공유하면서 게시글을 마치겠다.

 

이야기에 앞서, 상황을 가정하자.

나는 지금 회원정보의 수정을 구현하는 중이고,

회원정보 수정은 일부만 변경이 가능하여 Patch method를 사용하다.

회원 정보는 이름, 나이, 성별이 존재한다.

현재는

{
	"name" : "대환",
	"age" : 25,
	"gender" : "MALE"
}

이렇게 정보가 저장되어있다.

 

 

나이만 수정하는 경우

Client의 Request JSON은 어떤 모습을 띄어야 할까?

 

사실 가장 간단한 방법은 그냥 json에 기존 모든 값을 담아서 보내는 것이다.

 

나이만 수정하는 경우에도 모든 값을 함께 보내는 것이다.

{
	"name" : "대환",
	"age" : 24,
	"gender" : "MALE"
}

 

이 방법은 가장 무난하나,

뭔가 나는 찝찝했다.

 

"왜 나이만 변경하는데도 다른 값이 항상 같이 들어와야 하지?"

"다른 값이 같이 온다는 것은, 저 Patch 요청이 오기 전에 GET 요청으로 미리 보내진 정보가 존재해야 한다는 의미인 거네?"

 

즉 상식적으로 생각해보면

나이만 변경하는 상황이라면

{
	"age" : 24
}

이렇게만 요청이 들어와도 나이가 변경되면 된다는 의미이다.

 

나이만 보냈는데,

다른 값은 null로 처리가 되네..?

 

여기서부터 내 고민은 진짜 시작되었다.

저 요청을 통해 생성된 dto객체는 age는 24라는 값을 갖겠지만,

기타 매핑되는 값에 대해서는 null을 가지고 있다.

 

그래서 Service Logic에서는 null이 아닌 값에 대해서만 해당 Entity의 값 변경이 진행되어야 하는데,

나는 그냥 모르고 단순하게 구현해버렸었다.

 

@Entity
public class Member {
	...
    void modify(RequestDto requestDto) {
    	this.name = requestDto.getName();
        this.age = requestDto.getAge();
        this.gender = requestDto.getGender();
    }
}

이렇게 구현하게 되면

해당 회원이 갖고 있던 필드 값들이 null로 변경되는 문제를 발견하게 된다.

 

 

내가 고민해본 해결책은 이렇게 된다.

 

1. 그냥 첫 번째 방법처럼 사전에 GET 요청을 통해 받은 기존 데이터를 다시 request 요청에 담고,

   바뀐 데이터만 수정시켜 전체 데이터를 보낸다.

 

2. 내부에서 null 필드가 있는지 검증과정을 거쳐 null이 아닌 값들에 대해서만 데이터를 변경시킨다.

 

2번 방법이 나는 최선이라 생각되었고 구현을 해봤으나, 

코드가 상당히 길어지고 뭔가 복잡해지는 느낌을 받았다.

 

@Entity
public class Member {
	...
    void modify(RequestDto requestDto) {
    	if (requestDto.isNameChanged)) {
        	this.name = name;
        }
        
        if (requestDto.isAgeChanged()) {
        	this.age = age;
        }
        
        if (requestDto.isGenderChanged()) {
        	this.gender = gender;
        }
    }
}

지금이야 수정될 필드가 최대 3개니까 별거 없지만,

이게 많아지면 뭔가 너무 하드코딩을 하고 있는 느낌을 심하게 받게 된다.

 

뭔가 다른 방법이 또 있지 않을까 싶어서

레퍼런스를 찾아본 결과,

 

Entity와 Dto 간의 Mapping을 도와주는 라이브러리와 여러 방법들을 알아냈다.

 

대표적으로 MapStruct 라이브러리이다.

 

해당 라이브러리를 사용하면 위에서 내가 고민한 문제뿐 아니라,

기존에 Dto와 Entity를 매핑해주는 모든 작업에 대해 자동으로 구현체를 생성하여

개발 코드의 간결함, 가독성의 증가, 지루함의 감소라는 장점을 얻을 수 있다.

 

위의 예시로 들자면,

 

우선 만약 Member가 아니라 다른 객체에도 Dto Mapping이 있다는 가정에

interface를 생성하여 각각에 맞는 구현체를 구현하도록 한다.

(상세한 코드 구현은 하단의 docs를 참고하기 바란다.)

 

@Mapper
public interface GenericMapper<D, E> {
	D toDto(E entity);
    E toEntity(D dto);
    
    void updateEntityFromDto(D dto, @MappingTarget E entity);
}

이렇게 인터페이스를 구성하고, 알맞은 객체에 대해 구현체가 자동으로 생성되도록

인터페이스를 상속받아 제네릭을 채워준다.

 

@Mapper
public interface MemberMapper extends Generic<RequestDto, Member>{
}

 

그리고 Mapping이 필요한 로직에서 Mapping 작업을 진행하면 끝이다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
	private final MemberMapper memberMapper;
    
    ...
    
    @Transactional
    void modify(MemberAdapter memberAdater, Long memberId, RequestDto requestDto) {
    	memberMapper.updateMemberFromDto(requestDto, memberAdater.getMember());
    }
}

이렇게 금방 코드가 끝나버린다.

 

그런데 개인적으로 이 방법에도 단점이 있다고 생각한다.

 

우리는 객체지향적인 설계, 안정적인 개발을 위해서 

setter()에 대해 엄격히 생각하는 편이다.

 

하지만 해당 방식을 사용하는 경우에는

interface에 대해 구현체는 자동으로 생성하면서 setter()를 사용하여 내부를 채우기 때문에,

setter()가 열린 상태도 존재해야 한다는 점이다.

 

그럼 대체 무엇이 좋은 방법일까?

사실 나는 큰 프로젝트라면 

오히려 더 setter()에 대해 엄격해져야 한다고 생각한다.

더보기

이유는 객체지향 SOLID 중

개방 폐쇄 원칙에서 내부 로직을 숨기면서 외부에서의 접근을 무조건적으로 차단하는 것을 중요시하기 때문이다.

setter() 사용하면 객체들 간의 의존성이 증가하게 될 것이다.

 

그래서 나는 해당 프로젝트의 수정 방식을

내부에서 별도의 null 검증을 갖는 방식으로 채택했다.

 

음 이게 옳은 것인지는 나도 잘 모르겠다.

 

많은 여러 기업들의 기술 블로그와 고수들의 기술 블로그를 보면

Mapper를 통해서 dto와 entity 간의 mapping 시간을 절약하여 코드의 생산성을 높일 수 있다고 주장한다.

 

답은 없는 것 같다.

만약 이 글을 읽고 계신다면, 판단은 본인이 직접 하는 것이 맞는 것이다.

 

만약 제 글에 오류가 있다면,

언제든 지적을 환영합니다.

 

 reference

공식 문서

https://mapstruct.org/

 

MapStruct – Java bean mappings, the easy way!

Java bean mappings, the easy way! Get started Download

mapstruct.org

https://mapstruct.org/documentation/spring-extensions/reference/html/#mapperAsConverter

 

 

MapStruct Spring Extensions 0.1.0 Reference Guide

MapStruct Spring Extensions is a Java annotation processor based on JSR 269 and as such can be used within command line builds (javac, Ant, Maven etc.) as well as from within your IDE. Also, you will need MapStruct itself (at least version 1.4.0.Final) in

mapstruct.org

한글 블로그

ttps://meetup.toast.com/posts/213

 

Object Mapping 어디까지 해봤니? : NHN Cloud Meetup

이 글에서는 Object Mapping 라이브러리인 MapStruct에 대해 소개합니다. NHN Forward 2019에서 발표한 내용에 대해 조금 더 자세히 설명합니다.

meetup.toast.com

https://www.skyer9.pe.kr/wordpress/?p=1596 

 

MapStruct 사용하기 – 상구리의 기술 블로그

MapStruct 사용하기 MapStruct 를 이용하면 DTO – Entity 간 매핑을 간편하게 할 수 있고, 더불어 Entity 업데이트 코드를 매우 간단하게 줄일 수 있다. annotationProcessor 의 순서가 중요하다. mapstruct-processor

www.skyer9.pe.kr

 

"daehwan2yo"   contact

software, soongsil univ  <  now

udsward@gmail.com  < email

https://github.com/daehwan2yo <    git

 

daehwan2yo - Overview

daehwan2yo has 19 repositories available. Follow their code on GitHub.

github.com