일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 마법의 엘리베이터
- 프로그래머스
- Qoddi
- 알고리즘
- 코딩테스트
- GitHub
- maven
- Spring Framework
- Spring
- 프로젝트 설계
- 테이블 해시 함수
- 와이어 프레임
- 6주포트폴리오
- 빌드 툴
- LEVEL 2
- Java
- 토이 프로젝트
- 트랜잭션
- DFS
- couchcoding
- 그리디 알고리즘
- 토이프로젝트
- 유사 칸토어 비트열
- 배포
- 협업프로젝트
- 카우치코딩
- pom.xml
- 백준
- ERD
- Fun편log
- Today
- Total
소통 하고싶은 개발자
프로젝트 진행 ⑨ : 전략 패턴을 사용하여 글 목록을 보여주는 기능 확장 본문
목차
- 시작하며
- 글 목록을 보여주는 기능의 확장
- 전체 글 수를 구하는 기능의 확장
- 마치며
시작하며
어느 때와 같이 프로젝트에 전념하고 있었을 때, 친구와 우연히 내가 진행 중인 프로젝트에 대해 "어느 단계를 진행하고 있다" 이런 근황에 대해 이야기하고 있었다.
마침 그때가 "글 목록을 보여주고 페이지네이션 기능까지 넣어서 잘 동작하는 것 같다" 이런 이야기를 할 때였는데, 친구가 문득 "게시판이라면 검색 기능이나 해쉬태그를 사용할 수 있어야 할 것 같다." 라는 이야기를 한 것이다.
하지만 뭔가 머릿속이 어질어질했다. 왜냐면 당시에 구현해놓은 정렬 방식이 실시간 글, 일일 인기 글, 주간 인기 글의 3 가지인데, 하나를 더 추가하려면 뭔가 수정되어야 할 부분이 많게 느껴졌기 때문이다.
그렇게 뭔가 어질어질했는데, 문득 이전 회사에서 레거시를 제거하려고 여러가지 패턴을 공부하고 사용하고 없애고를 반복하며 공부했던 나날이 떠오르면서 "지금 어질어질한건 구조가 이상해서 코드를 유지보수하기 어려운 상태이기 때문이구나" 라는 생각을 했다.
그런 생각을 하는 순간 어질어질하던 머릿속에 갑자기 의욕이 솟아서 뭔가 확장성있도록 구조를 변경하기로 다짐했다. 물론 일정이 정해져있는 프로젝트이므로(이미 지나긴했지만) 많은 시간을 들여서는 안되기 때문에 기능을 추가하진 않고 구조만 변경시켜 후에 유지보수를 용이하게 하려고 했다.
글 목록을 보여주는 기능의 확장
기존에 글을 보여주는 방식은 다음과 같았다.
- 실시간 내림차순 정렬
- 일일 좋아요 수를 기준으로 내림차순 정렬
- 주간 좋아요 수를 기준으로 내림차순 정렬
그리고 기능을 추가해야 한다면 아래의 기능들을 추가해야 하는 상황이 올 수도 있었다.
- 검색결과를 실시간 기준으로 정렬
- 해쉬태그 결과를 실시간 기준으로 내림차순 정렬
아래 코드는 기존에 컨트롤러와 서비스 클래스 내부 코드이다.
@RequestMapping(uri = "/posts", method = "get")
public String showPosts(HttpServletRequest req, HttpServletResponse resp){
if (!ControllerUtils.isExistProfileSession(req))
return "index";
HttpSession session = req.getSession();
PostsOptionVO optionInSession =
(PostsOptionVO) session.getAttribute(this.postsOptionKey);
PostsOptionVO optionInRequest = new PostsOptionVO(
req.getParameter("pageNo"),
req.getParameter("type")
);
PostsOptionVO actualOption = postService.getNewPostsOption(optionInRequest, optionInSession);
session.setAttribute(this.postsOptionKey, actualOption);
List<Post> posts = postService.getPosts(actualOption);
req.setAttribute("posts", posts);
PostSortType postSortType = postService.getPostSortType(actualOption.getSortType());
switch (postSortType) {
case REAL_TIME: {
req.setAttribute("realtimeChecked", "checked");
break;
}
case DAYS_FAVORITE: {
req.setAttribute("daysChecked", "checked");
break;
}
case WEEKS_FAVORITE: {
req.setAttribute("weeksChecked", "checked");
break;
}
}
return "mainPage";
}
컨트롤러 코드에 대한 설명은 이전 포스팅에서 했으므로 오늘은 이야기하고자 하는 것에 대해서만 이야기하겠다.
요점은 컨트롤러에서 글 목록을 보여줄 새로운 옵션을 postService.getNewPostsOption() 메서드를 사용해 획득하고 actualOption 변수에 저장한다. 그리고 PostsOptionVO 타입을 매개변수로 하는 postService.getPosts() 메서드를 사용하여 글 목록을 반환받아 JSP에 넘겨주는 것이다.
그리고 아래 코드는 기존 서비스 객체의 getPosts() 메서드이다.
public List<Post> getPosts(PostsOptionVO options){
int pageNo = getPostPage(options.getPageNo());
PostSortType sortType = getPostSortType(options.getSortType());
List<Post> postList = postDao.getPosts(sortType, pageNo, POST_COUNT_IN_PAGE);
return postList;
}
상황에 따라 3가지 경우의 수로 글을 정렬하는 로직이 필요한데 if문이나 switch문이 하나도 없고 서비스 객체의 코드가 이렇게나 간결하다.
왜냐면 지금까지는 아래처럼 비즈니스 로직의 일부를 레포지토리 객체 즉 Persistence Layer의 postDao.getPosts() 메서드에서 정렬 방식에 따라 SQL을 분기 처리했었기 때문이다.
public List<Post> getPosts(PostSortType sortType, int pageNo, int postCountInPage){
int startNo = (pageNo - 1) * postCountInPage;
if (startNo < 0) startNo = 1;
String sql = "";
switch (sortType) {
case REAL_TIME: {
sql = "select * from post order by posted_date desc limit ?, ?";
break;
}
case DAYS_FAVORITE: {
sql = "select * from post " +
"where posted_date between date_add(now(), interval -1 day) and now() " +
"order by like_count desc limit ?, ?";
break;
}
case WEEKS_FAVORITE: {
sql = "select * from post " +
"where posted_date between date_add(now(), interval -1 week) and now() " +
"order by like_count desc limit ?, ?";
break;
}
default:
sql = "select * from post order by posted_date desc limit ?, ?";
}
try (
Connection conn = new DBUtil().getConnection();
PreparedStatement preparedStatement = conn.prepareStatement(sql);
) {
preparedStatement.setInt(1, startNo);
preparedStatement.setInt(2, postCountInPage);
ResultSet resultSet = preparedStatement.executeQuery();
List<Post> postList = new ArrayList<Post>();
Post post = null;
while (resultSet.next()) {
postList.add(new Post(
resultSet.getInt("post_no"),
resultSet.getString("title"),
resultSet.getString("content"),
resultSet.getTimestamp("posted_date"),
resultSet.getBoolean("is_use_anonymous_city"),
resultSet.getBoolean("is_use_anonymous_name"),
resultSet.getInt("comment_count"),
resultSet.getInt("like_count"),
resultSet.getInt("account_no"),
resultSet.getInt("picture_no"),
resultSet.getString("nicname")
)
);
}
return postList;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
물론 이런식으로 처리해서 문제가 해결되었기 때문에 다음 작업을 이어갈 수 있었지만, 새로 추가하려는 기능인 검색 기능과 해쉬태그 기능을 추가해야 한다면 조금 곤란해질 수 있다.
왜냐면 검색은 기본적으로 검색어가 존재하므로 실제 검색어를 통해 DB의 값을 가져오려면 매개변수에 검색어 항목이 필요하고, 해쉬태그도 마찬가지로 만약 DB의 post테이블에 해쉬태그를 추가한다고 하더라도 사용자가 입력한 해쉬태그를 읽어서 해당 값을 토대로 SQL에 반영해야 한다.
그렇다면 앞으로 Dao 객체의 getPosts() 메서드에 파라미터로 검색어, 해쉬태그 이름 등등 "추가해야 할 기능에서 필요한 파라미터를 getPosts() 메서드에 전부 추가해야 하는가?" 라고 묻는다면 누구도 "예"라고 하지 못할 거라고 생각한다.
당연하다. 누가봐도 비효율적이고 더러운 코드가 될 것이 뻔하기 때문이다. 그렇기 때문에 나는 이 문제에 대해서 이런 생각을 했다.
- 글 목록을 가져오는 기능만을 구조화해야 한다.
- Persistence 계층에서 비즈니스 로직을 없애야 한다.
- 글 목록을 가져오는 방법을 캡슐화해서 하나의 형태로 묶고, 각각의 방법을들 구현해서 상황에 맞는 글 목록을 가져와야 한다.
개인적으로 이렇게 "글 목록을 가져온다는 동일한 목적을 두고 행위 알고리즘들을 각각 캡슐화하여 기능을 확장"하는 것을 "전략 패턴"이라고 하는게 올바르지 않을까 하는 생각이다.
그래서 위의 구조를 만들기 위해 크게 아래 3 가지 객체들을 만들었다.
- BasePostsStrategy Interface ( 전략 인터페이스 )
- PostsStrategyContext Class ( 전략 인터페이스를 사용할 객체 )
- ImplPostsStrategy Class ( 전략 인터페이스를 구현한 클래스 )
public abstract class BasePostsStrategy {
int postCountInPage = 12;
protected PostDao postDao;
public BasePostsStrategy(int postCountInPage) {
this.postCountInPage = postCountInPage;
postDao = new PostDao();
}
public abstract List<Post> getPosts(PostsOptionVO optionVO, Map<String, String[]> paramMap);
}
먼저 전략들을 캡슐화할 때 베이스로 사용할 클래스라는 의미로 BasePostsStrategy로 네이밍했다.
- postCountInPage 맴버 변수는 생성자를 통해 입력받을 것이며, 기본 값은 12 로 한 페이지에 12 개의 글이 있을 때 디자인이 괜찮아보여서 12 로 설정했다.
- 공통 메서드인 getPosts()를 가지고 있는데, 글 목록을 보여주기 위한 옵션 객체와 Request Scope에 들어온 파라미터들을 받아올 Map 형태로 파라미터로 받는다.
- Map 형태로 받아오는 이유는 사용하는 전략에 따라 받아야 하는 인자가 각각 다르기 때문이다.
- Map<String, String[]>형태로 받아오는 이유는 Request 객체의 getParameterMap()를 사용하면 Map<String, String[]> 형태를 반환해주기 때문이다.
public class PostsStrategyContext {
private BasePostsStrategy postsStrategy;
private int postCountInPage = 12;
public PostsStrategyContext() {
}
public PostsStrategyContext(int postCountInPage) {
this.postCountInPage = postCountInPage;
}
public void setPostsStrategy(BasePostsStrategy postsStrategy){
this.postsStrategy = postsStrategy;
}
public void setPostsStrategy(PostSortType sortType){
switch (sortType) {
case REAL_TIME: setPostsStrategy(new RealTimeStrategy(this.postCountInPage)); break;
case DAYS_FAVORITE: setPostsStrategy(new DaysFavoriteStrategy(this.postCountInPage)); break;
case WEEKS_FAVORITE: setPostsStrategy(new WeeksFavoriteStrategy(this.postCountInPage)); break;
case SEARCH_TITLE_FROM_USER: setPostsStrategy(new TitleSearchStrategy(this.postCountInPage)); break;
case SEARCH_CONTENT_FROM_USER: setPostsStrategy(new ContentSearchStrategy(this.postCountInPage)); break;
case HASH_TAG: setPostsStrategy(new HashTagStrategy(this.postCountInPage)); break;
}
}
public List<Post> getPosts(PostsOptionVO postsOptionVO, Map<String, String[]> paramMap){
return this.postsStrategy.getPosts(postsOptionVO, paramMap);
}
}
위 코드는 PostsStrategyContext 클래스로 살펴보면,
- 해당 객체 생성 시 페이지에 몇 개의 글이 작성되어야 하는지에 대한 정보를 int 형태 파라미터로 전달받는다.
- 만약 전달받은 파라미터가 없다면 디폴트 값인 12 를 사용한다.
- setPostsStrategy(BasePostsStrategy Class)와 이를 오버로딩한 메서드인 setPostsStrategy(PostSortType) 메서드를 만들었다.
- 오버로딩한 이유는 전략을 사용하는 컨텍스트 객체인 PostsStrategyContext를 PostService가 사용할 때 편의성을 위함이다.
- 맨 아래의 getPosts() 메서드는 BasePostsStrategy를 상속받는 각각의 구현체들의 오버라이딩 메서드인 getPost()를 사용하게 만들 것이다.
그리고 위 코드를 봐도 알 수 있듯이 총 6 개의 구현체를 만들 것으로 항목은 아래와 같다.
- 글 목록을 실시간 내림차순으로 정렬하는 전략
- 일간 좋아요에 대해 내림차순으로 정렬하는 전략
- 주간 좋아요에 대해 내림차순으로 정렬하는 전략
- 글 제목으로 검색했을 때 결과에 대해 실시간 내림차순으로 정렬하는 전략
- 글 내용으로 검색했을 때 결과에 대해 실시간 내림차순으로 정렬하는 전략
- 해쉬태그를 입력했을 때 결과에 대해 실시간 내림차순으로 정렬하는 전략
그리고 이전에 언급했던 것처럼 기존에 사용하던 1, 2, 3번 전략은 구현할 것이지만, 나머지 4, 5, 6번 전략은 후에 기능 추가 건으로 구현하려고 한다.
PostService postService;
public PostController (){
postService = new PostService();
}
private PostsStrategyContext postsStrategyContext;
public PostService(){
postDao = new PostDao();
commentDao = new CommentDao();
profileDao = new ProfileDao();
replyDao = new ReplyDao();
postsStrategyContext = new PostsStrategyContext(POST_COUNT_IN_PAGE);
}
마지막으로 전체적인 흐름을 보면
- 이전에 직접 만들었던 프론트 컨트롤러(서블릿)에서 프로젝트가 실행될 때 init() 메서드를 통해 각 Controller들을 <URI : new Instance> 와 같이 Map형태로 저장한다.
- Controller가 매핑될 때 PostController가 생성되는데, 이 때 멤버변수인 PostService도 함께 생성된다.
- PostService가 생성될 때 맴버변수인 PostsStrategyContext가 생성되어 이 후 요청이 들어왔을 때 별도의 PostsStrategyContext 객체 생성을 하지 않고도 사용할 수 있는 상태가 된다.
- PostsStrategyContext의 생성자의 파라미터로 POST_COUNT_IN_PAGE 값을 받아오는데, 이 값은 한 페이지에 배치될 글의 갯수라는 의미로 해당 값을 넘겨주어 공통적으로 해당 갯수만큼의 크기를 가지는 글 리스트를 반환받기 위해 주입해줬다.
- 추가적으로 POST_COUNT_IN_PAGE 값은 상수인데, 나중에는 따로 설정 파일로 뺄 수도 있다고 생각하고 있다.
전체 글 수를 구하는 기능의 확장
위에서 새로운 구조를 만들어서 글 리스트를 여러 방법으로 가져오는 구조를 만들었다.
글 리스트를 가져오기 위해서는 한 페이지에 몇 개의 글을 표시할 것인가에 대한 정보가 필요하므로 필요한 갯수를 PostService에서 결정하여 PostsStrategyContext (Context 객체)를 생성할 때 파라미터로 몇 개인지 주입했었다.
그런데 글 목록을 디스플레이하는 것과는 별개로 페이지네이션을 하려면 각 전략을 사용했을 때 얻을 수 있는 글의 총 갯수를 알아야 한다.
왜냐면 각 전략에 대해 가져올 수 있는 글의 총 갯수를 모른다면 정확한 페이지네이션을 할 수 없기 때문이다.
예를 들어 한 페이지에 12 개의 글을 표시해야 하는데 총 갯수를 모르면 페이지네이션을 정확히할 수 없기 때문이다.
그래서 글 리스트를 불러오는 기능을 확장했듯이 글의 총 갯수를 불러오는 기능도 확장하기로 했다.
private PostsStrategyContext postsStrategyContext;
private PageCountStrategyContext pageCountStrategyContext;
public PostService(){
postDao = new PostDao();
commentDao = new CommentDao();
profileDao = new ProfileDao();
replyDao = new ReplyDao();
postsStrategyContext = new PostsStrategyContext(POST_COUNT_IN_PAGE);
pageCountStrategyContext = new PageCountStrategyContext(POST_COUNT_IN_PAGE);
}
public class PageCountStrategyContext {
private BasePageCountStrategy pageCountStrategy;
private int postCountInPage = 12;
public PageCountStrategyContext() {
}
public PageCountStrategyContext(int postCountInPage) {
this.postCountInPage = postCountInPage;
}
public void setPageCountStrategy(BasePageCountStrategy pageCountStrategy){
this.pageCountStrategy = pageCountStrategy;
}
public void setPageCountStrategy(PostSortType sortType){
switch (sortType) {
case REAL_TIME: setPageCountStrategy(new RealTimeStrategy(this.postCountInPage)); break;
case DAYS_FAVORITE: setPageCountStrategy(new DaysFavoriteStrategy(this.postCountInPage)); break;
case WEEKS_FAVORITE: setPageCountStrategy(new WeeksFavoriteStrategy(this.postCountInPage)); break;
case SEARCH_TITLE_FROM_USER: setPageCountStrategy(new TitleSearchStrategy(this.postCountInPage)); break;
case SEARCH_CONTENT_FROM_USER: setPageCountStrategy(new ContentSearchStrategy(this.postCountInPage)); break;
case HASH_TAG: setPageCountStrategy(new HashTagStrategy(this.postCountInPage)); break;
}
}
public int getPageCount(){
return this.pageCountStrategy.getPageCount();
}
}
기본적인 구조는 글 리스트를 불러오는 구조와 동일하므로 코드 리뷰는 생략하려고한다.
마치며
얼마전 유튜브를 시청하는 와중에 기술 블로그를 운영하는 것이 독이될 수도 있다라는 영상을 시청했다. 영상의 요점은 블로그 게시자가 잘 알지 못하는 지식에 대해서 포스팅을 함으로서 잘못된 지식이 올라오는 경우 다른 사람들이 해당 포스팅을 믿고 잘못된 지식이 퍼진다는 것이었다.
잘못된 정보를 게시하든 잘못된 정보를 접함으로써 잘못된 지식을 쌓든 둘 다 좋지 않은 것은 명확하기에 나도 유독 신경을 썼다.
왜냐면 정말 그럴일이 있을까 싶지만, 누군가 프로젝트를 진행하거나 잠깐 검색하여 내 블로그를 방문했을 때 내가 올린 글에 잘못된 정보가 있기를 원치 않기 때문이다.
특히 이런 구조 변경에 대한 글은 최대한 내가 생각하고 느낀 것들을 작성하는 것이므로 제대로된 지식을 원해서 방문한 사람이라면 나와 내 글을 믿기까지 각별한 고민을 해주었으면 좋겠고, 만약 잘못되었거나 부족한 내용이 있다면 댓글로 알려주기를 바랍니다..!!
감사합니다!!
'토이 프로젝트 > 익명 채팅 사이트' 카테고리의 다른 글
번외 - ② : 현재 구조에서 Transaction에 대한 고찰 - 두 번째 (0) | 2022.08.16 |
---|---|
번외 - ② : 현재 구조에서 Transaction에 대한 고찰 (0) | 2022.08.10 |
번외 - ① : git readme 작성 (0) | 2022.08.03 |
프로젝트 진행 ⑧ : 댓글 상세보기 페이지, 댓글 더보기 기능 구현 (0) | 2022.08.02 |
프로젝트 진행 ⑦ : 메인페이지 구성, 글 목록 조회, 글 상세 조회 구성 (0) | 2022.07.25 |