본문 바로가기

항해 99/Spring

Spring - 입문 2

3 Layer Architecture

서버의 처리과정을 Controller, Service, Repository 3개로 분리

1. Controller

  • Client의 요청을 받고 요청에 대한 로직 처리는 Service에 전담
    • Request 데이터 있을 시 Service에 같이 전달
  • Service의 처리 완료 결과를 Client에 응답

2. Service

 

  • 요구사항을 처리(비즈니스 로직)하는 실세
    • 현업에서는 서비스 코드가 계속 비대해짐
  • DB 저장 및 조회 필요 시 Repository에 요청

3. Repository

  • DB 관리(연결, 해제, 자원 관리)
  • DB CRUD 작업 처리(생성, 읽기, 수정, 삭제)

3 Layer Architecture 전체 구조

 

 

역할 분리 

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);
    }
}
  1. Cotroller는 API 요청을 받고 Service에 받아온 데이터와 함께 요청 전달
    • 요청 전달을 위해 MemoService 인스턴스 화 후 사용
    • ModelService에서 JdbcTemplate 사용을 위해 생성자 파라미터로 전달
  2. 요청을 전달 받은 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

  1. DB 연결 및 CRUD 작업은 Repository에서 진행
  2. 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)

  1. Database 탭을 클릭 + 버튼 클릭
  2. Data Source > MySQL 를 클릭
  3. User, Password, Database 정보를 추가한 후 Ok를 클릭
  4. 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들은 모두 준영속 상태로 변경, 영속성 컨텍스트가 종료되었기 때문에 계속해서 영속성 컨텍스트를 사용할 수 없음
  • 준영속 상태에서 다시 영속 상태로 바꾸는 방법
    • merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
      • merge(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 JPAJPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈
    • 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();
    }
}
  1. Timestamped 생성
  2. @EnableJpaAuditing 애너테이션을 추가
  3. 적용하고자하는 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();
    }
}

 

 

실습 파일

jpa-core.zip
0.25MB
memo.zip
0.40MB