Mapper는 소프트웨어 개발에서 두 데이터 모델 간의 데이터 변환을 담당하는 구성 요소를 말한다. 이는 주로 레이어 간 데이터 전송, 특히 데이터베이스의 엔터티(Entity) 객체와 사용자 인터페이스 레이어의 데이터 전송 객체(DTO) 사이에서 변환 작업을 수행한다. 객체 간의 매핑은 필드 간의 값을 복사하여 새 객체를 생성하거나, 기존 객체의 상태를 변경하는 방식으로 이루어진다.
Entity와 DTO 변환: 데이터베이스의 Entity 객체는 데이터베이스의 테이블 구조를 반영하지만, DTO는 보통 클라이언트에게 전달되는 데이터의 형식을 정의한다. Entity와 DTO 사이에서 데이터 변환을 수행할 때 Mapper가 사용된다.
API 레벨 데이터 변환: 외부 시스템이나 API와의 통신에서 받은 데이터를 내부 시스템의 데이터 모델로 변환할 때도 사용된다. 예를 들어, 외부 서비스에서 받은 JSON 데이터를 내부적으로 사용하는 객체로 매핑하는 경우가 이에 해당한다.
다양한 레이어 간의 데이터 전송: 비즈니스 로직 레이어와 데이터 액세스 레이어 간의 객체 변환에도 매핑이 사용된다.
코드의 가독성과 유지보수성 향상: 매핑 로직을 중앙화함으로써, 변환 로직이 분산되어 있는 것보다 코드를 이해하고 유지보수하기가 더 쉽다.
오류 감소: 수동으로 데이터를 복사하는 것보다 자동 매핑 도구를 사용하면 변환 중 발생할 수 있는 오류가 감소한다.
개발 효율성 증가: 매핑 로직을 자동화함으로써 개발자는 보일러플레이트 코드 작성에 소요되는 시간을 줄일 수 있다.
학습 곡선: 매핑 도구를 사용하기 위해서는 해당 도구의 사용 방법을 배워야 하며, 때로는 복잡할 수 있다.
성능 문제: 일부 매핑 도구는 리플렉션을 사용하며, 이는 성능에 영향을 줄 수 있다. 하지만, 컴파일 시간에 코드를 생성하는 도구(예: 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();
}
}
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
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");
}
}
}
이는 성능 저하를 최소화하는 동시에, 변환 로직의 오류 가능성을 줄여준다.