Skip to content

MVC 코드 작성 가이드

  1. sql 파일은 resources/mybatis/mapper 디렉토리에 xml 형태로 위치한다.
  2. sql 샘플
    1. select
      SELECT #{userId} AS USER_ID
      , A.MENU_ID
      , IFNULL(B.BOOKMARK_YN, 'N') AS BOOKMARK_YN
      FROM MENU_MS A
      LEFT OUTER JOIN BOOKMARK_MS B ON A.MENU_ID = B.MENU_ID AND B.USER_ID = #{userId}
      WHERE A.MENU_ID IN
      <foreach collection="menuIds" item="item" open="(" close=")" separator=",">#{item}</foreach>
    2. insert
      INSERT INTO MENU_MS ( MENU_ID
      , PARENT_ID
      , MENU_NAME
      , PATH
      , DEPTH
      , SORT_ORDER
      , USE_YN
      , CREATE_DT
      , CREATE_USER_ID
      , CREATE_IP
      , UPDATE_DT
      , UPDATE_USER_ID
      , UPDATE_IP
      )
      VALUES ( #{menuId}
      , #{parentId}
      , #{menuName}
      , #{path}
      , #{depth}
      , #{sortOrder}
      , 'Y'
      , NOW()
      , #{createUserId}
      , #{createIp}
      , NOW()
      , #{updateUserId}
      , #{updateIp}
      )
    3. update
      UPDATE USER_MS
      SET PASSWORD = #{password}
      , UPDATE_DT = NOW()
      , UPDATE_USER_ID = #{updateUserId}
      , UPDATE_IP = #{updateIp}
      WHERE EMAIL = #{email}
    4. delete
      DELETE
      FROM BOOKMARK_MS
      WHERE USER_ID = #{userId}
      AND MENU_ID = #{menuId}
    5. select…insert
      INSERT INTO API_LOG_MS
      WITH SAME_API_PATH AS (
      SELECT COUNT(0) AS CNT
      FROM ( SELECT S.API_ID
      FROM API_MS S
      WHERE REGEXP_LIKE(#{path}, S.PATH_PATTERN)
      AND S.METHOD = #{method}
      AND S.USE_PATTERN_YN = 'Y'
      UNION ALL
      SELECT T.API_ID
      FROM API_MS T
      WHERE T.PATH = #{path}
      AND T.METHOD = #{method}
      AND T.USE_PATTERN_YN = 'N' ) Z
      )
      SELECT #{method}
      , #{path}
      , NOW()
      , #{userId}
      , #{createIp}
      , NOW()
      , #{userId}
      , #{updateIp}
      FROM SAME_API_PATH
      WHERE CNT = 0
    6. merge
      INSERT INTO BOOKMARK_MS ( USER_ID
      , MENU_ID
      , BOOKMARK_YN
      , CREATE_DT
      , CREATE_USER_ID
      , CREATE_IP
      , UPDATE_DT
      , UPDATE_USER_ID
      , UPDATE_IP
      ) VALUES ( #{userId}
      , #{menuId}
      , 'Y'
      , NOW()
      , #{userId}
      , #{createIp}
      , NOW()
      , #{userId}
      , #{updateIp}
      )
      ON DUPLICATE KEY
      UPDATE BOOKMARK_YN = 'Y'
      , UPDATE_DT = NOW()
      , UPDATE_USER_ID = #{userId}
      , UPDATE_IP = #{updateIp}
  • Directoryapi.(MODULE).(UNIT_NAME)/
    • Directorycontroller/
    • Directorydto/
      • Directorycontext/
      • Directorycustom_model/
    • Directorymapper/
    • Directoryservice/
  1. api 디렉토리 하부에 작성한다.
    1. MODULE: 모듈 이름
    2. API_NAME: API 이름
    3. 예제
      api.common.code
  1. DTO 클래스는 요청/응답 데이터를 감싸는 클래스이다.
    1. 요청 DTO 클래스
      1. RequestDto 접미사를 가진다.
      2. RequestBody, PathVariable 방식으로 파라미터를 전달할 수 있다.
        1. RequestBody 데이터 전달
          1. 데이터 전달시 payload 하위에 실 데이터를 전달한다.
            {
            "payload": {
            "codeGroup": "FRUIT",
            "code": "APPLE",
            "page": {
            "page": 1,
            "pageSize": 20,
            "totalRows": 0
            }
            }
            }
        2. PathVariable 데이터 전달
          http://localhost:8080/v1/common/code/1000
      3. 요청 DTO 클래스는 다른 메소드를 호출할때 값을 전달하는 용도로 사용하지 않는다.
      4. 값을 전달하는 용도로 Context, Model 과 같은 클래스를 사용한다.
      5. Context 클래스
        1. Context 클래스는 dto/context 디렉토리에 위치한다.
        2. 요청 DTO 클래스는 Context 인스턴스를 반환하는 용도로 toContext 메소드를 제공한다.
        3. Context 클래스는 작성된 API 에서만 사용할 수 있다. 재활용시 원본 클래스가 변경되었을때 사이드 이펙트가 발생하기 때문이다.
      6. Model 클래스
        1. Model 클래스는 model 디렉토리에 위치한다.
        2. 데이터베이스의 테이블에 매핑하는 클래스이다.
        3. 요청 DTO 클래스는 Model 클래스 인스턴스를 반환하는 용도로 toModel 메소드를 제공한다.
    2. 응답 DTO 클래스
      1. RequestDto 접미사를 가진다.
      2. ResponseBody 방식으로 값을 반환한다.
        1. 응답 데이터는 payload 하위에 위치하도록 한다.
        2. isSuccess, resultCode, message 는 오류 발생시 자동으로 설정된다.
          {
          "payload": {
          "code": {
          "codeId": 1000,
          "codeGroup": "FRUIT",
          "code": "APPLE",
          "codeDesc": "사과",
          "orderNo": 1,
          "useYn": "Y",
          "createDt": "2025.12.31",
          "createUserId": 1000,
          "createIp": "0:0:0:0:0:0:0:1",
          "updateDt": "2025.12.31",
          "updateUserId": 1000,
          "updateIp": "0:0:0:0:0:0:0:1"
          }
          },
          "isSuccess": true,
          "resultCode": "SUCCESS",
          "message": null
          }
      3. 값을 반환하는 용도로 Model, Custom Model 클래스를 사용한다.
      4. Model 클래스
        1. Model 클래스는 model 디렉토리에 위치한다.
        2. 데이터베이스의 테이블에 매핑하는 클래스이다.
      5. Custom Model 클래스
        1. Custom Model 클래스는 dto/custom-model 디렉토리에 위치한다.
        2. 데이터베이스 조회 결과가 조인 등으로 확장되어 Model 클래스를 확장하는 경우에 사용한다.
        3. Custom Model 클래스는 작성된 API 에서만 사용할 수 있다. 재활용시 원본 클래스가 변경되었을때 사이드 이펙트가 발생하기 때문이다.
  2. DTO 클래스 작성 규칙
    1. kotlin 버전에서는 하나의 API에 사용되는 RequestDto, ResponseDto 클래스가 별개의 파일로 분리되어 있다.
    2. java 인 경우에는 하나의 클래스에 내부 클래스 형태로 RequestDto와 ResponseDto 클래스가 배치되어 있다.
    3. 유닛테스트코드 커버리지까지 작성했을 때, 별도의 파일로 분리하는게 유리하다는 결론을 도출하였다. 자바 버전은 향후 분리할 것이다.
  3. 예제
    1. RequestDto
      SelectCodesRequestDto.kt
      @Schema(description = "코드 목록 조회 요청 DTO")
      data class SelectCodesRequestDto(
      @field:Schema(description = "코드 그룹")
      val codeGroup: String?,
      @field:Schema(description = "코드")
      val code: String?,
      @field:Schema(description = "코드 상세")
      val codeDesc: String?,
      @field:Schema(description = "페이지 정보")
      @field:NotNull
      val page: Page?,
      ) {
      fun toContext(): SelectCodesContext =
      SelectCodesContext(
      codeGroup,
      code,
      codeDesc,
      page,
      )
      }
    2. ResponseDto
      SelectCodesResponseDto.kt
      @Schema(description = "코드 목록 조회 응답 DTO")
      data class SelectCodesResponseDto(
      @field:Schema(description = "코드 목록")
      val codes: List<Code>
      )
  1. 인터페이스 형태로 작성한다. 구현체 클래스는 Mybatis 라이브러리에서 자동으로 생성한다.
  2. SQL 파일과 1대 1로 매핑되도록 작성한다.
  3. 예제
    CodeMapper.kt
    @Mapper
    interface CodeMapper {
    fun selectCodes(param: SelectCodesContext): List<Code>
    fun selectCode(param: Int): Code
    fun insertCode(param: Code)
    fun updateCode(param: Code)
    fun deleteCode(param: Int)
    }
  1. @Autowire 어노테이션 대신 생성자를 통한 주입을 사용한다.
  2. 인터페이스를 정의하지 않는다.
  3. 코드 전체를 try catch 구문으로 감싸서 예외가 발생할시 BizException 으로 감싸서 상위 클래스로 전파한다.
  4. 서비스 단위로 트랜잭션을 관리한다.
  5. 예제
    CodeService.kt
    @Service
    @Transactional(rollbackFor = [BizException::class])
    class CodeService(private val codeMapper: CodeMapper) {
    fun selectCodes(param: SelectCodesContext): List<Code> {
    try {
    if (param.page !== null) {
    PageHelper.startPage<Code?>(param.page)
    return PageInfo.of(codeMapper.selectCodes(param)).list
    } else {
    return codeMapper.selectCodes(param)
    }
    } catch (e: Exception) {
    throw BizException(e)
    }
    }
    fun selectCode(param: Int): Code {
    try {
    return codeMapper.selectCode(param)
    } catch (e: Exception) {
    throw BizException(e)
    }
    }
    fun insertCode(param: Code) {
    try {
    return codeMapper.insertCode(param)
    } catch (e: Exception) {
    throw BizException(e)
    }
    }
    fun updateCode(param: Code) {
    try {
    return codeMapper.updateCode(param)
    } catch (e: Exception) {
    throw BizException(e)
    }
    }
    fun deleteCode(param: Int) {
    try {
    return codeMapper.deleteCode(param)
    } catch (e: Exception) {
    throw BizException(e)
    }
    }
    }
  1. @Autowire 어노테이션 대신 생성자를 통한 주입을 사용한다.
  2. 인터페이스를 정의하지 않는다.
  3. 예외 발생시 서비스에서 전달해준 BizException 기반으로 해서 오류 메시지를 반환한다.
  4. DTO 클래스에 Valid 설정이 되어 있는 경우 Valid 오류 메시지를 반환한다.
  5. 예제
    @Tag(name = "Code", description = "코드 관리 API")
    @RestController
    @RequestMapping("/v1/common/code")
    @ResponseBody
    class CodeController(private val codeService: CodeService) {
    private val log = logger()
    @Operation(summary = "코드 목록 조회", description = "코드 목록 조회 API")
    @PostMapping("/list")
    fun selectCodes(@Valid @RequestBody param: RequestDto<SelectCodesRequestDto>): ResponseEntity<ResponseDto<SelectCodesResponseDto>> {
    log.info("selectCodes param: $param")
    val context = param.getPayload()?.toContext() as SelectCodesContext
    val payload = SelectCodesResponseDto(codeService.selectCodes(context))
    return ResponseEntity.ok(ResponseDto(payload))
    }
    @Operation(summary = "코드 조회", description = "코드 조회 API")
    @GetMapping("/{codeId}")
    fun selectCode(@PathVariable codeId: Int): ResponseEntity<ResponseDto<SelectCodeResponseDto>> {
    log.info("selectCode codeId: $codeId")
    val payload = SelectCodeResponseDto(codeService.selectCode(codeId))
    return ResponseEntity.ok(ResponseDto(payload))
    }
    @Operation(summary = "코드 등록", description = "코드 등록 API")
    @PostMapping
    fun insertCode(@Valid @RequestBody param: RequestDto<InsertCodeRequestDto>): ResponseEntity<ResponseDto<Unit>> {
    log.info("insertCode param: $param")
    val model = param.getPayload()?.toModel() as Code
    codeService.insertCode(model)
    return ResponseEntity.status(HttpStatus.CREATED).body((ResponseDto(Unit)))
    }
    @Operation(summary = "코드 수정", description = "코드 수정 API")
    @PutMapping
    fun updateCode(@Valid @RequestBody param: RequestDto<UpdateCodeRequestDto>): ResponseEntity<ResponseDto<Unit>> {
    log.info("updateCode param: $param")
    val model = param.getPayload()?.toModel() as Code
    codeService.updateCode(model)
    return ResponseEntity.ok((ResponseDto(Unit)))
    }
    @Operation(summary = "코드 삭제", description = "코드 삭제 API")
    @DeleteMapping("/{codeId}")
    fun deleteCode(@PathVariable codeId: Int): ResponseEntity<ResponseDto<Unit>> {
    log.info("deleteCode codeId: $codeId")
    codeService.deleteCode(codeId)
    return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ResponseDto(Unit))
    }
    }