스프링 프로젝트 구조
DTO : 계층 간(Controlelr, View, Business Layer) 데이터 교환을 위한 객체
- DTO는 getter / setter 메소드를 포함한다. 하지만, 이외의 다른 비즈니스 로직은 포함하지 않는다.
Controller :
- 프레젠테이션 계층으로 클라이언트의 요청을 처리
- 서비스에 정의된 비즈니스 로직을 호출 ResponseBody에 데이터를 담아 반환
Service :
- 프레젠테이션(뷰)에서 엔티티에 직접 접근하지않고 비즈니스 로직을 처리할 수 있도록하는 계층
- Repository에 정의된 비즈니스 로직을 처리하거나 엔티티에 접근
Repository(DAO (Data Access Object)) : Entity를 통해 데이터를 DB에 저장
- 직접 DB에 접근하여 data를 삽입, 삭제, 조회 등 조작할 수 있는 기능을 수행
Domain(Entity) : DB 테이블과 1:1 매칭된다
DTO란?
DTO(Data Transfer Object)란 계층간 데이터 교환을 위해 사용하는 객체(Java Beans)이다.
원격 인터페이스로 작업을 할 때, 호출에 따른 비용이 비싸기 때문에 요청의 횟수를 줄여야 하고, 이를 위해 한번의 요청에 더 많은 데이터를 전송해야 한다. 외부와 통신하는 프로그램에게 있어 호출은 큰 비용이며, 이를 줄이고 더욱 효율적으로 값을 전달할 필요가 있다. 이를 위해 데이터를 모아 한번에 전달하는 클래스를 DTO라고 한다.
Entity를 통해 DB에서 데이터를 꺼내왔지만 데이터를 접근해야하는 경우 문제가 있다.
Controller와 Presentation Layer는 클라이언트와 직접 만나며, Entity는 프레젠테이션 계층과 완전히 분리되어야 한다.
도메인 객체를 View에 직접 전달할 수 있지만, 민감한 도메인 비즈니스 기능이 노출될 수 있으며 Model과 View 사이에 의존성이 생기기 때문이다. 이러한 경우에 DTO를 사용한다.
특징
Wrapping 된 순수한 데이터 객체
Entity에 직접 접근하지 않으므로, Entity 변경시, DTO만 변경하면 된다.
엔티티(Entity)란 무엇인가?
Entity 클래스는 실제 DB 테이블과 매핑되는 핵심 클래스로, 데이터베이스의 테이블에 존재하는 컬럼들을 필드로 가지는 객체입니다.
(DB의 테이블과 1:1로 매핑되며, 테이블이 가지지 않는 컬럼을 필드로 가져서는 안 됩니다.)
값을 전달하는 클래스로 사용하는 것은 좋지 않습니다.
또 많은 서비스 클래스와 비즈니스 로직들이 Entity 클래스를 기준으로 동작하기 때문에 Entity 클래스가 변경되면 여러 클래스에 영향을 줄 수 있습니다.
Entity에서는 setter 메서드의 사용을 지양해야 합니다.
이유는 변경되지 않는 인스턴스에 대해서도 setter로 접근이 가능해지기 때문에 객체의 일관성, 안전성을 보장하기 힘들어집니다.
(setter 메서드가 있다는 것은 불변하지 않다는 것이 됩니다.) 따라서 Entity에는 setter 대신 Constructor(생성자) 또는 Builder를 사용하게 됩니다.
- 데이터의 집합을 의미한다.
- 저장되고, 관리되어야하는 데이터이다.
- 개념, 장소, 사건 등을 가리킨다.
- 유형 또는 무형의 대상을 가리킨다.
DTO Mapper란?
DTO 클래스와 엔티티(Entity) 클래스를 서로 변환해주는 것이다.(DB에 저장하기위해 엔티티에 맞게 변환해 주는 과정이다)
다만, 도메인 기능이 늘어날 때마다 혹은 변경될 때 마다 매번 DTO 내부에 구현을 하는것은 굉장히 비효율적이므로, 속도가 빠른 외부 라이브러리를 가져다가 사용하면 편리하다.
Mapper의 기능
의존관계는 최대한 약하게 해야한다. (비용문제) 특히 컨트롤러와 서비스의 의존은 위험하다. -> 서비스 전용 DTO를 만들거나 Mapper를 통해 상호변환하여 의존관계를 줄여서 해결한다. Mapper(매퍼) 클래스는 DTO 클래스와 엔티티(Entity) 클래스를 서로 변환해준다.
DTO와 Entity를 Mapping해서 변환하는 이유
- 계층별 관심사 분리
- 코드 구성의 단순화
- REST API 스펙의 독립성 확보
Mapper 사용의 이점
- Mapper에게 DTO 클래스 → 엔티티(Entity) 클래스로 변환하는 작업을 위임함으로써 Controller는 더이상 두 클래스의 변환 작업을 신경쓰지 않아도 된다. 역할 분리로 인해 코드 자체가 깔끔해진다.
- Mapper가 엔티티(Entity)클래스를 DTO 클래스로 변환해주기때문에 서비스 계층에 있는 엔티티(Entity) 클래스를 API 계층에서 직접적으로 사용하는 문제가 해결된다.
어떤 외부 Mapper라이브러리가 있을까?
여기에서 주로 JAVA에서 사용하는 매퍼 라이브러리들을 비교한 것을 확인 할 수 있다.
MapStruct와 ModelMapper
어떤 도메인 업무 기능이 늘어날때 마다 개발자가 일일이 수작업으로 매퍼(Mapper) 클래스를 만드는 것은 비효율적이므로, Mapping 라이브러리를 사용한다.
Java에서 Object를 Mapping하는 라이브러리는 생각보다 많이 존재한다. 그 중에서 가장 많이 사용되는 Mapping 라이브러리에는 MapStruct와 >ModelMapper가 있다
결론을 이야기하자면 ModelMapper가 여전히 많이 사용되고 있지만 ModelMapper는 Runtime시 Java의 리플렉션 API를 이용해서 매핑을 진행하기 때문에 컴파일 타임에 이미 Mapper가 모두 생성되는 MapStruct보다 성능면에서 월등히 떨어진다.
따라서 ModelMapper의 대안으로 MapStruct가 많이 사용되고 있는 추세이다.
MapStruct
MapStruct는 DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는 매퍼(Mapper) 구현 클래스를 자동으로 생성해주는 코드 자동 생성기이다.
- 리플렉션을 사용하지 않고, 컴파일 타임(어노테이션 프로세서)에 처리한다. (= 앱 성능에 영향 없음)
- 생성된 코드를 직접 확인할 수 있어서 매핑 도중에 오류가 생겼을 때 디버깅이 쉽다.
- 처리속도가 굉장히 빠른편이라서, 컴파일 시간에도 거의 영향을 끼치지 않는다.
- 이미 많은 자바, 스프링 개발자들이 사용하고있는 검증된 라이브러리이다.
MapStruct 세팅
Mapper를 gradle에서 사용하기 위해서는 우선, 의존성을 추가해야한다.
https://www.baeldung.com/mapstruct
dependencies {
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
...
}
lombok을 같이 사용하면 컴파일 도중 충돌할 수 있다. 그래서 아래와 같이 lombok-mapstruct-binding을 추가한다.
* 기본 코드를 생성하는 롬복이 먼저 실행되어야 하기 때문에 순서를 맞춰주어야한다. (1. lombok -> 2. mapstruct)
참고로 롬복 최신버전(1.18.16 이상)을 사용한다면, 그냥 따로 추가하더라도 알아서 롬복이 먼저 실행된다.
*.순서에 영향 : 위와 같이 mapstruct 가 lombok 뒤에 오는 경우, target class 에 @Builder 가 있어도 무시하고 생성자 + setter 를 사용하므로 정상적으로 Mapper 클래스를 Generation 하기 위해서 @NoArgsConstructor, @Setter 가 필요하다.(최근 Setter 사용을 지양하는 추세라 선호하지 않는다. mapstruct를 먼저 오도록 해서 @Builder 방식으로 Generation 되도록 하는 것을 개인적으로 추천한다.)
*.lombok 버전이 1.18.16 이상인 경우, annotationProcessorPaths 에 lombok-mapstruct-binding 을 추가해줘야 한다.
annotationProcessor(
'org.projectlombok:lombok', // lombok 1.18.16 이상은 이렇게 순서지정 안해도 된다.
'org.projectlombok:lombok-mapstruct-binding'
)
Mapstruct 사용하기
https://mapstruct.org/documentation/dev/reference/html/#mapping-collections
Entity와 DTO 구현
Member관련 DTO와 Entity 클래스를 만듭니다.
@Entity //JPA를 사용해 테이블과 매핑할 클래스에 붙여주는 어노테이션
@Table(name = "MEMBER")
@Data
@Builder
@NoArgsConstructor // 파라미터가 없는 기본 생성자를 생성
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자 생성
public class MemberEntity {
@Id
@NotNull
private String userId; // 아이디
@NotNull
private String password; // 비밀번호
@NotNull
private String name; // 이름
@NotNull
private String regNo1; // 주민등록번호 앞자리
@NotNull
private String regNo2; // 주민등록번호 뒷자리
}
@Data
@Builder
@NoArgsConstructor // 파라미터가 없는 기본 생성자를 생성
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자 생성
public class MemberDTO {
@NotBlank(message = "아이디를 입력해주세요")
@Size(min = 4, max = 12, message = "아이디는 4글자 이상, 12글자 이하로 입력해주세요.")
@ApiModelProperty(example = "hong12") //DTO 예제 설명
public String userId;
@NotBlank(message = "비밀번호를 입력해주세요.")
@Size(min = 6, max = 20, message = "비밀번호는 6글자 이상, 20글자 이하로 입력해주세요.")
@ApiModelProperty(example = "123456")
public String password;
@NotBlank(message = "이름을 입력해주세요")
@Size(min = 2, max = 6, message = "이름은 2글자 이상, 6글자 이하로 입력해주세요.")
@ApiModelProperty(example = "홍길동")
public String name;
@NotBlank
@Size(min = 14, max = 14, message = " 주민등록번호는 -을 포함하여 14글자를 입력해주세요.")
@Pattern(regexp = "\\d{6}\\-[1-4]\\d{6}")
@ApiModelProperty(example = "860824-1655068")
public String regNo;
}
- 먼저 롬복을 이용해 Builder를 만들어주었습니다.
- From 쪽은 반드시 Getter를 만들어야 합니다. (Entity to DTO의 경우 From은 Entity가 됩니다.)
To 쪽은 반드시 Setter 혹은 Builder를 만들어야 합니다.- 매핑해줄 클래스에는 setter가 있어야 하고 매핑이 되는 클래스에는 getter가 있어야 사용 가능합니다. (Users Entity -> UserDTO로 변경시 entity쪽에 getter, dto쪽에 setter가 존재해야합니다)
- 제공되는 생성자만으로 인스턴스 생성에 문제가 없다면, Setter, Builder가 없어도 사용가능합니다.
- 둘의 차이가 있다면 DTO의 경우에는 regNo 가 있고 , Entity에는 regNo1, regNo2가 있습니다.
Mapper 인터페이스 구현
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);
}
- Entity와 DTO로 변환시켜줄 상위 인터페이스를 만들어주었습니다.
@Mapper // 어노테이션을 붙여줍니다.
인터페이스<DTO, Entity> {
인테페이스명 MAPPER = Mappers.getMapper(인터페이스명.class);
@Mapping(taget = 필드명, constant = 디폴트값) // 필드명과, 디폴트값은 string 형태로 해주어야 합니다.
메소드() ;
//DTO to Entity
//DTO와 Entity의 필드명이 다른경우
@Mapping(source = DTO필드명, target = Entity필드명)
UserEntity toEntity(final UserDTO dto);
}
- 본체는 MemberMapper 인터페이스 입니다.
- 오버라이딩 한부분은, 디폴트로 값을 넣어주고 싶은 경우에 사용합니다.
- 해당 부분을 생략한다면, DTO to Entity과정에서 id값에는 null값이 들어가게 됩니다.
@Getter
@AllArgsConstructor
public class UserEntity {
private Long id;
}
...
@Getter
@AllArgsConstructor
public class UserDTO {
private Long userId;
}
@Mapper
public interface UserMapper extends EntityMapper<UserDTO, UserEntity> {
UserMapper MAPPER = Mappers.getMapper(UserMapper.class);
@Override
@Mapping(source = "userId", target = "id")
UserEntity toEntity(final UserDTO dto);
@Override
@Mapping(source = "id", target = "userId")
UserDTO toDto(final UserEntity entity);
}
@Mapping anntation
qualifiedByName
qualifiedByName 에 매핑할때 이용할 메소드를 지정해주고, 커스텀 메소드에는 @Named() 를 이용해 매핑에 이용될 메소드라는 것을 명시해줍니다.
Policy & Strategy
매핑 정책(Policy)과 전략(Strategy)를 설정할 수 있다. 아래는 몇 가지 유용한 매핑 정책과 전략에 대한 설명이다.
정책 | 값 | 설명 |
unmappedSourcePolicy | IGNORE(default), WARN, ERROR |
Source의 필드가 Target에 매핑되지 않을 때 정책이다. 예, ERROR로 설정하면 매핑 시 Source.aField가 사용되지 않는다면 컴파일 오류가 발생시킨다. |
unmappedTargetPolicy | IGNORE, WARN(default), ERROR |
Target의 필드가 매핑되지 않을 때 정책이다. 예, ERROR로 설정하면 매핑 시 Target.aField에 값이 매핑되지 않는다면 컴파일 오류가 발생시킨다. |
typeConversionPolicy | IGNORE(default), WARN, ERROR |
타입 변환 시 유실이 발생할 수 있을 때 정책이다. 예, ERROR로 설정하면 long에서 int로 값을 넘길 때 값에 유실이 발생할 수 있다. 이런 경우에 컴파일 오류를 발생시킨다. |
전략 | 값 | 설명 |
nullValueMappingStrategy | RETURN_NULL(default), RETURN_DEFAULT |
Source가 null일 때 정책이다. |
nullValuePropertyMappingStrategy | SET_TO_NULL(default), SET_TO_DEFAULT, IGNORE |
Source의 필드가 null일 때 정책이다. |
연관된 글 :
[JAVA] DTO, VO, DAO, Entity 의 차이
[Spring] 빌더 패턴(Bulider Pattern)
참고:
[Java] DTO <-> Entity 변환(ModelMapper & method & ModelMapper List 바인딩)
DTO - Entity 변환 메서드 구현 위치는 어디가 좋을까?
JPA MapStruct, ModelMapper 설정 방법, 차이 Entity to DTO, DTO to Entity
[Spring Boot] mapstruct 적용 시 주의점
[개발자 경험기] 편리한 객체 간 매핑을 위한 MapStruct 적용기 (feat. SENS)
추가 매핑 방법 with Custom - [Spring] MapStruct의 @Mapper, @Mapping 에 대해
Java - Model(Object) mapping을 위한 Mapstruct (맵스트럭트)!
MapStruct 라이브러리를 이용해 DTO <-> Entity 변환 하기
[Spring] DTO와 Mapper, Mapper(Map Struct) 사용 방법 - gradle
[JPA] Entity Mapping Annotation 정리
Spring Data JPA로 데이터베이스 스키마 생성
'개발 > Spring' 카테고리의 다른 글
[Spring] Swagger 어노테이션 (0) | 2023.04.28 |
---|---|
[Spring] 빌더 패턴(Bulider Pattern) (0) | 2023.04.28 |
[Spring] 스웨거 (Swagger) 설정하기 (0) | 2023.04.27 |
[Spring] h2 DB 연결하고 JPA 사용하기 (0) | 2023.04.27 |
[Spring] 스프링시큐리티(Spring Security) 개념 (0) | 2023.04.26 |