ㄱ* 이 포스트는 학습 과정에서 그 내용을 기록한 글이기에 부정확한 정보가 포함될 수 있습니다.
따라서 해당 글은 참고용으로만 봐주시고 틀린 부분이 있다면 알려주시면 감사하겠습니다.
Index
1 준비
2 스프링 프로젝트 생성
3 컴포넌트
4 view 구성
5 이식을 위한 코드 분리
6 domain 설정
7 리포지토리
8 서비스
9 컨트롤러와 뷰
준비
이제 본격적으로 로직을 반영하여 웹으로 이식하겠습니다. 우선 이번 글에 작성된 스프링 코드는 일부러 정말, 매우, 아주 잘못된 코드이며 설계부터 이상합니다. 그 이유는 다음 글부터 올바른 코딩을 위해 겪는 과정을 설명하기 위한 빌드업이기에 이번 글을 읽으시면서 어떤 부분에서 무엇이 잘못되었는지 체크해보시는 것도 괜찮을 것 같습니다. 물론 인덱스 링크 기능만을 사용만 하실 예정이면 코드는 제대로 작동하니 다음 글을 읽지 않으셔도 되긴 하겠지만 별 문제 없이 이번 포스트를 받아들이시면 흠... 잘 모르겠네요.
시작하기에 앞서 그 전에 이전 포스트에 작성한 로직을 조금 살펴보겠습니다.
# 인덱스 중복
이전 포스트에서 언급했듯 상단의 목차 위치를 알려주는 'Index'라는 단어가 본문에 중복되어 등장하면 로직이 제대로 작동하지 않습니다. 목차 인식 단어를 'Index'에서 인용구를 포함한 '<blockquote data-ke-style="style2">Index</blockquote>'로 바꾸면 해결되는 문제긴 하지만 확실한 로직의 작동을 보증하기 위해 인덱스 중복 검사를 넣어줘야 합니다.
# 타이틀 html 코드
현재 각 목차들인 title은 다음과 같은 형식의 html 코드로 구성되어 있습니다.
Title 1 : <blockquote data-ke-size="size16" data-ke-style="style1"><span style="font-family: 'Noto Serif KR';">타이틀1</span></blockquote>
Title 2 : <blockquote data-ke-size="size16" data-ke-style="style1"><span style="font-family: 'Noto Serif KR';">타이틀2</span></blockquote>
...
여기서 우리가 작성한 코드는 반드시 위와 같은 형식을 지켜야 타이틀로 인식됩니다. 만약 다음처럼 타이틀 2를 실수나 포스팅 중 오류로 형식과 다르게 한다면 우리 코드는 타이틀2를 타이틀로 인식하지 않습니다. 실제로 포스트하다보면 다음과 같이 타이틀별로 html코드가 같지 않은 경우가 허다합니다.
Title 1 : <blockquote data-ke-size="size16" data-ke-style="style1"><span style="font-family: 'Noto Serif KR';">타이틀1</span></blockquote>
Title 2 : <blockquote data-ke-size="size16" [MISS] ><span style="font-family: 'Noto Serif KR';">타이틀2</span></blockquote>
...
따라서 html코드 전문을 사용자에게 받으면 타이틀의 총 개수를 되물어보도록 검증해야합니다. 알아서 이런 missmatch를 잡아주면 좋겠지만 티스토리 뿐만 아닌 다른 글에도 적용할 수 있도록 확장할 예정이기에 일단 너무 복잡한 로직은 후일로 미루겠습니다.
스프링 프로젝트 생성
우선 검증 사항은 더 많지만 제일 중요한 두 가지만 언급하고 이후 포스트를 작성해가며 차차 적겠습니다. 우선 스프링 이니셜라이저에서 프로젝트를 생성합니다.
설정은 위 그림처럼 두고 dependencies의 경우 web, lombok, thymeleaf를 필수로 h2, devtools는 이번 포스트에서 필수는 아닙니다. h2, devtools는 편의를 위해 넣은거라 둘다 이번 포스트를 진행할때 꼭 필요하진 않고 재량껏 선택하시면 됩니다.
컴포넌트
일단 가장 기초적인 기능만 먼저 만들 예정이므로 PACKAGE는 아래와 같이 domain, repository, service, controller만 생성해줍니다.
저는 원래 repository나 controller를 따로 패키징하진 않지만 포스트를 위해 딱딱 나누겠습니다. 그리고 혹시 모르시는 분이 있다면 setting에 가서 gradle을 검색하거나 Build, Execution,Deployment > Build Tools > gradle 로 가시면 나오는 아래 설정을 진행해주세요. 이 설정을 하면 run 속도도 빨라지고 실 배포 환경을 개발 환경과 동일하게 해줍니다.
view 구성
우선 view부터 생각해두고 진행하겠습니다. 아마 view는 입력, 검증, 결과창 총 세 화면을 둘 것이고 각 화면은 다음과 같이 만들 것입니다.
우선 다른 블로그가 아닌 티스토리 블로그용으로 화면을 간단하게 구성할 예정입니다.
이식을 위한 코드 분리
원래 로직은 단순 자바 코드이므로 스프링 웹으로 사용하기 위해 컴포넌트를 분리해주어야 합니다. 이전 포스트의 Logic.java를 살펴보며 얘기하겠습니다.
public class Logic {
public static void main(String[] args) throws IOException {
/*
준비
*/
// 텍스트 불러오기
FileReader reader = new FileReader("D:\\input.txt");
String inputText = "";
int charTxt;
while ((charTxt = reader.read()) != -1) {
inputText += (char)charTxt;
}
위의 코드는 파일로 원본 html코드를 변수화하기 위해 작성했으므로 view를 만들어 html 코드를 입력 받게 변경합니다. 맨 마지막 파일 출력도 마찬가지로 view에서 처리합니다.
// 인덱스 개수 + 타이틀 저장
int startIndex = inputText.indexOf("Index");
String startText = inputText.substring(startIndex);
String countTitleKeyword = "blockquote data-ke-size=\"size16\" data-ke-style=\"style1\">";
int count = 0;
String[] startTextArrBySpace = startText.split("<");
int textLen = startTextArrBySpace.length;
List<String> oldTitleList = new ArrayList<>();
for (int i = 0; i < textLen; i++) {
if (startTextArrBySpace[i].equals(countTitleKeyword)) {
count++;
// 타이틀명 스트림으로 저장
oldTitleList.add(startTextArrBySpace[i+1]);
}
}
해당 부분은 변수를 선언하고 각 값을 저장하는 과정이기 때문에 domain에 oldCode.java를 추가하고 변수들을 멤버로 선언하도록 변경합니다.
/*
기능 추가
*/
// 기능1 타이틀마다 id 추가
String splitTitleKeyword = "blockquote data-ke-size=\"size16\" data-ke-style=\"style1\">";
String[] inputTextArrByBrak = inputText.split(splitTitleKeyword);
String addTitleText = inputTextArrByBrak[0];
int arrLen = inputTextArrByBrak.length;
for (int i = 1; i < arrLen; i++) {
addTitleText += "blockquote data-ke-size=\"size16\" data-ke-style=\"style1\" "+"id=\""+i+"th\">";
addTitleText += inputTextArrByBrak[i];
}
// 기능2 상단 index 자동완성
String[] resultArr = addTitleText.split("Index");
String resultText = resultArr[0]+"Index";
List<String> titleList = new ArrayList<>();
for (int i = 0; i < oldTitleList.size(); i++) {
titleList.add(oldTitleList.get(i).split(">")[1]);;
}
for (int i = 0; i < titleList.size(); i++) {
int j = i+1;
resultText += "<br /><a href=\"#"+j+"th\">"+j+" "+titleList.get(i)+"</a>";
}
resultText += resultArr[1];
기능 추가 같은 경우는 domain에 newCode.java를 추가하여 변수들을 저장합니다. 뒤의 파일 출력은 앞서 언급했듯 view로 출력 결과를 보여주면 됩니다.
domain 설정
우선 모든 컴포넌트들에 공통으로 사용될 도메인을 먼저 구성하겠습니다. 앞서 말했듯 이제부터 나오는 코드는 정말 작동만 되는 practice에 가깝기 때문에 스프링을 공부하시는 분은 반드시 다음 포스트까지 이어 보시는 걸 추천합니다.
각설하고 우선 도메인은 수정 전 코드인 OldCode와 수정 후 코드인 NewCode 두 개를 작성하며 각 도메인마다 로직에 사용되는 변수들을 멤버로 가지며 내장된 init 메소드로 각 변수들을 세팅해줄 예정입니다. 큰 로직은 이전 포스트의 결과 완전히 같기에 자세한 설명은 생략하도록 하겠습니다.
#1 도메인 : OldCode.java
우선 OldCode의 멤버를 선언합니다.
// OldCode.java
package fadet.postLink.domain;
@NoArgsConstructor
@Getter @Setter
public class OldCode {
private Long id;
// 필수 입력
private String allCode;
private String titleHtmlKeyword;
private String indexHtmlKeyword;
// 입력값을 통해 init으로 값 세팅
private Long titleCount;
private List<String> oldTitleList;
private List<String> newTitleList;
private int indexOver;
롬복을 사용해서 getter, setter와 생성자를 세팅해주고 각 필드는 아래와 같이 구성합니다.
- id
- 필수 입력 필드
- allCode : html 원본 전문
- titleHtmlKeyword : 기존 로직에서 html 코드 중 타이틀들이 가지는 공통 키워드
- indexHtmlKeyword : 기존 로직에서 index 코드가 가진 키워드
- init 메소드에 의해 세팅되는 필드
- titleCount : titleHtmlKeyword에 의해 count되는 타이틀 개수
- oldTitleList : 기존 로직에서 수정되지 않은 각 타이틀 명
- newTitleList : 기존 로직에서 oldTitleList를 수정한 각 타이틀 명
- indexOver : 인덱스 중복 여부를 판단하기 위해 indexHtmlKeyword가 글에 하나만 존재하는지 세는 값
/*
init method
*/
public void init() {
// 여기서 타이틀을 추출하기위해 <로 나눴기때문에 이후 <를 고려해서 작성해야함
String[] startTextArrByBracket = this.allCode.split("<");
int textLen = startTextArrByBracket.length;
// titleCount, oldTitleList init
Long count = 0L;
List<String> oldList = new ArrayList<>();
//여기서 위에서 <를 잘랐기에 <를 빼줌
String addBracketKeyword = this.titleHtmlKeyword.substring(1);
for (int i = 0; i < textLen; i++) {
if (startTextArrByBracket[i].equals(addBracketKeyword)) {
count++;
// 타이틀명 스트림으로 저장
oldList.add(startTextArrByBracket[i+1]);
}
}
setTitleCount(count);
setOldTitleList(oldList);
// newTitleList init
List<String> newList = new ArrayList<>();
for (int i = 0; i < this.oldTitleList.size(); i++) {
newList.add(this.oldTitleList.get(i).split(">")[1]);;
}
setNewTitleList(newList);
// indexOver init
String[] resultArr = allCode.split(this.indexHtmlKeyword);
setIndexOver(resultArr.length);
init 메소드는 기존 로직을 바탕으로 하기에 이전 포스트와 같은 내용이지만 한 가지 달라진 점은 기존 로직이 수정 코드에 작성될 내용을 직접 string으로 제가 변수에 직접 넣었었는데 이번엔 titleHtmlKeyword, indexHtmlKeyword를 쪼개서 사용하였습니다.
#2 도메인 : NewCode.java
// NewCode.java
package fadet.postLink.domain;
@Getter @Setter
@NoArgsConstructor
public class NewCode {
private String allNewCode;
/*
init method
*/
public void init(OldCode oldCode) {
// 기능1 타이틀별 링크
String[] oldCodeArrByKeyword = oldCode.getAllCode().split(oldCode.getTitleHtmlKeyword());
String addTitleText = oldCodeArrByKeyword[0];
String[] modiTitleKeywordArr = oldCode.getTitleHtmlKeyword().split(" ");
int arrLen = oldCodeArrByKeyword.length;
for (int i = 1; i < arrLen; i++) {
addTitleText += modiTitleKeywordArr[0] + " id=\""+i+"\" "+modiTitleKeywordArr[1]+" "+modiTitleKeywordArr[2];
addTitleText += oldCodeArrByKeyword[i];
}
// 기능2 인덱스 자동완성
// String[] resultArr = addTitleText.split("<blockquote data-ke-style=\"style2\">Index</blockquote>");
String[] resultArr = addTitleText.split(oldCode.getIndexHtmlKeyword());
// String resultText = resultArr[0]+"<blockquote data-ke-style=\"style2\">Index";
String[] modiIndexKeywordArr = oldCode.getIndexHtmlKeyword().split("<");
String modiIndexKeyword = "<"+modiIndexKeywordArr[0]+modiIndexKeywordArr[1];
String resultText = resultArr[0]+modiIndexKeyword;
for (int i = 0; i < oldCode.getNewTitleList().size(); i++) {
int j = i+1;
resultText += "<br /><a href=\"#"+j+"\">"+j+" "+oldCode.getNewTitleList().get(i)+"</a>";
}
resultText += "<"+modiIndexKeywordArr[2]+resultArr[1];
this.setAllNewCode(resultText);
}
}
NewCode 역시 특이사항은 없고 수정되어 출력될 allNewCode를 필드로 가지며 마찬가지로 init 메소드로 세팅합니다. 다음 포스트에서 언급하겠지만 oldCode를 인자로 받기에 해당 클래스를 의존하는 도메인입니다.
리포지토리
빌드 과정을 설명할 때 view > controller... 순으로 설명하는 게 일반적이지만 이번 포스트에선 헷갈리지 않도록 도메인부터 리포지토리 순으로 언급하겠습니다.
// CodeRepository.java
package fadet.postLink.repository;
@Repository
public class CodeRepository {
private static Map<Long, OldCode> store = new HashMap<>();
private static long sequence = 0L;
public int size() {
return store.size();
}
public OldCode save(OldCode oldCode) {
oldCode.setId(++sequence);
store.put(oldCode.getId(), oldCode);
return oldCode;
}
public OldCode change(Long id, OldCode oldCode) {
oldCode.setId(id);
store.remove(id);
store.put(id, oldCode);
return oldCode;
}
public OldCode findOne(Long id) {
return store.get(id);
}
public NewCode result(Long id) {
OldCode oldCode = store.get(id);
NewCode newCode = new NewCode();
newCode.init(oldCode);
return newCode;
}
}
일반적으로 리포지토리는 교체를 염두해서 인터페이스를 먼저 작성하고 구현될 클래스를 추가하지만 우리 로직은 간단한 예제 정도이므로 리포지토리 하나면 충분합니다. 또한 필드의 store의 경우도 실무에서는 동시성 문제 등을 고려하여 HashMap 대신 ConcurrentHashMap 등을 사용하며 Null을 고려하여 Optional을 사용해야하지만 실습을 위해 과감히 생략하도록 하겠습니다.
리포지토리를 자주 접하셨다면 필드에 size()가 왜 있나하실텐데 이후 설명하겠습니다.
서비스
// LinkService.java
package fadet.postLink.service;
@Service
@RequiredArgsConstructor
public class LinkService {
private final CodeRepository codeRepository;
public void saveCode(OldCode oldCode) {
oldCode.init();
codeRepository.save(oldCode);
}
public OldCode findOne(Long id) {
return codeRepository.findOne(id);
}
public void changeCode(OldCode oldCode) {
int id = codeRepository.size();
codeRepository.change((long)id, oldCode);
}
public NewCode newCode(Long id) {
NewCode result = codeRepository.result(id);
return result;
}
}
서비스는 특이사항이 별로 없이 CodeRepository를 주입 받아 작동합니다. 언급할만한 사항이 있다면 아직 db를 사용하지 않기 때문에 @tracsactional은 필요 없습니다만 서비스 컴포넌트는 웬만하면 트랜잭션 안에서 이뤄지도록 구성하는 것이 좋습니다.
컨트롤러와 뷰
컨트롤러와 뷰는 위에서 소개했던 그림과 연결해서 살펴보겠습니다.
#1 컨트롤러와 뷰 : input
우선 input 홈에 접속하면 보여질 GetMapping부터 작성합니다.
// LinkController.java
package fadet.postLink.web.controller;
@RequiredArgsConstructor
@Controller
public class LinkController {
private final LinkService linkService;
private final CodeRepository codeRepository;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("form", new InputForm());
return "input";
}
우선 html 코드를 받는 index 메소드는 inputForm을 입력 받으며 뷰로 input.html을 넘겨줍니다.
// inputForm.java
package fadet.postLink.web;
@Getter @Setter
public class InputForm {
private Long id;
private String allCode;
private String titleHtmlKeyword;
private String indexHtmlKeyword;
}
inputForm은 필드로 도메인의 필수 입력값을 가지며 model에 담겨 이동합니다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
...
</head>
<body>
...
<form action="input.html" th:action="@{/}" th:object="${form}" method="post">
<div class="form-group">
<label th:for="allcode">원본 html코드 전문</label>
<textarea rows="10" cols="50" class="form-control" th:field="*{allCode}" placeholder="html코드를 입력해주세요."></textarea>
</div>
...
</body>
...
</html>
input.html은 전부 보여줄 필요는 없을 것 같습니다. form으로 위에 나온 필드들을 입력 받습니다.
이 코드들로 보여지는 페이지는 다음과 같습니다.
이 form에서 입력 값을 넣는 PostMapping을 이어서 작성합니다.
@PostMapping("/")
public String input(@ModelAttribute("form") InputForm form) {
OldCode newOne = new OldCode();
newOne.setAllCode(form.getAllCode());
newOne.setTitleHtmlKeyword(form.getTitleHtmlKeyword());
newOne.setIndexHtmlKeyword(form.getIndexHtmlKeyword());
newOne.init();
linkService.saveCode(newOne);
int id = codeRepository.size();
return "redirect:/valid/"+id;
}
이전에 size()를 언급했었는데 해당 메소드는 id를 리포지토리에 저장된 store의 크기로 주기 위해 작성했었습니다. 간단한 예제이기에 우선 사용자를 한 명으로 가정하고 사용한 임시 로직입니다. 해당 임시 로직으로 값이 저장되면 그 최신 값의 id를 불러옵니다만 정말로 '임시' 로직입니다.
#2 컨트롤러와 뷰 : valid
@GetMapping("/valid/{id}")
public String valid(@PathVariable("id") Long id, Model model) {
OldCode oldCode = linkService.findOne(id);
ValidForm validForm = new ValidForm();
validForm.setAllCode(oldCode.getAllCode());
validForm.setTitleHtmlKeyword(oldCode.getTitleHtmlKeyword());
validForm.setIndexHtmlKeyword(oldCode.getIndexHtmlKeyword());
model.addAttribute("validForm", validForm);
model.addAttribute("oldCode", oldCode);
model.addAttribute("titleNum", oldCode.getNewTitleList().size());
return "valid";
}
특이 사항으로 ValidForm을 미리 만들어서 oldcode값을 세팅해줬는데 valid.html에서 th:field를 사용하기 위해 코드를 이렇게 한 것입니다. 추후 다른 포스트에서 언급할텐데 th:field가 th:value를 포함하기에 둘은 중복되지 않습니다.
...
<form action="valid.html" th:action="@{/valid/{id}(id = ${oldCode.id})}" th:object="${validForm}" method="post">
<div class="form-group">
<label th:for="allcode">입력된 html코드 전문</label>
<textarea rows="10" cols="50" class="form-control" th:field="*{allCode}"></textarea>
</div>
...
<h3>유효성 검증</h3>
<p>타이틀 개수</p><p th:text="${titleNum}"></p>
<br>
<p>중복 여부</p>
<p th:if="${oldCode.indexOver == 2}">중복 없음</p>
<p th:unless="${oldCode.indexOver == 2}">중복 있음</p>
<br>
<a href="/" role="button" class="btn btn-secondary">취소</a>
<a th:if="${oldCode.indexOver == 2}" th:href="@{/{id}(id = ${oldCode.id})}" role="button" class="btn btn-primary">변환</a>
...
valid.html도 input과 똑같지만 하단에 타이틀 개수와 인덱스 중복 여부를 표시하며 우선 간단하게 검증이 가능한 인덱스 중복 여부만 통과하면 변환이 가능하도록 작성해줍니다.
@PostMapping("/valid/{id}")
public String validPost(@PathVariable("id") Long id, @ModelAttribute("validForm") ValidForm validForm, Model model) {
OldCode changeOne = new OldCode();
changeOne.setAllCode(validForm.getAllCode());
changeOne.setTitleHtmlKeyword(validForm.getTitleHtmlKeyword());
changeOne.setIndexHtmlKeyword(validForm.getIndexHtmlKeyword());
changeOne.init();
linkService.changeCode(changeOne);
OldCode findOne = linkService.findOne(id);
model.addAttribute("validForm", new ValidForm());
model.addAttribute("oldCode", findOne);
model.addAttribute("titleNum", findOne.getNewTitleList().size());
return "redirect:/valid/"+id;
}
코드 수정 버튼을 누르면 전송될 PostMapping의 경우 특별한 사항은 없습니다, 아래는 valid 페이지입니다.
#3 컨트롤러와 뷰 : result
@GetMapping("/{id}")
public String result(@PathVariable("id") Long id, Model model) {
NewCode result = linkService.newCode(id);
model.addAttribute("newCode", result);
return "result";
}
위에서 valid 페이지의 변환 버튼을 누르면 이동할 변환 결과 출력 페이지를 매핑해줍니다. html코드는 매우 간단하니 생략하겠습니다. 아래는 변환 결과 페이지입니다.
마지막으로 변환된 코드가 작동하는지 포스트에 적용해보고 작동이 되는 것을 확인하면 끝납니다.
이번 포스트의 경우 정말로 빠르게 웹에 이식하기 위해 정말 작동'만'되는 코드를 소개했습니다. 사실 정말 중요한건 다음 포스트부터 할 리팩토링 과정입니다. 위 코드는 정말 작동만 되도록 매우 빠르게 작성한 코드이기에 정말 많은 수정사항이 존재하며 아직 사용자를 고려하지도, 입력 값 검증 등 필수 사항도 마무리되지 않았습니다. 이 부분을 다음 포스트에서 하나하나 언급하겠습니다.
refer
https://github.com/kth1017/postLink
GitHub - kth1017/postLink: 티스토리 블로그 포스트에 사용될 인덱스 링크 자동 로직 프로젝트
티스토리 블로그 포스트에 사용될 인덱스 링크 자동 로직 프로젝트. Contribute to kth1017/postLink development by creating an account on GitHub.
github.com
'Project' 카테고리의 다른 글
P2_페이지 내 하이퍼 링크 달아주는 코드_4_TDD(2) (0) | 2022.07.02 |
---|---|
P2_페이지 내 하이퍼 링크 달아주는 코드_3_요구사항 분석과 TDD (0) | 2022.06.17 |
P2_페이지 내 하이퍼 링크 달아주는 코드_1_단순로직 (0) | 2022.06.08 |
P2_페이지 내 하이퍼 링크 달아주는 코드_준비 (0) | 2022.06.03 |
P1_게시판 프로젝트_4_페이징 기능 추가 (0) | 2022.05.12 |