> For the complete documentation index, see [llms.txt](https://tech.x2bee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tech.x2bee.com/dev-guide/dev-start/integrations/undefined-2.md).

# 서버 측 개발 방법

이 문서는 서버 측 개발 방식에 대해서 다룹니다.\
서버에서의 개발 패턴과 디렉토리 구조를 설명하고, 각 클래스 작성 방법에 대해 설명합니다.

***

## Controller, Service, (APIController, service), Mapper 구조

서버는 Spring MVC 패턴 기반으로 구성합니다.\
데이터 처리를 위하여 Controller → Service → Mapper 구조로 진행합니다.\
MAS 구조로 개발되며 Front(Next.js)에서 API서버를 호출하여 DB서버와 통신하는 형태로 개발합니다.

아래는 프로그램 호출 순서입니다

http request → (mapping) → Controller → Service → ServiceImpl → APIController → Service → ServiceImpl → Mapper → XML(query) (BO서버) (API서버)

***

**디렉토리 구조**

<figure><img src="https://200425-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVEZx3rsZsIv89GPS3d2J%2Fuploads%2F72tYWTrSSB15AWAEfurn%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202025-12-24%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%209.46.33.png?alt=media&amp;token=9ae07f29-bb3a-4d8e-bbe3-4448672e160d" alt=""><figcaption></figcaption></figure>

***

## DTO 클래스 작성

* src/main/java 하위 해당 업무 폴더에 dto, entity 폴더 생성 후 작업합니다.
* 데이터 전달을 위하여 사용될 객체를 사전 정의합니다.

**Sample.java**

{% code title="Sample.java" %}

```java
package com.x2bee.api.bo.app.entity;

import javax.validation.constraints.NotNull;
import org.apache.ibatis.type.Alias;
import com.x2bee.common.base.entity.BaseCommonEntity;
import lombok.Getter;
import lombok.Setter;

@Alias("Sample")
@Getter
@Setter
public class Sample extends BaseCommonEntity {
    private static final long serialVersionUID = -5756700830219562201L;
    private Long id;
    @NotNull
    private String name;
    private String description;
}
```

{% endcode %}

***

## API Controller 클래스 작성

* API 서버의 Controller 작성
* Response 객체로 Return

**SampleController.java**

{% code title="SampleController.java" %}

```java
package com.x2bee.api.bo.app.controller.sample;

import java.util.List;
import javax.validation.Valid;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.api.bo.app.entity.Sample;
import com.x2bee.api.bo.app.service.sample.SampleService;
import com.x2bee.api.bo.base.advice.ApiError;
import com.x2bee.api.bo.base.annotation.IndInfoLog;
import com.x2bee.common.base.exception.AppException;
import com.x2bee.common.base.rest.Response;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/samples")
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleController {

    private final SampleService sampleService;

    @GetMapping("")
    public Response<List<SampleResponse>> getAllSamples() {
        return new Response<List<SampleResponse>>().setPayload(sampleService.getAllSamples());
    }

    @GetMapping("/{id}")
    public Response<SampleResponse> getSample(@PathVariable Long id,
            @RequestHeader(value="test-header-key1", required = false) String testHeader1) {
        log.info("id: {}, testHeader1: {}", id, testHeader1);
        return new Response<SampleResponse>().setPayload(sampleService.getSample(id));
    }

    @GetMapping("/search")
    public Response<List<SampleResponse>> searchSamples(SampleRequest sampleRequest) {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
    }

    @PostMapping("")
    public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException {
        log.info("sampleRequest: {}", sample);
        return new Response<String>();
    }

    @PutMapping("/{id}")
    public Response<String> saveSample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
        log.info("id: {}, sampleRequest: {}", id, sampleRequest);
        return new Response<String>();
    }

    @PatchMapping("/{id}")
    public Response<String> modifySample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
        log.info("id: {}, sampleRequest: {}", id, sampleRequest);
        return new Response<String>();
    }

    @DeleteMapping("/{id}")
    public Response<String> removeSample(@PathVariable Long id) {
        log.info("id: {}", id);
        return new Response<String>();
    }

    @GetMapping("/error")
    public Response<String> getError() {
        if (true) {
            AppException.exception(ApiError.UNKNOWN);
        }
        return new Response<String>();
    }

    /**
     * @deprecated void 유형으로 응답하면 안됩니다. Response<T> 유형으로 응답 해야합니다.
     */
    @PostMapping("/void")
    public void registerVod(@RequestBody SampleRequest sampleRequest) throws InterruptedException {
        log.info("sampleRequest: {}", sampleRequest);
    }

    @PostMapping("/display-samples")
    public Response<String> registerDisplaySample(@RequestBody SampleRequest sampleRequest) throws Exception {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<String>().setPayload(sampleService.registerDisplaySample(sampleRequest));
    }

    @GetMapping("/call-order")
    public Response<List<SampleResponse>> callOrder(SampleRequest sampleRequest) throws Exception {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<List<SampleResponse>>().setPayload(sampleService.callChain(sampleRequest));
    }

    @GetMapping("/infInfoLog")
    @IndInfoLog
    public Response<List<SampleResponse>> infInfoLog(SampleRequest sampleRequest) {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
    }

    /**
     * 동영상 업로드 샘플.
     * 상품컨텐츠정보 등록 시 동영상이 업로드 되는 경우의 샘플임.
     * POST formData 로 업로드한다.
     */
    @PostMapping("/mpic")
    public Response<String> registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
        sampleService.registerGoodsContInfoWithMpic(sampleMpicRequest);
        return new Response<String>();
    }
}
```

{% endcode %}

***

## API Service 클래스 작성

**SampleService.java**

{% code title="SampleService.java" %}

```java
package com.x2bee.api.bo.app.service.sample;

import java.util.List;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;

public interface SampleService {
    public List<SampleResponse> getAllSamples();
    public SampleResponse getSample(Long id);
    public List<SampleResponse> searchSamples(SampleRequest sampleRequest);
    String registerDisplaySample(SampleRequest sampleRequest) throws Exception;
    public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception;
    public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest);
}
```

{% endcode %}

실제 구현체

**SampleServiceImpl.java**

{% code title="SampleServiceImpl.java" %}

```java
package com.x2bee.api.bo.app.service.sample;

import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.common.MpicResponse;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.api.bo.app.entity.PrGoodsContInfo;
import com.x2bee.api.bo.app.entity.StMpicMappInfo;
import com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper;
import com.x2bee.api.bo.app.repository.displayrwdb.sample.SampleTrxMapper;
import com.x2bee.api.bo.app.service.common.MpicService;
import com.x2bee.api.bo.base.advice.ApiError;
import com.x2bee.common.base.exception.AppException;
import com.x2bee.common.base.rest.Response;
import com.x2bee.common.base.rest.RestApiUtil;
import com.x2bee.common.base.upload.AttacheFileKind;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {

    private final SampleMapper sampleMapper;
    private final SampleTrxMapper sampleTrxMapper;
    private final RestApiUtil restApiUtil;
    private final MpicService mpicService;

    @Value("${app.apiUrl.display}")
    private String displayApiUrl;

    @Value("${app.apiUrl.order}")
    private String orderApiUrl;

    @Value("${sample.prop}")
    private String sampleProp;

    @Override
    public List<SampleResponse> getAllSamples() {
        log.debug("sample prop: {}", sampleProp);
        return sampleMapper.selectAllSamples();
    }

    @Override
    public SampleResponse getSample(Long id) {
        SampleResponse sampleResponse = sampleMapper.selectSampleById(id).orElse(null);
        if (sampleResponse == null) {
            AppException.exception(ApiError.DATA_NOT_FOUND);
        }
        return sampleResponse;
    }

    @Override
    public List<SampleResponse> searchSamples(SampleRequest sampleRequest) {
        return sampleMapper.selectSamples(sampleRequest);
    }

    @Override
    public String registerDisplaySample(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.post(displayApiUrl+ "/api/display/samples", sampleRequest, new ParameterizedTypeReference<Response<String>>() {}).getPayload();
    }

    @Override
    public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.get(orderApiUrl+ "/api/order/samples/call-display", sampleRequest, new ParameterizedTypeReference<Response<List<SampleResponse>>>() {}).getPayload();
    }

    /**
     * 동영상 업로드 샘플 서비스
     */
    @Override
    public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
        // 파일 업로드
        MpicResponse mpicResponse = mpicService.uploadMpic(sampleMpicRequest.getFile1(), sampleMpicRequest.getDirection(), AttacheFileKind.GOODS);

        // 업무컨텐츠정보 등록
        PrGoodsContInfo prGoodsContInfo = insertPrGoodsContInfo(sampleMpicRequest, mpicResponse);

        // 동영상변환상태정보 등록
        registerMpicMappInfo(prGoodsContInfo, mpicResponse);
    }

    // 업무컨텐츠정보 등록 - 업무별 비지니스 로직 구현필요
    private PrGoodsContInfo insertPrGoodsContInfo(SampleMpicRequest sampleMpicRequest, MpicResponse mpicResponse) {
        PrGoodsContInfo prGoodsContInfo = new PrGoodsContInfo();
        prGoodsContInfo.setGoodsNo("TEST_001");
        prGoodsContInfo.setCmtTypCd("02");
        prGoodsContInfo.setCmtSerialNo("10"+RandomStringUtils.randomNumeric(6));
        prGoodsContInfo.setOptnCatNo("1000");
        prGoodsContInfo.setOptnNo("BLACK");
        prGoodsContInfo.setImgGbCd("T01");
        prGoodsContInfo.setBaseImgYn("N");
        prGoodsContInfo.setContFilePathNm(null);
        prGoodsContInfo.setContFileNm(mpicResponse.getFileNm());
        prGoodsContInfo.setTrnfTextCont("testTrnfTextCont");
        prGoodsContInfo.setSysRegId("FRONT");
        prGoodsContInfo.setSysModId("FRONT");
        sampleTrxMapper.insertPrGoodsContInfo(prGoodsContInfo);
        return prGoodsContInfo;
    }

    // 동영상변환상태정보 등록
    private void registerMpicMappInfo(PrGoodsContInfo prGoodsContInfo, MpicResponse mpicResponse) {
        StMpicMappInfo stMpicMappInfo = new StMpicMappInfo();

        // 원본경로명이 "/" 로 시작하도록 처리함.
        String s3OrigPathNm = StringUtils.startsWith(mpicResponse.getS3OrigPathNm(), "/") ? mpicResponse.getS3OrigPathNm() : "/" + mpicResponse.getS3OrigPathNm();
        stMpicMappInfo.setOrgPathNm(s3OrigPathNm);
        stMpicMappInfo.setTblNm("pr_goods_cont_info");
        stMpicMappInfo.setRef1Val(prGoodsContInfo.getGoodsNo());
        stMpicMappInfo.setRef2Val(prGoodsContInfo.getCmtTypCd());
        stMpicMappInfo.setRef3Val(prGoodsContInfo.getCmtSerialNo());
        stMpicMappInfo.setSysRegId("SAMPLE");
        stMpicMappInfo.setSysModId("SAMPLE");

        mpicService.registerMpicMappInfo(stMpicMappInfo);
    }
}
```

{% endcode %}

***

## Mapper 클래스 및 Query 작성

* src/main/java 하위 해당 업무 폴더에 repository/{db연결명} 폴더 생성 후 Mapper 인터페이스 작성

**SampleMapper.java**

{% code title="SampleMapper.java" %}

```java
package com.x2bee.api.bo.app.repository.displayrodb.sample;

import java.util.List;
import java.util.Optional;

import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;

public interface SampleMapper {
    public List<SampleResponse> selectAllSamples();
    public Optional<SampleResponse> selectSampleById(Long id);
    public List<SampleResponse> selectSamples(SampleRequest request);
}
```

{% endcode %}

* src/main/resources 하위에 해당 쿼리(XML) 작성
* namespace 에 위 Mapper 클래스 명시
* resultType 혹은 parameterType 에 데이터 객체 명시

**SampleMapper.xml**

{% code title="SampleMapper.xml" %}

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper">

  <sql id="sampleList">
    SELECT 1 as id, 'name1' as name, 'desc1' as description
    union all
    SELECT 2 as id, 'name2' as name, 'desc2' as description
    union all
    SELECT 3 as id, 'name3' as name, 'desc3' as description
  </sql>

  <!-- 전체 샘플 조회 -->
  <select id="selectAllSamples" resultType="sampleResponse">
    /* SampleMapper.selectAllSamples */
    <include refid="sampleList" />
  </select>

  <!-- 샘플 단건 조회 -->
  <select id="selectSampleById" parameterType="long" resultType="sampleResponse">
    /* SampleMapper.selectSampleById */
    select * from (
      <include refid="sampleList" />
    ) a where id = #{id}
  </select>

  <!-- 샘플 목록 조회 -->
  <select id="selectSamples" parameterType="sampleRequest" resultType="sampleResponse">
    /* SampleMapper.selectSamples */
    select * from (
      <include refid="sampleList" />
    ) a
    <where>
      <if test="id != null">
        and id = #{id}
      </if>
      <if test="name != null and name != ''">
        and name = #{name}
      </if>
      <if test="description != null and description != ''">
        and description = #{description}
      </if>
    </where>
  </select>

</mapper>
```

{% endcode %}

***

**위 작업 진행 시 최종 패키지 구조는 다음과 같습니다.**

<figure><img src="https://200425-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVEZx3rsZsIv89GPS3d2J%2Fuploads%2FQxigY1UGlvbCAVTNbaxR%2Fimage-20220328-050404.png?alt=media&amp;token=06c5cd5e-6fe0-427a-838a-c9d371daaf72" alt=""><figcaption></figcaption></figure>
