- 효율적으로 MVC, Web layer를 설계하기 위해 기본적인 REST API를 짜보면서 기초를 학습했다.
MapStruct : Entity와 DTO의 Mapping
등장 배경
- Spring Framework 개발 환경을 예로, 각 레이어(Controller, Service, Repository 등)에서
데이터를 주고 받거나, 비즈니스 로직에서 정의된 객체 타입을 다른 타입으로 변환하는 일이 빈번하게 일어난다.
문제점
- 소개된 배경으로 인하여, 그동안 개발자가 코드를 작성할 때는 다음과 같은 문제가 생겼다.
- 반복적이고 코드 중복 발생
- 개발자의 실수 번복
- 코드 생산성 저하
- 비즈니스 로직에 섞여(독립적X), 코드가 복잡해진다.
등등의 이유로 "유지보수"가 힘들어진다.
해결 방법 : MapStruct
- Java 개발 환경에서 객체간 매핑에 대해 코드를 자동으로 생성해주는 매핑 라이브러리
Annotation Processor를 사용해 컴파일 시 매핑 코드를 생성한다.
그래서 뭐가 어떻게 좋은데?
- 컴파일 시 설정된 방식으로 오류 확인 가능 (코드 생성 시)
- 리플렉션을 사용하지 않아 매핑 속도가 빠르다.
속도 비교 자료
- ModelMapper와 Orika 에서는 Runtime 시점에 Reflection을 통해 Mapping을 해서 객체의 사이즈가 커질수록 메모리를 많이 사용해서 성능 저하의 주된 원인이 되는 듯 하다.
MVC, Web Layer 짜보기
- 스프링 계층은 크게 Presentation, Business, Data Access 3가지로 나뉜다.
1. Presentation Layer
- 브라우저 상 웹 클라이언트의 요청 및 응답 처리
- Service, DA 계층에서 발생하는 Exception 처리
- @Controller 어노테이션을 사용하여 작성된 Controller 클래스가 이 계층에 속한다.
2. Service Layer
- 애플리케이션 비즈니스 로직 처리와 비즈니스와 관련된 도메인 모델의 적합성 검증
- 트랜잭션 관리
- 프레젠테이션 계층과 데이터 액세스 계층 사이를 연결하는 역할로서 두 계층이 직접적으로 통신하지 않게 한다.
- Service 인터페이스와 @Service 어노테이션을 사용하여 Service 구현 클래스가 이 계층에 속한다.
3. Data Access Layer
- ORM (Mybatis, Hibernate)를 주로 사용하는 계층
- DAO 인터페이스와 @Repository 어노테이션을 사용하여 작성된 DAO 구현 클래스가 이 계층에 속한다.
- Database에 Data를 CRUD 하는 계층
+ Domain Model Layer
- DB의 테이블과 매칭될 클래스
- Entity 클래스라고도 부른다.
코드와 정의
Domain
@Data
@Entity
public class Vendor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Entity Class
- Domain 이라고도 부른다(JPA 사용할 때 사용)
- 실제 DB 테이블과 매칭될 클래스
- Entity 클래스 또는 가장 Core 한 클래스라고 부른다.
- Domain 로직만을 가지고 있어야 하며 Presentation Logic을 가지고 있어서는 안된다
DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VendorDto {
private Long id;
private String name;
}
...
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VendorListDto {
List<VendorDto> vendors;
}
DTO (Data Transfer Object)
- 각 계층 간 데이터 교환을 위한 객체 (데이터를 주고 받을 포맷)
- Domain, VO 라고도 부른다.
- DB에서 데이터를 얻어 Service, Controller 등으로 보낼 때 사용한다.
- 로직을 갖지 않고 순수하게 getter/setter 메소드를 가진다.
Entity 클래스와 DTO 클래스 분리하는 이유는?
- View Layer와 DB Layer 역할을 철저하게 분리하기 위해
- 이거 무시했다간 나중에 한 파일에 온갖 코드가 덕지 덕지 붙어있는 맛있는 스파게티 코드가 완성된다. (경험담)
- 테이블과 매핑되는 Entity 클래스가 변경되면 여러 클래스에 영향을 끼치게 되지만 View와 통신하는 DTO는 자주 변경되므로 분리해주도록 한다.
- 즉 DTO는 Domain 모델을 복사한 것, 다양한 프레젠테이션 로직을 추가한 정도로 사용하며 도메인 모델은 Persistent(영속성) 만을 위해서 사용한다.
Mapper
@Mapper
public interface VendorMapper {
VendorMapper INSTANCE = Mappers.getMapper(VendorMapper.class);
VendorDto vendorToVendorDto(Vendor vendor);
Vendor vendorDtoToVendor(VendorDto vendorDto);
}
- 사진과 같이 View 를 통해 요청이 들어오면 각 계층을 타고 DB까지 내려와 정보를 얻어 뿌려준다.
Repository
public interface VendorRepository extends JpaRepository<Vendor, Long> {}
DAO (Data Access Object)
- DB에 접근하는 객체, DB를 사용해 데이터를 조작하는 기능을 하는 객체 (MyBatis 사용 시 DAO, Mapper 사용)
- Repository 라고도 부른다. ( JPA 사용 시 Repository 사용 )
- Service 계층과 DB를 연결하는 고리 역할을 한다.
Service
public interface VendorService {
List<VendorDto> getAllVendors();
VendorDto getVendorByName(String name);
VendorDto getVendorById(Long id);
VendorDto createNewVendor(VendorDto vendorDto);
VendorDto saveVendorByDto(Long id, VendorDto vendorDto);
VendorDto patchVendor(Long id, VendorDto vendorDto);
void deleteVendorById(Long id);
}
... Impl 생략
Service
- Web Layer 바로 아래에 존재하는 계층으로 트랜잭션에 대한 경계 역할
- 어플리케이션과 인프라 서비스 모두를 포함
- 어플리케이션 서비스는 서비스 계층의 공개 API를 공급하며 트랜잭션의 경계 역할과 응답을 책임지고 있다.
- Controller와 Dao의 중간영역에서 많이 사용되며 @Transcational, @Service에 사용되는 영역이다.
Controller
@GetMapping
@ResponseStatus(HttpStatus.OK)
public VendorListDto getListOfVendors() {
return new VendorListDto(vendorService.getAllVendors());
}
@GetMapping({"/{id}"})
@ResponseStatus(HttpStatus.OK)
public VendorDto getVendorById(@PathVariable Long id) {
return vendorService.getVendorById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public VendorDto createNewCustomer(@RequestBody VendorDto vendorDto) {
return vendorService.createNewVendor(vendorDto);
}
@PutMapping("{/id}")
@ResponseStatus(HttpStatus.OK)
public VendorDto updateVendor(@PathVariable Long id, @RequestBody VendorDto vendorDto) {
return vendorService.saveVendorByDto(id, vendorDto);
}
@PatchMapping("{/id}")
@ResponseStatus(HttpStatus.OK)
public VendorDto patchVendor(@PathVariable Long id, @RequestBody VendorDto vendorDto) {
return vendorService.patchVendor(id, vendorDto);
}
@DeleteMapping("{/id}")
@ResponseStatus(HttpStatus.OK)
public void deleteVendor(@PathVariable Long id) {
vendorService.deleteVendorById(id);
}
- 사용자의 요청이 진입하는 지점 (Entry Point)
- 요청에 따라 어떤 처리를 할 지 결정한다.
- 단 Controller는 결정만 하고 실질적인 처리는 Service Layer에서 담당한다.
- 사용자에게 View 또는 통신 결과를 응답으로 보내준다.
Test Code
자세한 코드는 Github에!
출처
MapStruct
Web Layer 사진 및 자료 출처