문제 상황
서비스 초기에 CMS 혹은 Admin 사이트 없이 시작하는 상황이다. Json 벌크 데이터를 각 테이블에 맞게 넣을 수 있는 API를 개발한 코드를 정리한다.
// 요청 데이터 형태
// content
{
"content": [
{
"id": 1,
"description": "test"
},
{}
// ...
]
}
// user
{
"user": [
{
"id": 1,
"description": "test",
"auth": "admin"
},
{}
// ...
]
}
해결 방법
구현체와 dto만 추가하면 자동으로 API에서 인식가능하다.
// controlelr
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/main")
public class MainController {
private final MainCommandService mainCommandService;
@PostMapping
public void initialData(@RequestBody String body) {
mainCommandService.loadJson(body);
}
}
// service
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class MainCommandService {
private final ObjectMapper objectMapper;
private final List<GenericInitialDataLoader<?, ?>> dataLoaders;
private <D> List<D> toDto(List<Map<String, Object>> json, Class<D> dto) {
return json.stream().map(data -> objectMapper.convertValue(data, dto)).toList();
}
@SuppressWarnings("unchecked")
public void processInitialData(Map<String, Object> json) {
dataLoaders.forEach(loader -> {
String key = loader.getSupportedKey();
if (json.containsKey(key)) {
List<?> dto = toDto((List<Map<String, Object>>) json.get(key), loader.getDataType());
loader.loadData(dto);
}
});
}
public void loadJson(String content) {
try {
Map<String, Object> json = objectMapper.readValue(content,
new TypeReference<Map<String, Object>>() {
});
} catch (IOException e) {
log.debug(e.getMessage());
}
}
}
// 인터페이스
public interface GenericInitialDataLoader<D, E> {
void loadData(List<?> data);
Class<D> getDataType();
Class<E> getEntityType();
String getSupportedKey();
}
// 추상 클래스
@RequiredArgsConstructor
public abstract class AbstractInitialDataLoader<D, E> implements GenericInitialDataLoader<D, E> {
protected final JpaRepository<E, Integer> repository;
@Transactional
public void createInitialData(List<D> dto) {
List<E> entities = filterEntities(dto);
if (!entities.isEmpty()) {
repository.saveAll(entities);
}
}
protected abstract List<E> filterEntities(List<D> dtoList);
}
// 구현체 n
@Component
public class ContentInitialDataLoader extends
AbstractInitialDataLoader<ContentCreateRequestDto, Content> {
private final ContentRepository contentRepository;
public ContentInitialDataLoader(ContentRepository contentRepository) {
super(contentRepository);
this.contentRepository = contentRepository;
}
@Override
protected List<Content> filterEntities(List<ContentCreateRequestDto> dtoList) {
return dtoList.stream().map(dto -> contentRepository.findById(dto.id()).map(entity -> {
entity.updateEntityForInitializeData(dto.description());
return entity;
}).orElseGet(() -> Content.builder().description(dto.description()).build())).toList();
}
@SuppressWarnings("unchecked")
@Override
public void loadData(List<?> data) {
createInitialData((List<ContentCreateRequestDto>) data);
}
@Override
public Class<ContentCreateRequestDto> getDataType() {
return ContentCreateRequestDto.class;
}
@Override
public Class<Content> getEntityType() {
return Content.class;
}
@Override
public String getSupportedKey() {
return "content";
}
}
// dto
@Builder
public record ContentCreateRequestDto(Integer id, String description) {
}
// entity
@Getter
@Builder
@Entity
@ToString
@Table(name = "content")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column
private String description;
public void updateEntityForInitializeData(String description) {
this.description = description;
}
}
// repository
@Repository
public interface ContentRepository extends JpaRepository<Content, Integer> {
}