본문 바로가기
web/SpringBoot

[REST API] API 연동 하여 클라이언트와 통신하기

by 뽀리님 2023. 11. 16.

여태까지 정리해온 문서를 토대로

외부 API 에서 데이터를 가져와 DB 에 등록후, Client 에게 Body를 내려주는 간단한 미니성 프로젝트를 만들었다.

 

 

** IntelliJ 환경세팅 **

[ 개발환경 기준 ]

 OS : MAC Intel Ventura 13.2.1

✔ Tool : IntelliJ IDEA

✔ JDK : OpenJDK 17

✔ Spring Boot 3.1.5 (릴리즈)

✔ Gradle/Groovy

Spring Data JPA (프레임워크)

✔ 외부 API : https://jsonplaceholder.typicode.com/todos?userId=1

 

 

Maven보단 스크립트 방식으로 가독성이 좋고 빌드캐시와 병렬빌드를 지원하는 Gradle 로 진행하기로 하였다.

Spring Boot 3.x 대 부터는 Java 17 버전이 필수이므로, Open JDK 를 설치하였다.(참고로 Oracle JDK는 유료이다.)

(패키지관리를 위해 Homebrew가 설치되었다는 가정하에 진행하였다.)

 

1. Open JDK 설치

brew install openjdk@17

 

 

(설치후) 환경변수 설정

echo 'export PATH="/usr/local/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc 

source ~/.zshrc

 

(설치확인)

java -version 

 

 

2. 프로젝트 생성

File -> New -> Project

 

 

Next를 눌러 Spring Boot 버전 세팅과, 필요한 Dependency 를 미리 세팅한다(나는 간단히 테스트만 할 용도로 4개정도만 하였다.) 

의존성 추가는 추후 build.gradle 에서 수정이 가능하다.

( ** Spring security 를 따로 추가하지 않으면 WhiteError Label Page 가 보일것이다.)

 

 

2-1.  혹시나 Gradle JVM 설정이 안되어있을경우 아래와같은 에러가 뜰 수 있다.

 

고럴땐

Settings -> build,Excution,Devlopment -> Gradle 로 진입해서

Gradle JVM  값을 세팅해주면된다.

 

 

 

나는 API 문서를  Swagger 로 관리할 예정이므로 스웨거를 이용하였다.

 

*** 스웨거 세팅 ***

1. build.gradle 에 다음 의존성을 추가한다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // swagger
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
    // mysql
    implementation 'mysql:mysql-connector-java:8.0.22'
    // webclient
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    // Json
    implementation 'org.json:json:20230227'
    // mapper(순서중요)
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
    annotationProcessor 'org.projectlombok:lombok'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

 

Entity <-> Dto 객체 변환을 위해 MapSturct 를  쓸 예정인데, 여기서 순서를 잘조합해야한다.

참고로 순서를 잘못 조합했을시 lombok이 setter를 부르지 못해, 값이 Null 이 나올 수 있다. (이걸로 엄청나게 삽질했다.)

 

 

2. Swagger Class 를 추가한다. 

@OpenAPIDefinition(
        info = @Info(
                title = "API TITLE",
                description = "API DESCRIPTION",
                version = "v1"
        )
)
@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI();
    }
}

 

io.swagger.v3.oas.annotations 패키지에 있는 어노테이션을 사용하여OpenAPI 를 정의 하고 빈(@Bean)을 등록한다.

  • title: API의 이름 또는 제목
  • description: API에 대한 간단한 설명을 제공
  • version: API 버전

(Spring Security 를 사용한다면)

Spring Security 기본설정에 의해 보호된 리소스에 대한 접근 시 로그인이 필요하도록 설정되어 있으므로, Security 설정을 해줘야 한다.

 

3. SecurityFilterChain 설정

- SecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private static final String[] AUTH_WHITELIST = {
            "/api/test/**",
            "/swagger-ui/**",
            "/v3/api-docs/**",
    };

    /**
     * Spring Security 필터에서 제외
     * @return
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(AUTH_WHITELIST);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic(HttpBasicConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .build();
    }
}

 

Spring Security 5.7.0-M2 부터 WebSecurityConfigurerAdapter Deprecated 되었고 기존에 security 예외 url 설정하던 antMatchers 아예 삭제되었으므로 Spring Boot 3.x 버전부터는 SecurityFilterChain 를 사용하여 구현해야한다.

또한 non-lamda-DSL -> lamda-DSL 변경되었으며  인증,인가 옵션등을 HttpSecurity 쪽에 등록해야 한다.

 

@RequiredArgsConstuctor

@Autowired는 DI가 타입(Type)이 같은 빈(Bean)이 발견되면 그냥 주입한다.

지금까지 의존성 주입이 간편했던 이유인데 이것이 문제가 되는 이유는 같은 Type의 다른 객체가 여러 개일 때 문제가 발생한다.

DI Type 보고 내려주기에 서로 다른  A타입의 객체가 2 존재한다면 @Autowired Error 띄울 수밖에 없다.

 

 

3. 애플리케이션에서 쓸 공통 모듈 정의(common)

Custom Exception 과 Custom ErrorCode, Custom Return 객체정의

- Result.java (리턴객체)

@Data
public class Result<T> {
    public static final Boolean OK = Boolean.TRUE;
    public static final Boolean NOK = Boolean.FALSE;

    private int code;
    private Boolean result;
    private T data;
    private String message;

    public Result(int code, Boolean result, String message, T data) {
        this.code = code;
        this.result = result;
        this.message = message;
        this.data = data;
    }

    public Result(int code, Boolean result, String message) {
        this.code = code;
        this.result = result;
        this.message = message;
    }
    public Result(ResultErrorCode resultErrorCode, String message) {
        this.code = resultErrorCode.getCode();
        this.result = NOK;
        this.message = message;
    }
    public Result gridPut(Page<?> page){
        Map<String,Object> pageData = new HashMap<String,Object>();
        pageData.put("contents", page.getContent());
        pageData.put("pagination", Map.of("page", page.getNumber()+1 , "totalCount", page.getTotalElements()));
        this.data = (T) pageData;
        return this;
    }

    public Result(ResultErrorCode resultErrorCode) {
        this(resultErrorCode,resultErrorCode.getMessage());
    }

    public static <T> Result<T> ok(T data){
        return new Result<>(Constants.SUCCESS_STATUS, OK, Constants.SUCCESS_MESSAGE, data);
    }
    public static <T> Result<T> ok(){
        return new Result<>(Constants.SUCCESS_STATUS, OK, Constants.SUCCESS_MESSAGE);
    }

    public static <T> Result<T> nok(ResultErrorCode resultErrorCode){
        return new Result<>(resultErrorCode, resultErrorCode.getMessage());
    }
    public static <T> Result<T> nok(ResultErrorCode resultErrorCode, String message){
        return new Result<>(resultErrorCode, message);
    }
    public static <T> Result<T> nok(ResultException resultException){
        return new Result<>(resultException.getResultErrorCode() , resultException.getMessage());
    }

}

 

 

- ResultErrorCode.java (에러코드정의)

@AllArgsConstructor
@Getter
public enum ResultErrorCode {
    // API 관련
    API_RESPONSE_ERROR(4000, "API 통신 실패하였습니다."),

    // DB 관련
    DATABASE_REGIST_ERROR(5000, "등록에 실패하였습니다."),
    DATABASE_SEARCH_ERROR(5001, "조회에 실패하였습니다."),


    // 공통에러
    COMMON_UNKNOWN_ERROR(9999, "시스템 에러입니다.");
    private final int code;
    private final String message;
}

 

 

- ResultException.java (런타임예외커스텀클래스 정의)

@StandardException
public class ResultException extends RuntimeException {
    @Getter
    ResultErrorCode resultErrorCode;

    public ResultException(ResultErrorCode errorCode){
        super(errorCode.getMessage());
        this.resultErrorCode = errorCode;
    }
    public ResultException(String errorMsg){
        super(errorMsg);
    }

}

 

-GlobalExceptionHandler.java 정의(공통 Exception 정의)

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity systemException(Exception ex){
        log.error("시스템 에러",ex);
        return new ResponseEntity(Result.nok(ResultErrorCode.COMMON_UNKNOWN_ERROR), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(ResultException.class)
    public ResponseEntity resultExcpetion(ResultException ex){
        log.error("Application 에러",ex);
        return new ResponseEntity(Result.nok(ex.getResultErrorCode()),HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

- Constants.java (공통상수)

public class Constants {
    public static final String SUCCESS_MESSAGE = "SUCCESS";
    public static final String FAIL_MESSAGE = "FAILED";
    public static final int SUCCESS_STATUS = 200;

    public static final String TEST_API_URI ="https://jsonplaceholder.typicode.com/todos?userId=1";
}

 

 

- HttpWebClient.java ( 외부 API 통신을 위한 Http 통신모듈 만들기 )

@Slf4j
public class HttpWebClient {
    private WebClient webClient; // webclient 인스턴스
    private DefaultUriBuilderFactory factory; // URI 템플릿의 인코딩을 위한 설정(제대로된 파라미터 셋팅을 하기위해 필수)

    /**
     * 생성자를 통해 WebClient 초기화
     * @param uri
     */
    public HttpWebClient(String uri){
        factory = new DefaultUriBuilderFactory(uri);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); // URI 변수값만 인코딩
        webClient =  WebClient.builder()
                .uriBuilderFactory(factory)
                .baseUrl(uri)
                .filter(ExchangeFilterFunction.ofRequestProcessor( // 요청정보로깅
                        clientRequest -> {
                            log.debug("[HTTP_Request] : {} {}", clientRequest.method(), clientRequest.url());
                            clientRequest.headers().forEach((name,values) -> values.forEach(value -> log.debug("[HTTP_PARAM] {} : {}", name, value)));
                            return Mono.just(clientRequest);
                        }
                ))
                .build();
    }

    /**
     * Http GET
     * @param url
     * @param params
     * @return
     * @throws WebClientResponseException
     * @throws Exception
     */
    public Mono<String> httpRequestGet(String url, MultiValueMap<String,String> params) throws WebClientResponseException  {
        //log.debug("[Request_Param]:"+params);
        return webClient.mutate().build() // 기존 설정값을 상속해서 사용
                .get() // Http Method
                .uri(uriBuilder -> uriBuilder.path(url).queryParams(params).build()) // url 설정후, 쿼리매개변수 추가
                .accept(MediaType.APPLICATION_JSON) // 응답형식 JSON
                .retrieve() // HTTP 요청을 실행하고 응답을 받아옴
                .onStatus(httpStatus -> httpStatus != HttpStatus.OK, // 실패시 WebClientResponseException 예외생성(던짐)
                        clientResponse -> {
                            return clientResponse.createException();
                        })
                .bodyToMono(String.class);
    }
    public Mono<String> httpRequestGet() throws WebClientResponseException  {
        return httpRequestGet("", null);
    }

    /**
     * Http POST
     * @param url
     * @param params
     * @return
     * @throws WebClientResponseException
     * @throws Exception
     */
    public Mono<String> httpRequestPost(String url, JSONObject params) throws WebClientResponseException  {
        //log.debug("[Request_Param]:"+params);
        return webClient.mutate().build()
                .post()
                .uri(url)
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(params.toString()))
                .retrieve()
                .onStatus(httpStatus -> httpStatus != HttpStatus.OK,
                        clientResponse -> {
                            return clientResponse.createException();
                        })
                .bodyToMono(String.class);
    }
}

 

 

4 . API Component(config) 만들기

@Slf4j
@Component
public class ApiConfig {
    private static HttpWebClient httpWebClient;

    public ApiConfig(){
        // Test 전용 WebClient
        httpWebClient = new HttpWebClient(Constants.TEST_API_URI);
    }

    /**
     * API Request
     * @return
     */
    public JSONObject getTotalList(){
        try{
            String monoResult = httpWebClient.httpRequestGet().block();
            return ConvertUtils.strToJson(monoResult);
        } catch (WebClientResponseException we){
            String exceptMsg = we.getResponseBodyAsString(Charset.forName("UTF-8"));
            log.error("[API WEBCLIENT EXCEPTION "+we.getRequest().getURI()+"] : "+we.getStatusCode()+"/"+exceptMsg,we);
            throw new ResultException(ResultErrorCode.API_RESPONSE_ERROR);
        }
    }
    
    /**
     * API Request_Async
     * @return
     */
    public JSONObject getAsyncTotalList()  {
        try {
            CountDownLatch cdl = new CountDownLatch(1);
            JSONObject resultJson;
            httpWebClient.httpRequestGet()
                    .doOnSuccess(e -> {
                        result = e;
                        cdl.countDown(); // 성공적으로 완료되었을 때만 countDown
                    })
                    .doOnError(error -> {
                        // 에러 처리
                        log.error("통신실패:"+error.getMessage());
                        cdl.countDown(); // 에러 발생 시에도 countDown
                    })
                    .subscribe();
            cdl.await();
            resultJson = ConvertUtils.strToJson(result);
            return resultJson;
        }catch (InterruptedException ie){
            log.error("인터럽트 Exception:"+ie.getMessage());
            return null;
        }
    }

}

참고로 비동기와 동기방식  모두 만들어두었다. 나는 간단히 URL 1개만 사용예정이므로, 동기방식으로 진행하였다.

 

 

4-1. Utils 성 클래스 작성

날짜나 문자열, 데이터 타입등 변환을 위한 Util 클래스를 만들었다.

- ConvertUtils.java

@Slf4j
public class ConvertUtils {
    /**
     * String To JsonObject
     * @param jsonStr
     * @return
     */
    public static JSONObject strToJson(String jsonStr){
        try {
            JSONObject jsonObject = new JSONObject(jsonStr);
            return jsonObject;
        } catch (Exception e) {
            log.error("String to JSON Parsing EXCEPTION:"+e.getMessage(),e);
            return new JSONObject();
        }
    }


    /**
     * JsonString TO DTO Object
     * @param jsonStr
     * @param valueType
     * @param <T>
     * @return
     * @throws JsonProcessingException
     */
    public static <T> T strToDTO(String jsonStr, Class<T> valueType) {
        try {
            ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
            T obj = mapper.readValue(jsonStr, valueType);
            return obj;
        } catch (JsonProcessingException ex){
            log.error("String TO Dto Parsing Exception:"+ex.getMessage(),ex);
            return null;
        }

    }

    /**
     * DTO Object to JsonString
     * @param obj
     * @return
     * @throws JsonProcessingException
     */
    public static String dtoToJsonString(Object obj) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        String result = mapper.writeValueAsString(obj);
        return result;
    }

    /**
     * DTO Object TO JsonObject
     * @param obj
     * @return
     */
    public static JSONObject dtoToJson(Object obj) throws JsonProcessingException {
        return strToJson(dtoToJsonString(obj));
    }

    /**
     * Map To JsonString
     * @param map
     * @return
     */
    public static String mapToJsonStr(Map<String,Object> map){
        JSONObject json =  new JSONObject(map);
        return json.toString();
    }


    /**
     * Json Object To Map
     * @param obj
     * @return
     */
    public static Map<String, Object> jsonToMap(JSONObject obj){
        if (ObjectUtils.isEmpty(obj)) {
            log.error("BAD REQUEST obj : {}", obj);
            throw new IllegalArgumentException(String.format("BAD REQUEST obj %s", obj));
        } try {
            return new ObjectMapper().readValue(obj.toString(), Map.class);
        } catch (Exception e) {
            log.error(e.getMessage(), e); throw new RuntimeException(e);
        }
    }

    /**
     * Json Array To List Map
     * @param jsonArray
     * @return
     */
    public static List<Map<String, Object>> jsonArrToListMap(JSONArray jsonArray) {
        if (ObjectUtils.isEmpty(jsonArray)) {
            log.error("jsonArray is null.");
            throw new IllegalArgumentException("jsonArray is null");
        }
        List<Map<String, Object>> list = new ArrayList<>();
        for (Object jsonObject : jsonArray) {
            list.add(jsonToMap((JSONObject) jsonObject));
        } return list;
    }

    /**
     * map to Dto
     * @param obj
     * @param valueType
     * @param <T>
     * @return
     * @throws JsonProcessingException
     */
    public static <T> T mapToDto(Map<String,Object> obj, Class<T> valueType) throws JsonProcessingException {
        return strToDTO(mapToJsonStr(obj), valueType);
    }

    /**
     * Json String to Map
     * @param jsonString
     * @return
     */
    public static Map<String,Object> strToMap(String jsonString) {
        try {
            return new ObjectMapper().readValue(jsonString, Map.class);
        } catch (JsonProcessingException e) {
            log.error("String to MAP Parsing EXCEPTION:"+e.getMessage(),e);
            return new HashMap<>();
        }
    }


}

 

 

 

 

5. DB 생성

Test URI 를 포스트맨으로 호출해본다.

 

해당 정보를 토대로 DB 를 만들자.

-- todo.todo_info definition

CREATE TABLE `todo_info` (

`seq` int unsigned NOT NULL AUTO_INCREMENT,

`user_id` int unsigned DEFAULT NULL,

`id` int unsigned DEFAULT NULL,

`title` varchar(300) DEFAULT NULL,

`completed` tinyint(1) DEFAULT NULL,

`create_at` datetime DEFAULT NULL,

`update_at` datetime DEFAULT NULL,

PRIMARY KEY (`seq`)

) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

 

 

5-1. 프로퍼티추가

logging.level.com.temp.temp1=debug
server.error.whitelabel.enabled=false

## datasource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/todo?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234

## JPA 설정
spring.jpa.database=mysql
# SQL 쿼리로그 출력레벨 설정
logging.level.org.hibernate.SQL=debug
# SQL 로그형식 포맷팅여부
spring.jpa.properties.hibernate.format_sql=true
# SQL 내부에 주석추가
spring.jpa.properties.hibernate.use_sql_comments=true
# SQL문에 색상부여
spring.jpa.properties.hibernate.highlight_sql=true
# 바인딩된 파라미터 확인가능
hibernate.type.descriptor.sql=trace

 

6. Model 생성(Dto,Entity)

- TodoEntity.java

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "todo_info")
public class TodoEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer seq;
    @Column(name = "user_id")
    private Integer userId;
    private Integer id;
    private String title;
    private Boolean completed;
    @CreatedDate
    @Column(name = "create_at")
    private LocalDateTime createAt;
    @LastModifiedDate
    @Column(name = "update_at")
    private LocalDateTime updateAt;
}

 

- TodoDto.java

@Data
public class TodoDto {
    private Integer userId;
    private Integer id;
    private String title;
    private Boolean completed;
    private LocalDateTime createAt;
    private LocalDateTime updateAt;
}

 

Entity 실제 DB 테이블과 1:1 매핑되는 데이터므로  영속성목적으로 사용되는 객체며,

DTO 데이터 교환을 위한 객체로 클라이언트단에서 필요한 데이터를 가공하여 추가할수있고, 실제로 결과를 내려줄때도 DTO 사용한다.

 

 

 

 

7.  Mapper 생성

- EntityMapper.java

public interface EntityMapper<D, E> {
    E toEntity(final D dto);
    D toDto(final E entity);
    List<D> toDtoList(List<E> entityList);
    List<E> toEntityList(List<D> dtoList);
}

 

Entity DTO 변환시켜줄 상위 인터페이스부터 먼저 만든다

 

-TodoMapper.java

@Mapper
public interface TodoMapper extends EntityMapper<TodoDto, TodoEntity> {
    TodoMapper MAPPER = Mappers.getMapper(TodoMapper.class);
}

 

 

8. Repository 생성

JPA(ORM) 인터페이스를 좀 더 편하게 쓰기위해 Spring Data JPA 프레임워크를 이용하였다.

참고로 JPA 는 인터페이스 이고, Hibernate는 구현체이다. (JPA 인터페이스를 implement) 

Spring Data JPA 는 JPA 개발을 편하게 해주는 프레임워크/라이브러리 개념이다.

DB접근자로 Repository를 쓰며  인터페이스로 생성한다. 단순히 인터페이스를 생성 후, JpaRepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.

(JpaRepository 인터페이스를 상속받는 것만으로도, 많은 JPA 관련 메소드를 사용할 있다)

 

-TodoRepository.java

public interface TodoRepository extends JpaRepository<TodoEntity, Integer>  {
}

 

 

 

 

 

9. 컨트롤러 및 서비스 작성

테스트를 위해 API endpoint 정의와 Service 를 각각 간단하게 작성한다.

- TestContorller.java

@Tag(name = "test API", description = "test 관련 API") // swagger 에서 API 타이틀, 설명
@RestController
@RequestMapping("/api/test")
@RequiredArgsConstructor
@Slf4j
public class TempController {
    final private TempService tempService;

    @GetMapping("/result")
    @Operation(summary = "Total API Result", description = "API 전체 호출")
    @ApiResponse(responseCode = "200", description = "OK")
    public Result getTotalResult(){
        return  Result.ok(tempService.getResultTotalList());
    }

    @GetMapping("/list")
    @Operation(summary = "Todo API List", description = "TODO List API 조회")
    @ApiResponse(responseCode = "200", description = "OK")
    public Result getTodoResult(){
        return Result.ok(tempService.getTodoTitleList());
    }

    @PostMapping("/dbregist")
    @Operation(summary = "API Todo Data Regist DB", description = "DB에 API DATA를 등록시킨다")
    @ApiResponse(responseCode = "200", description = "OK")
    public Result registTodoListAPIData(){
        return tempService.registTodo();
    }

    @GetMapping("/dblist")
    @Operation(summary = "DB Todo List", description = "DB List 조회")
    @ApiResponse(responseCode = "200", description = "OK")
    public Result getTodoListFromDB(@Parameter(description = "현재 페이지번호",example = "1") @RequestParam(required = false) Integer page,
                                    @Parameter(description = "페이지에 보여줄 컬럼수",example = "10") @RequestParam(required = false) Integer cntPerPage){
        return tempService.searchTodoList(page,cntPerPage);
    }


}

@Contoller 와 @RestController 의 차이는 대부분 다 알고있을꺼라 넘어가려한다. 간단하게 설명하자면,

@Controller

-> 예전에 Spring MVC에서 View 반환하기 위해 사용했었다. 그래서 주로 ViewResolver를 통해 뷰를 찾아 랜더링한다.

ex) return "/users/detailView";

 

@RestController 

-> @Controller+@ResponseBody가 추가된 것으로 주 용도는 Json 형태로 객체 데이터를 반환하는 것이다.(ResponseEntity)

REST API를 개발할 때 주로 사용한다.

(실제로 @Controller @ReponseBody 붙인 것과 동일하게 작동한다.)

 

 

- TestService.java

@Service
@Slf4j
@RequiredArgsConstructor
public class TempService {
    private final TodoRepository todoRepository;
    private final ApiConfig apiConfig;

    public List<TodoDto> getResultTotalList(){
        JSONObject totalObj = apiConfig.getTotalList();
        JSONArray jsonArray = new JSONArray(totalObj);
        List<TodoDto> todoList = new ArrayList<>();
        for(int i=0; i<jsonArray.length(); i++){
            TodoDto dto = ConvertUtils.strToDTO(jsonArray.get(i).toString(), TodoDto.class);
            todoList.add(dto);
        }
        return todoList;
    }

    public List<TodoDto> getTodoTitleList(){
        List<TodoDto> titieList = new ArrayList<>();
        JSONObject totalObj = apiConfig.getTotalList();
        if(totalObj.has("title")){
            JSONArray titleArr = totalObj.getJSONArray("title");
            for(int i=0;i<titleArr.length();i++){
                TodoDto todoDto = ConvertUtils.strToDTO(titleArr.get(i).toString(), TodoDto.class);
                titieList.add(todoDto);
            }
        }
        return titieList;
    }

    public Result registTodo(){
        try {
            // 데이터등록
            todoRepository.saveAll(TodoMapper.MAPPER.toEntityList(getTodoTitleList()));
            return Result.ok();
        } catch (DataAccessException ex) {
            log.error("[등록 EXCEPTION]:"+ex.getMessage(),ex);
            return Result.nok(ResultErrorCode.DATABASE_REGIST_ERROR);
        }
    }

    public Result searchTodoList(Integer page, Integer cntPerPage){
        try {
            // 패이지세팅
            Pageable pageable = ObjectUtils.isNotEmpty(page) && ObjectUtils.isNotEmpty(cntPerPage)
                    && page > 0 && cntPerPage > 0 ? PageRequest.of(page-1,cntPerPage) : null;
            // 데이터조회
            Page<TodoDto> pageList = todoRepository.findAll(pageable).map(TodoMapper.MAPPER::toDto);
            return Result.ok().gridPut(pageList);
        } catch (DataAccessException ex) {
            log.error("[조회 EXCEPTION]:"+ex.getMessage(),ex);
            return Result.nok(ResultErrorCode.DATABASE_SEARCH_ERROR);
        }

   }
}

 

@Service 

@Component 는Spring에서 관리되는 객체임을 표시하기 위해 사용하는 가장 기본적인 annotation 이고,

@Service도 @Component 안에 포함되지만, 역할을 명시적으로 구분해주기 위해 각자 분리해서 사용한다. 즉 Spring AOP 에 부합하는 개념이고 Spring 에서 권장된다.

 

 

10. 구동

@EnableJpaAuditing
@SpringBootApplication
public class Temp1Application {

    public static void main(String[] args) {
        SpringApplication.run(Temp1Application.class, args);
    }

}