본문 바로가기

Spring 공부

spring - 블로그 예제 service, repository, viewContoroller, templates

728x90
반응형

 

Controller: BlogService

@RequiredArgsConstructor  // final 이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service

 

@RequiredArgsConstructor는 Lombok에서 제공하는 어노테이션으로, 클래스의 필드 중 final로 선언된 필드나 @NotNull 어노테이션이 붙은 필드들을 대상으로한 생성자를 자동으로 생성한다. 이 생성자를 통해 객체를 생성할 때 해당 필드들의 값이 필요하다.

 

@Service는 Spring Framework에서 사용되는 어노테이션으로, 해당 클래스가 서비스 역할을 한다는 것을 나타낸다. Spring 컨테이너에 의해 관리되며, 주로 비즈니스 로직을 담당하는 클래스에 사용된다.

 

private final BlogRepository blogRepository;

 

해당 클래스의 필드로써 BlogRepository 타입의 객체를 가리킨다. 이 필드는 private으로 선언되어 클래스 내부에서만 접근이 가능하며, final 키워드가 사용되어 한 번 초기화되면 변경할 수 없는 상수이다.

 

일반적으로 이러한 패턴은 의존성 주입(Dependency Injection)을 통해 BlogRepository 객체가 해당 클래스에 주입되어야 한다는 것을 나타낸다. 이것은 Spring Framework에서 사용되는 방식 중 하나로, Spring이 BlogRepository의 구현체를 생성하고 관리하여 필요한 곳에 주입한다.

 

따라서 해당 클래스가 Spring의 빈으로 등록되어 있다면, Spring이 자동으로 BlogRepository 객체를 생성하고 주입한다. 이를 통해 해당 클래스에서 blogRepository 필드를 사용하여 데이터베이스와의 상호작용을 수행할 수 있다.

 

  public Article save(AddArticleRequest request) {  // save: JpaRepository에서 제공
    return blogRepository.save(request.toEntity());
  }

 

public Article save(AddArticleRequest request): 이 메서드는 AddArticleRequest 객체를 매개변수로 받아인다. 이 객체는 새로운 블로그 글의 제목과 내용을 포함한다.

 

return blogRepository.save(request.toEntity());: request.toEntity()를 호출하여 AddArticleRequest 객체를 Article 엔티티로 변환한다. 그런 다음, blogRepository의 save() 메서드를 호출하여 이 Article 객체를 데이터베이스에 저장한다. 저장된 블로그 글이 반환된다.

 

public List<Article> findAll() {
    return blogRepository.findAll();
  }

 

public List<Article> findAll(): 이 메서드는 데이터베이스에 저장된 모든 블로그 글을 검색하여 리스트로 반환한다.

 

return blogRepository.findAll();: blogRepository의 findAll() 메서드를 호출하여 데이터베이스에 저장된 모든 블로그 글을 가져온다. 이를 통해 데이터베이스에서 가져온 블로그 글 목록이 반환된다.

public Article findById(long id) {  // 글 id 를 이용해 조회
    return blogRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("not found : " + id));
              // 없으면 IllegalArgumentException 예외 발생
  }

 

public Article findById(long id): 이 메서드는 주어진 ID에 해당하는 블로그 글을 검색하여 반환한다.

 

return blogRepository.findById(id): blogRepository의 findById() 메서드를 호출하여 데이터베이스에서 해당 ID에 해당하는 블로그 글을 검색한다.

 

.orElseThrow(() -> new IllegalArgumentException("not found : " + id)): 만약 해당 ID에 해당하는 블로그 글을 찾을 수 없으면, orElseThrow() 메서드를 사용하여 IllegalArgumentException 예외를 발생시킨다. 이 예외는 "not found : [ID]" 형식의 메시지와 함께 발생한다.

 

  public void delete(long id) {
    blogRepository.deleteById(id);
  }

 

public void delete(long id): 이 메서드는 주어진 ID에 해당하는 블로그 글을 데이터베이스에서 삭제한다.

 

blogRepository.deleteById(id): blogRepository의 deleteById() 메서드를 호출하여 데이터베이스에서 해당 ID에 해당하는 블로그 글을 삭제한다.

 

@Transactional  // 하나의 트랜잭션으로 묶는 역할 , 에러가 발생해도 제대로된 값 수정을 보장한다
  public Article update(long id, UpdateArticleRequest request) {
    Article article = blogRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("not found : " + id)); // id로 기존 게시글 찾기

    article.update(request.getTitle(), request.getContent()); // 새 제목, 내용으로 업데이트

    return article;
  }

 

@Transactional 어노테이션은 하나의 트랜잭션으로 묶는 역할을 한다. 이는 메서드 내에서 일어나는 모든 작업이 하나의 트랜잭션으로 처리되도록 보장한다. 따라서 메서드 실행 중에 예외가 발생하더라도 데이터베이스의 일관성을 유지하고, 제대로 된 값 수정을 보장한다.

 

public Article update(long id, UpdateArticleRequest request): 이 메서드는 주어진 ID에 해당하는 블로그 글을 업데이트한다. 업데이트할 내용은 UpdateArticleRequest 객체에 담겨있다.

 

Article article = blogRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("not found : " + id));: 주어진 ID로 데이터베이스에서 해당 블로그 글을 찾는다. 만약 해당하는 글이 없으면 예외를 발생시킨다.

 

article.update(request.getTitle(), request.getContent());: 찾아낸 블로그 글을 업데이트한다. article.update() 메서드를 호출하여 새로운 제목과 내용으로 업데이트한다.

 

return article;: 업데이트된 블로그 글을 반환한다.

 

Repository: BlogRepository

 

 

public interface BlogRepository extends JpaRepository<Article,Long> {
}

 

JpaRepository<Article, Long>: JpaRepository는 JPA 리포지토리를 구현하기 위한 인터페이스. Article은 엔티티의 타입을 나타내며, Long은 해당 엔티티의 기본 키(PK)의 타입을 나타낸다. 따라서 BlogRepository는 Article 엔티티에 대한 JPA 리포지토리이며, 기본 키로는 Long 타입을 사용한다.

 

JpaRepository에는 기본적인 CRUD(Create, Read, Update, Delete) 작업을 수행하는 메서드가 포함되어 있다. 이를 통해 데이터베이스에 쉽게 접근하여 데이터를 조작할 수 있다.

 

 

viewController: BlogViewController

 

 

@RequiredArgsConstructor
@Controller

 

@RequiredArgsConstructor 어노테이션은 Lombok에서 제공하는 기능으로, 클래스의 필드 중 final로 선언된 필드나 @NotNull 어노테이션이 붙은 필드들에 대한 생성자를 자동으로 생성한다.

 

@Controller 어노테이션은 Spring Framework에서 제공하는 어노테이션으로, 해당 클래스가 웹 애플리케이션에서 Controller 역할을 한다는 것을 나타낸다. Spring 컨테이너에 의해 관리되며, 클라이언트의 요청을 처리하고 응답을 반환하는 역할한다.

 

private final BlogService blogService;

 

의존성 주입(Dependency Injection)을 통해 해당 클래스에 주입되어야 한다. 이것은 Spring Framework에서 사용되는 방식 중 하나로, Spring이 BlogService의 구현체를 생성하고 관리하여 필요한 곳에 주입한다.

 

따라서 해당 클래스가 Spring의 빈으로 등록되어 있다면, Spring이 자동으로 BlogService 객체를 생성하고 주입된다.

@GetMapping("/articles")
  public String getArticles(Model model) {
    List<ArticleListViewResponse> articles = blogService.findAll()
            .stream()
            .map(ArticleListViewResponse::new)
            .toList();
    model.addAttribute("articles", articles);

    return "articleList";
  }

 

@GetMapping("/articles"): 이 어노테이션은 HTTP GET 요청을 처리하는 메서드임을 나타낸다. 즉, "/articles" 경로로 들어오는 GET 요청을 이 메서드가 처리한다.

 

public String getArticles(Model model): 이 메서드는 요청을 처리하고, 화면에 데이터를 전달하기 위해 Spring의 Model 객체를 매개변수로 받는다. 이 모델 객체를 사용하여 뷰 템플릿으로 데이터를 전달한다.

 

List<ArticleListViewResponse> articles = blogService.findAll()...: blogService를 사용하여 모든 블로그 글을 가져온다. 그리고 각 글을 ArticleListViewResponse 객체로 매핑한 후, 리스트로 변환한다.

 

model.addAttribute("articles", articles);: articles라는 이름으로 가져온 블로그 글 목록을 모델에 추가한다. 이를 통해 뷰 템플릿에서 해당 데이터를 사용할 수 있다.

 

return "articleList";: 화면에 표시할 뷰 템플릿의 이름을 반환합니다. 여기서는 "articleList"라는 이름의 뷰를 사용한다.

 

@GetMapping("/articles/{id}")  // article 페이지에서 사용
  public String getArticle(@PathVariable Long id, Model model) {
    Article article = blogService.findById(id);
    model.addAttribute("article", new ArticleViewResponse(article));

    return "article";
  }

 

@GetMapping("/articles/{id}"): 이 어노테이션은 HTTP GET 요청을 처리하는 메서드임. 즉, "/articles/{id}" 경로로 들어오는 GET 요청을 이 메서드가 처리한다. {id}는 동적으로 변하는 값으로, 여기에 해당하는 블로그 글을 조회하게 된다.

 

public String getArticle(@PathVariable Long id, Model model): 이 메서드는 요청을 처리하고, 화면에 데이터를 전달하기 위해 Spring의 Model 객체를 매개변수로 받는다. 또한 @PathVariable을 사용하여 경로 변수인 id를 매개변수로 받는다.

 

Article article = blogService.findById(id);: blogService를 사용하여 해당 id에 해당하는 블로그 글을 조회한다.

 

model.addAttribute("article", new ArticleViewResponse(article));: 조회한 블로그 글을 ArticleViewResponse 객체로 변환하여 모델에 추가한다. 이를 통해 뷰 템플릿에서 해당 데이터를 사용할 수 있다.

 

return "article";: 화면에 표시할 뷰 템플릿의 이름을 반환합니다. 여기서는 "article"이라는 이름의 뷰를 사용한다.

 

@GetMapping("/new-article")
  // id 키를 가진 쿼리 파라미터의 값을 id 변수에 매핑 (id는 없을 수도 있음)
  public String newArticle(@RequestParam(required = false) Long id, Model model) {
    if (id == null) {  // id가 없으면 생성
      model.addAttribute("article", new ArticleViewResponse());
    } else { // id가 있으면 생성
      Article article = blogService.findById(id);
      model.addAttribute("article", new ArticleViewResponse(article));
    }

    return "newArticle";
  }

 

@GetMapping("/new-article"): 이 어노테이션은 HTTP GET 요청을 처리하는 메서드임. 즉, "/new-article" 경로로 들어오는 GET 요청을 이 메서드가 처리한다.

 

public String newArticle(@RequestParam(required = false) Long id, Model model): 이 메서드는 요청을 처리하고, 화면에 데이터를 전달하기 위해 Spring의 Model 객체를 매개변수로 받는다. 또한 @RequestParam을 사용하여 쿼리 파라미터인 id를 매개변수로 받는다. required = false로 설정되어 있으므로, id가 없는 경우에도 요청을 처리할 수 있다.

 

if (id == null) { ... } else { ... }: id가 null이면 새로운 블로그 글을 생성하는 경우이며, 그렇지 않으면 해당 id에 해당하는 기존의 블로그 글을 수정하는 경우.

 

model.addAttribute("article", new ArticleViewResponse());: 새로운 블로그 글을 생성하는 경우, 빈 ArticleViewResponse 객체를 모델에 추가한다.

 

Article article = blogService.findById(id);: 기존의 블로그 글을 수정하는 경우, 해당 id에 해당하는 블로그 글을 조회한다.

model.addAttribute("article", new ArticleViewResponse(article));: 조회한 블로그 글을 ArticleViewResponse 객체로 변환하여 모델에 추가한다.

 

return "newArticle";: 화면에 표시할 뷰 템플릿의 이름을 반환한다. 여기서는 "newArticle"이라는 이름의 뷰를 사용한다.

 

templates

 

articleList

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>블로그 글 목록</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
    <h1 class="mb-3">My Blog</h1>
    <h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>

<div class="container">  //글 목록을 담는 컨테이너
    <button type="button" id="create-btn"
            th:onclick="|location.href='@{/new-article}'|"
            class="btn btn-secondary btn-sm mb-3">글 등록</button>
            //글 등록 버튼. 클릭하면 "/new-article" 경로로 이동
    <div class="row-6" th:each="item : ${articles}"> <!-- article 갯수만큼 반복 -->
    //글 목록을 나타내는 <div> 요소, th:each로 글 목록을 반복하며 각각의 글을 표시
        <div class="card"> //각 글을 담는 카드 요소
            <div class="card-header" th:text="${item.id}"> <!-- item id 출력 -->
            </div>
            <div class="card-body">
                <h5 class="card-title" th:text="${item.title}"></h5>
                <p class="card-text" th:text="${item.content}"></p>
                <a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러가기</a>
            </div>
        </div>
        <br>
    </div>

    <button type="button" class="btn btn-secondary" onclick="location.href='/logout'">로그아웃</button>
</div>

<script src="/js/article.js"></script>
</body>

 

article

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>블로그 글</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
    <h1 class="mb-3">My Blog</h1>
    <h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>

<div class="container mt-5">
    <div class="row">
        <div class="col-lg-8">
            <article>
                <input type="hidden" id="article-id" th:value="${article.id}">
                <header class="mb-4">
                    <h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
                    <div class="text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyyy-MM-dd HH:mm')}|"></div>
                    <!-- 날짜형식 포매팅 -->

                </header>
                <section class="mb-5">
                    <p class="fs-5 mb-4" th:text="${article.content}"></p>
                </section>
                <button type="button" id="modify-btn"
                        th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|"
                        class="btn btn-primary btn-sm">수정</button>
                <button type="button" id="delete-btn"
                        class="btn btn-secondary btn-sm">삭제</button>
            </article>
        </div>
    </div>
</div>

<script src="/js/article.js"></script>
</body>

 

 

newArticle

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>블로그 글</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
    <h1 class="mb-3">My Blog</h1>
    <h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>

<div class="container mt-5">
    <div class="row">
        <div class="col-lg-8">
            <article>
                <input type="hidden" id="article-id" th:value="${article.id}">
                <!-- id 저장 -->
                <header class="mb-4">
                    <input type="text" class="form-control" placeholder="제목" id="title" th:value="${article.title}">
                </header>
                <section class="mb-5">
                    <textarea class="form-control h-25" rows="10" placeholder="내용" id="content" th:text="${article.content}"></textarea>
                </section>
                <button th:if="${article.id} != null" type="button" id="modify-btn" class="btn btn-primary btn-sm">수정</button>
                <button th:if="${article.id} == null" type="button" id="create-btn" class="btn btn-primary btn-sm">등록</button>
            </article>
        </div>
    </div>
</div>

<script src="/js/article.js"></script>
</body>

 

 

 

 

static.js: article.js

 

 

// 삭제 기능
const deleteButton = document.getElementById('delete-btn');

if (deleteButton) {
    deleteButton.addEventListener('click', event => {
        let id = document.getElementById('article-id').value;
        fetch(`/api/articles/${id}`, {
            method: 'DELETE'
        })
            .then(() => {
                alert('삭제가 완료되었습니다.');
                location.replace('/articles');  // articles 페이지로 이동
            });
    });
}

 

const deleteButton = document.getElementById('delete-btn');: HTML에서 id가 'delete-btn'인 요소를 찾아서 deleteButton 변수에 할당한다.

 

if (deleteButton) { ... }: deleteButton이 존재하는 경우에만 이벤트 리스너를 추가한다.

 

deleteButton.addEventListener('click', event => { ... }): deleteButton이 클릭되었을 때의 이벤트를 처리하는 리스너를 추가한다.

 

let id = document.getElementById('article-id').value;: HTML에서 id가 'article-id'인 요소의 값을 가져와서 id 변수에 할당한다. 이는 삭제할 글의 id이다.

 

fetch(/api/articles/${id}, { ... }): fetch 함수를 사용하여 서버의 '/api/articles/{id}' 엔드포인트에 DELETE 요청을 보낸다. 이를 통해 해당 id에 해당하는 글을 삭제한다.

 

.then(() => { ... }): DELETE 요청이 성공적으로 처리되면 실행된다. 삭제가 완료되었다는 경고창을 띄우고, '/articles' 페이지로 이동한다.

 

// 수정 기능
const modifyButton = document.getElementById('modify-btn');

if (modifyButton) {
    modifyButton.addEventListener('click', event => {
        let params = new URLSearchParams(location.search);
        let id = params.get('id');

        fetch(`/api/articles/${id}`, {
            method: 'PUT',
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                title: document.getElementById('title').value,
                content: document.getElementById('content').value
            })
        })
            .then(() => {
                alert('수정이 완료되었습니다.');
                location.replace(`/articles/${id}`);
            });
    });
}

 

const modifyButton = document.getElementById('modify-btn');: HTML에서 id가 'modify-btn'인 요소를 찾아서 modifyButton 변수에 할당한다.

 

if (modifyButton) { ... }: modifyButton이 존재하는 경우에만 이벤트 리스너를 추가한다.

 

modifyButton.addEventListener('click', event => { ... }): modifyButton이 클릭되었을 때의 이벤트를 처리하는 리스너를 추가한다.

 

let params = new URLSearchParams(location.search);: 현재 URL의 쿼리 파라미터를 가져와서 params 변수에 할당한다.

let id = params.get('id');: params에서 'id'라는 이름의 쿼리 파라미터 값을 가져와서 id 변수에 할당한다. 이는 수정할 글의 id이다.

 

fetch(/api/articles/${id}, { ... }): fetch 함수를 사용하여 서버의 '/api/articles/{id}' 엔드포인트에 PUT 요청을 보낸다. 이를 통해 해당 id에 해당하는 글을 수정한다.

 

method: 'PUT': PUT 메서드를 사용하여 수정 요청을 보낸다.

 

headers: { "Content-Type": "application/json", },: 요청 헤더에 JSON 형식의 컨텐츠를 전달한다는 것을 명시한다.

 

body: JSON.stringify({ ... }): 수정할 내용을 JSON 형식으로 변환하여 요청의 본문에 포함시킨다. 제목과 내용을 가져와서 객체로 만든다.

 

.then(() => { ... }): PUT 요청이 성공적으로 처리되면 실행된다. 수정이 완료되었다는 경고창을 띄우고, 수정한 글의 상세 페이지로 이동한다.

 

// 생성 기능
const createButton = document.getElementById('create-btn');

if (createButton) {
    createButton.addEventListener('click', event => {
        fetch('/api/articles', {
            method: 'POST',
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                title: document.getElementById('title').value,
                content: document.getElementById('content').value
            })
        })
            .then(() => {
                alert('등록 완료되었습니다.');
                location.replace('/articles');
            });
    });
}

 

const createButton = document.getElementById('create-btn');: HTML에서 id가 'create-btn'인 요소를 찾아서 createButton 변수에 할당한다.

 

if (createButton) { ... }: createButton이 존재하는 경우에만 이벤트 리스너를 추가한다.

 

createButton.addEventListener('click', event => { ... }): createButton이 클릭되었을 때의 이벤트를 처리하는 리스너를 추가한다.

 

fetch('/api/articles', { ... }): fetch 함수를 사용하여 서버의 '/api/articles' 엔드포인트에 POST 요청을 보낸다. 이를 통해 새로운 글을 등록한다.

 

method: 'POST': POST 메서드를 사용하여 등록 요청을 보낸다.

 

headers: { "Content-Type": "application/json", },: 요청 헤더에 JSON 형식의 컨텐츠를 전달한다는 것을 명시한다.

 

body: JSON.stringify({ ... }): 등록할 내용을 JSON 형식으로 변환하여 요청의 본문에 포함시킵니다. 제목과 내용을 가져와서 객체로 만든다.

 

.then(() => { ... }): POST 요청이 성공적으로 처리되면 실행된다. 등록이 완료되었다는 경고창을 띄우고, 글 목록 페이지로 이동한다.

반응형