Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4기 황창현] URL Shortener 과제 제출 #40

Open
wants to merge 28 commits into
base: changhyeonh
Choose a base branch
from

Conversation

Hchanghyeon
Copy link
Member

@Hchanghyeon Hchanghyeon commented Oct 3, 2023

📌 과제 설명

  • 20글자 이상인 URL을 입력받아 Base62로 변환하여 단축된 URL을 제공하는 서비스입니다.
  • 프론트는 Thymeleaf를 사용하였고 SPA로 구현하고싶어서 @Controller가 아닌 @RestController를 사용하여 구현했습니다.
  • 현재 구현된 알고리즘은 Base62로 다른 알고리즘을 더 찾아보고 적합한게 있으면 추가할 예정입니다.

👩‍💻 요구 사항과 구현 내용

요구사항

  • URL 입력폼 제공 및 결과 출력
  • URL Shortening Key는 8 Character 이내로 생성
  • 단축된 URL 요청시 원래 URL로 리다이렉트
  • 단축된 URL에 대한 요청 수 정보저장
  • Shortening Key를 생성하는 알고리즘 2개 이상 제공하며 애플리케이션 실행 중 동적으로 변경 가능

구현 내용

shortenerURL_1

  • URL 입력폼 제공
  • 20글자 이상의 URL만 입력받도록 예외 처리
  • Pattern을 이용하여 URL 형식이 아닌 URL들은 입력할 수 없게 설정
  • URL 입력시 URL Shortening Key 결과 출력(8글자 이내)
  • 로직 수행시 사용자의 IP 정보 수집
    • 단축 URL의 경우 위험한 사이트들을 단축하여 뿌릴 수 있기에 조금이라도 해당 문제들을 방지하고 빠른 조치를 위해 IP를 수집
  • 단축된 URL 요청시 원래 URL로 리다이렉트
    • 단축된 URL의 경우 동시 접속 가능성이 높다고 판단되어 비관적 락을 적용하여 조회수 증가 구현

shortenerURL_2

  • 단축된 URL Shortening Key 입력시 단축된 URL에 대한 정보 출력

✅ PR 포인트 & 궁금한 점

  1. Base62의 경우 10진수를 Input 값으로 받아서 처리하기 때문에 보통 Database에 들어가는 ID값을 기준으로 Shortening Key를 만드는 것으로 알고 있습니다. 하지만 ID값 전략을 AUTO_INCREMENT로 설정하는 경우 Save하기 전까지는 DB 컬럼인 ID값이 생성되지 않아 Save 후 받아온 객체의 ID를 기준으로 Shortening Key를 만들고 업데이트 하는 방식을 채택했습니다. 이렇게 하는 경우 쿼리가 2번 날라가서 효율적이지 못하다고 생각하는데 어쩔 수 없는 것 일까요?
    • ID값 전략을 Sequence와 Table로 해보았을 때 Save하기 전 마지막 ID값을 불러와서 Shortening Key를 만들고 Save할 수 있었으나 테이블의 마지막 ID값을 가져오는 쿼리도 날려야해서 2개 쿼리가 날라가는 것은 마찬가지였고 오히려 로직이 더 복잡해지는 느낌이 들었습니다.

@Hchanghyeon Hchanghyeon self-assigned this Oct 3, 2023
@Hchanghyeon Hchanghyeon marked this pull request as ready for review October 3, 2023 15:22
Comment on lines 68 to 70
this.shortUrl = switch (algorithm) {
case BASE62 -> Base62Converter.encode(id.intValue());
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자의 입력 값으로 받는 알고리즘 종류에 따라 switch문으로 인코딩 된 값을 반환하는 것으로 했는데 괜찮은 로직인지 궁금합니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum 값에 인코딩 함수(람다)를 넣어주면 분기문 없이 해결할 수 있을것 같네요~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 말씀하신 방법을 처음 알았습니다! Enum 활용에 대해서 조금 더 공부해보아야겠네요.

말씀하신 코드가 아래와 같은 방식이 맞나요?

public enum Algorithm {
    BASE62 {
        public String getShortUrl(int num) {
            return Base62Converter.encode(num);
        }
    }
    public abstract String getShortUrl(int num);
}

// Url.class
public void convertToShortUrl() {
    if (id == null) {
        throw new UrlException(URL_NOT_SAVED);
    }

    this.shortUrl = algorithm.getShortUrl(id.intValue());
}

반영: 1ced81b

Comment on lines 14 to 15
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Url> findWithPessimisticLockByShortUrl(String shortUrl);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단축 URL 접속의 경우 동시성 문제가 발생할 가능성이 높아 비관적 락을 적용하여 조회수를 증가할 수 있도록 했습니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 부분 고려한 점 좋네요 👍
다만 조회수를 위해 성능을 포기하는 느낌인데, 과연 조회수가 그만큼의 가치를 가지는 값인지는 생각해볼만 한것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 고려해보았을 때 단축 URL 서비스의 경우 실제로 단축된 URL로 접속하는 것이 목적이라 생각이 들고 조회수가 정말로 중요한 가치를 지니지 않은 것 같아서 삭제 처리했습니다!

감사합니다!!!

반영: 55b4a31

import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Base62Converter {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원본 URL의 값을 DB에 저장하기 때문에 굳이 디코딩할 필요가 없다고 생각되어 만들지 않았습니다.

Copy link

@hanjo8813 hanjo8813 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

창현님 수고하셨습니다 👍

Base62의 경우 10진수를 Input 값으로 받아서 처리하기 때문에 보통 Database에 들어가는 ID값을 기준으로 Shortening Key를 만드는 것으로 알고 있습니다. 하지만 ID값 전략을 AUTO_INCREMENT로 설정하는 경우 Save하기 전까지는 DB 컬럼인 ID값이 생성되지 않아 Save 후 받아온 객체의 ID를 기준으로 Shortening Key를 만들고 업데이트 하는 방식을 채택했습니다. 이렇게 하는 경우 쿼리가 2번 날라가서 효율적이지 못하다고 생각하는데 어쩔 수 없는 것 일까요?
ID값 전략을 Sequence와 Table로 해보았을 때 Save하기 전 마지막 ID값을 불러와서 Shortening Key를 만들고 Save할 수 있었으나 테이블의 마지막 ID값을 가져오는 쿼리도 날려야해서 2개 쿼리가 날라가는 것은 마찬가지였고 오히려 로직이 더 복잡해지는 느낌이 들었습니다.

pk id를 이용해서 단축 url을 만드는 순간 쿼리 두번 날려야하는건 어쩔 수 없는것 같습니다. ㅜㅜ
하지만 가벼운 쿼리이기 때문에 성능상 큰 문제가 없을것 같고,
다른 방식을 사용하더라도 unique한 값을 외부 어딘가에 저장해야하는데, 관리 포인트가 늘어나기 때문에 비효율적인것 같네요

Comment on lines +20 to +24
@GetMapping("/{shortUrl}")
public void redirectOriginUrl(@PathVariable String shortUrl, HttpServletResponse response) throws IOException {
String originalUrl = urlService.findOriginalUrlByShortUrl(shortUrl);
response.sendRedirect(originalUrl);
}
Copy link

@hanjo8813 hanjo8813 Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요렇게 하면 http status code도 바뀌나요?? (모름)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네! 저도 직접 설정해줘야하나 싶었는데 302 Redirect 정상적으로 바뀌더라구요!!! 👍

Comment on lines +41 to +42
@Column(nullable = false)
private String originalUrl;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unique 설정이 필요할것 같아요~~
unique 제약조건 걸었을때의 index에 대해서도 알아보세요!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 url 길이 제한이 20인것 같으니 길이 설정도 해주면 좋겠죠?!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 재원님께서 말씀하신 것 처럼 unique 설정을 해서 동일한 originalURL로는 만들 수 없게 하려했으나 다른 단축 서비스들을 보니 똑같은 originalURL에 대해서 만들 수 있게 되어있더라구요! 그래서 originalURL에는 따로 unique를 걸지 않았습니다!

근데 생각해보니 shortURL은 단축된 URL이니 겹치면 안될 것 같네요! 아무리 id값으로 변환이 된다고해도 혹시 모를 위험을 위해 shortURL에 unique를 걸어두도록 하겠습니다!

Unique는 Real MySQL에서도 봤던 내용이었는데 조금 가물가물해서 다시 찾아보았습니다!

유니크 인덱스와 유니크하지 않은 일반 보조 인덱스는 사실 크게 다르지 않고 읽기는 성능이 비슷하지만 쓰기 성능에서는 유니크 한지 아닌지 체크하는 과정이 필요하여 보조 인덱스보다 더 느리다고 하네요.

사실 현재 이 프로젝트에서는 쓰기의 성능보다는 읽기의 성능이 더 중요하긴 하지만 쓰기의 양도 적은 편은 아니라고 생각합니다. 그래서 어차피 shortURL의 경우 고유한 ID값으로 변환되어 저장되니 고유하다고 가정하고 unique를 걸지 않을 수도 있을 것 같네요! 하지만 이런 방법은 안전한 방법은 아닌 것 같아서 조금 느리더라도 unique를 걸어서 처리하겠습니다!

반영: dd539b5

private LocalDateTime createdAt;

@Column(nullable = false)
private Long count;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

딱 봤을 때 어떤 count인지 알기가 어렵네요
view나 viewCount와 같이 명확한 네이밍을 써주면 좋을것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 비지니스 로직이 단순해서 하나 밖에 없다고 생각하고 했었는데 지금보니 정말로,, 명확하지 않은 것 같습니다. 피드백 감사드립니다!! 🙇🙇

반영: de6991e

Comment on lines 68 to 70
this.shortUrl = switch (algorithm) {
case BASE62 -> Base62Converter.encode(id.intValue());
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum 값에 인코딩 함수(람다)를 넣어주면 분기문 없이 해결할 수 있을것 같네요~

Comment on lines 77 to 80
@PrePersist
public void prePersist() {
this.count = 0L;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 컬럼의 경우 jpa의 영속성 컨텍스트 생명주기를 활용하는것보다 컬럼 default 값을 지정해주는게 더 명확할 것 같습니다.
(default값이 절대 변하지 않을것 같은 경우)

관련해서 영주님 엔티티를 참고해보세요!
이것도 트러블 슈팅이 있긴 합니다 ㅋㅋ
https://github.com/kylekim2123/url-shortener-back/pull/13/files#diff-dc7cc552e2ff3b2310c98b4ce6916768c03c6cf3e4feca558c57c7d0be4b0510

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 default값으로 지정했었는데 계속 null로 처리되서 뭐가 문제일까하다가 생명주기 활용해서 처리했었는데 @DynamicInsert @DynamicUpdate라는 것이 있었군요..?! 이것도 모르고 계속 null로 나와서 오래 헤맸었는데 그래도 이렇게 알게되어 너무너무 기쁩니다.

근데 생명주기로 처리했을 때는 영속성 컨텍스트에 0이 반영되어 response에 viewCount가 0으로 반환되어 나갈 수 있지만 위와 같이 어노테이션을 이용해서 처리할 경우 반환 값을 0으로 처리할 수는 없는 것 같네요 ㅠㅠ

반영: 9fd79f2

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 경우 그냥 long 타입을 쓰면 모든 문제가 해결될것 같긴하네요~~!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 진짜 그러네요! 감사합니다 ㅎㅎ

Comment on lines 30 to 31
shortUrlCreateRequest.updateIp(httpServletRequest.getRemoteAddr());
UrlResponse urlResponse = urlService.createShortUrl(shortUrlCreateRequest);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request dto에 굳이 추가하지 않고 service 파라미터에 ip를 추가해주는건 어떨까요?
ip는 사용자가 직접 입력하는 값이 아니라서 dto 내에 포함되니 조금 어색한것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 클라이언트가 직접 입력하는 값이 아니라서 어색한 것 같습니다! 서비스 파라미터에 값을 추가하고 toEntity로 변경될 때 매개변수로 넣어서 엔티티 변환할 수 있도록 처리했습니다!

감사합니다!!

반영: c24d8ca

import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response dto에는 요게 필요할까요??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역직렬화, 직렬화 매번 헷갈려서 위와 같이 적었던 것 같습니다! 역직렬화시 생성자와, Getter 직렬화시 Getter!!!! 👍👍

반영: 4e45131

Comment on lines 14 to 15
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Url> findWithPessimisticLockByShortUrl(String shortUrl);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 부분 고려한 점 좋네요 👍
다만 조회수를 위해 성능을 포기하는 느낌인데, 과연 조회수가 그만큼의 가치를 가지는 값인지는 생각해볼만 한것 같습니다.

Copy link
Member Author

@Hchanghyeon Hchanghyeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재원님~! 이것 저것 다른 것들 공부하느라 피드백 반영이 조금 느렸네요 ㅠㅠ

피드백 주신거 모두 확인하고 반영했습니다! 한 번만 더 확인 부탁드립니다 🙏🙏

감사합니다!!

Comment on lines +20 to +24
@GetMapping("/{shortUrl}")
public void redirectOriginUrl(@PathVariable String shortUrl, HttpServletResponse response) throws IOException {
String originalUrl = urlService.findOriginalUrlByShortUrl(shortUrl);
response.sendRedirect(originalUrl);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네! 저도 직접 설정해줘야하나 싶었는데 302 Redirect 정상적으로 바뀌더라구요!!! 👍

Comment on lines 30 to 31
shortUrlCreateRequest.updateIp(httpServletRequest.getRemoteAddr());
UrlResponse urlResponse = urlService.createShortUrl(shortUrlCreateRequest);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 클라이언트가 직접 입력하는 값이 아니라서 어색한 것 같습니다! 서비스 파라미터에 값을 추가하고 toEntity로 변경될 때 매개변수로 넣어서 엔티티 변환할 수 있도록 처리했습니다!

감사합니다!!

반영: c24d8ca

Comment on lines +41 to +42
@Column(nullable = false)
private String originalUrl;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 재원님께서 말씀하신 것 처럼 unique 설정을 해서 동일한 originalURL로는 만들 수 없게 하려했으나 다른 단축 서비스들을 보니 똑같은 originalURL에 대해서 만들 수 있게 되어있더라구요! 그래서 originalURL에는 따로 unique를 걸지 않았습니다!

근데 생각해보니 shortURL은 단축된 URL이니 겹치면 안될 것 같네요! 아무리 id값으로 변환이 된다고해도 혹시 모를 위험을 위해 shortURL에 unique를 걸어두도록 하겠습니다!

Unique는 Real MySQL에서도 봤던 내용이었는데 조금 가물가물해서 다시 찾아보았습니다!

유니크 인덱스와 유니크하지 않은 일반 보조 인덱스는 사실 크게 다르지 않고 읽기는 성능이 비슷하지만 쓰기 성능에서는 유니크 한지 아닌지 체크하는 과정이 필요하여 보조 인덱스보다 더 느리다고 하네요.

사실 현재 이 프로젝트에서는 쓰기의 성능보다는 읽기의 성능이 더 중요하긴 하지만 쓰기의 양도 적은 편은 아니라고 생각합니다. 그래서 어차피 shortURL의 경우 고유한 ID값으로 변환되어 저장되니 고유하다고 가정하고 unique를 걸지 않을 수도 있을 것 같네요! 하지만 이런 방법은 안전한 방법은 아닌 것 같아서 조금 느리더라도 unique를 걸어서 처리하겠습니다!

반영: dd539b5

private LocalDateTime createdAt;

@Column(nullable = false)
private Long count;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 비지니스 로직이 단순해서 하나 밖에 없다고 생각하고 했었는데 지금보니 정말로,, 명확하지 않은 것 같습니다. 피드백 감사드립니다!! 🙇🙇

반영: de6991e

Comment on lines 68 to 70
this.shortUrl = switch (algorithm) {
case BASE62 -> Base62Converter.encode(id.intValue());
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 말씀하신 방법을 처음 알았습니다! Enum 활용에 대해서 조금 더 공부해보아야겠네요.

말씀하신 코드가 아래와 같은 방식이 맞나요?

public enum Algorithm {
    BASE62 {
        public String getShortUrl(int num) {
            return Base62Converter.encode(num);
        }
    }
    public abstract String getShortUrl(int num);
}

// Url.class
public void convertToShortUrl() {
    if (id == null) {
        throw new UrlException(URL_NOT_SAVED);
    }

    this.shortUrl = algorithm.getShortUrl(id.intValue());
}

반영: 1ced81b

import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역직렬화, 직렬화 매번 헷갈려서 위와 같이 적었던 것 같습니다! 역직렬화시 생성자와, Getter 직렬화시 Getter!!!! 👍👍

반영: 4e45131

Comment on lines 14 to 15
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Url> findWithPessimisticLockByShortUrl(String shortUrl);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 고려해보았을 때 단축 URL 서비스의 경우 실제로 단축된 URL로 접속하는 것이 목적이라 생각이 들고 조회수가 정말로 중요한 가치를 지니지 않은 것 같아서 삭제 처리했습니다!

감사합니다!!!

반영: 55b4a31

Comment on lines 77 to 80
@PrePersist
public void prePersist() {
this.count = 0L;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 default값으로 지정했었는데 계속 null로 처리되서 뭐가 문제일까하다가 생명주기 활용해서 처리했었는데 @DynamicInsert @DynamicUpdate라는 것이 있었군요..?! 이것도 모르고 계속 null로 나와서 오래 헤맸었는데 그래도 이렇게 알게되어 너무너무 기쁩니다.

근데 생명주기로 처리했을 때는 영속성 컨텍스트에 0이 반영되어 response에 viewCount가 0으로 반환되어 나갈 수 있지만 위와 같이 어노테이션을 이용해서 처리할 경우 반환 값을 0으로 처리할 수는 없는 것 같네요 ㅠㅠ

반영: 9fd79f2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants