package com.lsj.blog;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.lsj.blog.repository.UserRepository;
import com.lsj.dto.ResponseDto;
@RestController
public class DummyController {
@Autowired // DI
private UserRepository userRepository;
@PostMapping("/api/login")
public ResponseDto<Integer> login(@RequestBody BlogUser user, HttpSession session){
BlogUser principal = loginService(user); //principal (접근주체)
if(principal != null) session.setAttribute("principal", principal);
System.out.println("principal : " + session.getAttribute("principal"));
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
}
@Transactional(readOnly = true) // Select 할 때 트랜잭션 시작, 서비스 종료시 트랜잭션 종료 (데이터 일관성)
public BlogUser loginService(BlogUser user) {
return userRepository.findByUsernameAndPassword(user.getUsername(), user.getPassword());
}
}
package com.lsj.blog.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.lsj.blog.BlogUser;
// extends JapRepository가 있는 경우 @Repository 생략가능
// JpaRepostiory는 다양한 기능을 가지고 있다. findAll (모든 레코드 select) 페이징해서 보내줄 수 있는 기능도 존재
public interface UserRepository extends JpaRepository<BlogUser, Integer>{ // BlogUser의 Repository이고 PK는 Integer이다.
// JPA Naming 쿼리
// SELECT * FROM bloguser WHERE username = ? AND password = ?;
// SELECT * FROM ${리턴타입} WHERE username = ${첫번째 파라미터} AND password = ${두번째 파라미터};
BlogUser findByUsernameAndPassword(String username, String password);
// nativeQuery 방식 위와 동일하다
@Query(value="SELECT * FROM bloguser WHERE username =1 ? AND password = 2?", nativeQuery = true)
BlogUser login(String username, String password);
}
package com.lsj.blog;
import java.util.Random;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.lsj.blog.model.RoleType;
import com.lsj.blog.repository.UserRepository;
import com.lsj.dto.ResponseDto;
@RestController
public class DummyController {
@Autowired // DI
private UserRepository userRepository;
@PostMapping("/api/user")
@Transactional
public ResponseDto<String> save(@RequestBody BlogUser user){
BlogUser usre2 = new BlogUser();
Random random = new Random();
usre2.setUsername(String.valueOf(random.nextInt(10)));
usre2.setPassword("12345");
usre2.setEmail("abc@naver.com");
userRepository.save(usre2);
new ResponseDto<String>(HttpStatus.OK.value(), "데이터 insert에 성공했습니다.");
// HttpStatus.OK.value() → 200이라는 코드 반환 (정상 성공)
}
}
package com.lsj.blog.handler;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import com.lsj.dto.ResponseDto;
@ControllerAdvice // 어떤 Exception이 오든 이 페이지로 오게 된다.
@RestController
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class) // 모든 부모 Exception
public ResponseDto<String> handleException(Exception e) {
return new ResponseDto<String>(HttpStatus.INTERNAL_SERVER_ERROR.value(),e.getMessage());
// HttpStatus.INTERNAL_SERVER_ERROR.value() → 500 코드 반환 (서버 에러)
//return "<h1>" + e.getMessage() + "</h1>";
}
}
package com.lsj.dto;
import org.springframework.http.HttpStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
int status;
// HttpStatus status;
T data;
}
package com.lsj.blog.handler; // Exception 처리 Handler를 따로 패키지를 구성한다.
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
@ControllerAdvice // 어떤 Exception이 오든 이 페이지로 오게 된다.
@RestController
public class GlobalExceptionHandler {
// @ExceptionHandler는 Controller계층에서 발생하는 에러만 잡는다.
@ExceptionHandler(value=Exception.class) // 모든 부모 Exception
public String handleException(Exception e) {
return "<h1>" + e.getMessage() + "</h1>";
}
@ExceptionHandler(value=IllegalArgumentException.class) // 해당 IllegalArgumentException이 발생할 경우 밑에 함수가 작동한다.
public String handleArgumentException(IllegalArgumentException e) {
return "<h1>" + e.getMessage() + "</h1>";
}
}
영속성 컨텐스트란 엔티티를 영구 저장하는 환경으로 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스라고 생각하면 된다.
JPA는 영속성 컨텍스트라는게 존재하고 그 안에 캐시가 있어서 DB에 접근 안 하고 캐시에 존재하는 데이터는 빠르게 가져오거나 DB에 부담을 줄일 수 있습니다. 그리고 그 캐시안에 있는 걸 영속화 되었다라고 표현합니다.
📝Insert
Insert시 영속성 컨텍스트에 해당 값이 올라가게 되고 flush해 영속성 컨텍스트에 있는 데이터를 DB에 데이터를 넣게 됩니다.
📝Select
Select시 DB에 해당 데이터를 영속성 컨텍스트에 올리게 됩니다. 그 후 컨트롤러에서는 영속성 컨텍스트에 있는 값을 가져가게 됩니다.
📝Update
Update의 경우 Select와 같은 동작이 선행이 되어서 영속성 컨텍스트에 올라가게 되고 그 후 업데이트된 값을 다시 영속화 시키고 DB에 flush해 데이터를 업데이트 시킵니다.
📝더티체킹
Update와 같은 행위를 Transaction 어노테이션을 이용해 하는 걸 더티체킹이라고 합니다.
Transaction 어노테이션이 선언된 곳에서 영속성 컨텍스트에 변경이 일어날 시 자동으로 데이터베이스에 반영하는 JPA 특징입니다.
@Transactional // 함수 종료시 자동으로 DB Commit (영속성컨테이너 데이터를 DB로 Flush)
// Update ( Put방식은 Body에 Json만 인식하고 @RequestBody는 두개 이상 사용이 불가능 (객체에 담던가 해야한다.)
@PutMapping("/dummy/users/{id}")
public String updateUser(@PathVariable int id, @RequestBody BlogUser requestUser) {
System.out.println("id : " + id);
System.out.println("password : " + requestUser.getPassword());
System.out.println("email : " + requestUser.getEmail());
BlogUser user = userRepository.findById(id).orElseThrow(() ->{
return new IllegalArgumentException("수정에 실패하였습니다.");
});
user.setPassword(requestUser.getPassword());
user.setEmail(requestUser.getEmail());
/** @Transactional 사용 안 할시 **/
// save 함수는 id를 전달하지 않으면 insert
// save 함수는 id를 전달하면 해당 id 존재시 udpate 없으면 insert
// userRepository.save(user);
return id + "가 update 가 되었습니다. ";
}
@Transaction 다른 예제
@RestController
public class DummyController {
@Autowired // DI
private UserRepository userRepository;
@PostMapping("/api/user")
//@Transacitonal
public ResponseDto<String> save(@RequestBody BlogUser user){
BlogUser usre2 = new BlogUser();
Random random = new Random();
usre2.setUsername(String.valueOf(random.nextInt(10)));
usre2.setPassword("abc@naver.com");
usre2.setEmail("abc@naver.com");
userRepository.save(usre2);
user.setRole(RoleType.USER); // Enum을 이용한 방식을 통해 Setter 주입 실수를 방지할 수 있다.
join(user);
return new ResponseDto<String>(HttpStatus.OK .value(), "1");
// new ResponseDto<String>(HttpStatus.OK, result);
}
@Transactional // Transaction 처리하겠다. 성공시에만 Commit
public void join(BlogUser user) {
userRepository.save(user); // UserRepostiory에 save하면 DB 테이블에 insert가 된다.
}
}
데이터 Body 형식 →
{
"username" : "lee6",
"password" : "1234",
"email" : "tjdwo37@naver.com"
}
- @Transaction이 join 함수에 있는 경우
join 함수를 실행시에 에러가 날시 join함수 내용만 rollback
위에 랜덤함수로 UserName을 넣고 insert 과정은 join함수를 안 쓰기 때문에
해당 API 계속 호출시 랜덤함수로 UserName 넣는 데이터 계속 insert는 되고 있음
- @Transcaation을 api 함수에 넣은 경우
api에서 에러가 날시 모든 내용을 rollback하기 때문에 위와 같이 랜덤함수로 UserName넣고 insert하는 과정도
rollback이 되어서 데이터가 계속 누적되지 않는다.
package com.lsj.blog;
import java.sql.Timestamp;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicInsert;
import com.lsj.blog.model.RoleType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** 하기 어노테이션 사용하려면 Javax Persistence API Gradle 추가 필요 **/
//@DynamicInsert // null 인 값일 때는 무시하고 Default값으로 넣어준다
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity // 하기 필드 기준으로 DB에 User 테이블을 생성하라는 의미
public class BlogUser {
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // 프로젝트에서 연결된 DB의 넘버링 전략을 따라간다.
// → 즉, DB에 따라 AutoIncrement 또는 Sequence가 결정 되어서 알아서 만들어준다. (자동 증가 PK 필요함으로 이와 같은 행동 함)
// (MySql에서는 AutoIncrement, Oracle에서는 Sequence 사용)
private int id;
@Column(nullable = false, length = 30, unique=true) // null 방지 및 길이 30 + 중복값 방지
private String username;
//@ColumnDefault(" 'no password' ") //→ @DynamicInsert 와 같이 사용한다. null일시에는 ColumnDefault에 선언된 값(no password)으로 넣어준다.
@Column(nullable = false, length = 100)
private String password;
@Column(nullable = false, length = 50)
private String email;
//@ColumnDefault("'user'") // → Column Default 값 String이기 때문에 ' ' 안에 넣어야한다.
//private String role; // Enum을
@Enumerated(EnumType.STRING) // DB에는 RoleType이라는 타입이 없기 때문에 String 이라고 선언
private RoleType role;
@CreationTimestamp // 현재 시각 바로 자동 입력
private Timestamp createDate;
}
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.lsj.blog.model.RoleType;
import com.lsj.blog.repository.UserRepository;
@GetMapping("/dummy/user/{id}")
public BlogUser detail(@PathVariable int id) { // PathVariable은 들어온 id값에 따른 동적 처리 {id}
// BlogUser user = userRepository.findById(id); // findById의 return 타입은 Optional
// → 자바에서 DB 데이터를 못찾아오게 되면 user가 null이 되어 return이 되기 때문에
// Optional로 객체를 감싸서 가져올테니 null인지 아닌지 판단해서 return 해라
/** 방법1 **/
// userRepository.findById(id).get(); // null 그런거 상관없이 가져오겠다. (위험)
/** 방법 2
BlogUser user = userRepository.findById(id).orElseGet(new Supplier<BlogUser>(){
@Override
public BlogUser get() { // 값이 없는 경우 BlogUser 객체 생성해 주입 (Null 가능성은 없음)
return new BlogUser();
}
});
**/
/** 방법3 (선호) 해당 에러 발생시 Throw **/
BlogUser user = userRepository.findById(id).orElseThrow(new Supplier<IllegalArgumentException>(){
@Override
public IllegalArgumentException get() {
return new IllegalArgumentException("해당 유저는 없습니다. id : " + id);
}
});
return user;
}
// 페이지리스트 모두 출력
@GetMapping("/dummy/list")
public List<BlogUser> list(){
return userRepository.findAll();
}
// 페이지네이션
// size → 보여줄 개수
// sort → order by 할 필드
// direction → 오름차순 또는 내림차순
// dummy/list/page?page=0 일시 id 3번, 2번 노출 | page=1로 호출시 id 1번 노출
// 아무것도 입력안 할시 ?page=0과 동일
@GetMapping("dummy/list/page")
public Page<BlogUser> pageList(@PageableDefault(size=2, sort="id", direction = Sort.Direction.DESC) Pageable pageable){
Page<BlogUser> users = userRepository.findAll(pageable);
return users;
}
package com.lsj.blog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.lsj.blog.model.RoleType;
import com.lsj.blog.repository.UserRepository;
@RestController
public class DummyController {
@Autowired // DI
private UserRepository userRepository;
@PostMapping("/dummy/join")
public String join(BlogUser user) {
//user.setRole("user"); // @DynamicInsert처럼 계속 어노테이션을 붙히다보면 어노테이션 지옥에 빠지기 때문에 직접 Setter에 주입해 어노테이션을 줄일 수 있지만 실수할 수 있다.
user.setRole(RoleType.USER); // Enum을 이용한 방식을 통해 Setter 주입 실수를 방지할 수 있다.
userRepository.save(user); // UserRepostiory에 save하면 DB 테이블에 insert가 된다.
return "회원가입이 완료되었습니다.";
}
}
DummyController.java
package com.lsj.blog.model;
//데이터 강제할 수 있다. (데이터의 도메인화 → 강제)
public enum RoleType {
USER,
ADMIN
}
RoleType.java
package com.lsj.blog.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.lsj.blog.BlogUser;
// extends JapRepository가 있는 경우 @Repository 생략가능
// JpaRepostiory는 다양한 기능을 가지고 있다. findAll (모든 레코드 select) 페이징해서 보내줄 수 있는 기능도 존재
public interface UserRepository extends JpaRepository<BlogUser, Integer>{ // BlogUser의 Repository이고 PK는 Integer이다.
}
UserRepository.java
package com.lsj.blog;
import java.sql.Timestamp;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicInsert;
import com.lsj.blog.model.RoleType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** 하기 어노테이션 사용하려면 Javax Persistence API Gradle 추가 필요 **/
@DynamicInsert // null 인 값일 때는 무시하고 Default값으로 넣어준다
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity // 하기 필드 기준으로 DB에 User 테이블을 생성하라는 의미
public class BlogUser {
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // 프로젝트에서 연결된 DB의 넘버링 전략을 따라간다.
// → 즉, DB에 따라 AutoIncrement 또는 Sequence가 결정 되어서 알아서 만들어준다. (자동 증가 PK 필요함으로 이와 같은 행동 함)
// (MySql에서는 AutoIncrement, Oracle에서는 Sequence 사용)
private int id;
@Column(nullable = false, length = 30, unique=true) // null 방지 및 길이 30 + 중복값 방지
private String username;
@ColumnDefault(" 'no password' ") //→ @DynamicInsert 와 같이 사용한다. null일시에는 ColumnDefault에 선언된 값(no password)으로 넣어준다.
@Column(nullable = true, length = 100)
private String password;
@Column(nullable = false, length = 50)
private String email;
//@ColumnDefault("'user'") // → Column Default 값 String이기 때문에 ' ' 안에 넣어야한다.
//private String role; // Enum을
@Enumerated(EnumType.STRING) // DB에는 RoleType이라는 타입이 없기 때문에 String 이라고 선언
private RoleType role;
@CreationTimestamp // 현재 시각 바로 자동 입력
private Timestamp createDate;
}
/**
위와 같은 엔터티일 때 세이브할 데이터 형식 예제
{
"username" : "lee6",
"email" : "abc@naver.com"
}
결과 → 2 22/12/03 12:31:26.986000000 tjdwo37@naver.com no password USER lee6
**/
plugins {
id 'org.springframework.boot' version '2.7.4'
id 'io.spring.dependency-management' version '1.0.14.RELEASE'
id 'java'
}
group = 'com.lsj'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
/** 추가 라이브러리 **/
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.58'
// https://mvnrepository.com/artifact/javax.persistence/javax.persistence-api
implementation group: 'javax.persistence', name: 'javax.persistence-api', version: '2.2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
gradle 설정입니다.
package com.lsj.chatting;
import java.sql.Timestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
/** 하기 어노테이션 사용하려면 Javax Persistence API Gradle 추가 필요 **/
@Entity // 하기 필드 기준으로 DB에 User 테이블을 생성하라는 의미
public class BlogUser {
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // 프로젝트에서 연결된 DB의 넘버링 전략을 따라간다.
// → 즉, DB에 따라 AutoIncrement 또는 Sequence가 결정 되어서 알아서 만들어준다. (자동 증가 PK 필요함으로 이와 같은 행동 함)
// (MySql에서는 AutoIncrement, Oracle에서는 Sequence 사용)
private int id;
@Column(nullable = false, length = 30) // null 방지 및 길이 30
private String username;
@Column(nullable = false, length = 100)
private String password;
@Column(nullable = false, length = 50)
private String email;
@ColumnDefault("'user'") // → Column Default 값 String이기 때문에 ' ' 안에 넣어야한다.
private String role; // Enum을
// → admin : 0
// → user : 0
// → manager : 0
@CreationTimestamp // 현재 시각 바로 자동 입력
private Timestamp createDate;
}
어노테이션을 이용해 테이블 생성하기 위한 Java 파일 입니다.
server:
port: 8000 # 톰캣 포트 (스프링부트는 톰캣이 내장되어 있다.)
servlet:
context-path: /chatting # 기본 url Path
encoding:
charset: UTF-8
enabled: true
force: true
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
# @GetMapping 에서 함수의 return 값이 String인 경우 return 에 대한 내용은 jsp의 파일 이름입니다.
# @GetMapping
# public String callJsp(){
# return "home" → /WEB-INF/views/home.jsp 화면을 뿌려라
# 일반적으로 스프링부트는 JSP를 지원하지 않기 때문에 JSP를 사용하기 위해서는 JSP 해석 템플릿 엔진인 jasper을 Gradle에서 다운로드를 해야한다.
#}
datasource:
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@localhost:1521:orcl
username: c##root
password: 1234
jpa:
open-in-view: true
hibernate:
ddl-auto: create # 최초 전략 Applicaiton 재시작시 테이블 있으면 지우고 다시 만들게 된다.
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# field 명 그대로 필드를 만들게 된다.
# physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
# CamelCase 전략을 따라가는데 field 명이 myEmail인 경우 my_email로 변환시켜서 만들어준다.
use-new-id-generator-mappings: false
# false : JPA 기본 전략을 쓴다.
# false : JPA 기본 전략을 쓰지 않는다. → User에 선언한 GenerationType.IDENTITY을 쓰겠다
show-sql: true
properties:
hibernate.format_sql: true
jackson:
serialization:
fail-on-empty-beans: false
yml 파일로 JPA 사용할 설정이 들어있습니다.
Hibernate:
drop table BlogUser cascade constraints
Hibernate:
create table BlogUser (
id number(10,0) generated as identity,
createDate timestamp,
email varchar2(50 char) not null,
password varchar2(100 char) not null,
role varchar2(255 char) default 'user',
username varchar2(30 char) not null,
primary key (id)
)
시작시 Hibernate라는 애가 Java 파일 설정을 읽어서 Query를 생성시켜줍니다.