+) 22.12.10. 추가
MapStruct 사용 여부에 대한 고찰은 여기로 이동해주세요!
https://kth990303.tistory.com/403
아래 글은 오래 전에 작성됐습니다.
지식이 부족할 때 작성된 글임을 유념해주시고 읽어주시면 감사하겠습니다 :)
그 동안 View layer에서 Entity에 직접적으로 접근하도록 코드를 짰던 나에게,
이번 DTO 적용은 상당히 고된 일이었다.
사실 dto는 단순한 entity의 클론 느낌이라 적용이 크게 어렵지 않을 줄 알았는데, 전혀 그렇지 않았다.
entity <-> dto로 서로 적절하게 변환해주는 mapper인 mapstruct를 새롭게 알게 되었고,
DTO가 왜 필요한지, 그리고 DTO가 필요한 이유를 적극 살리기 위해 Service<-> Controller에서 엔티티랑 DTO 중 무엇을 반환할지 여러모로 고민해보는 계기가 되었다.
MapStruct 사용
다른 블로그에도 mapstruct 사용방법은 흔히 볼 수 있지만, 그래도 간단하게 포스팅해보려 한다.
환경설정은 반드시 아래처럼 해주자.
build.gradle
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
반드시 lombok이 먼저, mapstruct가 나중에 오도록 디펜던시 설정을 해주어야 한다.
순서가 잘못되면 제대로 mapper가 설정되지 않는다!
디렉토리 구조
나의 경우 DTO를 2개로 했기 때문에 mapper 또한 2개이다.
사실 처음에는 DTO가 하나였는데, 좀 더 깔끔하게 만드려다보니 DTO가 두 개로 되었다.
모든 Entity <-> DTO 변환에 적용시킬 GenericMapper 인터페이스와,
각 도메인 <-> 각 DTO에 맞게 변환시킬 mapper 인터페이스 위치를 잘 보도록 하자.
GenericMapper
package algopa.study;
import org.mapstruct.BeanMapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import java.util.List;
public interface GenericMapper <D, E> {
D toDto(E e);
E toEntity(D d);
List<D> toDtoList(List<E> entityList);
List<E> toEntityList(List<D> dtoList);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateFromDto(D dto, @MappingTarget E entity);
}
코드를 보면 알겠지만,
toDto, toEntity가 각각 Dto, Entity로 변환시켜주는 코드이다.
updateFromDto는 dto에 변화가 있을 경우 Entity를 update시켜주는 코드이며, @BeanMapping의 nullValuePropertyMappingStrategy 덕분에 null 값이 전달될경우 변화시켜주지 않도록 하였다.
MemberMapper
package algopa.study.member.mapper;
import algopa.study.GenericMapper;
import algopa.study.member.domain.Member;
import algopa.study.member.dto.MemberDto;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface MemberMapper extends GenericMapper<MemberDto, Member> {
}
MemberMapper 인터페이스는 GenericMapper 인터페이스를 상속받는다.
@Mapper를 꼭 붙여주자. 그래야 mapstruct가 인식을 한다.
MemberIdMapper 코드 또한 Dto 이름이 MemberIdDto인 것을 제외하곤 똑같다.
메소드를 따로 작성하지 않아도, mapstruct가 자동으로 메소드들을 오버라이딩하고 Impl class를 생성하여 만들어준다!
이제 프로젝트를 새로 빌드해주자.
build.gradle 설정이나, mapstruct 버전이 최신이 아니라면 제대로 파일이 생성되지 않을 수 있다!
MapperImpl 파일은 generated 폴더에 생기므로, 안보인다고 허둥지둥대지 말 것!
MemberMapperImpl
짜잔, 잘 생성됐음을 확인할 수 있다.
이제 적절히 service 코드나 controller 코드를 변경해주면 된다.
Service <-> Controller 에서 DTO를 반환하는 게 좋을까?
우선 이 부분은 아래 블로그에 정말 상세하게 포스팅이 되어있다.
https://xlffm3.github.io/spring%20&%20spring%20boot/DTOLayer/
그런데 도중에 들은 생각이,
딱히 update나 write가 필요하지 않은 service 메소드에선 entity를 반환해도 어차피 수정작업이 없으니 DB나 entity 수정할 일이 없으니 @Transactional(readonly = true) 어노테이션을 붙이고 Entity를 반환해도 상관없지 않을까 하는 생각이 들었다.
사실 이렇게 해도 오답은 아니다.
그러나, 학교 선배님께, 그리고 동기에게 아래와 같은 답변을 들었다.
이런 고민을 한 이유가 있다.
사실 이 때 당시, service 메소드에선 findAllMembers 메소드만은 Entity를 반환하게 사용하고 있었다.
그러한 이유는 아래와 같았다.
- Entity는 회원id 정보를 가지고 있었고, DTO는 회원id 정보를 가지고 있지 않았다. 클라이언트 입장에서 회원 id는 필요하지 않기 때문이다. (로그인할 때 id가 아닌, 회원번호의 id이다.)
- index.html에서 findAllMembers 메소드를 사용하고 있었고, index.html에서 '내정보', '정보수정', '(관리자용)정보 삭제' 등의 기능을 쓸 때 @Pathvarible 로 id를 필요로 했기 때문에 회원id 정보가 필요한 상황이었다.
- findAllMembers는 단순히 회원 전체를 조회하는 기능이었기 때문에 write 작업이 요구되지 않았으며, @Transactional(readOnly=true) 로 write 작업 또한 막아놓은 상황이었고, 유지보수에도 크게 문제될 것 같지 않았따.
하지만 지금은 개인 프로젝트라 상관없지만, 이후에 협업을 할 때 기획이 바뀌거나 수정사항이 발생하여, 상대방이 내 로직 때문에 Entity에 잘못 접근해버린다면 많이 힘들어질 수 있기 때문에 결국 findAllMembers 메소드도 dto를 반환하도록 바꿔주기로 했다.
dto에 id를 존재하지 않게 하겠다는 마음엔 변화가 없어서 DTO를 2개로 만들기로 했다!
사실 id List를 만들어, dtoList와 id List를 동시에 반환하여 model에 addattribute할까도 생각했는데,
이렇게 할 경우, 나중에 회원을 삭제할 때 dtoList와 idList 두 배열에 모두 작업을 해야 하므로 상당히 귀찮아질 것 같았다.
이렇게 하여 DTO를 2개 만들기로 결심했고,
한 도메인 폴더에 파일이 굉장히 많아질 것 같아, 디렉토리 구조 또한 controller, service, repository, dto, mapper 끼리 깔끔하게 폴더로 정리하기로 결심했다.
폴더 정리는 아래 포스팅을 참고하자.
https://kth990303.tistory.com/130
이제 service 코드에 Entity를 반환하는 메소드는 존재하지 않게 되었다!
사실 개인프로젝트이다보니 DTO를 사용하지 않아도 불편함을 크게 못느꼈는데, 이렇게 적용함으로써 되게 많은 것을 배운 것 같아 DTO를 적용하길 잘했다는 생각이 든다.
그리고 프젝을 진행하면 할수록, 직접 경험하고 착오를 겪는 것이 매우매우 중요함을 깨닫는다~
+) 22.12.10. 추가
이 글이 작성된지 1년 후에 작성한 2탄도 있습니다~
https://kth990303.tistory.com/403
'JAVA > 소박한그룹 프로젝트' 카테고리의 다른 글
[Spring] MapStruct 수동으로 오버라이딩 코드 구현하기 (0) | 2021.09.14 |
---|---|
[Thymeleaf] 타임리프 Thymeleaf th:style, th:if 사용하기 (0) | 2021.09.14 |
[Spring] 인텔리제이 디렉토리 구조 변경 중 발생한 Error (0) | 2021.09.08 |
[Java] Spring Security를 이용한 로그인/로그아웃 기능 구현 1. 환경설정 (Thymeleaf + Gradle + IntelliJ + Spring Security + MySQL) (0) | 2021.09.03 |
[Spring] Solved API를 이용한 개발에 도움이 되는 글 (0) | 2021.06.09 |