
들어가며
오늘부터 본격적으로 디스코드 봇 활성화 하는 것에 도전했다.
관련 지식이 전혀 없는 상태라 어디서부터 시작해야 할지 막막했지만,
jda 위키 문서와 여러 블로그를 참고하며 천천히 감을 잡아가기 시작했다.
특히 디스코드 봇이 콘솔 프로그램과 전혀 다른 방식인 이벤트 기반 구조로 동작한다는 점에서 큰 혼란이 있었고,
GatewayIntent, Slash Command 같은 개념들도 처음엔 낯설게 느껴졌다...
그래도 작은 것부터 부딪혀보며 하나씩 이해해 나가다 보니 전체 구조가 조금씩 보이기 시작했고, 막막함을 뚫고 구현을 어찌저찌 해낸 것 같다!!
오늘 한 일
- 디스코드 서버에 robo-77 봇 활성화 시키기
- Discord 봇 토큰 환경변수 설정하기
- Gateway Intent 알아보기
- Discord 이벤트 기반 아키텍처 고민
- slash 기반 명령어 수행 기능 구현
- Slash Command 옵션 누락으로 발생한 NPE 디버깅
- Discord username 규칙 조사
- Git 브랜치 실수: 체리픽으로 해결
- 로깅 라이브러리 경고 확인 및 logback 도입
📌 디스코드 서버에 robo-77 봇 깨우기
▪️ First thing first: 디스코드 봇 토큰 관리하기
https://discord.com/developers/applications에서 디스코드 봇을 만들면 토큰을 준다!
해당 토큰이 있어야 명령어 등록*응답, 메세지 읽기*쓰기 등 디스코드 서버와 이벤트를 주고받을 수 있는 권한을 부여받을 수 있다.
JDA에서는 아래 코드와 같이 토큰 전달 → Discord Gateway 접속 → 이벤트(메시지, 명령어, 멤버 join 등) 수신
이 흐름 전체가 토큰 기반으로 동작한다고 한다.
JDA jda = JDABuilder.createDefault(DISCORD_BOT_TOKEN).build();
그래서 토큰은 봇을 활용함에 있어 반드시 필요하며, 유출되면 다른 사람이 내 봇을 조작할 수 있기에 절대 노출되면 안된다.
jda wiki에서도 소스 코드를 게시할 계획이라면 외부 파일이나 환경 변수에서 토큰을 관리하는 것이 더 좋다고 추천해준다.

따라서 이 토큰을 어떻게 안전하게 관리할 지에 대한 고민을 가장 먼저 하게 되었다.
스프링을 사용할 때는 application.yaml에 값을 정의하고 @Value로 주입하면 되었지만, 지금 프로젝트는 순수 자바 환경이기 때문에 설정 파일 기반의 의존성 주입이 불가능했다.
그래서 디스코드 토큰을 외부에서 주입하는 방법을 찾아보았고, 그 과정에서 System.getenv()로 환경변수를 가져오는 방식이 있다는 것을 알게 되었다.
여기서 자연스럽게 이어진 궁금증은
“환경변수를 쉘에 등록하는 것과 IntelliJ에서 Run Configuration에 환경변수를 설정하는 것은 뭐가 다른가?”였다.
- 쉘에 등록 = OS 전체에서 사용하는 “전역적인 환경변수”
- IntelliJ에 등록 = “특정 실행 설정에서만 사용되는 지역(environment) 환경변수”
따라서 둘 다 System.getenv("DISCORD_BOT_TOKEN")로 접근 가능하지만,
변수를 누가 관리하느냐(운영체제 vs IDE)가 다르고, 적용되는 범위(전역 vs 특정 실행)도 다르다.
나는 인텔리제이에 토큰을 등록하는 방식을 택했다.
현재는 개발 단계이기 때문에 굳이 내 OS 전체에 환경변수를 등록할 필요가 없고, IntelliJ의 Run Configuration에 넣어두면 해당 프로젝트를 실행할 때만 안전하게 로딩할 수 있기 때문이다. 배포를 진행한다 해도 내 컴퓨터가 아닌 AWS와 같은 별도의 서버 환경에서 애플리케이션이 구동될 것이기 때문에, 그때 서버 내부에 서버용 환경변수를 따로 설정하면 된다고 판단했다.
▪️ 메시지 내용을 읽기 위한 MESSAGE_CONTENT Intent 권한 문제, 이렇게 해결했다
Discord의 Gateway Intent는 내 봇이 어떤 종류의 이벤트를 받을 수 있는지 선택하는 옵션이다.

Intent 가 정말 많지만, 대표적인 Intent만 추려보면 다음과 같다.
- MESSAGE_CONTENT: 메시지 내용을 읽을 때(챗봇, prefix 명령어)
- GUILD_MESSAGES: 메시지가 도착했는지 알고 싶을 때
- GUILD_MESSAGE_REACTIONS: Reaction 기반 기능(투표/이벤트)
- GUILD_MEMBERS: 서버 멤버 정보 필요할 때(게임 참가자 관리 등)
다양한 Intent중에서 메세지 내용 읽는 이벤트를 활용하여, 코드를 아래와 같이 작성해 보았다.
JDA jda = JDABuilder.createDefault(botToken)
.enableIntents(GatewayIntent.MESSAGE_CONTENT) // 메시지 내용을 읽기 위한 Intent
.setActivity(Activity.playing("로보77")) // 봇의 '플레이 중' 상태 설정
.build()
.awaitReady(); // 봇이 완전히 준비될 때까지 대기
근데 에러가 발생했다!

왜일까?
MESSAGE_CONTENT는 사용자가 보낸 모든 메세지 내용을 읽을 수 있는 권한을 지니는 거기 때문에, 개인정보·프라이버시 관련된 이벤트이다.
따라서 코드뿐만 아니라 개발자 포털에서 직접 Message Content Intent 토글을 켜줘야 활성화 된다고 한다.

- 11/14 추가
Slash Command만 사용할 거면 Message Content Intent 를 활성화하는게 꼭 필요하지 않다고 한다.
Slash Command는 유저가 입력한 옵션을 Discord가 서버 측에서 이미 파싱해서 전달해주기 때문에 메시지 내용을 읽을 필요가 없다.
따라서 아래 코드를 지워도, 봇이 명령어 수행을 잘 하더라..!
.enableIntents(GatewayIntent.MESSAGE_CONTENT) // 메시지 내용을 읽기 위한 Intent
하지만 일반 메시지를 읽거나 prefix 명령어(!play) 같은 걸 쓰려면 반드시 필요하기 때문에, 필요에 의해서 잘 활용하는 걸로!!
▪️ MVC패턴은 만능이 아니었다: Discord의 이벤트 기반 구조로의 전환
Phase 1에서 콘솔 기반 게임은 전형적인 동기식(Synchronous) 구조로 개발이 이루어졌다.
입력 → 처리 → 출력 순으로 게임이 진행되었고, 이 흐름을 RoboGameController가 제어했다.
하지만 디스코드 봇은 완전히 다른 방식이다.
- 유저는 프로그램의 흐름을 따라오지 않는다.
- 대신 “명령어 이벤트”를 발생시키고,
- 봇은 그 이벤트에 반응하는(event-driven) 방식으로 동작한다.
Phase 1(콘솔 기반)에서 만든 Controller 중심의 흐름을 그대로 가져올 수 없었고,
결국 디스코드 환경에 맞게 이벤트를 받아 처리하는 Listener 기반 클래스를 별도로 만들게 되었다.
GameCommandListener가 명령어 이벤트를 받아 도메인 로직을 수행하는 구조로 확장되었다.
이 과정에서 가장 크게 느낀 점은,
내가 그동안 너무 자연스럽게 ‘MVC 구조 = 대부분의 애플리케이션이 따라야 하는 기본 패턴’라고 생각하고 있었다는 사실이었다.
나는 그동안 프로젝트를 진행할 때 대부분
Controller-Service-Repository-Domain으로 이어지는 구조나
프리코스 미션에서 썼던 MVC 패턴을 자연스럽게 떠올리곤 했다.
그래서 어떤 개발을 하든 결국 MVC나 그 비슷한 구조로 귀결될 줄 알았다.
하지만 이번 디스코드 봇 구현은 그 믿음을 완전히 깨버렸다.
MVC는 만능이 아니며, 설계 구조는 환경과 문제의 성격에 따라 달라져야 한다는 점을 직접 경험하며 이해하게 되었다.
▪️ slashCommand 등록하기
로보77 게임에 필요한 명령어들을 command enum에 모아두고, 등록하였다!
jda.upsertCommand(command.getCommand(), command.getDescription()).queue();
🤔 왜 Slash 명령어(Slash Command)를 선택했는가?
디스코드 봇에서 유저가 게임을 어떤 방식으로 플레이하면 가장 자연스러울지 고민하면서
JDA Wiki의 interactions 부분을 하나씩 읽어 내려갔다.
처음에는 DM으로 자유롭게 입력받는 방식도 고려했지만, 곧 문제점을 발견했다.
- 자유 입력 방식은 유저마다 입력 형태가 모두 다르다.
→ 입력 검증 로직이 너무 복잡해지고, 게임 흐름이 흔들릴 가능성도 높다. - 유저 입장에서도 매번 텍스트로 값을 입력하는 과정이 번거롭다.
→ 게임 자체에 몰입하기 귀찮을 수 있다.
반면 Slash 명령어는 이런 단점을 깔끔하게 해결해줬다.
- /play, /hand, /guide처럼 명령어 자체가 게임의 기능을 안내해준다.
- 옵션이 필요한 경우에도 미리 선택지를 제공할 수 있다.
- 유저는 “명령어 호출 → 옵션 선택”만 하면 되어 적은 정보로도 쉽게 플레이 가능하다.
즉, Slash 명령어는 유저 경험(UX) 측면에서도, 게임 제약 조건 설정 측면에서도
가장 안정적이고 명확한 방식이라고 판단해 채택하게 되었다.
▪️ Slash Command에서 옵션을 빼먹으면 생기는 일
/play 명령어 호출하면서 사용자로부터 card를 입력받고자 아래와 같은 코드를 작성하였다.
String cardValue = event.getOption("card").getAsString();
그리고 명령어를 호출하면서 card를 입력했는데, event.getOption("card")가 null을 반환하면서 NPE(NullPointerException)가 발생하였다.

처음엔 이벤트 자체가 잘 안 들어오는 건가, SlashCommand 설정이 잘못된 건가 싶어서 디버깅을 하며 한참을 헤맸다.
그러다 다시 JDA Wiki의 Slash Command 등록 예제를 천천히 읽어보면서 문제의 원인을 찾을 수 있었다.
👉 Slash Command를 등록할 때 옵션을 명시(addOption)하지 않으면, 디스코드에서 해당 옵션을 입력받지 않는다.
즉, 명령어 코드에서는 getOption("card")를 기대하고 있었지만, 정작 디스코드 서버에는 “이 명령어에 card라는 옵션이 있다”는 정보가 한 번도 등록된 적이 없었던 것이다.
원인을 이해하고 나서는, Slash Command 등록 로직에 아래처럼 옵션을 추가해 문제를 해결했다.
공식 문서를 더 적극적으로 활용하자!!!
if (command == Command.PLAY) {
jda.upsertCommand(command.getCommand(), command.getDescription())
.addOption(OptionType.STRING, "card", "내고 싶은 카드 (예: 10, -10, x2, reverse)", true)
.queue();
continue;
}
▪️ 봇 활성화


📌 username 규칙 변경에서 체감한 VO의 가치
Phase 1에서는 사용자 이름 규칙을 다음과 같이 정의해두었다.
- 최소 2자, 최대 10자
- 영문 소문자와 숫자만 허용
하지만 환경을 콘솔 → 디스코드로 옮긴 순간 문제가 생겼다.
내 디스코드 username(ji_hyunn)으로 테스트를 하려 했는데, 이름 검증 로직에서 통과하지 못한 것이다.
콘솔에서 정의한 이름 조건과 Discord의 실제 username 규칙이 달랐기 때문이다.

디스코드의 이름 조건은 다음과 같다.
- 최소 2자, 최대 32자
- 소문자 영문(a-z), 숫자(0-9), 그리고 밑줄(_) 또는 마침표(.)만 허용
- 마침표(.)는 연속으로 두 번 사용할 수 없음 (“..” 금지)
결국 게임의 이름 유효성 검증 역시 디스코드의 규칙을 따르도록 수정해주어야 했다.
다행히 Phase 1에서 PlayerName이라는 이름 전용 VO(Value Object) 를 만들어두었기 때문에,
이름 검증 로직은 한 곳에만 모여 있었다.
그 덕분에 딱 2줄의 코드만 변경해서 문제를 해결할 수 있었다.
🔗 커밋 링크
https://github.com/Jiihyun/robo77/commit/a4b47e0466f1b4018159afb040bca3da32d18be3
이 순간, “아, 유지보수성을 신경 쓰면서 구조를 만들었던 게 이렇게 빛을 발하는구나” 라는 생각이 들었다.
콘솔에서 디스코드로 환경이 바뀌었음에도 불구하고,
비즈니스 규칙을 수정해야 하는 지점이 단 하나의 VO뿐이었다는 사실이 특히 만족스러웠다.
내가 그동안 고민해온 객체지향 설계 기준이 이런 상황에서 적용된다는 걸 체감하면서,
객체지향 설계가 얼마나 큰 의미를 갖는지 새삼 느꼈다.
이 작은 변경을 통해 느낀 안정감은, 앞으로 기능들을 구현하는 데 있어
더 객체지향적인 구조를 지향해야겠다는 마음을 한층 더 굳히게 해주었다.
📌 잘못 올린 커밋을 되살리는 방법: cherry-pick 체득기
작업에 몰두하다 보니 console 브랜치에서 discord 브랜치로 넘어가는 것을 까먹고
discord와 관련된 커밋 몇 개를 console 브랜치에 올려버렸다.
이 때 console 브랜치에 있는 커밋들을 취소시키고, discord 브랜치에 다시 커밋을 새로 작성하기엔 너무 비효율적이라 느꼈다.
따라서 특정 커밋만 discord 브랜치로 옮겨오는 방법을 찾던 중 cherry-pick에 대해 알게되었다.
사실 cherry-pick 개념은 예전부터 여러 번 읽어보긴 했다.
하지만 ‘언제, 어떤 상황에서 쓰는 게 좋은지’ 를 제대로 이해하지 못해서
항상 흐릿하게만 알고 있던 기능이었다.
그런데 이번엔 실제 이슈를 해결하기 위해 직접 cherry-pick을 써볼 수밖에 없었고,
그 과정에서 체리픽이 어떤 상황에서 힘을 발휘하는지를 제대로 체감했다.
원래 브랜치를 되돌리거나 커밋을 재구성하는 수고로움 없이,
“딱 필요한 커밋만” 다른 브랜치에 가져올 수 있다는 점이 정말 깔끔했다.
한 번 직접 써보고 나니 그동안 문서로만 봤던 개념이 머릿속에서 ‘아 이런 기능이구나!’ 하고 또렷하게 자리 잡는 느낌이 들었다.
작은 실수였지만, 덕분에 cherry-pick이라는 Git 도구를 체득할 수 있었다는 점에서 큰 수확이었다.
📌 JDA 경고 원인 파악: logback 적용

디스코드 봇을 실행해보니, 콘솔에 계속 로깅 관련 경고가 출력되었다.
처음에는 “내 코드 문제인가?” 하고 살짝 걱정했지만,
로그 내용을 확인해보니 JDA에서 기본 로깅 구현체가 없어 발생하는 경고였다.
그래서 이를 해결하기 위해 가장 많이 쓰이면서 JDA에서 권장하는 로깅 프레임워크인 Logback을 도입했다.
Gradle에 의존성을 추가하고 다시 실행해보니 더 이상 경고가 뜨지 않아,
올바르게 설정이 적용되었음을 확인할 수 있었다.
아직은 단순히 경고 해결을 위해 로깅을 도입한 수준이지만,
프로젝트가 어느 정도 완성되면 로깅 레벨 관리, 로그 포맷 구성 등
조금 더 깊게 공부해보고 적용해보고 싶다는 생각이 들었다.
지금은 단순한 설정 하나일 뿐이지만,
앞으로 유지보수성과 디버깅 효율을 높이는 중요한 기반이 될 것 같기 때문이다.
이렇게 프로젝트를 진행하면서 계속 새로 해보고 싶은 것이 생긴다는 점에서
나 스스로도 이 도전에 몰입하고 있음이 느껴졌다.
앞으로도 이 흐름을 잃지 않고 끝까지 즐기면서 완주해야지.
마치며
오늘은 처음 접하는 기술들 때문에 계속 막막함과 어려움이 있었지만,
그 과정에서 한 단계씩 이해하고 문제를 해결해 나가는 경험이 정말 뿌듯하고 재미있었다.
그리고 이런 식으로 스스로 성장하며 배워나갈 수 있다는 사실이 새삼 소중하게 느껴졌다.
아직 모르는 것도 많고 앞으로 부딪힐 문제도 많겠지만,
직접 디버깅하고 문서를 찾아보고 여러가지 고민하며 해결해낸 오늘의 경험이
“아, 나 지금 제대로 배우고 있구나” 하는 확신을 주었다.
특히 이벤트 기반 구조, Slash 명령어, 체리픽 등
하나하나 해결해 나가면서 디스코드 봇이라는 새로운 환경에 조금씩 적응하고 있다는 게 뿌듯했다.
Phase 2는 이제 막 시작되었지만,
오늘처럼 모르는 것을 두려워하기보다 부딪혀보면서 배우는 방식을 이어가야겠다!
'Project > Robo77' 카테고리의 다른 글
| 로보 77 구현기 9일 차 - 불필요한 추상화를 걷어내고 Discord 리스너 테스트하기 (0) | 2025.11.15 |
|---|---|
| 로보 77 구현기 8일 차 - 동작 우선 코드, 객체지향으로 CPR 하기 (0) | 2025.11.14 |
| 로보 77 구현기 6일 차 - Phase 2를 향한 재정비와 첫 디스코드 봇 생성! (0) | 2025.11.12 |
| 로보 77 구현기 2일 차 - 전략 패턴과 씨름한 하루 (0) | 2025.11.08 |
| 로보 77 구현기 1일 차 - 예상보다 험난했던 프로젝트 세팅과 카드 제출 구현 (0) | 2025.11.07 |