스프링 부트 & DB연결


1. Coltroller:

- 사용자 요청을 URL을 통해 전달 받음. 결과를 클라이언트에 전달함.

- 여러가지 폼 데이터를 상위 객체로 묶어서 전달 (DTO)


2. Service

- 컨트롤러의 요청을 수행함

- db데이터를 가공하기 위한 요청을 받아서 전달

- 객체형식으로 db테이블의 구조를 생성에서 전달


3. Repositoy

- service의 요청을 수행할 때, db가 알아들을 수 있는 언어로 변환

- JPA (객체의 내용을 SQL문법으로 변환해서 DB작업 실행)


롬북 

함수명이 자동으로 생성되며, 이때의 함수명은 변수명을 카멜형태로 바꾼다. 변수명이 스네이크형이라고 해도 카멜형으로 바꾼다. (해당 부분은 롬북의 디폴트 옵션이므로 바꿀 수 있는 있다.)

적용전:


적용후:



Autowired 어노테이션을 생략할 수 있다.

적용전:


적용후:

@RequiredArgsConstructor
private final JoinService joinService;

final 키워드는 자바에서 "한 번만 할당할 수 있다", 즉 값이 변경될 수 없다는 의미를 갖는 예약어입니다.


DB연결( MY SQL)

준비

build.gradle

plugins { id 'java' id 'org.springframework.boot' version '3.4.4' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.mysql:mysql-connector-j' } tasks.named('test') { useJUnitPlatform() }


resources / application.yml

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/사용할db명 username: root password: 1234 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true


Repository 계층 정리

  • Entity: db테이블과 1:1로 매핑되는 자바 클래스
  • Reopository: Entity 내용을 바탕으로 DB의 데이터를 조작하는 계층
  • JPA: db입출력을 위한 명령어 제공
  • Hibernate: JPA 명령어를 실제로 처리하도록 구현한 엔진
  • db제어 실행흐름: 사용자 요청 > Controller > Service > Repository (JPA, Hibernate)


사용된 객체 정리

  • dto: form에서 넘겨 받은 객체의 규격을 정한다. (ex: JoinDTO.java)

  • entity: 테이블 스키마를 기준으로 Repo의 모양을 결정 (ex: JoinEntity.java)
    • table(Join_entity) = class(JoinEntity)
    • entity를 사용 중이라면, form에서 넘겨 받는 객체의 규격을 따로 dto로 정의할 필요가 없다. entity를 사용해도 된다. (단, entity로 받아서 다시 entity로 넘길 경우 받는 클래스도 entity로 선언하자.)

  • repository: db 처리를 위한 객체 (ex: JoinRepo.java)

  • model: 뷰템플릿(동적 html)에 전달하기 위한 객체 (ex: JoinController -> Viewhtml)


JoinEntity.java

package com.example.basic.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class JoinEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String uname;
private String email;
private String colors;
}


JoinRepo.java

JpaRepository 라이브러리를 상속.
JoinEntity 객체 타입 사용. 

package com.example.basic.repository;
import com.example.basic.entity.JoinEntity;
import org.springframework.data.jpa.repository.JpaRepository;

//JPA 인터페이스만 정의
//이때 JpaRepository를 상속하면서 전달되는 데이터를 JoinEntity내용으로 강제
public class JoinRepo extends JpaRepository<JoinEntity, Long> {
}


JoinController.java

join -> join.html -> join/create -> save(user) -> admin.html -> findAll()
@Controller
// 인스턴스를 담을 멤버변수에 final 지정 가능케 함
@RequiredArgsConstructor
public class JoinController {
// 서비스 계층으로부터 joinService라는 인스턴스 @Autowired라는 어노테이션을 통해 바로 해당 인스턴스 객체를 바로 가져올 수 있음 (new 연산자 호출 필요없음)
// @Autowired
private final JoinService joinService;

// 처음 조인을 화면 출력하는 get방식 유형
@GetMapping("/join")
public String join() {
return "join";
}

// 해당 조인폼 화면에서 폼의 전송 이벤트 발생시 데이터 가공하는 post 방식 요청
// 이때 폼의 모든 요소를 일일이 전달하는 것이 아닌 DTO파일로 감싸서 전달
// Model타입의 파라미터, 서비스에 전달된 데이터를 뷰템플릿에 저달하기 위한 전용 클래스
@PostMapping("/join/create")
public String create( @ModelAttribute JoinDTO formDTO, Model model ) {
joinService.processJoin(formDTO);
// String msg = joinService.processJoin(formDTO);
// model.addAttribute("data", msg);
return "redirect:/admin";
}

@GetMapping("/admin")
public String showAdminPage(Model model){
List<JoinEntity> users = joinService.getAllUsers();
System.out.println(users);
model.addAttribute("users", users);
return "admin";
}

@GetMapping("/admin/del/{id}")
public String delUser(@PathVariable Long id){
joinService.delete(id);
return "redirect:/admin";
}
}


JoinService.java

processJoin: db에 저장한다. ex) joinRepo.save(user);
getAllUsers: db값을 불러온다. ex) joinRepo.findAll();

package com.example.basic.service;
import com.example.basic.dto.JoinDTO;
import com.example.basic.entity.JoinEntity;
import com.example.basic.repository.JoinRepo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class JoinService {
private final JoinRepo joinRepo;

public void processJoin(JoinDTO dto) {
// Entity클래스로 부터 각 항목에 맞는 데이터를 인자로 전달하여 실제 db에 저장할 모델 인스턴스생성
JoinEntity user = new JoinEntity(null, dto.getUname(), dto.getEmail(), dto.getColors());

//위에서 생성한 user라는 인스턴스를 joinRepo의 save메서드로 전달하기만 하면 DB에 데이터 저장됨
//save() 리포지토리에서 JPA구현체가 자동으로 생성 DB저장 전용 메서드
joinRepo.save(user);

// 컨트롤러를 통해 실제 템플릿에 전달된 데이터가 매핑된 태그 문자열 반환
// String result = "</br><ul><li>name: " + dto.getUname() + "</li>"
// + "<li>email: " + dto.getEmail() + "</li>"
// + "<li>my color: " + dto.getColors() + "</li></ul></br>";
// return result;
}

//모든 db데이터 조회해서 반환
public List<JoinEntity> getAllUsers(){
return joinRepo.findAll();
}

//데이터 삭제하는 jpo메서드 호출
public void delete(Long id){
joinRepo.deleteById(id);
}
}


admin.html

<section  layout:fragment="body">
<h1>Admin Page</h1>
<ul>
<li th:each="user: ${users}">
<h2 th:text="${user.uname}"></h2>
<p th:text="${user.email}"></p>
<span th:text="${user.colors}"></span>
<!-- 동적인 url 요청시 { 앞에 @추가 -->
<form th:action="@{'/admin/del/' + ${user.id}}" method="get">
<button type="submit">delete user</button>
</form>
</li>
</ul>
</section>


수정 기능

JoinController.java

@GetMapping("/admin/edit/{id}")
public String editUser(@PathVariable Long id, Model model){
JoinEntity user = joinService.getUserById(id);
model.addAttribute("user", user);
return "edit";
}


edit.html

<form action="/admin/update" method="post">
<input type="hidden" name="uname" id="id" th:field="${user.id}">
<h2><label for="uname">User Name</label></h2>
<input type="text" name="uname" id="uname" th:field="${user.uname}">

<h2><label for="email">Email</label></h2>
<input type="email" name="email" id="email" th:field="${user.email}">

<h2>Favorait Color</h2>
<div>
<label for="red">red</label>
<input type="radio" name="colors" id="red" value="red" th:field="${user.colors}">

<label for="green">green</label>
<input type="radio" name="colors" id="green" value="green" th:field="${user.colors}">

<label for="blue">blue</label>
<input type="radio" name="colors" id="blue" value="blue" th:field="${user.colors}">
</div>

<div>
<input type="reset" value="cancel">
<input type="submit" value="send">
</div>
</form>


JoinController.java

@PostMapping("/admin/update")
public String updateUser(JoinEntity formUser){
joinService.updateUser(formUser);
return "redirect:/admin";
}


JoinService.java

public  void updateUser(JoinEntity user){
joinRepo.save(user);
}


페이지 사이징

JoinController.java

  • page: 현재 페이지
  • userPage: 현재 페이지에서 출력해야 할 데이터 배열

@GetMapping("/admin")
public String showAdminPage(@RequestParam(defaultValue="0") int page, Model model){
//각 페이지별 출력할 데이터 개수
int pageSize = 3;
Page<JoinEntity> userPage = joinService.getUsersByPage(page, pageSize);

model.addAttribute("userPage", userPage);
model.addAttribute("currentPage", page);
return "admin";
}


JoinService.java

//페이지 번호에 따라 유저 데이터 가져오는 메서드
public Page<JoinEntity> getUsersByPage(int page, int size){
//page: 페이지 번호, size: 한페이지에 불어올 데이터 갯수
return joinRepo.findAll(PageRequest.of(page, size));
}


admin.html

<section  layout:fragment="body">
<h1>Admin Page</h1>
<ul>
<li th:each="user: ${userPage.content}">
<h2 th:text="${user.uname}"></h2>
<p th:text="${user.email}"></p>
<span th:text="${user.colors}"></span>
<!-- 동적인 url 요청시 { 앞에 @추가 -->
<form th:action="@{'/admin/del/' + ${user.id}}" method="get">
<button type="submit" onclick="return confirm('정말 삭제하시겠습니까?')">delete user</button>
</form>
<form th:action="@{'/admin/edit/' + ${user.id}}" method="get">
<button type="submit">edit user</button>
</form>
</li>
</ul>
<nav>
<!-- #numbers.sequence()가 숫자형 리스트를 반환한다. -->
<a th:each="i: ${#numbers.sequence(0,userPage.totalPages-1)}" th:href="@{'/admin?page=' + ${i}}" th:text="${i+1}"></a>
</nav>
</section>


@RequestParam과 @PathVariable의 차이점

@RequestParam과 @PathVariable은 모두 Spring에서 HTTP 요청 데이터를 컨트롤러 메서드의 파라미터로 바인딩할 때 사용하는 어노테이션이지만, 추출하는 위치와 용도가 다릅니다.

구분@RequestParam@PathVariable
추출 위치쿼리 스트링 또는 폼 데이터URL 경로(URI Path)
예시 URL/admin?page=2/admin/del/5
메서드 예시showAdminPage(@RequestParam int page, ...)delUser(@PathVariable Long id)
파라미터 전달방식?key=value 형식/{value} 형식


로그인 (세션)

LoginDTO

package com.example.basic.dto;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginDTO {
private String uname;
private String email;
}


login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>

<body>
<section layout:fragment="body">
<h1>Login Page</h1>
<!-- 초기 접속시 컨트롤러부터 사용자 정보가 담길 빈 인스턴스 객체가 전달되고 사용자가 폼에 로그인 정보 입력하고 전송하는 순간 빈 DTO객체 틀에 값이 채워지면서 컨트롤러 재 전송하는-->
<form method="post" th:action="@{/login/check}">
<label>Name:</label> <input type="text" required th:field="${loginDTO.uname}" /><br />
<label>Email:</label> <input type="email" required th:field="${loginDTO.email}" /><br />
<button type="submit">Login</button>
</form>

<div th:if="${error}">
<p style="color:red;" th:text="${error}"></p>
</div>

</section>
</body>
</html>


LoginController.java

package com.example.basic.controller;
import com.example.basic.dto.LoginDTO;
import com.example.basic.entity.JoinEntity;
import com.example.basic.service.LoginService;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;

@GetMapping("/login")
public String loginPage(Model model) {
model.addAttribute("loginDTO", new LoginDTO());
return "login";
}

@PostMapping("/login/check")
public String checkLogin(@ModelAttribute LoginDTO loginDTO, Model model, HttpSession session) {
JoinEntity user = loginService.checkUser(loginDTO.getUname(), loginDTO.getEmail());

if (user != null) {
//로그인 성공
//인증된 사용자 정보를 session에 등록 처리
session.setAttribute("loginUser", user);
return "redirect:/admin";
} else {
model.addAttribute("error", "일치하는 사용자가 없습니다.");
return "login";
}
}

@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // 세션 초기화 (로그아웃)
return "redirect:/login";
}
}


LoginService.java

package com.example.basic.service;

import com.example.basic.entity.JoinEntity;
import com.example.basic.repository.JoinRepo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LoginService {
private final JoinRepo joinRepo;

public JoinEntity checkUser(String uname, String email){
//인수로 전달된 사용자 이름과 email 항목으로 매칭되는 DB데이터가 있는지 찾아서 반환
//아래 함수는 우리가 repository계층에 이름만 등록한 메서드임에도 불구하고 실제 동작 가능
//함수명에 들어가 있는 키워드를 자동 인지해서 로직을 짜줌
//findBy.uname, email 등의 실제 기존 DB함수명과 실제 entity에 등록되어 있는 필드명을 활용
// SELECT j FROM JoinEntity j WHERE j.uname = :uname AND J.email = :email
return joinRepo.findByUnameAndEmail(uname, email);
}
}




댓글 쓰기

다음 이전