3 Layer Architecture
서버의 처리과정을 Controller, Service, Repository 3개로 분리
1. Controller
- Client의 요청을 받고 요청에 대한 로직 처리는 Service에 전담
- Request 데이터 있을 시 Service에 같이 전달
- Service의 처리 완료 결과를 Client에 응답
2. Service
- 요구사항을 처리(비즈니스 로직)하는 실세
- 현업에서는 서비스 코드가 계속 비대해짐
- DB 저장 및 조회 필요 시 Repository에 요청
3. Repository
- DB 관리(연결, 해제, 자원 관리)
- DB CRUD 작업 처리(생성, 읽기, 수정, 삭제)
역할 분리
Controller - Service 분리 : Controller
package com.sparta.memo.controller;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.service.MemoService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class MemoController {
private final JdbcTemplate jdbcTemplate;
public MemoController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
MemoService memoService = new MemoService(jdbcTemplate);
return memoService.createMemo(requestDto);
}
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
MemoService memoService = new MemoService(jdbcTemplate);
return memoService.getMemos();
}
@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
MemoService memoService = new MemoService(jdbcTemplate);
return memoService.updateMemo(id,requestDto);
}
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
MemoService memoService = new MemoService(jdbcTemplate);
return memoService.deleteMemo(id);
}
}
- Cotroller는 API 요청을 받고 Service에 받아온 데이터와 함께 요청 전달
- 요청 전달을 위해 MemoService 인스턴스 화 후 사용
- ModelService에서 JdbcTemplate 사용을 위해 생성자 파라미터로 전달
- 요청을 전달 받은 Service는 요청을 수행
Controller - Service 분리 : Service
package com.sparta.memo.service;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
public class MemoService {
private final JdbcTemplate jdbcTemplate;
public MemoService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
public List<MemoResponseDto> getMemos() {
// DB 조회
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
return memoRepository.findAll();
}
public Long updateMemo(Long id, MemoRequestDto requestDto) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
// 해당 메모가 DB에 존재하는지 확인
Memo memo = memoRepository.findById(id);
if(memo != null) {
// memo 내용 수정
memoRepository.update(id, requestDto);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
public Long deleteMemo(Long id) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
// 해당 메모가 DB에 존재하는지 확인
Memo memo = memoRepository.findById(id);
if(memo != null) {
// memo 삭제
memoRepository.delete(id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
}
Service - Repository 분리 : Repository
- DB 연결 및 CRUD 작업은 Repository에서 진행
- Service는 DB 작업을 Repository 에 요청
package com.sparta.memo.repository;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
public class MemoRepository {
private final JdbcTemplate jdbcTemplate;
public MemoRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Memo save(Memo memo) {
// DB 저장
KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체
String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, memo.getUsername());
preparedStatement.setString(2, memo.getContents());
return preparedStatement;
},
keyHolder);
// DB Insert 후 받아온 기본키 확인
Long id = keyHolder.getKey().longValue();
memo.setId(id);
return memo;
}
public List<MemoResponseDto> findAll() {
// DB 조회
String sql = "SELECT * FROM memo";
return jdbcTemplate.query(sql, new RowMapper<MemoResponseDto>() {
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String username = rs.getString("username");
String contents = rs.getString("contents");
return new MemoResponseDto(id, username, contents);
}
});
}
public void update(Long id, MemoRequestDto requestDto) {
String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);
}
public void delete(Long id) {
String sql = "DELETE FROM memo WHERE id = ?";
jdbcTemplate.update(sql, id);
}
public Memo findById(Long id) {
// DB 조회
String sql = "SELECT * FROM memo WHERE id = ?";
return jdbcTemplate.query(sql, resultSet -> {
if (resultSet.next()) {
Memo memo = new Memo();
memo.setUsername(resultSet.getString("username"));
memo.setContents(resultSet.getString("contents"));
return memo;
} else {
return null;
}
}, id);
}
}
IoC(제어의 역전) / DI(의존성 주입)
IoC, DI : SOLID 원칙, GoF의 디자인 패턴 같은 설계 원칙 및 디자인 패턴
- IoC는 설계 원칙, DI는 디자인 패턴에 해당
- IoC : 개발자가 직접 처리하지 않고 Spring에게 권한을 줌(Spring이 직접 Container 흐름 제어), 그래서 제어의 역전
- DI : 밖에서 의존성을 주입, 개발자가 직접 만드는 것이 아니고 외부에서 가져오는 것(build.gradle)
- 외부 DI를 가져올 때 의존성을 추가하여 사용(build.gradle에 추가 후 build)
- DI 패턴을 사용하여 IoC 설계 원칙을 구현
의존성
- 예: 다리를 다쳐 목발을 사용할 경우 목발에 의존하고 있는 것(목발에 의존성을 두게 되었다고 할 수 있음)
강한 결합 예제
public class Consumer {
void eat() {
Chicken chicken = new Chicken();
chicken.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat();
}
}
class Chicken {
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
- 강한 결합의 위의 코드의 경우 eat의 메뉴가 변경될 때마다 많은 수의 코드가 변경되어야 함
약한 결합(약한 의존성) 예제
public class Consumer {
void eat(Food food) {
food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat(new Chicken());
consumer.eat(new Pizza());
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
- Interface의 다형성 원리를 사용하여 구현 시 eat의 메뉴가 변경되어도 새로운 메뉴를 구현하기만 하면 됨.
주입의 종류
- 필드 주입
- 메서드 주입
- 생성자를 통한 주입
필드 주입
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.food = new Chicken();
consumer.eat();
consumer.food = new Pizza();
consumer.eat();
}
}
// interface, interface 구현 코드 생략
- Food를 Cunsumer에 포함 시키고 Food에 필요한 객체를 주입받아 사용
메서드 주입
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public void setFood(Food food) {
this.food = food;
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.setFood(new Chicken());
consumer.eat();
consumer.setFood(new Pizza());
consumer.eat();
}
}
// interface, interface 구현 코드 생략
- set 메서드를 사용하여 필요한 객체를 주입받아 사용
생성자를 통한 주입
public class Consumer {
Food food;
public Consumer(Food food) {
this.food = food;
}
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer(new Chicken());
consumer.eat();
consumer = new Consumer(new Pizza());
consumer.eat();
}
}
// interface, interface 구현 코드 생략
- 생성자를 사용하여 필요한 객체를 주입받아 사용
- 생성자 주입을 주로 사용
제어의 역전
- 기존 코드는 새로운 Food 추가 시 Consumer 코드 변경이 필요함, 변경된 코드는 Food에서 Consumer로 전달하기 대문에 코드 변경 없이 추가 가능
- 제어의 흐름이 Consumer → Food 에서 Food → Consumer
IoC & DI 사용
- 객체 중복 생성 코드 정리 가능, 다른 클래스의 객체 생성 필요 x
- 강한 결합을 느슨한 결합으로 바꾸는 것으로 효울적인 코드로 변경
package com.sparta.memo.controller;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.service.MemoService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class MemoController {
private final MemoService memoService;
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
return memoService.createMemo(requestDto);
}
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
return memoService.getMemos();
}
@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
return memoService.updateMemo(id,requestDto);
}
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
return memoService.deleteMemo(id);
}
}
- 객체의 중복 생성 코드 제거, 다른 클래스에 객체 생성 x , jdbc template 제거
IoC Container 와 Bean 사용
- Bean : Spring이 관리하는 객체
- Spring IoC Container : Bean을 모아둔 컨테이터
Bean 등록 방법
- @Component : Bean으로 등록하려는 클래스 위에 설정
@Component
public class MemoService { ... }
// Component 설정된 클래스에 대해 Spring이 해주는 일
// 1. MemoService 객체 생성
MemoService memoService = new MemoService();
// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너
- @ComponentScan : 설정해 준 packages 위치와 하위 packages 들을 전부 확인하여 @Component가 설정된 클래스들을 'Bean'으로 등록
- @SpringBootApplication에 의해 default 설정
@Configuration
@ComponentScan(basePackages = "com.sparta.memo")
class BeanConfig { ... }
Bean 사용 방법
- 필드 위에 @Autowired
@Component
public class MemoService {
@Autowired
private MemoRepository memoRepository;
// ...
}
- IoC 컨테이너에 저장된 memoRepository 'Bean'을 해당 필드에 DI(의존성을 주입)
- Bean을 주입할 때 사용할 메서드 위에 @Autowired
@Component
public class MemoService {
private final MemoRepository memoRepository;
@Autowired
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
// ...
}
- 객체의 불변성을 확보할 수 있기 때문에 일반적으로 생성자를 사용해 DI하는 것이 좋음
@Autowired 생략 조건
- Spring 4.3 버전부터 생략 가능
- 생성자 선언이 1개일 때만 생략 가능
- Lombok의 @RequiredArgsConstructor 사용 가능
public class A {
@Autowired // 생략 불가
public A(B b) { ... }
@Autowired // 생략 불가
public A(B b, C c) { ... }
}
@Component
@RequiredArgsConstructor // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {
private final MemoRepository memoRepository;
// public MemoService(MemoRepository memoRepository) {
// this.memoRepository = memoRepository;
// }
...
}
3 Layer Annotation
- Controller, Service, Repository의 역할로 구분된 클래스들을 'Bean'으로 등록할 때 해당 'Bean' 클래스의 역할 명시하기 위해 사용
- @Controller, @RestController
- @Service
- @Repository
사용 예제
@RestController
@RequestMapping("/api")
public class MemoController {...}
@Service
public class MemoService {...}
@Repository
public class MemoRepository {...}
JPA(Java Persistence API)
- 자바 ORM 기술의 대표적인 표준 명세
- JPA는 애플리케이션과 JDBC 사이에서 동작
- JPA 사용 시 DB 연결 과정을 자동으로 처리
- 또한 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리할 수 있음.
하이버네이트(Hibernate)
- JPA를 실제 구현한 프레임워크 중 사실상 표준이 하이버 네이트
- 스프링 부트에서는 기본적으로 '하이버 네이트' 구현체를 사용 중
Entity
- DB 와 매핑된 객체, JPA에 의해 관리됨
프로젝트 세팅
- 자바 17
- Build System : Gradle
- Gradle DSL : Groovy
- Advanced Setting은 프로젝트에 따라 변경
- src > main > resources > META-INF > persistence.xml 추가
- build.gradle : JPA, MySQL 추가
- DB 연동
persistemce.xml 예시
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
build.gradle 추가
// JPA 구현체인 hibernate
implementation 'org.hibernate:hibernate-core:6.1.7.Final'
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'
DB 연동 순서(Intellij)
- Database 탭을 클릭 + 버튼 클릭
- Data Source > MySQL 를 클릭
- User, Password, Database 정보를 추가한 후 Ok를 클릭
- MySQL Database에 연결 완료
EntityTest
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class EntityTest {
EntityManagerFactory emf;
EntityManager em;
@BeforeEach
void setUp() {
emf = Persistence.createEntityManagerFactory("memo");
em = emf.createEntityManager();
}
@Test
void test1() {
}
}
Entity 클래스
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
@Id
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(name = "username", nullable = false, unique = true)
private String username;
// length: 컬럼 길이 지정
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
- @Entity : JPA가 관리할 수 있는 Entity 클래스로 지정
- @Entity(name = "Memo") : Entity 클래스 이름을 지정 (default: 클래스명)
- JPA가 Entity 클래스를 인스턴스화 할 때 기본 생성자를 사용하기 때문에 반드시 현재 Entity 클래스에서 기본 생성자가 생성되고 있는지 확인
- @Table : 매핑할 테이블을 지정
- @Table(name = "memo") : 매핑할 테이블의 이름을 지정 (default: Entity 명)
- @Column :
- @Column(name = "username") : 필드와 매핑할 테이블의 컬럼을 지정 (default: 객체의 필드명)
- @Column(nullable = false) : 데이터의 null 값 허용 여부를 지정 (default: true)
- @Column(unique = true) : 데이터의 중복 값 허용 여부를 지정 (default: false)
- @Column(length = 500) : 데이터 값(문자)의 길이에 제약조건을 걸 수 있음 (default: 255)
- @Id : 테이블의 기본 키를 지정
- 이 기본 키는 영속성 컨텍스트에서 Entity를 구분하고 관리할 때 사용되는 식별자 역할을 수행( 식별자 값을 넣어주지 않고 저장하면 오류가 발생)
- @Id 옵션만 설정하면 기본 키 값을 개발자가 직접 확인하고 넣어줘야 하는 불편함이 발생
- @GeneratedValue 옵션을 추가하면 기본 키 생성을 DB에 위임할 수 있음
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(name = "username", nullable = false, unique = true)
private String username;
// length: 컬럼 길이 지정
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- id bigint not null auto_increment : auto_increment 조건이 추가
- IDENTITY외에도 다양한 옵션이 있음
영속성 컨텍스트(Persietance Context) : 임시 메모리(저장소)
- persistence : 객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할수 있는 객체의 성질
- 영속성 컨텍스트 : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간
특징
- DB 와 클라이언트(서버) 사이에 위치
- 사용 이유: 조회 속도가 빠름 (DB에서 조회 : 하드 디스크 < 영속성 컨텍스트에서 조회 : 메모리 영역)
EntityManger
- 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요
- 개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제할 수 있음
- EntityManager는 EntityManagerFactory를 통해 생성하여 사용
EntityManagerFactory
- EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용
- 정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 됨
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
Transaction
- 트랜잭션은 DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념
- DB의 데이터들을 안전하게 관리하기 위해서 생겨난 개념
- 가장 큰 특징은 여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있다는 점
- 이때, 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌림
- JPA는 DB의 이러한 트랜잭션 개념을 사용하여 효율적으로 Entity를 관리
트랜잭션 예시
START TRANSACTION; # 트랜잭션을 시작합니다.
INSERT INTO memo (id, username, contents) VALUES (1, 'Robbie', 'Robbie Memo');
INSERT INTO memo (id, username, contents) VALUES (2, 'Robbert', 'Robbert Memo');
SELECT * FROM memo;
COMMIT; # 트랜잭션을 커밋합니다.
SELECT * FROM memo;
JPA의 트랜잭션
- 영속성 컨텍스트에 Entity 객체들을 저장했다고 해서 DB에 바로 반영 되지는 않음
- DB에서 하나의 트랜잭션에 여러 개의 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영하는 것 처럼 JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한번에 DB에 요청해 변경을 반영
- JPA에서 이러한 트랜잭션의 개념을 적용하기 위해서는 EntityManager에서 EntityTransaction을 가져와 트랜잭션을 적용할 수 있음
- EntityTransaction et = em.getTransaction();
- et.begin();
- 트랜잭션을 시작하는 명령어
- et.commit();
- 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어
- et.rollback();
- 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어
- 영속성 컨텍스트는 Intellij의 Debugging으로 확인 가능
예시
@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
Entity의 상태
비영속(Transient)
- new 연산자를 통해 인스턴스화 된 Entity 객체를 의미
- 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않음
영속(Managed)
- persist(entity) : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태
- JPA가 관리하는 영속 상태의 Entity
준영속(Detached)
- 준영속 상태는 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미
- 영속 상태에서 준영속 상태로 바꾸는 방법
- detach(entity) : 특정 Entity만 준영속 상태로 전환
- 영속성 컨텍스트에서 관리되다(Managed)가 분리된 상태(Detached)로 전환
- clear() : 영속성 컨텍스트를 완전히 초기화
- 영속성 컨텍스트 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태가 됨, 계속해서 영속성 컨텍스트를 이용할 수 있음
- close() : 영속성 컨텍스트를 종료
- 해당 영속성 컨텍스트가 관리하던 영속성 상태의 Entity들은 모두 준영속 상태로 변경, 영속성 컨텍스트가 종료되었기 때문에 계속해서 영속성 컨텍스트를 사용할 수 없음
- detach(entity) : 특정 Entity만 준영속 상태로 전환
- 준영속 상태에서 다시 영속 상태로 바꾸는 방법
- merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
- merge(entity) 메서드는 비영속, 준영속 모두 파라미터로 받을 수 있으며 상황에 따라 ‘저장’을 할 수도 ‘수정’을 할 수도 있음
- merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
삭제(Removed)
- remove(entity) : 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환
SpringBoot JPA
설정
- build.gradle에 spring-boot-starter-data-jpa 추가
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- application.properties : Hibernate 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
- SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성
- application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성
- @PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용할 수 있음
Spring의 트랜잭션
- Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에 적용할 수 있도록 트랜잭션 관리자를 제공
- @Transactional 애너테이션을 클래스나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있음
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
- 메서드가 호출되면, 해당 메서드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶임
- 이때, 해당 메서드가 정상적으로 수행되면 트랜잭션을 커밋하고, 예외가 발생하면 롤백
- 클래스에 선언한 @Transactional은 해당 클래스 내부의 모든 메서드에 트랜잭션 기능을 부여
- 이때, save 메서드는 @Transactional 애너테이션이 추가되어있기 때문에 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly = false 옵션으로 적용(수정 사항 저장 가능)
트랜잭션 테스트
@Test
@Transactional
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
Memo memo = new Memo();
memo.setUsername("Robbert");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
@Test
@DisplayName("메모 생성 실패")
void test2() {
Memo memo = new Memo();
memo.setUsername("Robbie");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
영속성 컨텍스트와 트랜잭션의 생명주기
- 스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치
Spring Data JPA
- Spring Data JPA는 JPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈
- JPA를 추상화시킨 Repository 인터페이스를 제공
- Repository 인터페이스는 Hibernate와 같은 JPA구현체를 사용해서 구현한 클래스를 통해 사용
Spring Data JPA의 SimpleJpaRepository
- 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용할 수 있음
Spring Data JPA 사용방법
- JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 interface 로 선언
package com.sparta.memo.repository;
import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemoRepository extends JpaRepository<Memo, Long> {
}
JPA Auditiong 적용
Timestamped
- Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공
- @MappedSuperclass
- JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt 처럼 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있음
- @EntityListeners(AuditingEntityListener.class)
- 해당 클래스에 Auditing 기능을 포함
- @CreatedDate
- Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장(updatable = false 옵션을 추가)
- @LastModifiedDate
- 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장, 변경이 일어날 때마다 해당 변경시간으로 업데이트
- @Temporal
- 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
- DB에는 Date(날짜), Time(시간), Timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재
- DATE : ex) 2023-01-01
- TIME : ex) 20:21:14
- TIMESTAMP : ex) 2023-01-01 20:22:38.771000
사용 예시
public class Memo extends Timestamped {...}
package com.sparta.memo.entity;
import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
package com.sparta.memo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class MemoApplication {
public static void main(String[] args) {
SpringApplication.run(MemoApplication.class, args);
}
}
package com.sparta.memo.dto;
import com.sparta.memo.entity.Memo;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class MemoResponseDto {
private Long id;
private String username;
private String contents;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.username = memo.getUsername();
this.contents = memo.getContents();
this.createdAt = memo.getCreatedAt();
this.modifiedAt = memo.getModifiedAt();
}
}
- Timestamped 생성
- @EnableJpaAuditing 애너테이션을 추가
- 적용하고자하는 Entity 클래스에서 Timestamped를 상속
Query Methods
- Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공
- JpaRepository 인터페이스에서 해당 인터페이스와 매핑되어있는 테이블에 요청하고자하는 SQL을 메서드 이름을 사용하여 선언할 수 있음
- 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면 해당 메서드 이름을 분석하여 SimpleJpaRepository에서 구현
- 인터페이스에 필요한 SQL에 해당하는 메서드 이름 패턴으로 메서드를 선언 하기만 하면 따로 구현하지 않아도 사용할 수 있음
예시
package com.sparta.memo.repository;
import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// 설정
public interface MemoRepository extends JpaRepository<Memo, Long> {
List<Memo> findAllByOrderByModifiedAtDesc();
}
// 사용
package com.sparta.memo.service;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
public List<MemoResponseDto> getMemos() {
// DB 조회
return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}
}
실습 파일
'항해 99 > Spring' 카테고리의 다른 글
Spring - JPA Entity 연관 관계 (1) | 2024.02.27 |
---|---|
Spring - Bean, 로그인/회원가입, Security, Validation (1) | 2024.02.26 |
RESTful API, 관심사 분리, @Setter 지양 (0) | 2024.02.25 |
프로젝트 세팅 - UCD, API 명세서, ERD, Git 연동 (0) | 2024.02.22 |
Spring - 입문 (1) | 2024.02.20 |