개발소설

DI를 통한 서비스 계층 ↔ API 계층 연동, 매퍼(Mapper), 엔티티(Entity) 본문

Spring Framework

DI를 통한 서비스 계층 ↔ API 계층 연동, 매퍼(Mapper), 엔티티(Entity)

ChaeHing 2023. 4. 14. 03:20

서비스(Service) 계층

  • 애플리케이션에 있어 Service는 도메인 업무 영역을 구현하는 비즈니스 로직을 처리하는 것을 의미한다.
  • Controller 클래스에 @RestController 애너테이션을 추가하면 Spring Bean으로 등록된다.
  • Service 클래스에 @Service 애너테이션을 추가하면 Spring Bean으로 등록된다.
  • 생성자 방식의 DI는 생성자가 하나일 경우에는 @Autowired 애너테이션을 추가하지 않아도 DI가 적용된다.
  • 서비스 계층에서 데이터 액세스 계층과 연동하면서 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스를 도메인 엔티티(Entity) 클래스

Mapper

  • DTO ↔ Entity 클래스를 서로 변환해주는 변환자
  • Mapper를 사용해서 DTO 클래스와 Entity 클래스 간의 관심사를 분리할 수 있다.
  • Mapper를 개발자가 직접 구현하기보다는 MapStruct 같은 매핑 라이브러리를 사용하는 것이 생산성 측면에서 더 나은 선택이다.
  • MapStruct는 DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는 매퍼(Mapper) 구현 클래스를 자동으로 생성해 주는 코드 자동 생성기
  • 매핑 우선 순위 조건  Builder > Constructor > Setter

DTO 클래스와 Entity 클래스의 역할 분리가 필요한 이유

  • 계층별 관심사의 분리
    • DTO : client의 요청 데이터를 하나의 객체로 받기 위해  (API 계층)
    • Entity : 비즈니스 로직을 처리하는데 사용하는 클래스 이후 데이터 액세스 계층에서 DB와 관련됨 (서비스 계층)
  • 코드 구성의 단순화
    • DTO에서는 유효성검사 애너테이션, Entity에서는 JPA 애너테이션을 사용하는데 두개가 섞이면 코드가 복잡해진다.
  • REST API 스펙의 독립성 확보
    • 필요한 데이터만 Response Body로 전달하기 용이
      • 비밀번호같은 민감 데이터 노출 방지
      • ResponseDto에는 비밀번호등의 필드를 뺀다.

 

Service 클래스 만들기 (서비스 계층)

import com.codestates.coffee.entity.Coffee;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CoffeeService {
    public Coffee createCoffee(Coffee coffee){
		
        // DB에서 생성(create)하는 코드 필요 (데이터 액세스)
        Coffee createCoffee = coffee;
        return createCoffee;
    }

    public Coffee updateCoffee(Coffee coffee){
		
        // DB에서 수정(update)하는 코드 필요
        Coffee updateCoffee = coffee;
        return updateCoffee;
    }

    public Coffee findCoffee(long coffeeId){

		// DB에서 조회(select)하는 코드 필요
        Coffee coffee = new Coffee(coffeeId, "아메리카노", "Americano", 2500);
        return coffee;
    }

    public List<Coffee> findCoffees(){

		// DB에서 전체 조회(select)하는 코드 필요
        List<Coffee> coffees = List.of(
                new Coffee(1L, "아메리카노", "Americano", 2500),
                new Coffee(2L, "카라멜 라떼", "Caramel Latte", 5000 )
        );
        return coffees;
    }

    public void deleteCoffee(long coffeeId){
      // DB에서 삭제 (delete)하는 코드 필요
    }
}
  • 데이터 액세스 계층으로의 연동은 아직 미구현 (stub 데이터 사용)
  • @Service : SpringBean으로 생성하기 위한 애너테이션
    • 생성자가 하나일 경우 자동으로 DI 적용
    • 생성자가 하나 이상일 경우, DI를 적용하기 위한 생성자에 반드시 @Autowired을 사용
  • Controller 클래스의 핸들러 메서드와 1대1로 매치하여 생성

 

Entity 클래스 만들기 - lombok 이용

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Coffee {
    private long coffeeId;
    private String korName;
    private String engName;
    private int price;
}

lombok이라는 라이브러리에서 제공하는 애너테이션으로 엔티티클래스 생성

  • @Getter, @Setter :  getter/setter 메서드를 자동으로 생성
  • @AllArgsConstructor : 현재 클래스에 추가된 모든 멤버 변수를 파라미터로 갖는 생성자를 자동으로 생성
  • @NoArgsConstructor는 파라미터가 없는 기본 생성자를 자동으로 생성

 

mapper 생성 - MapStruct 사용

//build.gradle

dependencies {
	...
	...
	implementation 'org.mapstruct:mapstruct:1.4.2.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
  • build.gradle에 MapStruct 의존 라이브러리 설정
// mapper 인터페이스

package com.codestates.coffee.mapper;

import com.codestates.coffee.dto.CoffeePatchDto;
import com.codestates.coffee.dto.CoffeePostDto;
import com.codestates.coffee.dto.CoffeeResponseDto;
import com.codestates.coffee.entity.Coffee;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring")
public interface CoffeeMapper {
    Coffee coffeePostDtoToCoffee(CoffeePostDto coffeePostDto);
    Coffee coffeePatchDtoToCoffee(CoffeePatchDto coffeePatchDto);
    CoffeeResponseDto coffeeToCoffeeResponseDto(Coffee coffee);
    List<CoffeeResponseDto> coffeesToCoffeeResponseDtos(List<Coffee> coffees);
}
  • MapStruct 기반의 매퍼(Mapper) 인터페이스 정의
    • @Mapper 애너테이션을 추가함으로써 해당 인터페이스는 MapStruct의 매퍼 인터페이스로 정의
    • @Mapper 애너테이션의 애트리뷰트로 componentModel = "spring"을 지정해 주면 Spring의 Bean으로 등록
    • 메서드 작성
      • Dto를 Entitiy로 변경하는 메서드 작성 (coffeePostDtoToCoffee)
      • Entitiy를 Dto로 변경하는 메서드 작성 (coffeeToCoffeeResponseDto)
      • 메서드 시그니처를 현재데이터to변경할데이터로 명확히
      • List등의 자료구조도 사용 가능 (다중 데이터일 경우) 
        • List<CoffeeResponseDto> coffeesToCoffeeResponseDtos(List<Coffee> coffees);
        • 사용하지 않으면 Controller에서 다중 처리로직을 Stream등으로 해야함 (밑에있는 Controller 코드내 getCoffes확인)
// 자동생성 mapper 구현클래스

import com.codestates.coffee.dto.CoffeePatchDto;
import com.codestates.coffee.dto.CoffeePostDto;
import com.codestates.coffee.dto.CoffeeResponseDto;
import com.codestates.coffee.entity.Coffee;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class CoffeeMapperImpl implements CoffeeMapper {
    public CoffeeMapperImpl() {
    }

    public Coffee coffeePostDtoToCoffee(CoffeePostDto coffeePostDto) {
        if (coffeePostDto == null) {
            return null;
        } else {
            Coffee coffee = new Coffee();
            coffee.setKorName(coffeePostDto.getKorName());
            coffee.setEngName(coffeePostDto.getEngName());
            coffee.setPrice(coffeePostDto.getPrice());
            return coffee;
        }
    }

    public Coffee coffeePatchDtoToCoffee(CoffeePatchDto coffeePatchDto) {
        if (coffeePatchDto == null) {
            return null;
        } else {
            Coffee coffee = new Coffee();
            coffee.setCoffeeId(coffeePatchDto.getCoffeeId());
            coffee.setKorName(coffeePatchDto.getKorName());
            coffee.setEngName(coffeePatchDto.getEngName());
            if (coffeePatchDto.getPrice() != null) {
                coffee.setPrice(coffeePatchDto.getPrice());
            }

            return coffee;
        }
    }

    public CoffeeResponseDto coffeeToCoffeeResponseDto(Coffee coffee) {
        if (coffee == null) {
            return null;
        } else {
            long coffeeId = 0L;
            String korName = null;
            String engName = null;
            Integer price = null;
            coffeeId = coffee.getCoffeeId();
            korName = coffee.getKorName();
            engName = coffee.getEngName();
            price = coffee.getPrice();
            CoffeeResponseDto coffeeResponseDto = new CoffeeResponseDto(coffeeId, korName, engName, price);
            return coffeeResponseDto;
        }
    }

    public List<CoffeeResponseDto> coffeesToCoffeeResponseDtos(List<Coffee> coffees) {
        if (coffees == null) {
            return null;
        } else {
            List<CoffeeResponseDto> list = new ArrayList(coffees.size());
            Iterator var3 = coffees.iterator();

            while(var3.hasNext()) {
                Coffee coffee = (Coffee)var3.next();
                list.add(this.coffeeToCoffeeResponseDto(coffee));
            }

            return list;
        }
    }
}

 

  • 자동 생성된 Mapper 인터페이스 구현 클래스 확인 (CoffeeMapperImpl)
    • Gradle의 build task를 실행하면 자동으로 생성 -> 버전에 따라 그냥 build인 경우도 있다.

Gradle bulid
Mapper 구현클래스(*impl) 위치

Controller의 핸들러 메서드에서 서비스 계층 연동 및 mapper 적용 

import com.codestates.coffee.dto.CoffeeResponseDto;
import com.codestates.coffee.entity.Coffee;
import com.codestates.coffee.mapper.CoffeeMapper;
import com.codestates.coffee.dto.CoffeePatchDto;
import com.codestates.coffee.dto.CoffeePostDto;
import com.codestates.coffee.service.CoffeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/v5/coffees")
@Validated
public class CoffeeController {

    //(1)
    private final CoffeeService coffeeService;
    private final CoffeeMapper mapper;

    //(2)
    public CoffeeController(CoffeeService coffeeService, CoffeeMapper mapper) {
        this.coffeeService = coffeeService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto) {

        Coffee coffee = mapper.coffeePostDtoToCoffee(coffeePostDto); //(3)
        Coffee response = coffeeService.createCoffee(coffee); //(4)

        //(5)
        return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(response), HttpStatus.CREATED);
    }

    @PatchMapping("/{coffee-id}")
    public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
                                      @Valid @RequestBody CoffeePatchDto coffeePatchDto) {
        coffeePatchDto.setCoffeeId(coffeeId);

        Coffee coffee = mapper.coffeePatchDtoToCoffee(coffeePatchDto);
        Coffee response = coffeeService.updateCoffee(coffee);

        return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(response), HttpStatus.OK);
    }

    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId) {
        
        Coffee response = coffeeService.findCoffee(coffeeId);

        return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(response), HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getCoffees() {

        List<Coffee> coffees = coffeeService.findCoffees();

        //list 처리를 stream으로
        /*
        List<CoffeeResponseDto> response = coffees.stream()
                .map(coffee -> mapper.coffeeToCoffeeResponseDto(coffee))
                .collect(Collectors.toList());
         */

        // Mapper 인터페이스내 list처리 메서드 만들어서
        List<CoffeeResponseDto> response = mapper.coffeesToCoffeeResponseDtos(coffees);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/{coffee-id}")
    public ResponseEntity deleteCoffee(@PathVariable("coffee-id") long coffeeId) {

        coffeeService.deleteCoffee(coffeeId);

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}
  • (1) : service 클래스와 mapper 인터페이스를 멤버변수로 생성
  • (2) : 생성자를 통한 의존성 주입(DI) 받기 - Spring Bean
  • (3) : mapper를 통해 Dto를 entity로 변환
  • (4) : Service를 통해 서비스 계층으로 entity데이터 전달하여 응답(Response)entity 데이터로 저장 (서비스 계층 연동)
  • (5) : mapper를 통해 ResponseEntity를 ResponseDto로 변환
  • 나머지 핸들러 메서드에도 위와 같이 서비스계층 연동과 mapper를 통한 데이터 변환 적용 

'Spring Framework' 카테고리의 다른 글

비즈니스 로직에 대한 예외 처리, 사용자 정의 예외(Custom Exception)  (0) 2023.04.18
Spring MVC 예외처리  (0) 2023.04.16
DTO(Data Transfer Object)  (0) 2023.04.13
SpringMVC - Controller  (0) 2023.04.12
Spring MVC  (0) 2023.04.12
Comments