λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
☘️ λ°±μ—”λ“œ: Backend

[Spring] Slack API ν™œμš©ν•˜μ—¬ μ—λŸ¬ λͺ¨λ‹ˆν„°λ§ν•˜κΈ°

by 🐀 쀀콩이 2023. 8. 3.

μ„œλΉ„μŠ€λ₯Ό λ°°ν¬ν•œ ν›„, ν”„λ‘œμ νŠΈλ₯Ό μœ μ§€ κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄μ„œ μ—λŸ¬ λŒ€μ‘μ€ ν•„μˆ˜μ μž…λ‹ˆλ‹€. μ—λŸ¬λ₯Ό λͺ¨λ‹ˆν„°λ§ν•˜κΈ° μœ„ν•œ μˆ˜λ‹¨μ€ λ‹€μ–‘ν•˜μ§€λ§Œ, 이번 κΈ€μ—μ„œλŠ” Spring Boot μ—μ„œ μŠ¬λž™μ„ ν™œμš©ν•˜μ—¬ μ—λŸ¬λ₯Ό 확인할 수 μžˆλŠ” 방법을 μ†Œκ°œν•˜κ³ μž ν•©λ‹ˆλ‹€.

 

ν•΄λ‹Ή κΈ€μ—μ„œ μ†Œκ°œν•œ 방법을 λ”°λΌμ˜€μ‹œλ©΄ λ‹€μŒκ³Ό 같이 μŠ¬λž™μœΌλ‘œ μ—λŸ¬λ₯Ό ν™•μΈν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€! 😎

 

 

 

🎯 1. Slack 연동을 μœ„ν•œ 토큰 λ°œκΈ‰

 

  1. λ¨Όμ € Slack 봇을 μƒμ„±ν•˜κΈ° μœ„ν•΄ https://api.slack.com/apps/ μ— μ ‘μ†ν•©λ‹ˆλ‹€.
  1. Create New App 클릭 -> From scratch 클릭

 

  1. App Name 을 자유둭게 μ„€μ •ν•˜κ³  μŠ¬λž™ μ•Œλ¦Όμ„ 받을 μ›Œν¬μŠ€νŽ˜μ΄μŠ€λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.

 

  1. Permissions 클릭

 

  1. Bot Token Scopes μ—μ„œ Add an Oauth Scope λ₯Ό 클릭해 λ‹€μŒκ³Ό 같이 3κ°€μ§€ κΆŒν•œμ„ λΆ€μ—¬ν•©λ‹ˆλ‹€.

 

  1. OAuth Tokens for Your Workspace μ—μ„œ Install to Workspace λ₯Ό μ„ νƒν•˜μ—¬ μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ— 등둝이 μ™„λ£Œλ˜λ©΄ λ‹€μŒκ³Ό 같이 토큰이 λ°œκΈ‰λ©λ‹ˆλ‹€. ν•΄λ‹Ή 토큰은 Spring μ• ν”Œλ¦¬μΌ€μ΄μ…˜ yml μ„€μ • νŒŒμΌμ— μ‚¬μš©λ˜κΈ° λ•Œλ¬Έμ— λ³΅μ‚¬ν•΄λ‘‘λ‹ˆλ‹€.

 

 

🎯 2. build.gradle 에 Slack API μ˜μ‘΄μ„± μΆ”κ°€

 

dependencies {
    // slack
    implementation "com.slack.api:slack-api-client:1.25.1"
}

 

Slack API λ₯Ό ν™œμš©ν•˜κΈ° μœ„ν•΄ μœ„μ™€ 같이 μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•΄μ€λ‹ˆλ‹€.

 

 

🎯 3. application.yml 에 토큰, 채널λͺ… 등둝

 

slack:
  token: {λ°œκΈ‰ 받은 토큰값}
  channel:
    monitor: '#server-error-dev'

 

λ‹€μŒκ³Ό 같이 token μ—λŠ” λ°œκΈ‰ 받은 토큰값을 μž…λ ₯ν•΄μ£Όκ³ 

slack.channel.monitor μ—λŠ” ‘#{μ„€μ •ν•œ μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ—μ„œ μ—λŸ¬ μ•Œλ¦Όμ„ 받을 채널λͺ…}’ 을 μž…λ ₯ν•΄μ€λ‹ˆλ‹€.

 

 

🎯 4. SlackService, SlackServiceUtils κ΅¬ν˜„

 

SlackService

@Slf4j
@Service
public class SlackService {

	@Value(value = "${spring.profiles.active}")
	String profile;
	@Value(value = "${slack.token}")
	String token;
	@Value(value = "${slack.channel.monitor}")
	String channelProductError;

	private static final String LOCAL = "local";
	private static final String PROD_ERROR_MESSAGE_TITLE = "🀯 *500 μ—λŸ¬ λ°œμƒ*";
	private static final String ATTACHMENTS_ERROR_COLOR = "#eb4034";

	public void sendSlackMessageProductError(Exception exception) {
		if (!profile.equals(LOCAL)) {
			try {
				List<LayoutBlock> layoutBlocks = SlackServiceUtils.createProdErrorMessage(exception);
				List<Attachment> attachments = SlackServiceUtils.createAttachments(ATTACHMENTS_ERROR_COLOR,
					layoutBlocks);
				Slack.getInstance().methods(token).chatPostMessage(request ->
					request.channel(channelProductError)
						.attachments(attachments)
						.text(PROD_ERROR_MESSAGE_TITLE));
			} catch (SlackApiException | IOException e) {
				log.error(e.getMessage(), e);
			}
		}
	}
}

 

SlackServiceUtils

public class SlackServiceUtils {

	private static final String ERROR_MESSAGE = "*Error Message:*\n";
	private static final String ERROR_STACK = "*Error Stack:*\n";
	private static final String FILTER_STRING = "blossom";

	public static List<Attachment> createAttachments(String color, List<LayoutBlock> data) {
		List<Attachment> attachments = new ArrayList<>();
		Attachment attachment = new Attachment();
		attachment.setColor(color);
		attachment.setBlocks(data);
		attachments.add(attachment);
		return attachments;
	}

	public static List<LayoutBlock> createProdErrorMessage(Exception exception) {
		StackTraceElement[] stacks = exception.getStackTrace();

		List<LayoutBlock> layoutBlockList = new ArrayList<>();

		List<TextObject> sectionInFields = new ArrayList<>();
		sectionInFields.add(markdownText(ERROR_MESSAGE + exception.getMessage()));
		sectionInFields.add(markdownText(ERROR_STACK + exception));
		layoutBlockList.add(section(section -> section.fields(sectionInFields)));

		layoutBlockList.add(divider());
		layoutBlockList.add(section(section -> section.text(markdownText(filterErrorStack(stacks)))));
		return layoutBlockList;
	}

	private static String filterErrorStack(StackTraceElement[] stacks) {
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.append("```");
		for (StackTraceElement stack : stacks) {
			if (stack.toString().contains(FILTER_STRING)) {
				stringBuilder.append(stack).append("\n");
			}
		}
		stringBuilder.append("```");
		return stringBuilder.toString();
	}
}

 

이제 μœ„μ™€ 같이 μŠ¬λž™ μ•Œλ¦Όμ„ μ „μ†‘ν•˜κΈ° μœ„ν•œ μ„œλΉ„μŠ€ λ‘œμ§μ„ κ΅¬ν˜„ν•΄μ€λ‹ˆλ‹€.

 

SlackService μ—μ„œλŠ” Slack 에 μ•Œλ¦Ό 전솑을 μš”μ²­ν•˜λŠ” Request λ₯Ό μƒμ„±ν•΄μ„œ λ³΄λ‚΄λŠ” 역할을 λ‹΄λ‹Ήν•˜κ³ ,

SlackServiceUtils μ—μ„œλŠ” Slack 에 ν‘œμ‹œλ  μ•Œλ¦Όμ˜ ν˜•νƒœλ₯Ό λ§Œλ“œλŠ” 역할을 λ‹΄λ‹Ήν•©λ‹ˆλ‹€.

μ•Œλ¦Όμ„ λ‹€λ₯Έ ν˜•νƒœλ‘œ μ»€μŠ€ν…€ν•˜κΈ° μœ„ν•΄μ„œλŠ” ν•˜λ‹¨μ˜ κ³΅μ‹λ¬Έμ„œλ₯Ό μ°Έκ³ ν•΄μ„œ μ§„ν–‰ν•΄λ³΄μ‹œλ©΄ λ©λ‹ˆλ‹€.

 

λ˜ν•œ 상단 μ½”λ“œμ˜ 경우 배포 ν™˜κ²½μΈ dev, prod ν™˜κ²½μ—μ„œλ§Œ μŠ¬λž™ μ•Œλ¦Όμ΄ λ™μž‘ν•˜λ„λ‘ κ΅¬ν˜„λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€.

local ν™˜κ²½μ—μ„œλŠ” Slack μ•Œλ¦Όμ΄ μš”μ²­λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— ν…ŒμŠ€νŠΈ ν•˜μ‹€ λ•Œ ν•΄λ‹Ή 뢀뢄을 주석 μ²˜λ¦¬ν•˜κ³  μ§„ν–‰ν•΄μ£Όμ„Έμš”.

 

 

Reference: Secondary message attachments

Another way to attach content to messages is the old attachments system. We prefer Block Kit now.

api.slack.com

 

 

🎯 5. ControllerAdvice μ—μ„œ SlackService 호좜

 

ControllerAdvice

@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice
public class ControllerAdvice {

	private final SlackService slackService;

	/**
	 * 500 Internal Server Error
	 */
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ExceptionHandler(Exception.class)
	protected ApiResponse<Object> handleException(final Exception exception) {
		log.error(exception.getMessage(), exception);
		slackService.sendSlackMessageProductError(exception);
		return ApiResponse.error(INTERNAL_SERVER_EXCEPTION);
	}
}

 

이제 μ„œλ²„κ°€ μ˜ˆμƒν•˜μ§€ λͺ»ν•œ μ—λŸ¬μ— λŒ€ν•΄ Slack μ•Œλ¦Όμ„ μ „μ†‘ν•˜κΈ° μœ„ν•΄ μœ„μ™€ 같이 500λ²ˆλŒ€ μ—λŸ¬κ°€ λ°œμƒν•˜λ©΄ SlackService λ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€.

 

 

μ‹€μ œ μ„œλΉ„μŠ€ λ‘œμ§μ—μ„œ ν…ŒμŠ€νŠΈν•˜κΈ° μœ„ν•΄ Runtime Exception 을 λ°œμƒμ‹œμΌœλ³΄λ©΄ μœ„μ™€ 같이 μ—λŸ¬ μ•Œλ¦Όμ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.