새소식

TIL

[TIL] 230517 <Spring> 3 Layer Architecture, IoC와 DI, JPA CORE

  • -
  • 현재 메모장 프로젝트는 Controller 클래스 하나로 모든 API를 처리하고 있음
  • 현재는 API 수가 적고 기능이 단순하여 코드가 복잡해 보이지 않을 수 있지만 앞으로 기능이 추가되고 복잡해진다면 문제 발생 가능
    • 한 개의 클래스에 너무 많은 양의 코드가 존재하기 때문에 코드를 이해 어려움
    • 현업에서는 코드의 추가 혹은 변경 요청이 계속 생길 수 있음
  • 이러한 문제점들을 해결하기 위해 서버 개발자들은 서버에서의 처리과정이 대부분 비슷하다는 걸 깨닫고,
    처리 과정을 크게 Controller, Service, Repository 3개로 분리하였음!

 

 

 

1. Controller

  • 클라이언트의 요청을 받음
  • 요청에 대한 로직 처리는 Service에게 전담함
    ㄴ Request 데이터가 있다면 Service에 같이 전달함
  • Service에서 처리 완료된 결과를 클라이언트에게 응답함

 

2. Service

  • 사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세 중에 실세!!!
    ㄴ 따라서 현업에서는 서비스 코드가 계속 비대해지고 있음
  • DB 저장 및 조회가 필요할 때는 Repository에게 요청함

 

3. Repository

  • DB 관리 (연결, 해제, 자원 관리)
  • DB CRUD 작업을 처리

 

 


1. Controller는 API 요청을 받고 Service에 받아온 데이터와 함께 요청을 넘김

  • 요청을 전달하기 위해 MemoService를 인스턴스화한 후 사용
  • MemoService에서 JdbcTemplate을 사용해야하기 때문에 생성자 파라미터로 전달

MemoController.java

package com.sparta.memo.controller;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.service.MemoService;

@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);
	}
}

 

2. 요청을 전달받은 Service는 해당 요청을 수행

MemoService.java

package com.sparta.memo.service;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

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 com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;

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 저장
		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);

		// Entity -> ResponseDto
		MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

		return memoResponseDto;
	}

	public List<MemoResponseDto> getMemos() {
		// 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 Long updateMemo(Long id, MemoRequestDto requestDto) {
		// 해당 메모가 DB에 존재하는지 확인
		Memo memo = findById(id);
		if(memo != null) {
			// memo 내용 수정
			String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
			jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);

			return id;
		} else {
			throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
		}
	}

	public Long deleteMemo(Long id) {
		// 해당 메모가 DB에 존재하는지 확인
		Memo memo = findById(id);
		if(memo != null) {
			// memo 삭제
			String sql = "DELETE FROM memo WHERE id = ?";
			jdbcTemplate.update(sql, id);

			return id;
		} else {
			throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
		}
	}


	private 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);
	}
}

 

 

 

1. DB와 연결 및 CRUD 작업은 Repository에서 진행

MemoRepository.java

package com.sparta.memo.repository;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

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 com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;

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() {

		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);
	}
}

 

2. DB 작업을 Repository에 요청하는 Service

MemoService.java

package com.sparta.memo.service;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;

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(memo);

		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("선택한 메모는 존재하지 않습니다.");
		}
	}

}

 

 


 

 

 

 

 


  • MemoService에서 "new MemoRepository(jdbcTemplate);" 코드가 중복
    → MemoService가 생성될 때, MemoRepository를 딱 한 번 생성해서 계속 사용하도록 변경
  • MemoController에서 "new MemoService(jdbcTemplate);" 코드가 중복
    → MemoController가 생성될 때, MemoService를 딱 한 번 생성해서 계속 사용하도록 변경

 

MemoService.java

package com.sparta.memo.service;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;

public class MemoService {

	private final MemoRepository memoRepository;

	public MemoService(JdbcTemplate jdbcTemplate) {
		this.memoRepository = new MemoRepository(jdbcTemplate);
	}

	public MemoResponseDto createMemo(MemoRequestDto requestDto) {

		// RequestDto -> Entity
		Memo memo = new Memo(requestDto);

		// DB 저장
		Memo saveMemo = memoRepository.save(memo);


		// Entity -> ResponseDto
		MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

		return memoResponseDto;
	}

	public List<MemoResponseDto> getMemos() {
		// DB 조회
		return memoRepository.findAll();
	}

	public Long updateMemo(Long id, MemoRequestDto requestDto) {
		// 해당 메모가 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) {
		// 해당 메모가 DB에 존재하는지 확인
		Memo memo = memoRepository.findById(id);
		if(memo != null) {
			// memo 삭제
			memoRepository.delete(id);

			return id;
		} else {
			throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
		}
	}

}

MemoController.java

package com.sparta.memo.controller;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.service.MemoService;

@RestController
@RequestMapping("/api")
public class MemoController {

	private final MemoService memoService;

	public MemoController(JdbcTemplate jdbcTemplate) {
		this.memoService = new MemoService(jdbcTemplate);
	}

	@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);
	}
}

 

MemoController → MemoService → MemoRepository

 

아래와 같이 느슨한 결합으로 만들어 주자!

MemoRepository → MemoService MemoController

나중 내용 추가

 

 

 

 

나중 내용 추가


 

DI를 사용하기 위해서는 객체 생성이 우선 되어야 하는데, Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신해줌

  • 빈 (Bean): Spring이 관리하는 객체
  • Spring IoC 컨테이너: 'Bean'을 모아둔 컨테이너

 

  • @Component
    - ‘Bean’으로 등록하고자하는 클래스 위에 설정
@Component
public class MemoService { ... }

        - Spring 서버가 뜰 때 IoC 컨테이너에 'Bean'을 저장해줌
          ㄴ Spring 'Bean' 이름: 클래스의 앞글자만 소문자로 변경함 (public class MemoServicememoService)

// 1. MemoService 객체 생성
MemoService memoService = new MemoService();

// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너

 

  • @ComponentScan(@SpringBootApplication에 의해 default 설정 되어있음)
    Spring 서버가 뜰 때 @ComponentScan에 설정해 준 packages 위치와 하위 packages 들을 전부 확인하여 @Component가 설정된 클래스들을 ‘Bean’으로 등록해줌

@Configuration
@ComponentScan(basePackages = "com.sparta.memo")
class BeanConfig { ... }

 

  • @Autowired
    - 필드 위에 @Autowired
       (Spring에서 IoC 컨테이너에 저장된 memoRepository ‘Bean’을 해당 필드에 DI 즉, 의존성을 주입 해줌)
@Component
public class MemoService {
		
    @Autowired
    private MemoRepository memoRepository;
		
		// ...
}

 

     - 'Bean'을 주입할 때 사용할 메서드 위에 @Autowired
        (객체의 불변성을 확보할 수 있기 때문에 일반적으로는 생성자를 사용하여 DI하는 것이 좋으며,
          set… Method를 만들고 @Autowired를 적용하여 DI 할 수도 있음)

@Component
public class MemoService {

    private final MemoRepository memoRepository;

    @Autowired
    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
		
		// ...
}

       - Spring 4.3 버전부터 생성자 선언이 1개 일 때만 @Autowired 생략가능!
       - Lombok의 @RequiredArgsConstructor를 사용하면 다음과 같이 코딩 가능

@Component
@RequiredArgsConstructor // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {

    private final MemoRepository memoRepository;
    
//    public MemoService(MemoRepository memoRepository) {
//        this.memoRepository = memoRepository;
//    }

		...

}

 

  • ApplicationContext : BeanFactory등을 상속하여 기능을 확장한 Container
    ㄴ BeanFactory는 ‘Bean’ 의 생성, 관계설정등의 제어를 담당하는 IoC 객체

        - 스프링 IoC 컨테이너에서 ‘Bean’을 수동으로 가져오는 방법

@Component
public class MemoService {

		private final MemoRepository memoRepository;

    public MemoService(ApplicationContext context) {
        // 1.'Bean' 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

        // 2.'Bean' 클래스 형식으로 가져오기
        // MemoRepository memoRepository = context.getBean(MemoRepository.class);

        this.memoRepository = memoRepository;
    }

		...		
}

 

 

Controller, Service, Repository의 역할로 구분된 클래스들을 ‘Bean’으로 등록할 때 해당 ‘Bean’ 클래스의 역할을 명시하기위해 사용됨 → 앞으로는 @Component가 아닌 3 Layer Annotation을 사용해서 ‘Bean’으로 등록!

  1. @Controller, @RestController
  2. @Service
  3. @Repository

 

 


 

 

Object-Relational Mapping
ㄴ Object : "객체"지향 언어 (자바, 파이썬),  Relational : "관계형" 데이터베이스 (H2, MySQL)

(반복적이고 번거로운 애플리케이션 단에서의 SQL 작업을 줄여주기 위해서) 객체와 DB의 관계를 매핑 해주는 도구

 

▼ ▼ ▼ ▼ ▼

Java Persistence API

자바 ORM 기술의 대표적인 표준 명세

  • JPA는 애플리케이션과 JDBC 사이에서 동작되고 있음
  • DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해줌
  • 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리 가능

▼ ▼ ▼ ▼ ▼

  • JPA 는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준(비 공식적 표준)하이버네이트!
  • 스프링 부트에서는 기본적으로 ‘하이버네이트’ 구현체를 사용 중

 

 


  • JPA에서 관리되는 클래스 즉, 객체를 의미
  • Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리됨

 

나중 내용 추가

 

Memo Entity

package com.sparta.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@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;
}
  • @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에 위임가능
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    • id bigint not null auto_increment : auto_increment 조건이 추가된 것을 확인 가능
    • 해당 옵션을 추가해주면 개발자가 직접 id 값을 넣어주지 않아도 자동으로 순서에 맞게 기본 키가 추가됨

 

 


  • Persistence를 한글로 번역하면 영속성, 지속성 이라는 뜻
  • 객체의 관점으로 해석해 보자면 ‘객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할수 있는 객체의 성질’을 의미
  • Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간

 

개발자들은 이제 직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장하거나 조회할 수 있으며 수정, 삭제 또한 가능!
이러한 일련의 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통!!

 

▶ Entity Manager

  • 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요
    ㄴ Entity를 관리하는 관리자
  • 개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제
  • EntityManager는 EntityManagerFactory를 통해 생성하여 사용

 

▶ EntityManagerFactory

  • EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용됨
  • EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야함
    ㄴ 정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 됨!

 

  • EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
    ㄴ JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성
  • EntityManager em = emf.createEntityManager();
    ㄴ EntityManagerFactory를 사용하여 EntityManager를 생성

 

 

  • 트랜잭션은 DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념
    ㄴ DB의 데이터들을 안전하게 관리하기 위해서 생겨난 개념
  • 가장 큰 특징은 여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있다는 점!
  • 이때, 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만
    SQL 중 단 하나라도 실패한다면 모든 변경을 되돌림!!

▼ ▼ ▼ ▼ ▼

영속성 컨텍스트에 Entity 객체들을 저장했다고 해서 DB에 바로 반영 되지는 않음

DB에서 하나의 트랜잭션에 여러 개의 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영하는 것처럼
JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가
마지막에 SQL을 한번에 DB에 요청해 변경을 반영함

 

EntityTest

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.sparta.entity.Memo;

public class EntityTest {

	EntityManagerFactory emf;
	EntityManager em;

	@BeforeEach
	void setUp() {
		emf = Persistence.createEntityManagerFactory("memo");
		em = emf.createEntityManager();
	}

	@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 를 종료합니다.
	}
}
  • JPA에서 이러한 트랜잭션의 개념을 적용하기 위해서는 EntityManager에서 EntityTransaction을 가져와 트랜잭션 적용가능
    • EntityTransaction et = em.getTransaction();
      ㄴ 해당 코드를 호출하여 EntityTransaction을 가져와 트랜잭션을 관리
  • et.begin(); : 트랜잭션을 시작하는 명령어
  • et.commit(); : 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어
  • et.rollback(); : 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어



memo.setId(1L);과 같이 식별자 값을 넣어주거나, 
@GeneratedValue(strategy = GenerationType.IDENTITY)을 넣어주어 id에 auto_increment 조건을 추가해주어야
오류가 발생하지 않음!!!

 

영속성 컨텍스트 Debugging으로 확인


영속성 컨텍스트: Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간

 

  • 영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있음
    • 우리가 저장하는 Entity 객체들이 1차 캐시 즉, 캐시 저장소에 저장됨
    • 캐시 저장소는 Map 자료구조 형태로 되어있음
      • key - @Id로 매핑한 기본 키 즉, 식별자 값을 저장
      • value - 해당 Entity 클래스의 객체를 저장
      • 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리

 

▶ Entity 저장

em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장

PersistenceTest

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.sparta.entity.Memo;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

public class PersistenceTest {

	EntityManagerFactory emf;
	EntityManager em;

	@BeforeEach
	void setUp() {
		emf = Persistence.createEntityManagerFactory("memo");
		em = emf.createEntityManager();
	}

	@Test
	@DisplayName("1차 캐시 : Entity 저장")
	void test1() {
		EntityTransaction et = em.getTransaction();

		et.begin();

		try {

			Memo memo = new Memo();
			memo.setId(1L);
			memo.setUsername("Robbie");
			memo.setContents("1차 캐시 Entity 저장");

			em.persist(memo);

			et.commit();

		} catch (Exception ex) {
			ex.printStackTrace();
			et.rollback();
		} finally {
			em.close();
		}

		emf.close();
	}
}

 

em > persistenceContext > entitiesBykey를 확인해보면 key-value 형태로 정보가 저장되어있음

 

 

▶ Entity 조회

  • 캐시 저장소에 조회하는 Id가 존재하지 않은 경우

        (1) 캐시 저장소 조회

        (2) DB SELECT 조회 후 캐시 저장소에 저장

             ㄴ em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인 한 후 해당 값이 없다면
                 DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환!

PersistenceTest

	@Test
	@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
	void test2() {
		try {

			Memo memo = em.find(Memo.class, 1);
			System.out.println("memo.getId() = " + memo.getId());
			System.out.println("memo.getUsername() = " + memo.getUsername());
			System.out.println("memo.getContents() = " + memo.getContents());


		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			em.close();
		}

		emf.close();
	}

             ㄴ DB에서 데이터를 조회만 하는 경우에는 데이터의 변경이 발생하는 것이 아니기 때문에 트랜잭션이 없어도 조회가 가능
             ㄴ Memo memo = em.find(Memo.class, 1); 호출 시 캐시 저장소에 해당 값이 존재하지 않기 때문에
                 DB에 SELECT 조회하여 캐시 저장소에 저장한 후 반환

 

 

  • 캐시 저장소에 조회하는 Id가 존재하는 경우

         em.find(Memo.class, 1); 호출 시 캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회하여 값이 있다면
        해당 Entity 객체를 반환

PersistenceTest

	@Test
	@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하는 경우")
	void test3() {
		try {

			Memo memo1 = em.find(Memo.class, 1);
			System.out.println("memo1 조회 후 캐시 저장소에 저장\n");

			Memo memo2 = em.find(Memo.class, 1);
			System.out.println("memo2.getId() = " + memo2.getId());
			System.out.println("memo2.getUsername() = " + memo2.getUsername());
			System.out.println("memo2.getContents() = " + memo2.getContents());


		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			em.close();
		}

		emf.close();
	}

         ㄴ Memo memo1 = em.find(Memo.class, 1); 호출 때는 캐시 저장소에 존재하지 않기 때문에
             DB에 SELECT 조회하여 캐시 저장소에 저장
         ㄴ Memo memo2 = em.find(Memo.class, 1); 를 호출 했을 때는 이미 캐시 저장소에 해당 값이 존재하기 때문에
             DB에 조회하지 않고 캐시 저장소에서 해당 값을 반환

  • '1차 캐시' 사용의 장점
    1. DB 조회 횟수를 줄임
    2. '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)

PersistenceTest

	@Test
	@DisplayName("객체 동일성 보장")
	void test4() {
		EntityTransaction et = em.getTransaction();

		et.begin();

		try {
			Memo memo3 = new Memo();
			memo3.setId(2L);
			memo3.setUsername("Robbert");
			memo3.setContents("객체 동일성 보장");
			em.persist(memo3);

			Memo memo1 = em.find(Memo.class, 1);
			Memo memo2 = em.find(Memo.class, 1);
			Memo memo  = em.find(Memo.class, 2);

			System.out.println(memo1 == memo2);
			System.out.println(memo1 == memo);

			et.commit();
		} catch (Exception ex) {
			ex.printStackTrace();
			et.rollback();
		} finally {
			em.close();
		}

		emf.close();
	}

            ㄴ 같은 값을 조회하는 memo1과 memo2는 == 결과 true를 반환 (객체 동일성 보장)
            ㄴ memo1과 다른 값을 조회하는 memo는 == 결과 false를 반환

 

 

▶ Entity 삭제

1. 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에 조회해서 저장

em.find(Memo.class, 2); 호출 하여 memo 객체를 캐시 저장소에 저장한 후 entityEntry를 확인해보면 memo Entity 객체가 영속성 컨텍스트가 관리하는 MANAGED 상태임

2. em.remove(entity); 

em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청됨

em.remove(memo); 호출 후 memo Entity 객체가 DELETED 상태로 바뀐 것 확인가능
트랜잭션 commit 후 DB 데이터를 확인해보면 해당 데이터가 삭제 되어있음

PersistenceTest

@Test
@DisplayName("Entity 삭제")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

       Memo memo = em.find(Memo.class, 2);

       em.remove(memo);

       et.commit();

    } catch (Exception ex) {
       ex.printStackTrace();
       et.rollback();
    } finally {
       em.close();
    }

    emf.close();
}

 

 

.

JPA는 트랜잭션 처럼 SQL을 모아서 한번에 DB에 반영함
ㄴ JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영!

PersistenceTest

	@Test
	@DisplayName("쓰기 지연 저장소 확인")
	void test6() {
		EntityTransaction et = em.getTransaction();

		et.begin();

		try {
			Memo memo = new Memo();
			memo.setId(2L);
			memo.setUsername("Robbert");
			memo.setContents("쓰기 지연 저장소");
			em.persist(memo);

			Memo memo2 = new Memo();
			memo2.setId(3L);
			memo2.setUsername("Bob");
			memo2.setContents("과연 저장을 잘 하고 있을까?");
			em.persist(memo2);

			System.out.println("트랜잭션 commit 전");
			et.commit();
			System.out.println("트랜잭션 commit 후");

		} catch (Exception ex) {
			ex.printStackTrace();
			et.rollback();
		} finally {
			em.close();
		}

		emf.close();
	}

em > actionQueue를 확인해보면 insertions > executables에 Insert할 memo#2, memo#3 Entity 객체 2개가 들어가 있음 (쓰기 지연 저장소)
트랜잭션 commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한번에 Insert SQL 2개가 순서대로 요청됨

▶ flush()

  • 사실 트랜잭션 commit 후 추가적인 동작이 있음 → em.flush(); 메서드의 호출
  • flush 메서드는 영속성 컨텍스트의 변경 내용들을 DB에 반영하는 역할을 수행
    쓰기 지연 저장소의 SQL들을 DB에 요청

      ㄴ em.flush(); 메서드가 호출되자 바로 DB에 쓰기 지연 저장소의 SQL이 요청되었음
      ㄴ 이미 쓰기 지연 저장소의 SQL이 요청 되었기 때문에 더 이상 요청할 SQL이 없어 트랜잭션이 commit 된 후에 SQL 기록이 안보임

 


트랜잭션을 설정하지 않고 flush() 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생!

  • Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요!!!

 

  • 영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면?
    ㄴ 하나의 Update SQL로 처리할 수 있는 상황을 여러번 Update SQL을 요청하게 되기 때문에 비효율적
  • 그렇다면 JPA는 어떻게 Update를 처리할까?
    em.update(entity); 같은 메서드를 지원할 것 같지만 찾아볼 수 없음!

  • JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장
    • 트랜잭션이 commit되고 em.flush(); 가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교
    • 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기지연 저장소의 SQL을 DB에 요청
    • 마지막으로 DB의 트랜잭션이 commit 되면서 반영됨
  • 따라서 변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영됨
    ㄴ 이러한 과정을 변경 감지, Dirty Checking이라 부름

PersistenceTest

	@Test
	@DisplayName("변경 감지 확인")
	void test8() {
		EntityTransaction et = em.getTransaction();

		et.begin();

		try {
			System.out.println("변경할 데이터를 조회합니다.");
			Memo memo = em.find(Memo.class, 4);
			System.out.println("memo.getId() = " + memo.getId());
			System.out.println("memo.getUsername() = " + memo.getUsername());
			System.out.println("memo.getContents() = " + memo.getContents());

			System.out.println("\n수정을 진행합니다.");
			memo.setUsername("Update");
			memo.setContents("변경 감지 확인");

			System.out.println("트랜잭션 commit 전");
			et.commit();
			System.out.println("트랜잭션 commit 후");

		} catch (Exception ex) {
			ex.printStackTrace();
			et.rollback();
		} finally {
			em.close();
		}

		emf.close();
	}

  • entityInstance는 Entity 객체의 현재 상태
  • entityEntry > loadedState는 조회했을 때 즉, 해당 Entity의 최초 상태
  • 트랜잭션 commit 후 em.flush(); 메서드가 호출되면 현재 상태와 최초 상태를 비교하고 변경이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장한 후 DB에 요청

수정을 진행하고 트랜잭션 commit 후 Update SQL이 요청된 것을 확인가능

 

 


 

Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");
  • new 연산자를 통해 인스턴스화 된 Entity 객체를 의미
  • 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않음

 

persist(entity) : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듦

@Test
@DisplayName("비영속과 영속 상태")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo(); // 비영속 상태
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("비영속과 영속 상태");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • 비영속 상태이기 때문에 entitiesByKey=null 
  • 비영속 상태는 JPA가 관리하지 못하기 때문에 해당 객체의 데이터를 변경해도 변경 감지가 이루어지지 않음

em.persist(memo); 메서드 호출 후 영속성 컨텍스트에 저장되었고 MANAGED 상태 즉, JPA가 관리하는 영속 상태의 Entity가 됨

 

영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미

 

<영속 상태에서 준영속 상태로 바꾸는 방법>

  • detach(entity) : 특정 Entity만 준영속 상태로 전환
    ㄴ 영속성 컨텍스트에서 관리되다(Managed)가 분리된 상태(Detached)로 전환됨
@Test
@DisplayName("준영속 상태 : detach()")
void test2() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 1);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("detach() 호출");
        em.detach(memo);
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("memo Entity 객체 수정 시도");
        memo.setUsername("Update");
        memo.setContents("memo Entity Update");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.find(Memo.class, 1); 메서드 호출 후 MANAGED 상태

  • em.detach(memo); 메서드를 호출하여 특정 Entity 객체 Memo#1를 영속성 컨텍스트에서 제거
  • 준영속 상태로 전환되면 1차 캐시 즉, 캐시 저장소에서 제거되기 때문에 JPA의 관리를 받지 못해 영속성 컨텍스트의 어떠한 기능도 사용불가

  • 따라서 memo Entity 객체의 데이터를 수정해도 변경감지 기능을 사용할 수 없어 Update SQL이 수행안됨
  • em.contains(memo); 는 해당 객체가 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드로 em.detach(memo); 이후 확인했을 때 false가 출력된 것을 확인가능

 

 

  • clear() : 영속성 컨텍스트를 완전히 초기화
    • 영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환
    • 영속성 컨텍스트 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태가 됨
    • 따라서 계속해서 영속성 컨텍스트를 이용가능
@Test
@DisplayName("준영속 상태 : clear()")
void test3() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 2);

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("clear() 호출");
        em.clear();
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("memo#1 Entity 다시 조회");
        Memo memo = em.find(Memo.class, 1);
        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("\n memo Entity 수정 시도");
        memo.setUsername("Update");
        memo.setContents("memo Entity Update");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.clear(); 메서드 호출 후 완전히 비워진 영속성 컨텍스트를 확인가능

다시 memo#1 Entity를 조회하여 영속성 컨텍스트에 저장된 것을 확인가능

  • em.clear(); 메서드 호출 후 em.contains(memo1,2); 확인했을 때 false가 출력된 것을 확인가능
  • 다시 memo#1 Entity를 조회한 후 em.contains(memo); 확인했을 때 true가 출력된 것을 확인가능
  • 또한 memo Entity 객체의 데이터를 수정하자 트랜잭션 commit 후 Update SQL이 수행된 것을 확인가능

 

 

  • close() : 영속성 컨텍스트를 종료
    • 해당 영속성 컨텍스트가 관리하던 영속성 상태의 Entity들은 모두 준영속 상태로 변경
    • 영속성 컨텍스트가 종료되었기 때문에 계속해서 영속성 컨텍스트를 사용불가
@Test
@DisplayName("준영속 상태 : close()")
void test4() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 2);

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("close() 호출");
        em.close();
        Memo memo = em.find(Memo.class, 2); // Session/EntityManager is closed 메시지와 함께 오류 발생
        System.out.println("memo.getId() = " + memo.getId());

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em.close(); 메서드 호출 이후 EntityManager를 사용하려고 하자 오류가 발생
    ㄴ 영속성 컨텍스트가 종료되면 계속해서 영속성 컨텍스트를 사용할 수 없음!

 

 

<준영속 상태에서 다시 영속 상태로 바꾸는 방법>

  • merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
    • 파라미터로 전달된 Entity의 식별자 값으로 영속성 컨텍스트를 조회
      1. 해당 Entity가 영속성 컨텍스트에 없다면?
        • DB에서 새롭게 조회
        • 조회한 Entity를 영속성 컨텍스트에 저장
        • 전달 받은 Entity의 값을 사용하여 병합
        • Update SQL이 수행됨(수정)
      2. 만약 DB에서도 없다면 ?
        • 새롭게 생성한 Entity를 영속성 컨텍스트에 저장
        • Insert SQL이 수행됨(저장)
  • 따라서 merge(entity) 메서드는 비영속, 준영속 모두 파라미터로 받을 수 있으며 상황에 따라 ‘저장’을 할수도 ‘수정’을 할수도 있음

 

1. merge(entity) 저장

@Test
@DisplayName("merge() : 저장")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(3L);
        memo.setUsername("merge()");
        memo.setContents("merge() 저장");

        System.out.println("merge() 호출");
        Memo mergedMemo = em.merge(memo);

        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.merge(memo); 호출 후 영속성 컨텍스트에 Memo#3 객체가 저장되고 Insert SQL이 추가된 것을 확인 가능

  • em.merge(memo); 호출 후 영속성 컨텍스트에 해당 값이 없어 DB에 조회 했는데도 해당 값이 없기 때문에 새롭게 생성하여 영속성 컨텍스트에 저장하고 Insert SQL이 수행됨
    • 비영속 상태의 memo는 merge() 호출 후에 해당 memo 객체가 영속성 컨텍스트에 저장된게 아니라 새롭게 생성되어 영속성 컨텍스트에 저장되었기 때문에 false가 반환됨
    • 새롭게 저장된 영속 상태의 객체를 반환받은 mergedMemo는 true가 반환됨

 

 

2. merge(entity) 수정

@Test
@DisplayName("merge() : 수정")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 3);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("detach() 호출");
        em.detach(memo); // 준영속 상태로 전환
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("준영속 memo 값 수정");
        memo.setContents("merge() 수정");

        System.out.println("\n merge() 호출");
        Memo mergedMemo = em.merge(memo);
        System.out.println("mergedMemo.getContents() = " + mergedMemo.getContents());

        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • detach() 메서드 호출로 조회해온 영속 상태의 memo 객체를 준영속 상태로 전환
  • 준영속 상태의 memo의 값을 수정한 후 memo 객체를 사용해서 merge() 메서드를 호출
  • memo 객체는 준영속 상태이기 때문에 현재 영속성 컨텍스트에는 해당 객체가 존재하지 않음
    • 따라서 DB에서 식별자 값을 사용하여 조회한 후 영속성 컨텍스트에 저장하고 파라미터로 받아온 준영속 상태의 memo 객체의 값을 새롭게 저장한 영속 상태의 객체에 병합하고 반환
    • 그 결과 반환된 mergedMemo의 contents를 출력하였을 때 변경되었던 내용인 “merge() 수정”이 출력
    • 트랜잭션 commit 후 Update SQL이 수행됨
  • 준영속 상태의 Entity memo는 merge() 호출 후에도 영속성 컨텍스트에 저장되어 있지 않기 때문에 false가 반환되었고
    새롭게 저장된 영속 상태의 객체를 반환받은 mergedMemo는 true가 반환됨

 

 

remove(entity) : 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.