안녕하세요, 국내 최초로 Atlassian Cloud Specialization을 취득한 Atlassian Cloud 전문 파트너이자 플래티넘 파트너, 오픈소스컨설팅에서 Atlassian 엔지니어로 근무하고 있는 은서진(Justin) 입니다.
Atlassian의 다양한 솔루션은 이전부터 국내외 많은 기업들이 활용해왔지만 COVID 팬데믹을 기점으로 Atlassian 제품을 사용하여 원격 환경에서의 협업 솔루션을 강화하는 조직들이 정말 많아졌는데요.
사용자가 많아지고 활용도가 높아지면서 어떻게 하면 더 잘 사용할 수 있는지 고민하는 조직이 많아졌고 그러다보면 자연스럽게 조직의 고유한 업무 프로세스에 더 적합하게 제품을 커스터마이징 하고 싶어하는 고객분들을 많이 접하게 되었습니다.
그럴 때 우리는 필요한 기능을 쉽게 추가하고 확장하기 위해 Atlassian Marketplace에 올라와있는 다양한 Add-on을 활용합니다.
Atlassian Marketplace에 올라와있는 수천개의 Add-on들은 Atlassian 제품을 사용하는 큰 장점 중 하나라고 생각이 드는데요. 많은 분들이 공감하실 거라고 생각합니다.
그래서 이번 포스팅은 Atlassian Add-on 중 국내는 물론 전세계에서 가장 많이 사용하는 Add-on 중 하나로 꼽히는 Adaptavist의 ScriptRunner for Jira를 주제로 정했습니다.
- ScriptRunner 는?
Atlassian 솔루션의 자동화 및 사용자 커스터마이징을 위한 애플리케이션으로 Groovy 스크립트 및 JQL을 통해 워크플로우를 자동화함으로써 Jira를 확장하고 UX를 개선할 수 있는 앱입니다. Atlassian Marketplace의 앱 중 가장 많이 기업에서 활용되고 있는 Top-selling app입니다.
저는 지금까지 다양한 아틀라시안 프로젝트를 수행하면서 ScriptRunner는 사용하기에 따라 유용한 기능들을 접목하고 엄청난 효율을 낼수도 있는 제품인 것을 경험하였는데요.
지금부터 제가 여러 고객사에서 Atlassian 제품 구축이나 최적화 프로젝트 중 ScriptRunner를 활용한 여러 유용한 사례들 중에서도 초보자도 쉽게 이해할 수 있는 케이스를 위주로 소개해 드리고자 합니다.
ㅤ
| Jira에서 외부 DB의 데이터를 불러와 출력
예전에 ScriptRunner로 Jira에서 외부 DB의 데이터를 불러와 필드에 출력할 수 있다는 사실을 처음 알았을 때, 저에게 엄청나게 희망적인 기능이었습니다.
당시에 고객이 요구한 기능은 처음 들었을 때 어떻게 구현해야 할지 막막했거든요.
“Jira에서 Issue를 생성할 때 생성하는 사람의 팀장이 자동으로 담당자로 들어가게 해주세요. 그리고 인사 변경이 있어도 저희가 따로 설정을 안 했으면 좋겠어요.”
구세주같은 ScriptRunner의 Resource와 Workflow 기능을 활용해 다음과 같은 구조로 구성했습니다!
ScriptRunner의 Resource 기능으로 DB에서 인사 데이터를 Jira로 가져오고, Workflow에 이슈가 생성되는 시점의 Post-Function에서 스크립트를 작성하여 이슈 생성 시 이슈를 생성하는 사용자의 팀장이 Assignee 필드로 자동으로 들어가도록 했습니다.
이것을 구성할 때 인상깊었던 부분이 있었는데,
def manager = DatabaseUtil.withSql('HR_VIEW') { sql ->
sql.firstRow("SELECT Manager from HRmaster.dbo.userlist where cn = ?",reporter)
}
위와 같이 가져온 데이터를 간편하게 사용하여 제가 원하는대로 동작할 수 있도록 만들어 놓았다는게 정말 사용자 친화적인 제품이라는 생각이 들었습니다.
그리고 이어서 경험했던 것은 저에게 ScriptRunner를 이용하면 생각했던것 보다 기능 확장이 가능하구나 라는것을 깨닫게 해주었습니다.
그리고 이어서 ScriptRunner는 제가 생각했던 것보다 더 많은 기능 확장이 가능한 제품임을 경험하였던 사례입니다.
ㅤ
| 특정 사용자 그룹에 포함된 사용자만 User Picker 필드 리스트에 출력
제가 처음 Custom Picker 기능을 찾아본 이유는 고객이 다음과 같이 요구했기 때문입니다.
“티켓을 발행할 때 사용자 필드에서 특정 사용자 그룹에 있는 사용자들만 리스트에 노출 되었으면 좋겠어요.“
당시 프로젝트를 진행 중이던 고객사는 8개의 계열사가 있는 큰 조직이었고 각 계열사 마다 각기 다른 서비스데스크를 운영하도록 구성해 드렸습니다. 중앙 관리자는 내부 고객이 티켓을 발행할 때 Assignee와 Reporter 필드 외로 특정 사용자를 지정할 수 있는 필드를 요구했는데요. 해당 필드의 상세 요구는 다음과 같았습니다.
● 사용자를 선택할 수 있는 User Picker 이어야한다.
● 다음의 이유로 필드의 리스트에 노출되는 사용자를 특정 사용자 그룹으로 제한 하고싶다.
ㅤ● 해당 필드에서 각 계열사 별로 다른 사용자의 리스트가 서로 노출되면 안된다.
ㅤ● Atlassian 운영 관리자가 직접 사용자 그룹의 사용자를 관리하여 리스트에 노출되는 사용자를 관리하고 싶다.
당시 저는 ScriptRunner로 Script Field를 만들 수 있다는 사실만 알고있었고, 직접 만들어 본적은 없었기 때문에 이것이 가능할지 의문이었습니다. 하지만 찾아보니 ScriptRunner로 가능한 기능이었고, 가이드 조차 너무 잘 되어 있었습니다!
Groovy 경험이 많지 않았던 저에게는 가이드가 너무 고마웠죠.
가이드 덕분에 다음과 같은 스크립트를 작성하여 고객이 원하는 User Picker를 만들수 있었습니다.
import com.atlassian.jira.avatar.Avatar
import com.atlassian.jira.bc.user.search.UserSearchParams
import com.atlassian.jira.bc.user.search.UserSearchService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.UserFilter
import com.onresolve.scriptrunner.canned.jira.fields.model.PickerOption
def userSearchService = ComponentAccessor.getComponent(UserSearchService)
def userManager = ComponentAccessor.userManager
def avatarService = ComponentAccessor.avatarService
def authenticationContext = ComponentAccessor.jiraAuthenticationContext
def groupManager = ComponentAccessor.groupManager
def userFilter = new UserFilter(true, null, ['<Gropup Name>'])
def userSearchParams = new UserSearchParams.Builder().allowEmptyQuery(true).filter(userFilter).maxResults(30).build()
search = { String inputValue ->
userSearchService.findUsers(inputValue, userSearchParams)
}
getItemFromId = { String id ->
userManager.getUserByKey(id)
}
validate = { ApplicationUser user ->
groupManager.getGroupNamesForUser(user).intersect(['Gropup Name'])
}
renderItemViewHtml = { ApplicationUser user ->
user.displayName
}
renderItemTextOnlyValue = renderItemViewHtml
toOption = { ApplicationUser user, Closure<String> highlight ->
def remoteUser = authenticationContext.loggedInUser
new PickerOption(
value: user.key,
label: user.displayName,
html: highlight(user.displayName, false),
icon: avatarService.getAvatarURL(remoteUser, user, Avatar.Size.SMALL)?.toString()
)
}
이것을 구성하면서 가이드 문서에 있는 많은 Custom Script Field와 Built-In Script Field 가이드를 봤었는데, 생각 이상으로 ScriptRunner는 굉장한 앱이고 Jira에 꼭 필요한 앱 이라는 것을 알게 되었습니다.
지금까지는 On-premise 버전의 Jira에서 경험했던 이야기 였습니다.
이어서 Jira Cloud 기반의 프로젝트를 진행하며 겪은 특이한 경험을 공유해 드리려 합니다.
ㅤ
| Jira Cloud의 타 Add-on에서 부족한 점을 보완
당시의 고객사는 On-premise 버전의 제품을 사용했었고 금융감독원의 검수를 받기위해 Jira Service Management의 Approval(결재) 내역을 포함한 모든 Issue의 모든 정보를 문서로 제출하는 것을 필요로 했습니다.
이것을 위해서 기존 On-premise 버전에서는 이슈의 데이터를 Word로 Export할 수 있는 기능의 Xporter라는 Add-on을 사용해 왔습니다. 이후 새롭게 Cloud 버전을 도입 하고나서 동일하게 Approval(결재) 내역을 포함한 모든 Issue의 모든 정보를 문서로 제출하는 것을 필요로해서 동일하게 Xporter를 사용했습니다.
그런데 여기서 문제가 생겼습니다!
당시에 Cloud 버전의 Xporter는 Jira Service Management의 Approval 데이터를 가져오지 못했습니다. Xporter의 벤더사인 Xblend에 문의도 해봤지만, 당분간은 해당 기능을 업데이트할 계획이 없다는 답변만 돌아왔습니다.
하지만 고객에게는 중요한 데이터였고 검수 시간이 얼마 안남았기 때문에 어떻게든 빨리 다른 대안이나 방법을 찾아달라고 요구하는 난감한 상황이었죠. 처음에는 다른 Add-on을 찾아봤지만 당시엔 Xporter 만큼 고객의 요구사항을 충족할 만한 Add-on을 찾을 수 없었습니다.
그래서 저는 직접 고객이 원하는 기능을 만들기로 하였고, 첫번째 방안으로 Forge를 이용해 Approval 데이터만 따로 뽑을 수 있는 App을 직접 생성하기로 하였습니다.
처음엔 Rest API로 Approval 데이터를 가져오는 것이 가능한지 먼저 검증해 보았습니다.
검증을 완료하고 바로 App으로 만들기 시작했습니다.
하지만 App을 완성 하기까지는 시간이 조금 더 필요했는데, 시간이 너무 촉박했고 고객사의 요청으로 인해 다른 방안으로 방향을 틀어야 할 것 같았습니다.
그때 생각난 것이 ScriptRunner 였습니다. ScriptRunner는 항상 거창한 개발을 하지 않아도 간단한 스크립트 작성을 통해 원하는 기능을 구성할 수 있도록 도와 줬었기에 ScriptRunner를 이용하면 짧은 기한 안에 문제를 해결할 수 있지않을까 하는 생각이 들었기 때문입니다.
그래서 대안으로 생각한 것은 ScriptRunner를 이용해서 데이터를 사용자에게 안보이는 특정 필드에 저장해두고 Xporter로 Export할 때 해당 필드의 값을 불러오는 방법이었습니다. 어떻게 보면 황당한 생각이었지만, 당시에는 마음이 너무 조급해 뭐라도 해보자는 심정이었죠.
그래서 ScriptRunner의 Script Listeners 기능을 이용하여 다음과 같이 스크립트를 작성했습니다.
def issueKey = issue.key
def approvalData = get('https://company.atlassian.net/rest/servicedeskapi/request/'+issueKey+'/approval')
.queryString("notifyUsers", Boolean.FALSE)
.header('Content-Type', 'application/json')
.asObject(Map)
.body
def aprData = approvalData.values.reverse() as Map
def inData
aprData.each { data ->
if (data['completedDate'] != null) {
def line = '--------------------------------------------------------'
def displayName = data['approvers']['approver']['displayName'].toString().replace('[', '').replace(']', '')
def decision = data['approvers']['approverDecision'].toString().replace('[', '').replace(']', '')
def completeD1 = data['completedDate'].toString().split(':')[1].replace('T', ' ')
def completeD2 = data['completedDate'].toString().split(':')[2]
inData = inData.toString().replace('null', ' ') + data['name'] + ' | ' + displayName + ' | ' + decision + ' | ' + completeD1 + ':' + completeD2 + '\n' + line + '\n'
}
}
def title = 'ㅤ상태ㅤㅤ|ㅤㅤ승인자ㅤㅤ|ㅤㅤ승인 상태ㅤㅤ|ㅤㅤ승인일ㅤ'
def line = '--------------------------------------------------------'
def result = put("/rest/api/2/issue/"+issueKey)
.queryString("overrideScreenSecurity", Boolean.TRUE)
.queryString("notifyUsers", Boolean.FALSE)
.header('Content-Type', 'application/json')
.body([
fields: [
'<Customfied ID>': title + '\n' + line + '\n' + inData
]
])
.asString()
if (result.status == 204) {
return 'Success'
} else {
sleep(500)
return "${result.status}: ${result.body}"
}
이어서 구상 했던대로 테스트를 진행해 보았습니다. Xporter의 템플릿을 변경하고, 테스트 Issue에서 승인 절차를 다 진행한 뒤 이슈의 데이터를 Xporter로 Export 했습니다.
결과적으로 위 이미지와 같이 Export된 Word 안에 Approval 내역까지 담길수 있도록 설정이 되었습니다!
물론 시간이 더 있어 데이터가 테이블 형태로 좀 더 깔끔하게 출력되도록 가공 했었다면 더 좋았을것 같다는 아쉬움이 남지만..
중요한건 ScriptRunner를 이용해서 짧은 시간안에 고객이 원하는 요구사항을 충족하며 문제를 해결했다는 것이고, ScriptRunner를 이용해 타 Add-on에서 필요한 기능을 보완했다는 것이죠.
이 외에도 이야기 하고싶은 ScriptRunner를 사용하며 좋았던 경험들이 더 있지만 너무 길어질것 같아 이만 정리하겠습니다.
이제 저에게 ScriptRunner는…
고객분들이 요구사항 중에 Jira에 없는 기능을 요구한다면 다른 좋은 Add-on들도 많지만, 저에게 가장 먼저 생각나는 Add-on은 ScriptRunner가 되었습니다. 그리고 아직도 제가 모르는 무수히 많은 기능과 확장성이 있을것이라고 생각이 됩니다.
앞으로도 꾸준히 ScriptRunner에 대해서 알아갈 것이고 많이 사용하게 될 것 같습니다.
ㅤ
| 참고 사이트
● Database Connection
● Post Functions Tutorial
● Custom Picker