[프로젝트 회고] 단축 URL 서비스 Shortify

김선호(해로)·2023년 5월 4일
0

프로젝트 회고

목록 보기
1/1
post-thumbnail

들어가며

국비 수업을 마친지 약 6주의 시간이 흘렀다. 그동안 이력서 준비와 기존에 진행했던 프로젝트의 고도화를 진행하며 나날을 보냈다. 몇 건의 서류 탈락을 겪으며 약간 자신감을 잃긴 했지만 공부를 멈추고 싶지는 않았다. 나는 프로젝트를 기반으로 학습하는 것을 좋아하므로, 새 프로젝트를 시작하기로 했다.

프로젝트 주제는 URL 단축 서비스로 정했다. 이력서에 하이퍼링크가 그대로 드러나는 지원 플랫폼이 있어 구글링하여 찾은 임의의 URL 단축 서비스를 이용했다. 그런데 이게 웬 걸, 일주일이 지나니까 해당 URL이 만료되어버린 것이다. 이력서를 검토하는데 링크가 제대로 동작하지 않는다면 아무래도 담당자님께 좋은 인상을 주기는 어려울 것이다. (실제로 이미 이력서는 제출했는데 URL이 만료되어버린 경우가 있었다. 결과는 불 보듯 뻔했다.)

이왕 이렇게 된 것 bit.ly 같은 URL 단축서비스를 직접 만들어봐야겠다는 생각이 들었고, 동시에 국비 과정에서 동기들과 같이 공부하며 알게 된 OOP, Spring 지식을 새 프로젝트에 녹여보고 싶었다. 타이밍 좋게 평소 관심있던 넘블에서 원했던 주제의 챌린지가 열려 챌린지와 프로젝트를 같이 진행하게 되었다.

(넘블 프로젝트 소개 링크)

챌린지 참여 자체가 유료라서 더 강제성이 생길 것 같았고, 좋은 평가를 받게되면 현업자 분께서 내 코드도 살펴봐주신다고 하니 참여하지 않을 이유가 없었다.


구현 내용

BASE62 인코딩을 이용한 단축 URL 생성

public class Base62Encoder {

	private static final int BASE = 62;
	private static final String CODEC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

	private Base62Encoder() {
	}

	public static String encode(long target) {
		StringBuilder encoded = new StringBuilder();

		do {
			int mod = Math.toIntExact(target % BASE);
			encoded.insert(0, CODEC.charAt(mod));
			target /= BASE;
		} while (target != 0);

		return encoded.toString();
	}
}

(소스 코드)

단축 URL 서비스의 핵심은 URL을 얼마나 단축시키는가에 달렸다. 그러면서도 단축된 URL은 중복이 되지않게 하여 유저가 의도했던 원본 URL로 리다이렉트 시켜주는 것이 중요했다.

서로 다른 URL에 대해서 서로 다른 단축 URL, 즉 원본 URL가 단축된 URL의 관계가 일대일 대응이 되도록 단축 시킬 방법이 필요했고, PK 값을 기준으로 BASE62 인코딩하는 방식을 택했다.

URL을 단축시키기 위한 방법으로는 난수를 사용하거나 해싱 등의 방법이 있겠지만 이들은 근본적으로 일대일대응(물론 극히 드문 경우이기는 하다.)을 보장할 수 없다는 점에서 제외하였다.

반면 BASE64, BASE62 등 진법 변환의 경우 input 이 서로 다른 경우에 output이 서로 다름을 보장할 수 있으므로 진법 변환을 통해 인코딩하는 것으로 마음을 굳혔다. BASE64BASE62 중에서는 BASE62가 URL-SAFE 하다는 특징을 살려 BASE62를 택하게 되었고, 일반적으로 많이 사용되는 진법 변환 코드를 참고하며 인코딩하는 메소드를 구현하게 되었다.


SRP를 준수하는 클래스 설계

(클래스 다이어그램)

전체 클래스에 대한 설계는 많이 사용되는 레이어드 아키텍쳐를 기준으로 설계했다. Presentation Layer to Domain 방향으로 의존하는 것에 신경썼으며 역방향으로 의존성이 생기지 않도록 주의했다.

하나의 엔드포인트를 갖는 컨트롤러 객체

하나의 컨트롤러는 하나의 URI만 담당하도록 했다. SRP를 지킬 수 있을 뿐만 아니라, 어느 엔드포인트가 어느 클래스에 있는지 힘들게 찾아볼 필요가 없이 편리했다.

단 하나의 메소드를 제공하는 서비스 객체

필요한 기능이 있을 때 객체를 정의하고, 객체의 역할은 그 객체의 public 메소드를 통해서 명확히 드러낼 수 있다고 생각한다. 따라서 모든 클래스가 하나의 public 메소드를 갖게 하고 필요하면 추가적으로 private 메소드를 만들어 public 메소드가 갖는 책임을 완수하게끔 하는 방식을 선호한다.

이번 프로젝트에서 서비스 객체들을 정의할 때 이같은 방식을 적용했다. Data Read 를 담당하는 Reader 객체는 여러 객체들에게 메시지를 받아야하므로 어쩔 수 없이 public 메소드를 여러개 열어두게 되었지만, 그를 제외한 모든 서비스 객체는 단 하나의 public 메소드를 갖도록 설계했다. 또한 Reader 객체는 오로지 Data Read만 하는 역할을 하게 되므로 다른 서비스 객체를 의존할 필요도 없었고 자연스레 Bean의 순환참조 문제도 피할 수 있었다.

이렇게 하니 이전에 경우 하나의 Service 클래스를 만들고 모든 기능을 집어 넣어 생기는 문제(필요한 기능을 찾는데 너무나 오랜 시간이 걸리는 문제)를 해결할 수 있었고, 컨트롤러 객체도 필요한 서비스 객체만 주입받으면 되니까 유연하게 코드를 작성할 수 있었다.


마치며

소감

일주일간의 짧은 경험으로

  • @Transactional 사용시 프록시 내부 호출 문제
  • 조회수를 기록하기 위한 스키마 설계
  • SRP가 가져다주는 편리함

등을 경험할 수 있던 재밌는 프로젝트였다. 추가할 수 있는 기능도 많다보니 프로젝트를 계속 이어갈 것이다.

Top2 선정!!

넘블에 프로젝트 제출을 마치고, 심사 결과가 나왔다.
부족한 결과물이지만 좋게 봐주신 덕분에 감사하게도 멘토님께서 Top2에 선정해주셨다!! 온라인으로 내 코드에 대한 피드백도 진행해주신다고 한다고 하니, 이 내용을 바탕으로 리팩토링을 해봐야겠다.


향후 계획

현재 인증/인가 부분이나 스케쥴러를 활용한 URL 만료 처리 등은 구현되지 않은 상태라, 앞으로 이러한 부분을 보완해나갈 생각이다. 추가적으로도 타임리프를 이용하여 프론트단도 구현하여 bitly 같은 서비스를 만들어 배포까지 해볼 생각이다.

추가적으로 공부해 볼 것

  • 스프링 스케쥴러
  • 인증/인가

※ Reference

profile
Every Run, Learn Counts.

0개의 댓글