▼ What ?
이번 커리큘럼에선 먼저 패키지를 도메인(Domain) 별로 정리해본 다음, 특정 URL에 대한 매핑을 해놓지 않으면 어떤 오류가 발생하며, 이 오류를 해결하는 과정에서 사용되는 애너테이션(@ResponseBody)에 대해서도 알아보았다. 그리고, 템플릿(Template)과 ROOT URL에 대해 살펴보고, 서비스(Service) 클래스가 추가되었을 때의 데이터 처리의 흐름에 대해 알아보았다.
▼ 도메인(Domain)별로 파일 분류
어떤 기준?
- 도메인("질문", "답변", "사용자"처럼 굵직한 요구사항 또는 문제 영역)별로 패키지를 나눠 분류한다.

▼ URL 매핑
URL 매핑 ?
- "http://localhost:8080/question/list" URL에 대한 매핑을 해놓지 않으면 아래와 같은 404 에러가 발생한다 !

- 이를 해결하기 위해선 "/question/list" URL에 대한 매핑이 있는 컨트롤러를 생성해주면 된다.
[ .../webboard/question/QuestionController.java ]
package com.gdsc.webboard.question;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class QuestionController {
@GetMapping("/question/list")
@ResponseBody
public String list() {
return "question list";
}
}
@ResponseBody ?
- 자바 객체(문자열(String 객체) - "question list")를 HTTP 요청(Requset)의 본문(Body)내용으로 매핑하여 클라이언트로 전송한다.
➜ 즉, '@ResponseBody'를 사용하면 HTTP 요청의 Body를 자바 객체로 대신 전달할 수 있는 것이다 ! - @RestController ➜ @Controller + @ResponseBody
@RequestBody ?
- '@RequestBody'가 붙은 인자(Parameter)에는 HTTP 요청의 본문(Body)이 그대로 전달된다.
( 일반적인 GET/POST의 요청 Parameter라면 '@RequestBody'를 사용할 일 X ) - xml이나 json기반의 메시지를 사용하는 요청의 경우에 이 방법이 매우 유용하다
➜ HTTP 요청의 Body를 통째로 자바 객체로 변환해서 매핑된 메소드 파라미터로 전달해주기 때문이다 !
@RequestBody와 @ResponseBody의 흐름
- 클라이언트에서 서버로 필요한 데이터를 요청하기 위해 JSON 데이터를 요청 본문(Body)에 담아서 서버로 보내면, 서버에서는 @RequestBody 애너테이션을 사용하여 HTTP 요청 본문에 담긴 값들을 자바 객체로 변환시켜, 객체에 저장한다.
- 서버에서 클라이언트로 응답 데이터를 전송하기 위해 @ResponseBody 어노테이션을 사용하여 자바 객체를 HTTP 응답 본문(Body)의 객체로 변환하여 클라이언트로 전송한다.
참고자료 - [Network] 웹의 동작 원리 + HTTP & HTTPS — Uykm_Note (tistory.com)
[Network] 웹의 동작 원리 + HTTP & HTTPS
▼ Why ? What ? HTTP는 서버와 클라이언트가 데이터를 주고 받기 위한 Protocol이기 때문에, 웹이 동작하는 과정을 이해하려면 웹 개발을 하기 위해서 알아야 하는 가장 중요한 개념들 중 하나이다. 그
ukym-tistory.tistory.com
▼ 템플릿 (Template)
템플릿(Template) ?
- 하지만, 보통 브라우저에 응답하는 문자열("question list")은 위의 예처럼 자바 코드에서 직접 만들지는 않는다 !
➜ 템플릿(Template)을 활용하자 ! - 템플릿 엔진인 '타임리프(thymeleaf)' 설치.
➜ /webboard/build.gradle ➜ "Refresh Gradle Project"
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}
- 템플릿 사용
➜ 템플릿 생성.
[ /src/main/resources/templates/question_list.html ]
➜ 컨트롤러 수정.
@Controller
public class QuestionController {
@GetMapping("/question/list")
// @ResponseBody
public String list() {
return "question_list"; // 템플릿 파일 이름
}
}
🔻 "localhost:8080/question/list" 에 접속해보면, question_list.html 파일에 해당하는 내용이 브라우저에 출력되는 것을 확인 가능하다.
데이터를 조회하여 템플릿에 전달
- 질문 목록 데이터를 조회(questionRepository.findAll())하기 위해선 리포지터리(Repository)를 활용해야 한다 !
➜ 리포지터리를 이용해서 조회한 데이터를 Model 클래스를 이용해서, "questionList" 라는 이름으로 Model 객체에 질문 목록 데이터를 저장하고 해당 객체를 템플릿에 전달 !
package com.gdsc.webboard.question;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionRepository questionRepository;
@GetMapping("/question/list")
public String list(Model model) {
List<Question> questionList = this.questionRepository.findAll();
model.addAttribute("questionList", questionList);
return "question_list";
}
}
🔻 Lombok에서 제공하는 @RequiredArgsConstructor 애너테이션을 이용해서 자동으로 생성자를 생성해주고, DI에 의해 'questionRepository' 객체가 자동으로 주입되었다.
➜ 테스트 코드에선 '@Autowired' 애너테이션을 이용해서 객체를 주입했지만, 위처럼 생성자를 작성해서 객체를 주입하는 것이 적절한 방법이다 !
참고자료 (DI) - [Spring] DI & @Autowired — Uykm_Note (tistory.com)
[Spring] DI & @Autowired
▼ Why ? What ? ▼ DI (Dependency Injection) 의존성 종속; class 간의 의존관계를 Spring Container가 자동으로 연결해주는 것 ➜ class A가 class B, C와 상호작용한다면, 객체 A는 객체 B, C와 의존관계이다 의존관
ukym-tistory.tistory.com
참고자료 (테스트 코드) - 커리큘럼 2주차 - Repository & CRUD (+ Fetch type(Lazy, Eager)) — Uykm_Note (tistory.com)
커리큘럼 2주차 - Repository & CRUD (+ Fetch type(Lazy, Eager))
▼ What ? 이전 내용에 이어서 커리큘럼 2주차에 공부한 내용인 리포지터리(Repository), CRUD 연산, 그리고 Fetch type에 대해 정리해보려고 한다. ▼ 리포지터리 (Repository) 리포지터리가 왜 필요할까 ? 엔
ukym-tistory.tistory.com
템플릿에서 전달받은 데이터 사용
- "questionList" 라는 이름의 Model 객체에 저장된 값을 템플릿에서 사용한다.
[ /web-board/src/main/resources/templates/question_list.html ]
➜ 타임리프는 Model 객체에 저장된 값을 읽을 수 있으므로 템플릿에서 questionList를 사용.
( th: 로 시작하는 속성은 타임리프 템플릿 엔진이 사용하는 속성이다. )
<table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${questionList}"> <!-- 자바 코드와 연결된 부분 -->
<td th:text="${question.subject}"></td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
🔻 "localhost:8080/question/list" 에 접속해보면, 아래와 같은 화면을 확인해볼 수 있다.

<tr th:each="question : ${questionList}">
🔻 <tr> ... </tr> 엘리먼트를 questionList의 갯수만큼 반복하여 출력하는 역할.
자주 사용하는 타임리프의 속성
- 분기문 속성
th:if="${question != null}"
🔻 question 객체가 null 이 아닌 경우에 해당 엘리먼트가 표시된다.
- 반복문 속성 ( ~ 자바의 for each문 )
➜ 반복횟수만큼 해당 엘리먼트를 반복하여 표시한다.
th:each="question : ${questionList}"
th:each="question, loop : ${questionList}"
🔻 loop 객체를 이용하면 루프 내에서 다음과 같은 속성 사용 가능하다.
loop.index - 반복 순서, 0부터 1씩 증가
loop.count - 반복 순서, 1부터 1씩 증가
loop.size - 반복 객체의 요소 갯수 (예: questionList의 요소 갯수)
loop.first - 루프의 첫번째 순서인 경우 true
loop.last - 루프의 마지막 순서인 경우 true
loop.odd - 루프의 홀수번째 순서인 경우 true
loop.even - 루프의 짝수번째 순서인 경우 true
loop.current - 현재 대입된 객체 (예: 위의 경우 question과 동일)
- 텍스트 속성
➜ th:text=값 속성은 해당 엘리먼트(question.subject)의 텍스트로 "값"을 출력한다.
th:text="${question.subject}"
<tr th:each="question : ${questionList}">
<td>[[${question.subject}]]</td>
<td>[[${question.createDate}]]</td>
</tr>
🔻 이처럼 대괄호를 사용해서도 값을 출력 가능하다.
▼ ROOT URL
ROOT URL ?
- "http://localhost:8080" 처럼 도메인명과 포트 뒤에 아무것도 붙이지 않은 URL을 말한다.
- 이러한 ROOT URL에 대한 매핑을 만들지 않았다면?
➜ 브라우저에서 ROOT URL에 접속했을 때 404 에러가 발생한다 !
ROOT URL 매핑
- MainController에 "/" URL을 매핑한 root 메서드 추가.
➜ "http://localhost:8080" 로 접속을 하면 root 메서드가 실행되어 질문 목록이 표시되는 것을 확인할 수 있다.
package com.gdsc.webboard;
@Controller
public class MainController {
...
@GetMapping("/")
public String root() {
return "redirect:/question/list";
}
}
🔻 return한 "redirect:/question/list" 는 "/question/list" URL로 페이지를 리다이렉트 하라는 명령어이다.
- redirect:<URL> - URL로 리다이렉트 ( 리다이렉트 - 완전히 새로운 URL로 요청 )
- forward:<URL> - URL로 포워드 ( 포워드 - 기존 요청 값들이 유지된 상태로 URL이 전환 )
▼ What ?
이번 커리큘럼에선 먼저 패키지를 도메인(Domain) 별로 정리해본 다음, 특정 URL에 대한 매핑을 해놓지 않으면 어떤 오류가 발생하며, 이 오류를 해결하는 과정에서 사용되는 애너테이션(@ResponseBody)에 대해서도 알아보았다. 그리고, 템플릿(Template)과 ROOT URL에 대해 살펴보고, 서비스(Service) 클래스가 추가되었을 때의 데이터 처리의 흐름에 대해 알아보았다.
▼ 도메인(Domain)별로 파일 분류
어떤 기준?
- 도메인("질문", "답변", "사용자"처럼 굵직한 요구사항 또는 문제 영역)별로 패키지를 나눠 분류한다.

▼ URL 매핑
URL 매핑 ?
- "http://localhost:8080/question/list" URL에 대한 매핑을 해놓지 않으면 아래와 같은 404 에러가 발생한다 !

- 이를 해결하기 위해선 "/question/list" URL에 대한 매핑이 있는 컨트롤러를 생성해주면 된다.
[ .../webboard/question/QuestionController.java ]
package com.gdsc.webboard.question;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class QuestionController {
@GetMapping("/question/list")
@ResponseBody
public String list() {
return "question list";
}
}
@ResponseBody ?
- 자바 객체(문자열(String 객체) - "question list")를 HTTP 요청(Requset)의 본문(Body)내용으로 매핑하여 클라이언트로 전송한다.
➜ 즉, '@ResponseBody'를 사용하면 HTTP 요청의 Body를 자바 객체로 대신 전달할 수 있는 것이다 ! - @RestController ➜ @Controller + @ResponseBody
@RequestBody ?
- '@RequestBody'가 붙은 인자(Parameter)에는 HTTP 요청의 본문(Body)이 그대로 전달된다.
( 일반적인 GET/POST의 요청 Parameter라면 '@RequestBody'를 사용할 일 X ) - xml이나 json기반의 메시지를 사용하는 요청의 경우에 이 방법이 매우 유용하다
➜ HTTP 요청의 Body를 통째로 자바 객체로 변환해서 매핑된 메소드 파라미터로 전달해주기 때문이다 !
@RequestBody와 @ResponseBody의 흐름
- 클라이언트에서 서버로 필요한 데이터를 요청하기 위해 JSON 데이터를 요청 본문(Body)에 담아서 서버로 보내면, 서버에서는 @RequestBody 애너테이션을 사용하여 HTTP 요청 본문에 담긴 값들을 자바 객체로 변환시켜, 객체에 저장한다.
- 서버에서 클라이언트로 응답 데이터를 전송하기 위해 @ResponseBody 어노테이션을 사용하여 자바 객체를 HTTP 응답 본문(Body)의 객체로 변환하여 클라이언트로 전송한다.
참고자료 - [Network] 웹의 동작 원리 + HTTP & HTTPS — Uykm_Note (tistory.com)
[Network] 웹의 동작 원리 + HTTP & HTTPS
▼ Why ? What ? HTTP는 서버와 클라이언트가 데이터를 주고 받기 위한 Protocol이기 때문에, 웹이 동작하는 과정을 이해하려면 웹 개발을 하기 위해서 알아야 하는 가장 중요한 개념들 중 하나이다. 그
ukym-tistory.tistory.com
▼ 템플릿 (Template)
템플릿(Template) ?
- 하지만, 보통 브라우저에 응답하는 문자열("question list")은 위의 예처럼 자바 코드에서 직접 만들지는 않는다 !
➜ 템플릿(Template)을 활용하자 ! - 템플릿 엔진인 '타임리프(thymeleaf)' 설치.
➜ /webboard/build.gradle ➜ "Refresh Gradle Project"
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}
- 템플릿 사용
➜ 템플릿 생성.
[ /src/main/resources/templates/question_list.html ]
➜ 컨트롤러 수정.
@Controller
public class QuestionController {
@GetMapping("/question/list")
// @ResponseBody
public String list() {
return "question_list"; // 템플릿 파일 이름
}
}
🔻 "localhost:8080/question/list" 에 접속해보면, question_list.html 파일에 해당하는 내용이 브라우저에 출력되는 것을 확인 가능하다.
데이터를 조회하여 템플릿에 전달
- 질문 목록 데이터를 조회(questionRepository.findAll())하기 위해선 리포지터리(Repository)를 활용해야 한다 !
➜ 리포지터리를 이용해서 조회한 데이터를 Model 클래스를 이용해서, "questionList" 라는 이름으로 Model 객체에 질문 목록 데이터를 저장하고 해당 객체를 템플릿에 전달 !
package com.gdsc.webboard.question;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionRepository questionRepository;
@GetMapping("/question/list")
public String list(Model model) {
List<Question> questionList = this.questionRepository.findAll();
model.addAttribute("questionList", questionList);
return "question_list";
}
}
🔻 Lombok에서 제공하는 @RequiredArgsConstructor 애너테이션을 이용해서 자동으로 생성자를 생성해주고, DI에 의해 'questionRepository' 객체가 자동으로 주입되었다.
➜ 테스트 코드에선 '@Autowired' 애너테이션을 이용해서 객체를 주입했지만, 위처럼 생성자를 작성해서 객체를 주입하는 것이 적절한 방법이다 !
참고자료 (DI) - [Spring] DI & @Autowired — Uykm_Note (tistory.com)
[Spring] DI & @Autowired
▼ Why ? What ? ▼ DI (Dependency Injection) 의존성 종속; class 간의 의존관계를 Spring Container가 자동으로 연결해주는 것 ➜ class A가 class B, C와 상호작용한다면, 객체 A는 객체 B, C와 의존관계이다 의존관
ukym-tistory.tistory.com
참고자료 (테스트 코드) - 커리큘럼 2주차 - Repository & CRUD (+ Fetch type(Lazy, Eager)) — Uykm_Note (tistory.com)
커리큘럼 2주차 - Repository & CRUD (+ Fetch type(Lazy, Eager))
▼ What ? 이전 내용에 이어서 커리큘럼 2주차에 공부한 내용인 리포지터리(Repository), CRUD 연산, 그리고 Fetch type에 대해 정리해보려고 한다. ▼ 리포지터리 (Repository) 리포지터리가 왜 필요할까 ? 엔
ukym-tistory.tistory.com
템플릿에서 전달받은 데이터 사용
- "questionList" 라는 이름의 Model 객체에 저장된 값을 템플릿에서 사용한다.
[ /web-board/src/main/resources/templates/question_list.html ]
➜ 타임리프는 Model 객체에 저장된 값을 읽을 수 있으므로 템플릿에서 questionList를 사용.
( th: 로 시작하는 속성은 타임리프 템플릿 엔진이 사용하는 속성이다. )
<table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${questionList}"> <!-- 자바 코드와 연결된 부분 -->
<td th:text="${question.subject}"></td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
🔻 "localhost:8080/question/list" 에 접속해보면, 아래와 같은 화면을 확인해볼 수 있다.

<tr th:each="question : ${questionList}">
🔻 <tr> ... </tr> 엘리먼트를 questionList의 갯수만큼 반복하여 출력하는 역할.
자주 사용하는 타임리프의 속성
- 분기문 속성
th:if="${question != null}"
🔻 question 객체가 null 이 아닌 경우에 해당 엘리먼트가 표시된다.
- 반복문 속성 ( ~ 자바의 for each문 )
➜ 반복횟수만큼 해당 엘리먼트를 반복하여 표시한다.
th:each="question : ${questionList}"
th:each="question, loop : ${questionList}"
🔻 loop 객체를 이용하면 루프 내에서 다음과 같은 속성 사용 가능하다.
loop.index - 반복 순서, 0부터 1씩 증가
loop.count - 반복 순서, 1부터 1씩 증가
loop.size - 반복 객체의 요소 갯수 (예: questionList의 요소 갯수)
loop.first - 루프의 첫번째 순서인 경우 true
loop.last - 루프의 마지막 순서인 경우 true
loop.odd - 루프의 홀수번째 순서인 경우 true
loop.even - 루프의 짝수번째 순서인 경우 true
loop.current - 현재 대입된 객체 (예: 위의 경우 question과 동일)
- 텍스트 속성
➜ th:text=값 속성은 해당 엘리먼트(question.subject)의 텍스트로 "값"을 출력한다.
th:text="${question.subject}"
<tr th:each="question : ${questionList}">
<td>[[${question.subject}]]</td>
<td>[[${question.createDate}]]</td>
</tr>
🔻 이처럼 대괄호를 사용해서도 값을 출력 가능하다.
▼ ROOT URL
ROOT URL ?
- "http://localhost:8080" 처럼 도메인명과 포트 뒤에 아무것도 붙이지 않은 URL을 말한다.
- 이러한 ROOT URL에 대한 매핑을 만들지 않았다면?
➜ 브라우저에서 ROOT URL에 접속했을 때 404 에러가 발생한다 !
ROOT URL 매핑
- MainController에 "/" URL을 매핑한 root 메서드 추가.
➜ "http://localhost:8080" 로 접속을 하면 root 메서드가 실행되어 질문 목록이 표시되는 것을 확인할 수 있다.
package com.gdsc.webboard;
@Controller
public class MainController {
...
@GetMapping("/")
public String root() {
return "redirect:/question/list";
}
}
🔻 return한 "redirect:/question/list" 는 "/question/list" URL로 페이지를 리다이렉트 하라는 명령어이다.
- redirect:<URL> - URL로 리다이렉트 ( 리다이렉트 - 완전히 새로운 URL로 요청 )
- forward:<URL> - URL로 포워드 ( 포워드 - 기존 요청 값들이 유지된 상태로 URL이 전환 )