단축 Url 관리 애플리케이션 만들어보기 1

송은석·2023년 2월 9일
0
post-thumbnail

단축 URL 생성 및 조회 서비스(like bitly)를 만들어보고, 그 과정을 기록해보았습니다. (아직 완성 아님!)
프로젝트 GitHub 주소


어떤 프로젝트인가?

<Bitly 사이트 페이지>

https://bitly.com 에서 제공하는 기능 중 아래의 3가지를 제공할 수 있는 스프링 애플리케이션을 만들어보는 미니 프로젝트를 진행해보았습니다.

1) Url을 받아 이를 단축한 Url을 제공하는 기능
2) 단축된 Url로 검색 시 기존 Url로 리다이렉트하는 기능
3) 각 단축된 URL이 검색된 횟수를 저장하여, Url별 조회 수 조회하는 기능

기본 환경은 아래와 같습니다.

  • Java 17
  • Spring Boot 3.0.2
  • Gradle
  • Windows OS, IntelliJ

그리고 기본적인 Spring MVC 구조를 사용했습니다. DB는 사용하지 않았고, ArrayList에 데이터를 저장했습니다. 시간은 3일 정도 소요되었습니다.


코드 작성 시 주의한 부분

1) 기능 구현

기능이 잘 작동하는 것이 최우선 순위가 된다고 생각했습니다. (여기서 기능은 로버트 C. 마틴이 클린 아키텍처에서 언급한 행위라고 말한 부분과 비슷하다고 생각했습니다.) 작동하지 않으면 설계고 뭐고 없는 것이기 때문에..

그래서 처음에 Url을 단축 Url로 변환해주는 모듈을 만들 때 생각보다 많은 시간이 들었습니다. Side Effect가 없는 함수적이고 기능적인 코드가 단축 Url로 변환하는 과정에서 필요하다고 생각했기 때문입니다. 이는 또한 변환된 Url을 통한 기존 Url Redirect에 대한 요청이 발생했을 경우를 함께 고려해야만 했습니다.

처음에는 기존 Url에 해시 함수를 사용해서 단축 Url을 만드는 것을 고려했으나, 최종적으로는 기존 Url을 저장하고 반환 받은 Index를 Base64에서 영문 대소문자, 숫자만 가져온 Base62를 통해 인코딩하는 방식을 사용하게 되었습니다.

<해시 충돌 처리에 대한 오버 헤드가 염려 되었음>

이렇게 한 이유를 간단히 설명하자면, 해시 함수를 사용할 경우 해시 충돌 방안에 대한 코드를 추가해야 하므로 비교적 많은 오버헤드가 야기될 것이라고 생각했기 때문입니다.

반면 Base62를 활용한 인코딩 방식은 해시 충돌이 발생하지 않으므로 오버헤드가 적고, 로직이 단순하기 때문에 코드 작성 및 수정에 용이할 것이라고 생각했습니다.

<Base62를 통해 작성한 Index to Base62 인코딩 방식>

한편 각 모듈들도 기능적으로 문제가 없게 끔 하려고 노력했습니다. 이는 먼저 각 모듈의 책임과 역할을 정의하는 것에서 시작됩니다. 바로 아래에서 이어 다루어보겠습니다.


2) 객체 간 책임과 역할의 정의 및 분리

클린 아키텍처에서 말한 변화(액터나 여러 상황에 의해 변할 수 있는)에 대한 관점이 적용될 수 있다고 생각했습니다. 각 객체는 책임과 역할이 분리되어야 하고, 이는 한 객체의 변화가 다른 객체에게 영향을 미쳐서는 안된다는 말입니다.

결합도를 적게 만드는 것에 관한 이야기라고도 할 수 있고, 더 넓게 보면 객체 지향적으로 코드를 작성한다는 말일 수도 있겠습니다. 각 레이어가 높은 응집도와 낮은 결합도를 갖도록 하고 싶었습니다.

구체적으로는

1) 레포지토리 레이어에서 인터페이스를 사용했고, 이를 통해 서비스 레이어와의 의존 관계를 만들어 주었습니다. 물론 구현 클래스를 통해 구체적인 로직이 표현되었습니다.

2) 컨트롤러에서 파라미터를 받을 때, 필요 시 각 메서드 별로 dto를 만들어 주었습니다. 하나의 dto를 여러 메서드에서 함께 사용할 수도 있지만, 이는 메서드 간의 결합도를 높일 수 있다고 생각했기 때문입니다.

3) 각 레이어 객체의 메서드 로직이 성공하지 못하면(정의된 행위가 성공하지 못하면) 호출 메서드를 향해 예외를 던지도록 했습니다. 그러나 이는 컨트롤러 단에서는 동일하게 시행되지 못했습니다. 이를 아래에서 다루어 보겠습니다.

놓쳤던 부분들

1) 예외 처리 (특별히 컨트롤러에서🤣)

처음에는 각 레이어의 책임 분리가 명확해야 한다고 생각했고, 따라서 서비스와 레포지토리에서는 로직이 처리되지 못할 경우(책임이 완료되지 못할 경우), 호출 메서드를 향해 예외를 던지도록 했습니다.

그러나 이 방식이 일관성 있게 사용되지는 않았습니다😂

서비스와 레포지토리까지는 예외를 던져도 받아서 처리할 수 있는 상위 영역이 있었으나(레포지토리는 서비스, 서비스는 컨트롤러), 컨트롤러 단에서 예외를 던질 경우, 제어할 수 없는 영역(당시 판단하기로는 Application 전역)으로 예외가 던져져 클라이언트의 요청에 대해 응답을 하지 못한 채로 해당 스레드가 종료될 수도 있겠다고 생각했습니다. 그래서 컨트롤러에는 예외가 전파되지 않아야 하겠다고 생각했고, 이는 곧 최소한 서비스 레이어에서 정제된 반환 값을 컨트롤러에 전달함으로써 가능할 것이라고 생각했습니다.

<서비스 단의 코드 중 예외 발생 시, null의 값을 반환합니다>

<컨트롤러는 null이 반환될 때, 예외가 발생했다는 것을 알고 있습니다>

그래서 그렇게 구현을 했는데, 이렇게 할 경우 예외 발생 시의 반환 값을 서비스 레이어에서 최종적으로 정의해야 했고, 컨트롤러 또한 서비스 레이어에서 지정한 예외에 대한 반환 값이 무엇인지에 대해서 알고 있어야만 했습니다. 이는 OCP에도 맞지 않고, 레이어 간 높은 결합도가 생기게 하는 (지금 생각했을 때) 잘못된 방식이었습니다.

멘토님께서 이 부분을 지적해주셨고, 저 또한 명확하게 풀지 못했던 부분이 짚어졌다는 생각이 들었습니다. 그래서 어떻게 하면 컨트롤러에도 예외가 전파되면서 클라이언트에 대한 응답이 문제 없이 이루어질 수 있을까를 고민하게 되었습니다. 그러다 “스프링” “컨트롤러” “예외” 키워드들로 검색해보니 @ControllerAdvice, @ExceptionHandler라는 아주 좋은 방식들이 있다는 것을 알게 되었습니다ㅎ

2) Java 8 문법 (Optional, Stream)

Stream을 제대로 써본 적이 없었기 때문에, 사용이 서툴렀습니다. 예를 들어 Repository에서 데이터의 중복 여부를 확인하기 위해 데이터의 존재 유무를 검색할 때, 리턴 값을 boolean 타입으로 받아 if 절을 통해 분기를 나누어 예외를 던지는 방식을 사용했었습니다.

<기존의 스트림 사용 방식>

그런데 이 때 Stream을 사용하고 있었기 때문에 사실 Optional을 반환 받아 사용하면 if 분기를 사용하지 않아도 됐었습니다. 지금은 수정해서 사용하고 있습니다ㅎ

<Optional 사용 시, 훨씬 깔끔한 코드가 됩니다>


3) 필요하지 않았던 도메인 → ViewUrl

구현하고자 한 애플리케이션의 핵심 기능은 위에서 언급한 것처럼 3가지였습니다.

  • Url을 받아 이를 단축한 Url을 제공할 것
  • 단축된 Url로 검색 시 기존 Url로 리다이렉트 해줄 것
  • 각 단축된 URL이 검색된 횟수를 저장하여, Url별로 이를 보여줄 것

이 때 Url을 하나의 도메인으로 만들었고(이 표현이 맞는지 잘 모르겠습니다. Entity라고 하는 것이 더 맞는 표현일까요?), 추가로 각 Url 검색 횟수를 저장하기 위한 도메인을 추가로 만들어 보았습니다. 그리고 ViewUrl이라는 이름을 붙였습니다.

이를 만든 나름대로의 이유는 Url이라는 도메인은 기존 Url과 단축 Url을 함께 담고 있는 것이었고, 따라서 Url에 대한 조회수를 담고 있는 내용을 만들기 위해서는 분리되어 저장되는 것이 맞다고 생각했기 때문입니다.

한편, 이는 로직을 더 복잡하게 만들기도 했습니다. 분리된 도메인에 대한 별도의 코드들이 추가되어야 하기 때문입니다. 그러나 역할적인 면에서, 그리고 애플리케이션이 확장되었을 때를 대비하기 위해 이러한 복잡성은 감수해야 하는 부분이라고 생각했고, 그렇게 코드를 작성했습니다.

그러나 정말 그러한지에 대해서는 조금 더 생각해볼 필요가 있었습니다. 확실히 판단하기 위해서는 ViewUrl이 분리 될 만큼의 의미를 담고 있는지를 판단해봐야 한다고 생각했습니다. 그래서 그 분리의 기준을 나름대로 정리해보았습니다.

1) 추가되는 코드의 복잡성을 가지고 갈만큼 의미를 담고 있는 단위인지

2) 기존 도메인에 더해서는 안될 내용인지

그러나 명확하고 확실한 분리의 기준에 대한 고민은 여전히 남아 있습니다.

앞으로 할 일들

1) 예외 처리

  • 각 레이어의 예외 처리가 적절한지를 “토비의 스프링3.1 - 4장 예외”의 내용을 통해 다시 한 번 점검해보고, 재구성해봅니다.
  • 컨트롤러에서 예외가 명확히 다루어지면서도 클라이언트에 대한 응답이라는 책임이 다해질 수 있도록 @ControllerAdvice, @ExceptionHandler를 사용해봅니다.

2) Stream과 Optional 문법

  • “모던 자바 인 액션”의 관련 섹션을 통해 학습해보고, 코드에 적용해 봅니다.

3) 도메인 분리에 대한 고민

  • 다른 사람들과 함께 논의해 보려고 합니다.

정리

오랜만의 코드 작성이라 헤매기도 했지만, 그동안 공부했던 내용들(자바, 스프링, 객체지향)을 조금이나마 실제로 적용해볼 수 있어 즐거운 시간이었습니다.

위에서 언급한 부분들을 보완하여, 다음에는 더 업그레이드된 프로젝트의 내용을 가지고 돌아오겠습니다. To be continue!


참고

해시 충돌 이미지, https://ko.wikipedia.org/wiki/해시_함수
profile
Done is better than perfect🔥

0개의 댓글