본문 바로가기
Projects/MIACC 2022 Summer Retreat

MIACC 2022 학생/청년 여름 수련회 웹페이지 - 회고록

by Kloong 2022. 8. 30.

https://github.com/Kloong1/miacc_youth_2022_summer_retreat

 

GitHub - Kloong1/miacc_youth_2022_summer_retreat: 미아중앙교회 2022 여름 수련회에 사용될 웹 어플리케이션

미아중앙교회 2022 여름 수련회에 사용될 웹 어플리케이션. Contribute to Kloong1/miacc_youth_2022_summer_retreat development by creating an account on GitHub.

github.com

0. 추가된 요구사항

인증샷을 찍어서 서버에 업로드하면, 서버에 직접 접속할 수 없는 전도사님들도 업로드 된 사진을 확인할 수 있어야 한다는 요구사항이 추가되었다.

1. 설계

0. 딜레마

사실 절대로 확장할 일도 없고, 재사용 될 일도 없고, 누군가 내 코드를 볼 일도 없는 사적이고 간단한 프로젝트라는 것을 잘 알고 있지만서도, 개발자로서 아무렇게나 코드를 짤 수는 없었다.

그런데 이게 딜레마인게, 프로젝트 규모가 너무 작다보니까 오히려 프로젝트 설계에서 내적인 갈등이 많이 생겼다. 요구사항이 몇 개 없어서 하나의 클래스에 많은 기능을 담아도 될 것 같으면서도, "좋은 코드" 라는 관점에서 보면 SOLID를 지키기 위해 역할을 쪼개고 상속 관계를 활용해야 할 것 같았다.

또 수련회 당일날 3시간 정도 간단하게 사용될 예정인 웹 어플리케이션이긴 하지만, 그래도 20명이 넘는 일반 사용자들이 실제로 사용하는 프로그램이라고 생각을 하니 다양한 변수에 대해서 고려하지 않을 수가 없었다 (사실 이 변수라는게, 사용자 중 한 명이 갑자기 해커가 되어서 URL을 직접 조작하며 미션을 해결하려고 한다던가 하는 확률이 매우 낮은 경우이긴 하지만 말이다).

아무튼 청년부 회장으로서가 아닌 개발자로서 쓸데없이 진지하게 프로그래밍을 하다보니, 사용자들이 내 프로그램을 사용하면서 미션을 진행할 때 생길 수 있는 변수를 최소화하는 동시에, 좋은 코드를 작성하고자 하는 마음이 컸다. 그래서 이 작디 작은 프로젝트의 구조를 3번이나 바꿨다. 지금 생각해보면 좀 어이가 없긴 하다 ㅎㅎ...

1. URI 설계

클래스 설계를 하기 전에 먼저 URI 설계를 했다.

이 프로젝트는 웹 페이지를 통해 미션 내용을 전달한다. 사용자가 해당 미션을 해결하면 다음 미션에 대한 암호를 얻게 되고, 미션 페이지에서 올바른 암호를 입력하면 다음 미션으로 넘어가는 방식이다.

미션을 모두 클리어하면 스톤을 얻게 되는데, 이 스톤이 총 4개가 있다. 교회 학생 청년들이 4조로 나뉘어서 각각의 스톤에 대한 미션을 해결하는 것이다.

미션 구조


"미션"이 4개의 "스톤" 으로 구분되고, 각 스톤에 대한 미션이 "여러개" 존재하므로 URI 를 다음과 같이 설계하였다.

/missions/{스톤}/{미션 번호}


예를 들어 아가페 스톤의 3번째 미션에 대한 URI는 "/missions/agape-stone/3" 이 되는 것이다.

다음 미션으로 진행하기 위해서는 주어진 미션을 해결해서 암호를 알아내야 한다.

미션에 대한 요청은 GET 방식이고, 암호는 쿼리 파라미터 형태로 전달하므로 암호까지 포함한 HTTP 요청은 다음과 같다.

GET /missions/{스톤}/{다음 미션 번호}?password={현재 미션 암호}


예를 들어 디사이플 스톤의 5번째 미션에 대한 HTTP 요청은 다음과 같다.

GET /missions/disciple-stone/5?password=1q2w3e4r


또 특정 미션에서는 인증샷 업로드를 요구하기 때문에, 사진 업로드에 대한 URI도 필요했다.

/missions/{스톤}/{미션 번호}/upload


위 URI로 HTML Form 을 통해 이미지 업로드를 하게끔 설계했다.
그리고 서버에 업로드 된 이미지를 전도사님들이 확인해야 하므로 그에 대한 URI 는 다음과 같이 설계하였다.

/files/{사진 파일 이름}


이 부분에서 고민이 많이 있었다.

일단 전도사님들이 인증샷 확인할 때 HTML 문서 안에 렌더링 된 이미지 형태로 확인하는 것이 아니라 사진 파일 원본을 다운받아서 확인하게끔 구현할 예정이었다.

왜냐하면 각 팀의 실시간 미션 진행 상황을 슬랙 메시지로 전송하는 기능을 구현할 예정이었는데, 이 기능을 활용해서 어떤 팀이 이미지를 업로드 하면, 그 팀이 진행중인 미션에 대한 정보와 함께 업로드한 사진 파일에 대한 링크를 슬랙 메시지에 함께 포함시켜서 전송하려고 했기 때문이다.

이렇게 하면 사진을 보여주기 위한 웹 페이지(게시판 등의 페이지)도 따로 구현할 필요 없고, 전도사님들이 인증샷 업로드 여부를 실시간으로 확인할 수 있기 때문이다.

그래서 인증샷이 업로드 되면, 해당 사진 파일에 대한 URI를 슬랙 메시지로 전송해서 전도사님들이 직접 URI를 입력하지 않고 보내진 링크를 클릭한다는 가정 하에, 사진 파일 이름을 직접 URI에 포함시키는 방향으로 URI를 설계하게 되었다.

처음에는 사진 파일 이름 대신 랜덤한 ID, 혹은 스톤 이름과 미션 번호를 조합해서 만든 문자열을 URI로 사용해야 하나 고민을 많이 했지만, 어차피 업로드 된 사진을 실시간으로 확인만 할 수 있으면 되기 때문에 가장 단순한 방식을 선택했다.

물론 이 URI는 너무 일반적인 느낌이 강하긴 하다. 단순히 "/files" 다음에 아무런 계층 없이 바로 파일 이름이 나오기 때문이다. 그래서 "/files/{스톤}/{미션 번호}/{사진 파일 이름}" 같은 URI도 고민하긴 했는데, 사실 이 프로젝트 자체가 미션 진행 하나만을 위한 아주 작은 크기의 프로젝트이기 때문에 이렇게 복잡하게 해봤자 달라질 부분이 없어서 그냥 단순하게 설계했다.

2. 클래스 설계

1) 첫 번째 시도

가장 단순하게 접근한 방식이다. 각 스톤에 대한 미션마다 컨트롤러 클래스를 구현하고, @RequestMapping 을 해줬다.

그러나 이 구조를 사용하지 않은 이유는 위 그림만 봐도 누구나 알 수 있을 것이다. 중복이 너무 많다. 각 스톤 컨트롤러의 핵심 로직은 완전히 동일하다. 단지 스톤별로 미션이 다르기 때문에 다른 HTML 파일을 보여주는 것 정도의 차이만 존재할 뿐이다.

물론 위 그림에서 "이미지 업로드 핸들러 메소드" 같은 경우는 두 개의 컨트롤러에만 존재하는 일종의 확장된 기능이긴 하지만, 아무튼 이것도 중복이다. 이렇게 개발해도 동작은 잘 하겠지만, 개발자는 자고로 중복을 배척해야 하기 때문에 이렇게나 중복 투성이인 구조를 사용할 수는 없었다.

2) 두 번째 시도

중복을 해결하기 위한 가장 단순한 해결책으로 4개의 컨트롤러를 하나로 합쳐버렸다. 그리고 @PathVariable 을 사용해서 각 스톤에 대한 요청을 구분했다.

이렇게 하면 실제로 중복을 해결할 수 있었지만 여전히 나에게는 눈에 밟히는 부분이 많았다.

첫 번째로 모든 핸들러 메소드에서 어떤 스톤에 대한 요청인지 구분을 하는 작업을 해야 한다는 것이었다. 별다른 작업 없이 뷰 이름만 반환하는 아주 단순한 핸들러에서도 @PathVariable 에 대한 검증을 해줘야만 했다. 메소드로 따로 분리한다고 해도 핵심 로직과는 관련 없는 추가적인 로직이 모든 핸들러에 추가되는 것이어서 좋은 코드는 아니라고 느껴졌다.

두 번째로 컨트롤러 내부에 "agape-stone", "disciple-stone" 과 같은 스톤 별 URI를 문자열 형태로 그대로 가지고 있고, 이 문자열로 @PathVariable 을 검증하는데 이 부분이 OCP를 위반하게 만들었다. 절대 그럴 일은 없지만 만약에 새로운 스톤이 추가되면, 해당 문자열을 검증 로직에 추가하려면 컨트롤러 코드를 직접 건드려야하기 때문이다. 이 부분을 해결하기 위해 스톤에 대한 정보와 상수들을 갖고 있는 Enum 을 만들까도 했지만, 결국엔 이 구조를 채택하지 않기로 하면서 폐기했다.

 

3) 세 번째 시도

여기서부터는 (설명하기가) 복잡해진다.

먼저 중복을 제거하기 위해서 상속을 사용했다. 모든 스톤 컨트롤러의 부모가 되는 abstract class 인 StoneController가 모든 핸들러 메소드를 가지고 있고, 4개의 스톤 컨트롤러가 StoneController를 상속받고 있다.

그런데 여기서 중요한 점은 더이상 스톤 종류에 대해서는 @PathVariable을 사용하지 않는다는 것이다. StoneController를 상속받는 4개의 스톤 컨트롤러 클래스에서 자신의 스톤에 맞게 클래스 범위로 @RequestMapping이 되어있기 때문이다. 즉 더이상 @PathVariable String stone 을 검증할 필요가 없어졌다는 것이다.

대신 그 자리를 StoneController의 멤버 변수인 String stone 이 대신한다. StoneController를 상속받은 4개의 스톤 컨트롤러가 생성자에서 stone 변수에 스톤 종류에 맞는 문자열을 넣어주기만 하면 (예를 들어 AgapeStoneController의 경우 "agape"), StoneController가 멤버 변수 stone에 올바른 값이 들어있다는 가정 하에 로직을 잘 구현하고 있기 때문에 문제 없이 동작한다.

그리고 여기서 컴포넌트 스캔 대신 config 클래스에서 수동으로 컨트롤러를 등록해주고, 컨트롤러를 등록하는 코드에서 스톤 이름에 대한 문자열을 생성자에 넣어주는 방식을 선택했다.

수동 등록을 선택하고, 수동 등록에서 문자열을 넣어준 이유는 config 클래스만 보고 미션의 구조를 한 눈에 파악할 수 있게 하고, 클래스 코드에서 문자열을 직접 가지고 있는게 좋지 않게 느껴져서 그런건데 솔직히 이건 너무 간 것 같다. 어차피 스톤 이름같은 건 일종의 상수값이니까, 문자열을 직접 가지고 있는게 싫었다면 그냥 Enum이나 인터페이스를 사용해서 상수 형태로 스톤 이름을 가지고 있고 클래스에서 가져다 쓰게 하면 되는건데 말이다.

지금 생각해보면 수동 등록을 하지 않고, 스톤 이름을 상수 클래스로 따로 관리했으면 이 구조가 제일 적절하지 않을까 하는 생각이 든다. 일단 확장이 편하다. 공통 기능은 StoneController에 추가 구현하거나 수정하면 되고, 특정 스톤에만 필요한 기능은 해당 클래스에만 구현하면 된다.

그런데 굳이 따지면 특정 스톤에만 필요한 기능은 없는 상황이었다. 물론 확장할 가능성도 0에 가깝다. 그래서 그런지 그 때의 나는 이 구조가 너무 가버린(일명 뇌절) 설계라고 생각했고 이 전의 구조로 돌아가기로 결정한다.

근데 지금의 나는 이 결정을 후회하긴 한다 ㅋㅋ;

 

4) 마지막 시도

결국엔 돌고 돌아서 두 번째로 시도했던 구조를 개선해보기로 했다. 이 구조의 핵심적인 문제인 @PathVariable String stone 검증 이슈를 해결하기 위해서 Stones 라는 새로운 클래스를 만들어 DI를 해줬다.

Stones 클래스가 스톤 이름 등의 정보를 문자열 형태로 가지고 있고, 간단한 검증 메소드도 가지고 있기 때문에 컨트롤러는 이 객체만 DI 받으면 더이상 실제 스톤 이름과 검증 로직에 대해서는 신경쓸 필요가 없어진다.

물론 해당 객체를 호출하는 코드가 핸들러 메소드마다 있어야한다는 문재는 여전히 존재했지만 이 구조를 선택한 이상 안고가야했다.

Stones 에 스톤 관련 정보를 입력할 때는 클래스 내부에서 자체적으로 초기화를 하지 않고, 수동으로 빈 등록을 해줄 때 외부에서 초기화를 해주는 방식을 사용했다. 그래야 미션 전체적인 구조를 config 파일에서 볼 수 있다는 생각이었는데, 지금 생각해보면 이 판단 역시 뇌절인 것 같다.

 

5) 클래스 설계 - 결론

지금 돌이켜보면 여러가지 시도를 한 것 자체는 너무 좋았지만, 그 과정이 시간적으로 너무 오래 걸렸고, 품도 많이 들어갔던 것 같다. 크지도 않은 프로젝트에 너무 과투자를 한 것 같다는 생각이 든다.

그리고 최종적으로 선택한 설계 구조가 딱히 가장 좋아보이지도 않았다. 지금의 나라면 세 번째 구조를 선택하고, 스톤 이름과 URI 에 대한 정보는 따로 Enum이나 상수만 가지고 있는 인터페이스를 사용해서 관리했을 것 같다. 수동 빈 등록을 할 필요도 없고, 스톤 정보를 가지고 있는 객체를 따로 구현해서 DI 받을 이유는 없는 것 같다.

적절한 시간과 노력을 통해 가장 적절한 결론을 얻어내는 것도 개발자의 중요한 능력 중 하나인 것 같다. 무조건 시간과 노력을 쏟아붓는다고 해서 좋은 결과가 나오지는 않는 것 같다.

 

3. 실제 서비스를 한 후기

말이 실제 서비스지 배포도 그냥 내 AWS ec2 서버에 대충 올리고, 실제로 내 프로그램이 동작한 시간은 2시간 남짓이다. 그래도 생각보다 아무런 문제 없이 잘 동작했고, 내가 필요로 해서 구현했던 기능이 제 역할을 잘 해줘서 수련회가 잘 마무리될 수 있었다. 나름 뿌듯했다 ㅎㅎ

 

1) 실시간 미션 진행 상황 로깅 기능의 캐리

인증샷 미션을 제외한 대부분의 미션이 문제를 풀어서 암호를 알아내는 방식이었다. 따라서 참가자들이 문제 풀이 방향을 잘못 잡게 되면, 정답과는 전혀 관련 없는 엉뚱한 답을 계속 입력하는 상황이 벌어질 수가 있다. 혹은 표기법 등의 차이 때문에 문제는 풀었는데 철자가 틀려서 진행을 못하게 되는 경우도 발생할 수 있다. 두 경우 모두 참가자들에게 안 좋은 경험을 주고, 미션 진행에 대한 동기를 떨어트린다.

이런 이슈들은 사실 예방하기가 어렵다. 참가자들의 뇌를 하나하나 들여다보고 미리 변수를 예상해서 차단할 수는 없는 노릇이기 때문이다.

그래서 실시간 미션 진행 상황을 슬랙 메시지로 보내는 기능을 구현하기로 했다. 이는 단순히 모니터링을 위해서이기도 하지만, 참가자들이 틀린 암호를 입력하면 해당 암호가 메시지와 함께 전송되는 기능도 같이 구현해서, 참가자들이 왜 해당 미션에서 막혔는지 직접적인 개입 없이 추측이 가능하게끔 했다.

이를 통해서 참가자들이 특정 미션에서 막히면, 불쾌감을 느끼기 전에 미리 연락을 취해서 적절한 도움을 주는 것이다. 그러면 참가자들은 미션의 컨셉 (스톤 수호자들의 감시를 받고 있다던가 하는 그런 ㅎㅎ..) 에도 더 몰입할 수 있게 되고, 불쾌감도 덜 느끼면서 적절한 난이도의 미션이라고 느낄 수 있게 된다.

이 기능을 통해서 오타 때문에 계속 재시도를 하던 팀의 미션 진행을 도울 수 있었고, 미션 이해를 잘못해서 틀린 답을 입력하던 팀도 도울 수 있었다.

 

2) DB의 부재, 갑작스러운 미션 내용 변경

이 프로젝트에서는 DB를 사용하지 않았다. 이유는 단순하다. 쓸 필요가 없었기 때문이다.

만약의 만약 (수련회 참가자 중 한 명이 수련회 프로젝트 리포지토리를 알아내서 코드를 직접 뜯어보는 극악의 확률을 가진 상황) 을 대비해서 미션 암호는 코드 상에 남겨두고 싶지 않았기 때문에 DB를 쓸까도 했지만, 그냥 서버에 파일 형태로 저장해두고 서버가 올라갈 때 해당 파일을 읽어와서 메모리에 저장해두면 되기 때문에 그렇게 구현했다.

그런데 미션 진행 도중에 생각지도 못한 변수가 생겼다. 당일날 특정 미션을 진행할 까페에 자리가 없어서 다른 까페로 장소를 옮겨야 하는 일이 벌어진 것이다. 해당 까페의 이름이 미션 암호였는데, 미션 암호는 메모리에 올라와 있어서 해당 암호를 수정하려면 파일 내용을 수정하고 WAS를 재시작해야 하는 상황이었다.

다행히도 모든 참가자가 첫 번째 미션 장소로 이동중이어서, 서버에 아무런 요청도 없었기 때문에 파일 내용을 수정하고 WAS를 재빨리 재부팅 하는 데 성공했긴 했다.

하지만 이게 수련회가 아닌 실제 서비스였다면? 서버를 통채로 리부트하는 상황은 절대로 일어나서는 안될 것이다.

내 프로젝트에서는 DB가 필요 없어서 사용하지 않았지만, 위와 같은 변수를 겪고 난 후 DB를 사용했다면 이런 일이 일어나지 않았을 것이라는 생각이 들었다. 미션 암호를 DB에 저장되어 있는 값을 읽어서 비교하는 방식으로 구현했더라면, 쿼리 하나로 암호를 변경하는 작업을 끝낼 수 있었을 것이다. 물론 서버를 재부팅 하는 일도 없었을 테고.

아무튼 이렇게 단순한 서비스에도 변수가 생기는데, 실제 비즈니스 서비스에서는 얼마나 변수가 많을까 하는 생각이 들었던 시간이었다.

 

4. 마치며

수련회까지 다 마치고 회고록을 쓰면서 든 생각은, 개발 기간이 쓸데없이 길었다는 것이다.

수련회 준비를 일찍 시작해서 개발도 일찍 시작했는데, 그래서 그런지 설계도 여러번 갈아 엎었고, 개발 속도도 매우 느렸다.

물론 그 과정에서 배우는 게 많았지만, 실제 서비스에는 마감이 존재하고, 개발 일정이라는 것이 존재한다는 것을 생각하면 좋지 않은 개발 방식이 아니었나 싶다.

마감을 정하고, 그 안에 최적의 결과를 내는 방식으로 개발을 해 나가는 습관을 들여야 더 좋은 개발자가 될 수 있을 것 같다.

 

5. 이건 더 공부해야겠다

먼저 DB. 토이 프로젝트에 DB를 쓴 적이 없어서 (다음 프로젝트에는 적용할 예정) DB를 구축하고 사용하는 연습을 많이 해야할 것 같다. 이런 간단한 프로젝트에는 DB를 그냥 바로 바로 붙일 수 있을 정도는 되어야 하지 않을까? 싶다.

또 암호화. 미션 암호 같은 건 실제 서비스에서는 아마도 암호화가 되어야 할 내용일 것이다. 내 프로젝트에서는 말이 암호지 그냥 평문처럼 관리해도 되는 값이지만, 실제 서비스에서 암호는 정말 중요하게 관리되어야 하는 정보이기 때문이다. 현재는 암호화를 실제로 적용하는 방법에 대해서 아는 게 거의 없어서 공부가 더 필요하다.

마지막으로 디자인 패턴. 다양한 디자인 패턴을 알고 적용하는 것도 중요하다고 생각이 든다. 프로젝트 규모가 더 커지면 더 체감이 될 것 같다.

댓글