-
Notifications
You must be signed in to change notification settings - Fork 106
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
base: changhyeonh
Are you sure you want to change the base?
Changes from all commits
e94b801
5219f38
443a96a
910b678
b2e7a79
ab6fdfe
6233638
3fde736
9a77c4f
5b17cb3
df14a51
0ff6b6b
74b2b14
8a92e3a
5210f20
1246112
14f5a61
3554631
b4828f1
88552d9
413974e
c24d8ca
dd539b5
de6991e
1ced81b
4e45131
55b4a31
9fd79f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.programmers.urlshortener.common.config; | ||
|
||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
@Configuration | ||
public class WebConfig implements WebMvcConfigurer { | ||
|
||
@Override | ||
public void addViewControllers(ViewControllerRegistry registry) { | ||
registry.addViewController("/").setViewName("index"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.programmers.urlshortener.common.converter; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
public class Base62Converter { | ||
|
||
private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | ||
private static final int BASE = ALPHABET.length(); | ||
|
||
public static String encode(int value) { | ||
StringBuilder sb = new StringBuilder(); | ||
|
||
while (value != 0) { | ||
sb.append(ALPHABET.charAt(value % BASE)); | ||
value /= BASE; | ||
} | ||
|
||
return sb.reverse().toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.programmers.urlshortener.common.exception; | ||
|
||
import org.springframework.http.HttpStatus; | ||
|
||
import lombok.Getter; | ||
|
||
@Getter | ||
public class BusinessException extends RuntimeException { | ||
|
||
private HttpStatus status; | ||
private String message; | ||
|
||
protected BusinessException(ExceptionRule exceptionRule) { | ||
super(exceptionRule.getMessage()); | ||
this.status = exceptionRule.getStatus(); | ||
this.message = exceptionRule.getMessage(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.programmers.urlshortener.common.exception; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class ErrorResponse { | ||
|
||
private int statusCode; | ||
private String message; | ||
|
||
@Builder | ||
private ErrorResponse(int statusCode, String message) { | ||
this.statusCode = statusCode; | ||
this.message = message; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.programmers.urlshortener.common.exception; | ||
|
||
import org.springframework.http.HttpStatus; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum ExceptionRule { | ||
|
||
SHORTENURL_NOT_EXIST(HttpStatus.NOT_FOUND, "해당 URL은 단축된 URL 목록에서 찾을 수 없습니다."), | ||
NOT_FOUND(HttpStatus.NOT_FOUND, "요청하신 URL은 없는 URL입니다."), | ||
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다. 입력 값을 다시 확인해주세요."), | ||
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에서 알 수 없는 오류가 발생했습니다. 관리자에게 문의해주세요."), | ||
URL_NOT_SAVED(HttpStatus.INTERNAL_SERVER_ERROR, "해당 URL은 데이터베이스에 저장된 URL이 아닙니다."), | ||
; | ||
|
||
private final HttpStatus status; | ||
private final String message; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package com.programmers.urlshortener.common.exception; | ||
|
||
import static com.programmers.urlshortener.common.exception.ExceptionRule.*; | ||
|
||
import java.util.List; | ||
|
||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.validation.FieldError; | ||
import org.springframework.web.bind.MethodArgumentNotValidException; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
@RestControllerAdvice | ||
public class GlobalExceptionHandler { | ||
|
||
@ExceptionHandler(MethodArgumentNotValidException.class) | ||
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { | ||
List<FieldError> errors = e.getBindingResult() | ||
.getAllErrors() | ||
.stream() | ||
.map(FieldError.class::cast) | ||
.toList(); | ||
|
||
log.error("[에러]: {}", e.getMessage(), e); | ||
|
||
errors.forEach( | ||
error -> log.error("메시지: {} / 원인: {} : {}", error.getDefaultMessage(), error.getField(), | ||
error.getRejectedValue())); | ||
|
||
ErrorResponse errorResponse = ErrorResponse.builder() | ||
.statusCode(BAD_REQUEST.getStatus().value()) | ||
.message(BAD_REQUEST.getMessage()) | ||
.build(); | ||
|
||
return ResponseEntity.status(BAD_REQUEST.getStatus()).body(errorResponse); | ||
} | ||
|
||
@ExceptionHandler(BusinessException.class) | ||
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) { | ||
log.error("[에러]: {}", e.getMessage(), e); | ||
|
||
ErrorResponse errorResponse = ErrorResponse.builder() | ||
.statusCode(e.getStatus().value()) | ||
.message(e.getMessage()) | ||
.build(); | ||
|
||
return ResponseEntity.status(e.getStatus()).body(errorResponse); | ||
} | ||
|
||
@ExceptionHandler(Exception.class) | ||
public ResponseEntity<ErrorResponse> handleException(Exception e) { | ||
log.error("[에러]: {}", e.getMessage(), e); | ||
|
||
ErrorResponse errorResponse = ErrorResponse.builder() | ||
.statusCode(INTERNAL_SERVER_ERROR.getStatus().value()) | ||
.message(INTERNAL_SERVER_ERROR.getMessage()) | ||
.build(); | ||
|
||
return ResponseEntity.status(INTERNAL_SERVER_ERROR.getStatus()).body(errorResponse); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.programmers.urlshortener.common.exception; | ||
|
||
public class UrlException extends BusinessException { | ||
|
||
public UrlException(ExceptionRule exceptionRule) { | ||
super(exceptionRule); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.programmers.urlshortener.common.validator; | ||
|
||
import static com.programmers.urlshortener.common.exception.ExceptionRule.*; | ||
|
||
import java.util.regex.Pattern; | ||
|
||
import com.programmers.urlshortener.common.exception.UrlException; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
public class UrlValidator { | ||
|
||
private static final Pattern URL_PATTERN = Pattern.compile( | ||
"https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)"); | ||
|
||
public static void validateUrlPattern(String url) { | ||
if (!URL_PATTERN.matcher(url).matches()) { | ||
throw new UrlException(BAD_REQUEST); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.programmers.urlshortener.url.controller; | ||
|
||
import java.io.IOException; | ||
|
||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import com.programmers.urlshortener.url.service.UrlService; | ||
|
||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
public class RedirectController { | ||
|
||
private final UrlService urlService; | ||
|
||
@GetMapping("/{shortUrl}") | ||
public void redirectOriginUrl(@PathVariable String shortUrl, HttpServletResponse response) throws IOException { | ||
String originalUrl = urlService.findOriginalUrlByShortUrl(shortUrl); | ||
response.sendRedirect(originalUrl); | ||
} | ||
Comment on lines
+20
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요렇게 하면 http status code도 바뀌나요?? (모름) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네! 저도 직접 설정해줘야하나 싶었는데 302 Redirect 정상적으로 바뀌더라구요!!! 👍 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.programmers.urlshortener.url.controller; | ||
|
||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import com.programmers.urlshortener.url.dto.request.ShortUrlCreateRequest; | ||
import com.programmers.urlshortener.url.dto.response.UrlResponse; | ||
import com.programmers.urlshortener.url.service.UrlService; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/urls") | ||
public class UrlController { | ||
|
||
private final UrlService urlService; | ||
|
||
@PostMapping | ||
public ResponseEntity<UrlResponse> createShortUrl(@Valid @RequestBody ShortUrlCreateRequest shortUrlCreateRequest, | ||
HttpServletRequest httpServletRequest) { | ||
UrlResponse urlResponse = urlService.createShortUrl(shortUrlCreateRequest, httpServletRequest.getRemoteAddr()); | ||
|
||
return ResponseEntity.status(HttpStatus.CREATED).body(urlResponse); | ||
} | ||
|
||
@GetMapping("/{shortUrl}") | ||
public ResponseEntity<UrlResponse> getShortUrlInfo(@PathVariable String shortUrl) { | ||
UrlResponse urlResponse = urlService.findUrlByShortUrl(shortUrl); | ||
|
||
return ResponseEntity.ok(urlResponse); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.programmers.urlshortener.url.domain; | ||
|
||
import com.programmers.urlshortener.common.converter.Base62Converter; | ||
|
||
public enum Algorithm { | ||
BASE62 { | ||
public String getShortUrl(int num) { | ||
return Base62Converter.encode(num); | ||
} | ||
}; | ||
|
||
public abstract String getShortUrl(int num); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package com.programmers.urlshortener.url.domain; | ||
|
||
import static com.programmers.urlshortener.common.exception.ExceptionRule.*; | ||
import static com.programmers.urlshortener.common.validator.UrlValidator.*; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
import org.hibernate.annotations.ColumnDefault; | ||
import org.hibernate.annotations.DynamicInsert; | ||
import org.hibernate.annotations.DynamicUpdate; | ||
import org.springframework.data.annotation.CreatedDate; | ||
import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
||
import com.programmers.urlshortener.common.exception.UrlException; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.EntityListeners; | ||
import jakarta.persistence.EnumType; | ||
import jakarta.persistence.Enumerated; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@Getter | ||
@DynamicInsert | ||
@DynamicUpdate | ||
@EntityListeners(AuditingEntityListener.class) | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class Url { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Enumerated(EnumType.STRING) | ||
private Algorithm algorithm; | ||
|
||
@Column(nullable = false) | ||
private String originalUrl; | ||
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unique 설정이 필요할것 같아요~~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추가로 url 길이 제한이 20인것 같으니 길이 설정도 해주면 좋겠죠?! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
@Column(unique = true) | ||
private String shortUrl; | ||
|
||
@Column(nullable = false) | ||
private String ip; | ||
|
||
@CreatedDate | ||
@Column(columnDefinition = "TIMESTAMP", nullable = false, updatable = false) | ||
private LocalDateTime createdAt; | ||
|
||
@Column(nullable = false) | ||
@ColumnDefault("0") | ||
private Long viewCount; | ||
|
||
@Builder | ||
public Url(String originalUrl, Algorithm algorithm, String ip) { | ||
validateUrlPattern(originalUrl); | ||
this.algorithm = algorithm; | ||
this.originalUrl = originalUrl; | ||
this.ip = ip; | ||
} | ||
|
||
public void convertToShortUrl() { | ||
if (id == null) { | ||
throw new UrlException(URL_NOT_SAVED); | ||
} | ||
|
||
this.shortUrl = algorithm.getShortUrl(id.intValue()); | ||
} | ||
|
||
public void increaseCount() { | ||
viewCount += 1; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
원본 URL의 값을 DB에 저장하기 때문에 굳이 디코딩할 필요가 없다고 생각되어 만들지 않았습니다.