티스토리 뷰
이번글에서는 현재 진행 중인 애완동물 분양 플랫폼 프로젝트에서 백엔드 API를 개발중 발생하는 중복 및 단순 매핑 코드들을 최소화하기 위한 개선 시도 및 결과에 대해 공유드리겠습니다.
개선 전 상황과 목표
JPA를 사용시 Entity 클래스를 통해 데이터베이스와 매핑함으로 Entity를 외부에 노출시 데이터 무결성 및 보안 그리고 캡슐화에 좋지 않습니다. 이러한 외부 노출을 피하기 위하여 dto를 이용하며 dto to entity 또는 entity to dto 와 같은 매핑 로직을 통해 데이터를 주고 받습니다. 하지만 이러한 매핑 과정이 많은 비용을 발생시키고 있다고 판단하였으며 어떻게 최소화 할 수 있을까 고민했습니다.
또한, 현재 프로젝트에서는 총 3개의 이미지 테이블을 사용하고 있으며 5개의 컬럼이 동일한 역할을 하는 상황입니다. 정규화를 고려할 수 있지만 유지 관리 측면에서 반정규화가 효율적이다 판단되어 중복 컬럼이 발생하게 되었습니다. 이러한 중복 컬럼로 인하여 매핑 필드가 늘어나는 것이 유지관리 측면에서 좋지 않다고 판단되었으며 어떻게 묶어서 처리할 수 있을까에 대한 고민도 들었습니다.
이러한 문제점들을 통해 'Entity 매핑 자동화 및 중복 코드 최소화'라는 목표를 잡고 개선 방안을 모색해보았습니다.
개선 및 결과
중복 컬럼 관리 코드 최소화
데이터베이스 테이블과 매핑되는 Entity들 중에서 동일한 이름을 가진 중복되는 필드를 한 객체를 통해 처리하는 방법으로 중복 컬럼을 위한매핑 코드를 최소화 할 수 있겠다는 판단이 들었습니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseImageEntity{
@CreatedDate //생성일자 자동입력
@Column(name = "reg_date")
private LocalDateTime regDate;
@Column(name = "reg_id")
private String regId;
@Column(name = "image_url")
private String imageUrl;
@Column(name = "image_name")
private String imageName;
@Column(name = "image_type")
private String imageType;
public void addImageUrl(String url){
this.imageUrl = url;
}
public void addRagId(String regId){
this.regId = regId;
}
public void addImageName(String name){
this.imageName = name;
}
public void addImageType(String type){
this.imageType = type;
}
}
- @MapperSuperClass : 상속 받는 Entity에 매핑 정보가 적용되는 클래스를 지정할 때 사용합니다. 즉, BaseImageEntity를 상속받는 Entity는 위 코드에 포함된 필드를 상속 받는 것으로 JPA는 인식하게 됩니다.
- @EntityListeners(AuditingEntityListener.class)
- EntityListeners는 Entity 또는 매핑된 수퍼클래스에 사용할 콜백 리스너 클래스를 지정합니다.
- AuditingEntityListener 클래스는 Entity의 생성일 , 수정일 등과 같은 정보를 관리하기 위해 사용되는 리스너 클래스 입니다. @PrePersist와 @PreUpdate 어노테이션과 함께 사용되는데 @PrePersis는 엔티티가 저장되기전 호출되는 메서드에 적용 되며, @PreUpdate는 엔티티가 업데이트되기 전에 호출되는 메서드에 적용됩니다.
- 결과적으로 @EntityListeners(AuditingEntityListener.class)를 Entity에 지정할 경우 해당 Entity의 변경사항이 발생되었을때 JPA가 자동으로 감지하여 데이터를 입력하거나 수정하는 역할을 하게됩니다.
BaseEntity를 통해 15개의 매핑 필드를 5개로 감소시켰으며, @EntityListeners(AuditingEntityListener.class)와 @CreateDate를 통해 생성일자를 개발자가 따로 입력하지 않고 JPA가 자동으로 처리하게 하여 유지보수가 필요한 사항들을 최소화 할 수 있었습니다.
Entity, DTO 매핑 자동화
두 번째로는 Entity와 DTO간의 매핑을 자동화하여 아래 이미지의 코드와 같은 단순 매핑 코드를 최소화하는 것이 주요 개선 사항중 하나였습니다.
리서치 결과 객체간의 매핑시 자동화를 위한 라이브러리가 있었으며 그 중 보편적으로 사용하는 ModelMapper, MapStruct를 비교해 보았습니다.
ModelMapper의 경우 MapStruct에 비해 상대적으로 간단하게 도입 가능하지만 속도가 MapStruct에 비해 상대적으로 느리며, 컴파일 단계에서 오류를 확인할 수 없다는 단점으로 인해 MapStruct를 도입하기로 결정했습니다.
Generic Mapper
MapStruct 라이브러리를 사용하기 위해 의존성을 추가한 후 공통 매핑을 위한 인터페이스를 만들었습니다.
public interface GenericMapper<DTO,Entity>{
DTO toDTO(Entity entity);
Entity toEntity(DTO dto);
ArrayList<DTO> toDtoList(List<Entity> list);
ArrayList<Entity> toEntityList(List<DTO> dtoList);
/* Null 값이 전달될 경우 변화 시키지 않도록 설정 */
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateFromDto(DTO dto, @MappingTarget Entity entity);
}
Mapper
GenricMapper를 상속 받은 서비스 로직의 Mapper를 만들어 추가적인 커스텀화를 진행했습니다. Mapper는 인터페이스임으로 실제 구현체가 필요하며, 실제 구현체는 컴파일 과정에서 생성됩니다. 이때 제네릭에 입력된 타겟 클래스(dto,entity)의 접근자(getter)와 수정자(setter)를 정보를 바탕으로 구현체의 로직이 생성되며 각각의 비즈니스 환경에 맞게 타겟 클래스의 코드 조정이 필요할 수 있습니다.
// 해당 매퍼를 스프링 빈(bean)으로 등록하기 위한 설정
@Mapper(componentModel = "spring",
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL)
public interface CreateArticleMapper extends GenericMapper<ArticleDto, Community> {
CreateArticleMapper INSTANCE =
Mappers.getMapper(CreateArticleMapper.class);// Mapper class의 instance를 얻어 초기화
@Override
Community toEntity(ArticleDto articleDto); // plan method - @Mapping 으로 옵션 지정가능
@Override
@Mapping(target = "image", ignore = true) // imgNo은 매핑 제외
ArticleDto toDTO(Community community);
}
Service
최종적으로 위 Mapper 인터페이스를 통해 생성된 객체를 사용하여 아래 코드와 같이 한 줄로 DTO를 Entity로 매핑을 할 수 있게 되었습니다.
@Transactional
public void insertArticle(ArticleDto articleDto){
// CreateArticleMapper 인스턴스 생성
final CreateArticleMapper createArticleMapper = CreateArticleMapper.INSTANCE;
// DTO를 Entity로 매핑
Community community = createArticleMapper.toEntity(articleDto);
// 카테고리 번호 검증
checkCategoryNo(community.getCategoryNo());
// Community DB 저장
Community saveArticle = communityRepository.save(community);
// 이미지 업데이트
updateImageByArticleNo(articleDto.getImage(), saveArticle.getArticleNo());
}
참고 자료
BaseEnity 구현 - https://www.inflearn.com/course/ORM-JPA-Basic
MapStruct 활용법 - https://madplay.github.io/post/mapstruct-in-springboot
'Spring > JPA' 카테고리의 다른 글
[Spring Data JPA] 효율적인 페이지네이션을 위한 Pageable, Slice 분석 (0) | 2023.05.01 |
---|---|
[JPA] 엔티티와 영속성 컨텍스트 (0) | 2023.03.18 |
[JPA] JPA What? Why? (0) | 2023.03.18 |
- Total
- Today
- Yesterday