솔류션의 표준소스에 보면 동시성 제어를 상태값만을 바라보고 체크하고 있다.
but 이건 허점이 있다.
동시성 제어란 상태값과는 상관없이 같은 자원을 동시에 접근하여 제어하는 경합을 방지 하기 위함이다.
즉 상태값이 변경 없이도 같은 자원을 동시에 변경을 시도하는 경우는 일상적으로 발생한다.
다시말해 상태값 변경이 발생해야만 동시접근으로 판단하는 솔류션의 로직은 문제가 있다.
그래서 본인은 프로젝트를 진행할때 마다 본인이 구현하는 모듈은 동시성제어를 항상 새로 만든다.
얼마나 엔트로피 낭비인가.
그래서 로직 정리를 언제할까 미루다가 이렇게 하루 날잡아 정리를 한다.
1. Query
사용자 화면에서는 조회 쿼리의 SELECT LIST에 "TO_CHAR(T.MOD_DT, 'YYYYMMDDHH24MISS') AS MOD_DT"를 추가하여 조회 당시의 최종변경일시를 가지고 온다.
아래 쿼리는 데이터 변경을 시도하기 전에 변경 여부를 확인하는 쿼리이다.
<select id="selValidAPP" resultType="map">
<![CDATA[
/* app.selValidAPP */
SELECT T.APP_NO
, T.APP_REV
, CASE
WHEN T.APP_REV_YN != 'Y' THEN 'N'
WHEN #{p.app_prog_cd} IS NULL THEN 'N'
WHEN T.APP_PROG_CD != #{p.app_prog_cd} THEN 'N'
WHEN #{p.mod_dt} IS NULL THEN 'N'
WHEN T.MOD_DT != TO_TIMESTAMP(#{p.mod_dt}, 'YYYYMMDDHH24MISS') THEN 'N'
ELSE 'Y'
END AS VALID_YN
, CASE
WHEN T.APP_REV_YN != 'Y' THEN 'STD.VALID0001' -- 변경할 수 있는 최종차수가 아닙니다.
WHEN #{p.app_prog_cd} IS NULL THEN 'STD.VALID0002' -- 최신 데이터가 아닙니다.<br/>새로고침 후 재시도 하세요.
WHEN T.APP_PROG_CD != #{p.app_prog_cd} THEN 'STD.VALID0002' -- 최신 데이터가 아닙니다.<br/>새로고침 후 재시도 하세요.
WHEN #{p.mod_dt} IS NULL THEN 'STD.VALID0002' -- 최신 데이터가 아닙니다.<br/>새로고침 후 재시도 하세요.
WHEN T.MOD_DT != TO_TIMESTAMP(#{p.mod_dt}, 'YYYYMMDDHH24MISS') THEN 'STD.VALID0002' -- 최신 데이터가 아닙니다.<br/>새로고침 후 재시도 하세요.
ELSE 'OK'
END AS VALID_MSG
, DBO.FC_GET_NAME(T.SYS_ID, #{g.locale}, 'CODE', 'APP01', T.APP_PROG_CD, NULL, NULL, NULL, NULL)
AS APP_PROG_NM
FROM (SELECT ISNULL(MAX(APP.SYS_ID) , #{g.tenant}) AS SYS_ID
, ISNULL(MAX(APP.APP_NO) , #{p.rfq_no}) AS APP_NO
, ISNULL(MAX(APP.APP_REV) , #{p.rfq_rev}) AS APP_REV
, ISNULL(MAX(APP.APP_REV_YN) , 'N') AS APP_REV_YN
, ISNULL(MAX(APP.APP_PROG_CD) , NULL) AS APP_PROG_CD
, ISNULL(MAX(APP.MOD_DT) , #{g.now}) AS MOD_DT
FROM APP_TABLE APP
WHERE APP.SYS_ID = #{g.tenant}
AND APP.APP_NO = #{p.app_no}
AND APP.APP_REV = #{p.app_rev}) T
]]>
</select>
2. AppValidator.java
package smartsuite.app.bp.app.validator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.ibatis.session.SqlSession;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import smartsuite.app.bp.admin.validator.Validator;
import smartsuite.app.bp.admin.validator.ValidatorConst;
import smartsuite.app.common.shared.Const;
@SuppressWarnings("unchecked")
@Service
public class AppValidator implements Validator
{
@Inject
private SqlSession sqlSession;
@Inject
private MessageSource messageSource;
/** The namespace. */
private static final String NAMESPACE = "app.";
/*
* 다건처리
*/
public Map<String, Object> validate(List<Map<String, Object>> appList)
{
Map<String, Object> resultMap = Maps.newHashMap();
resultMap.put(Const.RESULT_STATUS, Const.SUCCESS);
int _seq = 0;
String _msg = "";
List<Map<String, Object>> _validList = Lists.newArrayList();
List<Map<String, Object>> _invalidList = Lists.newArrayList();
if (appList != null && !appList.isEmpty())
{
for (Map<String, Object> app : appList)
{
Map<String, Object> _validResultMap = this.validate(app);
_validResultMap.put(ValidatorConst.VALID_SEQ, ++_seq); // 유효성 체크 순번발번
if (Const.SUCCESS.equals(_validResultMap.get(Const.RESULT_STATUS)))
{
_validList.add(_validResultMap);
}
else
{
_invalidList.add(_validResultMap);
// 전체메시지 누적
_msg = String.format("%s%s%s"
, _msg
, Strings.isNullOrEmpty(_msg) ? "" : "<br/>"
, _validResultMap.get(Const.RESULT_MSG));
}
}
}
Map<String, Object> _checkedData = Maps.newHashMap();
_checkedData.put(ValidatorConst.VALID_DATAS , _validList);
_checkedData.put(ValidatorConst.INVALID_DATAS , _invalidList);
resultMap.put(ValidatorConst.VALID_CNT , _validList.size ()); // 유효한 건수
resultMap.put(ValidatorConst.INVALID_CNT , _invalidList.size()); // 무효한 건수
resultMap.put(Const.RESULT_STATUS , _invalidList.isEmpty() ? Const.SUCCESS : Const.FAIL);
resultMap.put(Const.RESULT_DATA , _checkedData);
resultMap.put(Const.RESULT_MSG , _msg);
return resultMap;
}
/*
* 단건처리
*/
public Map<String, Object> validate(Map<String, Object> app)
{
Map<String, Object> resultMap = Maps.newHashMap();
resultMap.put(Const.RESULT_STATUS, Const.SUCCESS);
String app_no = app == null ? "" : (String)app.get("app_no");
String app_rev = app == null ? "" : String.valueOf(app.get("app_rev"));
// for insert
if (Strings.isNullOrEmpty(app_no))
{
}
// for update
else
{
Map<String, Object> valid = sqlSession.selectOne(NAMESPACE + "selValidAPP", app);
if ("N".equals(valid.get(ValidatorConst.VALID_YN)))
{
String msg = String.format("[%s / %s][%s] %s"
, valid.get("app_no")
, valid.get("app_rev")
, valid.get("app_prog_nm")
, messageSource.getMessage((String)valid.get(ValidatorConst.VALID_MSG), null, LocaleContextHolder.getLocale()));
resultMap.put(ValidatorConst.VALID_REQUIRED_YN , "Y");
resultMap.put(Const.RESULT_MSG , msg);
resultMap.put(Const.RESULT_STATUS , Const.FAIL);
}
}
return resultMap;
}
}
다건처리 method는 부모 클래스인 Validator.java에 정의하고 상속받은 각 모듈의 Validator class에는 고유한 단건처리 method만 정의하여
다건처리 Validator 활용을 극대화 할 수 있는 구조로가 바람직하겠다.
3. AppService.java
public Map<String, Object> saveAPP(Map<String, Object> param)
{
Map<String, Object> resultMap = Maps.newHashMap();
resultMap.put(Const.RESULT_STATUS, Const.SUCCESS);
Map<String, Object> _app = (Map<String, Object>)param.get("app");
// 0. APP Validate
resultMap = appValidator.validate(_app);
if (Const.SUCCESS.equals(resultMap.get(Const.RESULT_STATUS)))
{
....
}
return resultMap;
}
4. UI javascript
onResponse : function(event)
{
var me = this;
var _result = event.target.lastResponse;
switch (event.target.id)
{
case "selAPP" :
if (UT.isEmpty(_result.app))
{
// 데이터가 삭제되었거나 존재하지 않는 데이터 혹은 정상 데이터가 아닙니다.<br/>새로고침 후 재시도 하세요.
UT.alert("STD.N5900", function()
{
me.$.btnClose.fire("click");
});
}
else
{
var _app = _result.app;
_app.old = UT.copy(_app); // Data 변경비교를 위한 복사본저장
me.set("app" , _app);
// reload (refresh button click시 첨부파일에 저장되지 않은 dirty list 새로고침 필요)
[].slice.call(me.querySelectorAll("sc-upload")).forEach(function(upload, index, array)
{
upload.load();
});
me.applyFormula();
}
break;
case "saveAPP" :
if (_result.result_status == DEF.SUCCESS)
{
// 저장 하였습니다.
UT.alert("STD.N2400", function()
{
me.set("searchParam", _result.result_data);
me.onSearch();
});
}
else
{
UT.alert(_result.result_message, function()
{
me.$.btnClose.fire("click");
}, true);
}
break;
낙관적 동시성제어와 비관적 동시성제어 : https://blog.daonelab.com/post/24/417/