1월 5일 -Mapper

Yullgiii·2024년 1월 5일
0
post-thumbnail

Mapper 란?

Mapper는 소프트웨어 개발에서 두 데이터 모델 간의 데이터 변환을 담당하는 구성 요소를 말한다. 이는 주로 레이어 간 데이터 전송, 특히 데이터베이스의 엔터티(Entity) 객체와 사용자 인터페이스 레이어의 데이터 전송 객체(DTO) 사이에서 변환 작업을 수행한다. 객체 간의 매핑은 필드 간의 값을 복사하여 새 객체를 생성하거나, 기존 객체의 상태를 변경하는 방식으로 이루어진다.

Mapper의 주요 사용 사례

  • Entity와 DTO 변환: 데이터베이스의 Entity 객체는 데이터베이스의 테이블 구조를 반영하지만, DTO는 보통 클라이언트에게 전달되는 데이터의 형식을 정의한다. Entity와 DTO 사이에서 데이터 변환을 수행할 때 Mapper가 사용된다.

  • API 레벨 데이터 변환: 외부 시스템이나 API와의 통신에서 받은 데이터를 내부 시스템의 데이터 모델로 변환할 때도 사용된다. 예를 들어, 외부 서비스에서 받은 JSON 데이터를 내부적으로 사용하는 객체로 매핑하는 경우가 이에 해당한다.

  • 다양한 레이어 간의 데이터 전송: 비즈니스 로직 레이어와 데이터 액세스 레이어 간의 객체 변환에도 매핑이 사용된다.

Mapper의 장점

  • 코드의 가독성과 유지보수성 향상: 매핑 로직을 중앙화함으로써, 변환 로직이 분산되어 있는 것보다 코드를 이해하고 유지보수하기가 더 쉽다.

  • 오류 감소: 수동으로 데이터를 복사하는 것보다 자동 매핑 도구를 사용하면 변환 중 발생할 수 있는 오류가 감소한다.

  • 개발 효율성 증가: 매핑 로직을 자동화함으로써 개발자는 보일러플레이트 코드 작성에 소요되는 시간을 줄일 수 있다.

Mapper의 단점

  • 학습 곡선: 매핑 도구를 사용하기 위해서는 해당 도구의 사용 방법을 배워야 하며, 때로는 복잡할 수 있다.

  • 성능 문제: 일부 매핑 도구는 리플렉션을 사용하며, 이는 성능에 영향을 줄 수 있다. 하지만, 컴파일 시간에 코드를 생성하는 도구(예: MapStruct)를 사용하면 이러한 문제를 완화할 수 있다.

  • 매핑 복잡성: 매우 복잡한 객체 구조의 경우, 매핑 로직이 복잡해질 수 있으며, 이는 오류를 일으킬 수 있다.

실 프로젝트 적용 사례

개요 : entity 안에 Dto 변화 로직을 쓸것이냐 아니면 Dto를 따로 만들어 쓸것이냐는 주제로 이야기를 하다가 튜터님에게 찾아갔다.
그러자 튜터님께서는 Mapper를 주로 쓰신다 하셨고 그 방법을 도입해보고자 하였다.

원래의 코드

GameResponseDto

@Builder
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class GameResponseDto {
    private String sportType;
    private String teamA;
    private String teamB;
    private LocalDateTime gameDateTime;
    private String location;

    public static GameResponseDto fromMap(Map<String, Object> gameData) {
        return GameResponseDto.builder()
            .sportType((String) gameData.get("sportType"))
            .teamA((String) gameData.get("teamA"))
            .teamB((String) gameData.get("teamB"))
            .gameDateTime(LocalDateTime.parse((String) gameData.get("gameDateTime")))
            .location((String) gameData.get("location"))
            .build();
    }
}

Mapper & record 사용

public record GameResponseDto(
    String sportType,
    String teamA,
    String teamB,
    LocalDateTime gameDateTime,
    String location
) {}





@Mapper
public interface GameMapper {
    GameMapper INSTANCE = Mappers.getMapper(GameMapper.class);

    GameResponseDto gameToGameResponseDto(Game game);

    @Mapping(target = "sportType", source = "gameData.sportType")
    @Mapping(target = "teamA", source = "gameData.teamA")
    @Mapping(target = "teamB", source = "gameData.teamB")
    @Mapping(target = "gameDateTime", expression = "java(LocalDateTime.parse(gameData.get(\"gameDateTime\")))")
    @Mapping(target = "location", source = "gameData.location")
    GameResponseDto mapToDto(Map<String, Object> gameData);
}

그로 인해 변화한 Service 코드!!

@Service
public class GameService {

    private final RestTemplate restTemplate;

    @Autowired
    public GameService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public ApiResponse<List<GameResponseDto>> getMatchesBySport(String sportType) {
        String apiUrl = determineApiUrl(sportType);
        ResponseEntity<ApiResponse> response = restTemplate.getForEntity(apiUrl, ApiResponse.class);

        if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
            List<GameResponseDto> matches = convertToMatchResponseDtoList(response.getBody().getData());
            return ApiResponse.of(ResponseCode.OK, matches);
        } else {
            throw new GlobalException(ErrorCode.EXTERNAL_API_ERROR);
        }
    }

    private List<GameResponseDto> convertToMatchResponseDtoList(Object apiResponseData) {
        if (!(apiResponseData instanceof List<?>)) {
            throw new GlobalException(ErrorCode.INVALID_API_RESPONSE);
        }

        List<?> responseDataList = (List<?>) apiResponseData;
        List<GameResponseDto> matchResponseDtos = new ArrayList<>();

        for (Object responseData : responseDataList) {
            // JSON 객체를 Map으로 캐스팅할 수 있도록 처리해야 함
            Map<String, Object> matchData = (Map<String, Object>) responseData;

            GameResponseDto matchResponseDto = new GameResponseDto();
            matchResponseDto.setTeamA((String) matchData.get("teamA"));
            matchResponseDto.setTeamB((String) matchData.get("teamB"));
            matchResponseDto.setMatchDateTime(LocalDateTime.parse((String) matchData.get("matchDateTime")));
            matchResponseDto.setLocation((String) matchData.get("location"));

            matchResponseDtos.add(matchResponseDto);
        }

        return matchResponseDtos;
    }

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

@RequiredArgsConstructor
@Service
public class GameService {

   private final RestTemplate restTemplate;

   public ApiResponse<List<GameResponseDto>> getGamesBySport(String sportType) {
       String apiUrl = determineApiUrl(sportType);
       ResponseEntity<ApiResponse> response = restTemplate.getForEntity(apiUrl, ApiResponse.class);

       if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
           List<Map<String, Object>> responseDataList = (List<Map<String, Object>>) response.getBody().getData();
           List<GameResponseDto> gameList = convertToGameResponseDtoList(responseDataList);
           return ApiResponse.of(ResponseCode.OK, gameList);
       } else {
           throw new GlobalException(ErrorCode.EXTERNAL_API_ERROR);
       }
   }

   private List<GameResponseDto> convertToGameResponseDtoList(List<Map<String, Object>> responseDataList) {
       List<GameResponseDto> gameResponseDtos = new ArrayList<>();
       for (Map<String, Object> gameData : responseDataList) {
           gameResponseDtos.add(GameMapper.INSTANCE.mapToDto(gameData));
       }
       return gameResponseDtos;
   }

   private String determineApiUrl(String sportType) {
       switch (sportType.toLowerCase()) {
           case "football":
               return "https://api-football-v1.p.rapidapi.com/v3/timezone";
           case "basketball":
               return "https://api-basketball.p.rapidapi.com/timezone";
           case "baseball":
               return "https://api-baseball.p.rapidapi.com/timezone";
           default:
               throw new IllegalArgumentException("Invalid sport type");
       }
   }
}

변경 후 직접 느끼는 장점

  • 간결성과 명확성: 레코드를 사용함으로써 GameResponseDto의 정의가 간결하고 명확해졌다.
  • 변환 로직의 중앙화: GameMapper를 사용하여 변환 로직을 중앙화함으로써, 유지보수 및 재사용성이 향상되었다

결론

이는 성능 저하를 최소화하는 동시에, 변환 로직의 오류 가능성을 줄여준다.

profile
개발이란 무엇인가..를 공부하는 거북이의 성장일기 🐢

0개의 댓글