Spring - 공유 페이지 제공하기
공유페이지 제공
- 기존에 진행 중인 사이드 프로젝트
문덕
은 문화생활에 대한 리뷰를 작성하고 보관할 수 있는 서비스다. - 커뮤니티 기능 없기 때문에 자신만 자기가 쓴 글을 볼 수 있었다.
- 공유를 원하는 사용자를 위해 우리는 공유페이지를 제공하기로 했다.
- 이 글에서는 임시방편으로 만들었던 공유페이지에서 완성도를 끌어올리기 위해 어떻게 로직을 바꾸었는지를 기술한다.
공유페이지 설계
- 공유페이지를 제공하기 위해 다음 2가지의 로직을 생각했다.
[1. URL에 모든 데이터를 담아 제공]
- 말그대로 모든 데이터를 URL 안에 담아 정적 페이지에 해당 데이터를 파싱하여 제공한다.
- 쿼리 파라미터는 모든 데이터가 URL에 노출되기에 base64로 인코딩하여 경로 변수로 데이터를 담는 방식을 생각했다.
쿼리 파라미터도 상관은 없겠지만 그대로 노출되는건 멋이 없지 않은가?
- 그러나 경로 변수에 모든 데이터를 담는 방식에는 문제가 있었다.
- 페이지에는 이미지가 들어가고 최대 10MB의 이미지가 제공된다.
- base64 인코딩을 하면 30% 더 긴 문자열을 생성한다.
- URL은 일반적으로 2000자 내외로만 허용이 된다.
- 따라서 10MB 이미지를 base64로 인코딩하면 1300만 길이의 문자열이 나와 URL에 데이터를 담는 방식은 사용할 수 없다.
[2. 리뷰 인덱스 id를 인코딩하여 동적 페이지 제공 ✅]
- 인덱스 id 값을 인코딩하여 URL로 제공하기 때문에 URL의 길이가 매우 짧다.
- 요청자가 사용자이기 때문에
ThymeLeaf
로 동적인 페이지를 만들어 사용자에게 제공한다.
공유페이지 로직
[예시]
- 리뷰 인덱스가
1364
인 페이지를 공유한다고 가정하자. - 서버에서는
1364
를 base64 인코딩하여MTM2NA==
값을 앱으로 보내준다. - 앱에서는 서버 주소와 합쳐 완전한 URL 링크를 제공한다.
- https://moonduck.o-r.kr/share/MTM2NA==
- 다른 유저들은 해당 링크에 접속하여 공유된 페이지를 볼 수 있다.
문제점
- 짧아도 너무 짧다.
- 또한 base64는 암호화 기법이 아닌 데이터가 깨지지 않게 전달하는
인코딩
이다. - 때문에 아무 숫자나 인코딩해서 서버로 요청을 보내면 공유되지 않았음에도 다른 사람들이 언제든지 자유롭게 접근해서 볼 수 있다.
- 해결해야 하는 문제점을 정리하면 다음과 같다.
- URL만으로 정보를 얻을 수 있으면 안된다.
- 사용자가 원하는 페이지만 공유가 되어야 한다. 공유한 적이 없음에도 다른 사람들에게 보여지면 안된다.
- 유효기간이 존재하여 일정 시간이 지난 후 접근할 수 없게 해야 한다.
개선
- 위에서 나열한 문제점들을 다음과 같이 해결했다.
- 고유한 식별번호인
UUID
를 사용한다. - 사용자가 공유하기 버튼을 눌렀을 때에만 UUID를 매핑하여 서버에 저장한다.
- Redis의
TTL
기능을 사용하여 하루가 지났을 때 자동으로 매핑 데이터가 사라지게 설계한다.
코드
[공유페이지의 URL 파라미터 생성]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String getShareUrl(Long userId, Long boardId) {
// 올바르지 않은 요청은 에러 반환
boardRepository.findByIdAndUserId(boardId, userId)
.orElseThrow(() -> new ErrorException(ErrorCode.BOARD_NOT_FOUND));
String uuidStr = UUID.randomUUID().toString(); // 랜덤 UUID 문자열 생성
long expirations = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000L).getTime() / 1000; // 하루를 long 타입의 초단위로 변환
Share share = new Share(uuidStr, boardId, expirations); // URL 매핑 객체 생성
shareRepository.save(share); // 레디스 서버에 저장
return uuidStr; // 매핑된 UUID 반환
}
- 크게 복잡하지 않다.
- 매핑할 UUID, 리뷰 인덱스, 유효기간을 서버에 저장하고 UUID를 반환한다.
- 이 UUID를 통해서 매핑된 인덱스 값을 가져오고 해당 리뷰의 데이터들을 가져올 수 있게 된다.
[공유된 페이지 접속]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public ShareDataResponse getShareData(String param) { // param = UUID 문자열
// 인자로 받은 UUID로 매핑 데이터를 조회한다.
Optional<Share> share = shareRepository.findById(param);
if (share.isEmpty()) {
return null;
}
Long boardId = share.get().getBoardId();
// 실제 데이터를 가져온다.
Optional<Board> boardOp = boardRepository.findByIdWithProgram(boardId);
if (boardOp.isEmpty()) {
return null;
}
Board board = boardOp.get();
// 데이터 파싱 로직 생략
.
.
// 최종 데이터들을 객체에 담아 반환한다.
return ShareDataResponse.builder()
.title(board.getTitle())
.category(category)
.program(board.getProgram() == null ? null : board.getProgram())
.content(board.getContent())
.image(board.getImage1())
.score(board.getScore())
.createdAt(createdAt)
.build();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public String getSharePage(String param, Model model) {
// 서비스단에서 데이터를 가져온다.
ShareDataResponse shareData = shareService.getShareData(param);
// 잘못된 데이터를 요청하거나 데이터가 삭제된 경우 에러페이지를 호출한다.
if (shareData == null) {
return "error";
}
// 정상적으로 데이터를 가져왔으면 Model에 담아 타임리프 페이지를 호출한다.
model.addAttribute("data", shareData);
return "share";
}
결과
- 아래는 정상적으로 공유된 페이지에 접속했을 때와 에러 페이지 화면이다.
- 정상적으로 공유받은 URL에 접속하면 내용들이 보여지고, 잘못된 URL에 접속한 경우 에러페이지가 보여진다.
느낀점
- 다양한 서비스들에서 제공했던 공유 기능을 직접 만들어보면서 생각보다 꽤 간단하다는 것을 알 수 있었다.
- 특히 공유페이지의 유효기간을 설정하는 것이 고민이었는데 Redis에 대해 공부하면서 정말 쉽게 해결할 수 있었다.
물론 RDB, 스케쥴러로 구현할 수도 있었지만 최대한 로직의 단숨함을 유지하고 싶었다.
- 구현방식에 폭넓은 선택지를 떠올리고, 개발 편의성을 증진시킬 수 있기 때문에 새로운 기술에 대한 학습은 무조건 필요하다고 생각한다.
- 이렇게 개선한 것 외에도 도메인을 분리시켜 트래픽 분산, 서버 주소 노출로 인한 보안 이슈를 해결할 수도 있을 것 같다.
This post is licensed under CC BY 4.0 by the author.