본문 바로가기

[부트캠프] kdt 심화 과정

[스파르타코딩] 스프링 입문 주차(2)

DTO

 

DTO(data transfer object) 데이터 전송 및 이동을 위해 생성되는 객체를 의미하며,

클라이언트에서 전송되는 데이터를 객체로 처리할 때 사용된다.

서버의 계층간의 이동?에도 사용된다.

*서버의 계층간의 이동 → 컨트롤러TO(data transfer object) 데이터 전송 및 이동을 위해 생성되는 객체를 의미하며,

클라이언트에서 전송되는 데이터를 객체로 처리할 때 사용된다.

또 서버의 계층간의 이동?에도 사용된다.

 

*서버의 계층간의 이동

1. 서버의 대표 계층 ex. 컨트롤러, 서비스, 레파지토리 등

2. A라는 클래스 객체에서 B라는 클래스 객체로 데이터를 보내줘야 될 때 dto 객체가 사용된다.

dto는 순수한 (오래된, 옛날, 舊) 자바 객체다_pojo(plain old java object) 

 

DTO는 DB와의 소통을 담당하는 Java 클래스

              └ 엔터티(Entity)

그대로 클라이언트에게 반환하는 것이 아니라

DTO로 한 번 변환한 후 반환할 때도 사용된다.


JdbcTemplate

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

	// JdbcTemplate은 스프링에서 관리하는 클래스
    // org.springframework.jdbc.core.JdbcTemplate

    private final JdbcTemplate jdbcTemplate;
    
    /*
        서버가 시작되면 MemoController 객체가 만들어지는데
        MemoController 객체가 만들어질 때
        JdbcTemplate 객체가 MemoController의 필드인 jdbcTemplate에 대입됨
        --> jdbcTemplate 참조변수가 객체 JdbcTemplate 객체를 가리킨다는 의미

        클라이언트로부터 전달된 url이 /api일 경우
        MemoController 객체가 사용됨.
    */

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    
    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
    	// RequestDto --> Entity로 변환
    	Memo memo = new Memo(requestDto);
    /*
    	어플리케이션 서버와 연결된 데이터베이스의 테이블의 기본키인(PK) id의 값이
        데이터베이스에서 자동으로 값을 설정해주는 AUTO_INCREMENT로 설정되어 있음.
        (개발자가 값을 설정해줄 필요가 없다)
        --> insert문을 실행할 때 id 값을 설정하지 않는다.
    
    	JdbcTemplate객체가 갖는 update() 메서드는
        Connection을 전달받아 Connection으로 PreparedStatement객체를 만들 수 있다.
        --> Connection(애플리케이션 서버와 데이터베이스간 통로를 만들고)
        --> Connection을 통해 완성된 sql문을 보낼 수 있음.
        
        PreparedStatement 객체를 통해 sql문에 작성되어야할 인자를 채운다.
        
        jdbcTemplate 두 번째 파라미터로 keyHolder를 전달한다.
        
        jdbcTemplate의 update() 메서드가 반환하는 int타입
        --> JdbcTemplate객체를 통해 사용하는 update() 메서드의 첫 번째 인자로
        sql문을 완성하고, 두 번째 인자로 KeyHolder 인터페이스를 구현한 객체를 전달하면,
        sql문을 실행하고 난 뒤의 생성된 id 값을 KeyHolder 인터페이스를 구현한 객체에 담겨?
        자바에서 확인할 수 있다.
        
    */
    
    KeyHolder keyHolder = new GeneratedKeyHolder();
    
    /*

        jdbcTemplate 객체를 사용해서 dml하는 방법
        문자열 타입으로 sql문을 작성한다.

        예를 들면
    */

    
    String sql = "INSERT INTO memo(username, contents) VALUES(?, ?)";
    
    /* 	물음표를(?) 채우는 방법
		
        sql문이 insert, update, delete인 경우
        JdbcTemplate 객체의 update() 메서드를 사용,
        select문인 경우 query() 메서드를 사용한다.

	*/
    
    jdbcTemplate.update(connection -> {
    	// 어플리케이션 서버와 데이터베이스간 연결을 얻어서: Connection
        // PreparedStatement 객체를 생성하는데,
        // 미완성된 sql문과 sql문을 실행했을 때 데이터베이스에서 자동생성된 컬럼값을 반환하도록
        // PreparedStatement 객체를 설정한다 --> Statement.RETURN_GENERATED_KEYS
        // PreparedStatement 객체의 getGeneratedKeys() 메서드를 통해 key값을 가져올 수 있다.
        
        PreparedStatement preparedStatement = connection.prepareStatement(sql,
        Statement.RETURN_GENERATED_KEYS);
        
        preparedStatement.setString(1, memo.getUsername());
        ...
        },
        keyHolder);
    
    // insert문을 실행한 후 데이터베이스에서 자동생성된 
    // id의 값을 갖고 있는 KeyHolder(를 구현한)객체
    // 해당 객체에서 id의 값을 갖고 오는 방법
        Long id = keyHolder.getKey().longValue();
        memo.setId(id);
    ...
    }
    
}

 

KeyHolder 인터페이스를 구현한 객체는 데이터베이스에서 sql문을 실행하고난 뒤의 (자동생성된) id의 값을 갖는다.

해당 id의 값을 얻기 위해 KeyHolder 인터페이스를 구현한 객체.getKey() 를 작성하면 Number타입의 객체가 반환된다.

Number타입의 객체: Long, Integer 등등


# 테이블 만들기
# CREATE TABLE IF NOT EXISTS MANAGER -> MANAGER 테이블이 존재하지 않는다면 생성
CREATE TABLE MANAGER
(
	id bigint primary key,
    name varchar(100) not null,
    student_code varchar(100) not null,
    # 제약조건 설정 CONSTRAINT 제약조건명 제약조건(제약조건을 설정할 컬럼명) REFERENCES 참조할 테이블명(참조할 테이블의 참조할 컬럼명)
    CONSTRAINT manager_fk_student_code foreign key(student_code) REFERENCES student(student_code)  
);

# 테이블 컬럼에 기능을 부여하기 ALTER, MODIFY를 사용해서 MANAGER테이블의 id에 AUTO_INCREMENT 기능 부여하기
ALTER TABLE MANAGER MODIFY COLUMN id bigint auto_increment;

# auto_increment 기능을 사용하여 managerA가 관리하는 s1의 정보를 삽입하기
INSERT INTO MANAGER(name, student_code) VALUES('managerA', 's1');

# join을 사용하여 managerA가 관리하는 수강생 이름과 시험 주차 별 성적을 가져오기
SELECT s.name, e.exam_seq, e.score
FROM MANAGER m JOIN STUDENT s
ON m.student_code = s.student_code
JOIN EXAM e
ON s.student_code = e.student_code
WHERE m.name = 'managerA';

 

테이블에 설정되어 있는 제약조건을 삭제할 때 ALTER와 DROP을 사용하기

ALTER TABLE 테이블명: ALTER는 테이블의 컬럼과 제약 조건을 변경/추가/삭제할 때 즉 테이블에 대한 정의를 수정할 때 사용된다. 1) ALTER TABLE EXAM 2) DROP CONSTRAINT exam_fk_student_code;

ALTER TABLE EXMA: EXAM 테이블의 정의를 수정한다.

exam테이블의 컬럼/컬럼에 설정된 혹은 설정할 제약조건 관련 수정작업을 의미컬럼 추가,

컬러명 변경, 컬럼 삭제, (컬럼에) 제약조건 추가, 제약조건 삭제 등의 작업을 의미 

 

------ cascade, constraint

cascade: a small, steep waterfall → 작고 가파른/급격한/매우 높은 폭포

폭포처럼 흐르다. 

constraint: 제약(이 되는 것), 제약·통제

alter처럼 테이블의 정의를 수정하는 명령어와 함께 쓰여 컬럼명을 변경하거나 컬럼을 추가 또는 삭제하거나

컬럼에 설정되어 있는 제약조건을 삭제하거나(drop constraint 제약조건명) 제약조건을 추가하는 등의 작업을 할 수 있다.

제약조건을 추가할 때(add constraint 제약조건명) 그리고 제약조건에 옵션을 설정할 수도 있다.

 

예를 들면 'EXAM' 테이블의 student_code 컬럼에 foreign key 제약조건을 설정하고,

fk는 'STUDENT' 테이블의 student_code 컬럼을 참조한다.

제약조건에 옵션 설정) 그리고 'STUDENT' 테이블에서 student_code 컬럼에 대응하는 값이 삭제되면

'EXAM' 테이블의 student_code 컬럼에 대응하는 값의 레코드도 삭제된다 → on delete cascade

ALTER TABLE EXAM ADD CONSTRAINT exam_fk_student_code FOREIGN KEY(student_code)

REFERENCES STUDENT(student_code) on delete cascade;


3 Layer Architecture

 

클라이언트로부터 요청을 받으면: 클라이언트 request → 서버( → 컨트롤러)

클라이언트로부터 전달받은 요청에 data가 있다면 컨트롤러는 서비스에 data를 전달한다.

서비스에서 처리가 완료된 그 결과를 컨트롤러가 받아서 컨트롤러가 클라이언트에게 응답한다.

 

서비스는 사용자의 요구사항을 실질적으로 처리하는 곳,

사용자의 요구사항 처리를 비즈니스 로직이라고도 한다.

서비스 계층은 레파지토리 계층과 상호작용하는데 예를 들어 db에 데이터를 저장하거나

데이터를 조회할 때 서비스 계층은 레파지토리 계층에게 요청한다.

 

레파지토리 계층은 데이터베이스와 상호작용한다.

데이터베이스를 연결하고, 해제, 자원관리 등 데이터베이스 crud 작업을 수행한다.

 

스프링 프레임워크에서 Resource Controller 생성하기

Resource Controller는 클라이언트의 요청에 대응하는 적절한 비즈닉스 로직을 연결해준다.

그리고 그 결과를 http 응답으로 반환한다.

Resource Controller는 @RestController 어노테이션으로 정의된다.

@RestController 어노테이션은 클래스가 RESTful 웹 서비스의 컨트롤러임을 나타내며

@RestController 어노테이션이 붙은 클래스의 모든 메서드의 반환 값은 뷰(view → 파일 및 문서)가 아닌

http 응답 본문으로 직접 전송된다.

@RestController 어노테이션은 @Controller와 @ResponseBody 어노테이션을 결합한 기능을 갖는다.

resource controller는 http 요청을 처리하는 역할만하며

실제 비즈니스 로직은 Service Layer에서 처리된다.

 

restful 웹 서비스란 웹의 기본 원칙을 기반으로 한다.

http 프로토콜을(http 규약, 통신 규약) 사용하여 클라이언트와 서버간 데이터를 송수신하는데

그 과정에서 리소스(데이터) 단위로 클라이언트와 서버가 상호작용한다?

 

스프링에서 @Controller, @RestController 어노테이션은 컨트롤러 계층임을 정의하는데 사용되는 어노테이션이다.

@Controller 어노테이션은 전통적인 Spring MVC 에서 사용되는 어노테이션으로

주로 뷰(view)를 반환하는 컨트롤러를 정의할 때 사용된다. 여기서 뷰란 확장자가 .html인 문서의 이름을 반환하는 것이다.

문서에 데이터를 전달하기 위해 Model 객체를 사용할 수 있지만 http 응답 본문에는 직접 데이터를 반환할 수 없다.

만약 http 응답 본문에 직접 데이터를 반환하려면 해당 메서드에 @ResponseBody 어노테이션을 추가해야 한다.

 

@RestController 어노테이션은 @Controller와 @ResponseBody 어노테이션을 합친 기능을 갖는다.

메서드의 반환 값이 뷰가 아닌 http 응답 본문으로 직접 전송된다.

메서드의 반환 값은 JSON, XML 등과 같은 데이터 형식으로 직렬화되어 http 응답 본문에 포함된다.

 

restful하다는 것? restful 웹 서비스는 클라이언트와 서버간의 상호작용을 리소스 중심으로 설계하고,

http 프로토콜을 효과적으로 사용하여 통신하는 것을 의미한다. 리소스 중심으로 설계된다는 것은

리소스는 데이터의 단위를 의미한다. 클라이언트로부터 받은 요청에 대응하는 메서드 연결을 리소스를 기준으로 식별한다.

 

컨트롤러, 서비스 계층 분리하기

// 컨트롤러 계층

/* 
	@RestController: @Controller + @ResponseBody
	restful한 웹 서비스를 구축하기 위해 @RestController 어노테이션 사용
    컨트롤러 계층임을 스프링에게 알려주고,
    해당 클래스에 작성된 모든 메서드는 http 응답 본문에 데이터를 반환한다.
    restful하다는 것은 리소스 중심의 설계, 
    http 통신 규약을 활용하여 http 응답 본문에 데이터를 반환할 수 있다는 것,
    http 메서드를 사용할 수 있다는 것 등을 의미한다.

*/
@RestController 
// 클라이언트의 요청이 들어왔을 때 uri를 데이터 단위로 인식하여
// /api 를 감지했을 때 해당 경로에 맞는 메서드를 매칭해준다.
@RequestMapping("/api")
public class MemoController {

    private final JdbcTemplate jdbcTemplate;


	// MemoController 객체가 생성될 때 JdbcTemplate 객체를 전달받아
    // MemoController 객체의 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);
    }
}

// 서비스 계층

	/*
    	public class MemoService {
        	private final JdbcTemplate jdbcTemplate;
            
            public MemoService(JdbcTemplate jdbcTemplate) {
            	this.jdbcTemplate = jdbcTemplate;
            }
            
            /*
            	MemoRequestDto 클래스
                
                @Getter
                public class MemoRequestDto {
                	private String username;
                    private String contents;
                }
            
            
            
            */
            
            public MemoResponseDto createMemo(MemoRequestDto requestDto) {
            	
                // 클라이언트로부터 전달받은 객체 requestDto에는
                // username과 contents 필드가 있다.
                // 이를 Memo 생성자에 전달하고 Memo 인스턴스를 생성하면
                // id의 값이 비어있게 된다.
                Memo memo = new Memo(requestDto);


				// db저장 과정
                // 데이터베이스에 저장할 때 데이터베이스가 자동으로 id의 값을 채우도록
                // auto_increment 설정이 되어 있는 상황이고, 해당 값을 반환받기 위해
                // KeyHolder 인터페이스를 구현한 객체를 생성하기
                
                // 데이터베이스가 자동생성한 기본 키 값을 반환받기 위한 객체 생성
                KeyHolder keyHolder = new GeneratedKeyHolder();
                
                String sql = "INSERT INTO memo(username, contents) VALUES(?, ?)";
                // JdbcTemplate 객체가 갖는 update 객체는 insert, update, delete sql문을 수행한다.
                jdbc.update(connection -> {
                	// 어플리케이션 서버와 데이터베이스를 연결하는 연결객체를 전달받아
                    // 연결객체로부터 sql문을 완성할? 객체를 얻는다.
                    // 값이 미완성된 sql문과 값을 채우고 해당 sql문을 데이터베이스에서 실해할 경우
                    // 생성된 키를 반환받을 수 있도록 설정하는 작업
                    PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                
                
                	preparedStatement.setString(1, memo.getUsername());
                    preparedStatement.setString(2, memo.getContents());
                    
                    // sql문 완성
                    return preparedStatement;
                }, keyHolder);
                
                // db에 insert 후 받아온 기본 키 확인
                // .getKey() 메서드의 반환타입은 Number
                // Number객체는 모든 숫자타입을 기본타입으로 변환하는 메서드를 제공한다.
                // long타입을 반환하는 Number 객체의 longValue() 메서드,
                // 기본타입 long타입의 값을 Long타입 객체로 auto boxing
                Long id = keyHolder.getKey().longValue();
                
                memo.setId(id);
                
                // Entity 객체를 ResponseDto 객체로 변환
                MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
                
                return memoResponseDto;
            }
        }
}

 

서비스, 레파지토리 계층 분리하기

    // MemoService --> 서비스 계층에서
	private final JdbcTemplate jdbcTemplate;
    
    public MemoService(JdbcTemplate jdbcTemplate){
    	this.jdbcTemplate = jdbcTemplate;
    }

    public MemoResponseDto createMemo(MemoRequestDto requestDto){

        // 클라이언트가 서버에 요청 --> 컨트롤러가 클라이언트가 보낸 데이터를 전달받고,
        // 컨트롤러가 서비스 계층에 해당 데이터를 전달함
        // 유저이름과 내용만 들어있는 객체를 Memo객체로(Entity) 생성함

        Memo memo = new Memo(requestDto);

        // DB에 저장 --> 데이터베이스와 소통하는 Repository 계층에 데이터를 전달하기 위해
        // 서비스 계층에서 레파지토리 객체를 생성한다. 생성할 때 JdbcTemplate 객체를 전달한다.
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        
        
        /*
        	레파지토리 계층에 username과 contents값만 있는 Memo객체를 전달한다.
            해당 데이터를 갖고 레파지토리 계층에서 데이터베이스에 작업을 한다.
            
            public class MemoRepository{
            	private final JdbcTemplate jdbcTemplate;
                
                public MemoRepository(JdbcTemplate jdbcTemplate){
                	this.jdbcTemplate = jdbcTemplate;
                }
            
            	public Memo save(MemoRequestDto requestDto) {
                	KeyHolder keyHolder = new GeneratedKeyHolder();
                    
                    String sql = "INSERT INTO memo(username, contents) VALUES(?, ?)";
                    
                    jdbcTemplate.update(connection -> {
                    	PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                    	preparedStatement.setString(1, requestDto.getUsername());
                    	preparedStatement.setString(2, requestDto.getContents());                        
                    	return preparedStatement;                    
                    }, keyHolder);
                    
                    Long id = keyHolder.getKey().longValue();
                    
                    memo.setId(id);
                    return memo;
                }
            }
        
        */
        Memo saveMemo = memoRepository.save(memo);

		// Entity 객체를 ResponseDto 객체로 변환하기
        MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
        
        return memoResponseDto;
    }

Ioc(Inversion of Control) 제어의 역전, di(dependency injection) 의존성 주입

개발자가 객체를 생성하고, 자원을 해제하는 등 객체의 생성 주기를 통제했다면

스프링 프레임워크는 스프링이 객체의 생애주기를 관리한다. 객체를 관리하는 흐름이 개발자가 아닌

스프링이 하기 때문에 제어의 역전이라 하고, 제어의 역전이 이루어지는 곳,

스프링이 관리하는 객체들을 빈이라 하고, 그 빈들이 모여있는 곳을 IoC 컨테이너라고 한다.

IoC 컨테이너에서 빈들의 결합?이 이루어지고, 유연하게 결합된 빈이 프로젝트에서 사용된다.

→ 스프링 프레임워크는 di패턴을 사용하여 ioc 설계 원칙을 구현하고 있다.

 

dependency injection(di, 의존성 주입)이 되려면 객체들이 미리 생성되어 있어야 한다.

스프링 (프레임워크)가 관리하는 객체들을 bean이라고 하고, 이 빈들은 IoC Container에 모여져 있다.

어떤 클래스를 스프링 (프레임워크)이 빈으로 생성해서 관리하게 하려면

해당 클래스에 @Component 애너테이션을 작성해준다.

 

상기와 같이 작성해 주면 스프링 프레임워크가 작동할 때(run) IoC 컨테이너라는 곳에

MemoRepository 클래스로부터 인스턴스화된 싱글톤 인스턴스를? 만들어서 등록해준다.

개발자가 작성한 클래스가 di 되려면 스프링 프레임워크가 빈으로 관리하게 만들어야 하는데

빈임을 인식할 수 있게 하는 방법이 해당 클래스 위에 @Component 애너테이션을 작성해 주어야 한다.

어플리케이션 서버가 작동하면서(run) IoC 컨테이너를 만들고?

컴포넌트 스캔을 하여 @Component 애너테이션이 작성된 클래스를

인스턴스화하여 빈으로 관리한다. 이 빈들이 IoC 컨테이너에 담겨져 프로그램이 작동되면서

필요한 곳에 스프링 프레임워크가 해당 빈들을 주입해준다(dependency injection)


영속성 컨텍스트(persistence context)

JPA는 Java Persistence API의 약어로 자바 어플리케이션 서버와 관계형 데이터베이스간 통신을 효율적으로 관리하기 위한 api다.

자바 프로그래밍에서는 객체가 데이터를 다루는 핵심?이고 → 객체지향 기반 프로그래밍

관계형 데이터베이스에서는 데이터를 다루는 단위가 테이블인데,

자바기반 프로그램과 관계형 데이터베이스간 데이터를 다루는 형식?이 다르기 때문에 통신과정에서 데이터 형식을 맞추어야 하는 문제점이 있었는데, 이를 해소하고, 좀 더 객체지향 프로그래밍에 집중할 수 있도록 도와주는 라이브러리인 것이다.

→ JPA는 객체지향 프로그래밍과 관계형 데이터베이스간 데이터를 송수신할 때 데이터 형식의 불일치를 해결하여

JdbcTemplate 객체 및 MyBatis에서 작성했던 기본 crud SQL을 직접 작성하는 대신

자바 객체를 데이터베이스의 테이블과 매핑하여 객체지향적인 방식으로 데이터를 관리할 수 있도록 도와주는 라이브러리다.

예를 들면 객체를 데이터베이스의 테이블의 한 행의 형식으로 변환하여 정보를 저장하거나

데이터베이스에서 조회한 정보를 객체로 변환하여 자바 프로그램에서 해당 정보를 사용할 수 있다.

 

JPA는 인터페이스로 JPA를 구현한 객체를 통해 JPA의 기능을 사용할 수 있고,

JPA는 다양한 구현체가 있으며, 대표적으로는 Hibernate가 있다.

 

→ JPA는 웹 어플리케이션과 데이터베이스간 통신을 효율적으로 관리하고,

자바 객체와 데이터베이스 테이블간의 매핑을 규격화하는데 사용되는 api며,

JPA를 사용하면 객체지향적인 방식으로 데이터를 관리할 수 있어 편리하다.

 

영속성 컨텍스트는 내부적으로 캐시 저장소가 있고, Map 자료형태로 Entity 객체가 관리된다.

 

JPA가 Entity 객체를 관리하기 위해 영속성 컨텍스트를 만들며

Entity 객체를 관리하기 위해서는 EntityManager 객체가 필요한데,

EntityManager 객체가 생성되기 위해서는 EntityManagerFactory가 필요하고,

EntityManagerFactory는 resources > META-INF 폴더 아래 설정파일을 참고하여 만들어진다.

엔터티 매니저가 관리해야할 객체를 persist() 메서드의 파라미터로 전달하면 영속성 컨텍스트에서 해당 객체가 관리된다.

 

영속성 컨텍스트에 자바객체를 저장하려면 EntityManager 객체를 통해서 persist() 메서드를 호출하고,

파라미터로 해당 자바객체를 전달한다. EntityManager 객체를 통해서 find() 메서드를 호출하면서 파라미터에 찾고자 하는

Entity 클래스 타입과 Entity 클래스에 작성된 @Id 애너테이션이 작성된 필드의 값을 전달하면 영속성 컨텍스트가 지닌

캐시 메모리에서 조회한 후 없다면 데이터베이스에 해당 데이터를 찾는 sql문을 수행하여 반환된 값을 캐시 메모리에 보관한다.

그리고 그 값을 자바객체로 변환한 후 반환한다. 영속성 컨텍스트에 1차 캐시 기능이 존재하여 데이터베이스 조회 횟수가 줄어든다.

1차 캐시 기능을 사용해서 데이터베이스 하나의 row당 객체 1개가 사용되는 것을 보장한다: 객체동일성 보장

 

쓰기 지연 저장소(ActionQueue)

JPA는 데이터베이스에서 작업이 처리되는 단위인 트랜잭션처럼 SQL을 모아서 한 번에 데이터베이스에 반영한다.

JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가

트랜잭션 commit 후 한 번에 데이터베이스에 반영한다.

// JPA에서 EntityManagerFactory 객체는 Persist 클래스를 통해 얻어온다.
// Persistence는 JPA 표준에 정의된 클래스로 persistence.xml 파일에 정의된
// 영속성 유닛을 기반으로 EntityManagerFactory 객체를 생성한다.
EntityManagerFactory emf = Persist.createEntityManagerFactory("
	persistence.xml 파일에 정의된 영속성 유닛 이름을 작성한다.
");

EntityManager em = emf.createEntityManager();

// EntityManager 객체를 통해 EntityTransaction 객체를 얻어낸다.
// EntityManager 객체를 통해 데이터베이스 작업을 수행하기 위해 준비한다.
// 트랜잭션 객체가 반환되어 et가 해당 객체를 가리키게 된다.
EntityTransaction et = em.getTransaction();


// 트랜잭션 객체를 통해 데이터베이스에서 작업을 시작하기 위해 트랜잭션을 시작한다.
et.begin();

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

em.persist(memo);

System.out.println("트랜잭션을 커밋하기 전");
et.commit(); // --> hibernate가 작동함. 영속성 컨텍스트에 저장된 객체를 데이터베이스에 저장함
System.out.println("트랜잭션을 커밋후");

 

트랜잭션 객체를 통해 commit() 메서드를 호출하면

영속성 컨텍스트에 저장되어 있는 객체의 상태를 데이터베이스에 저장하게 되는데

commit() 메서드 호출 후에 flush() 메서드 또한 호출된다. commit() → flush()

flush() 메서드는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 역할을 수행한다.

flush() 메서드를 호출하면 쓰기 지연 저장소에 저장되어 있는 객체를 데이터베이스에 바로 저장한다.

데이터를 변경하는 sql문(예를 들면 insert문, update문, delete문)은 데이터베이스에 요청 및 반영하기 위해서는

트랜잭션 환경이 필요하다. select는 데이터베이스를 변경하는 것이 아닌 조회하는 것이기에 트랜잭션 환경이 아니어도 된다.

 

영속성 컨텍스트에 저장된 Entity 객체의 데이터에 변경이 발생할 때마다 update sql문이 쓰기 지연 저장소에 저장된다면

하나의 update sql문으로 처리할 수 있는 여러 상황을 여러 번의 update sql문을 요청하게 되기 때문에 비효율적이게 된다.

이런 문제를 해결하기 위해서 JPA는 변경 감지 기능(dirty checking)이 있다.

변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고, 해당 Entity 객체의 데이터를 변경하면

자동으로 update sql문이 만들어지고, db에 반영된다.

 

JPA에서 update가 이루어지는 과정

EntityManager 객체를 통해서 flush() 메서드를 호출한다.

영속성 컨텍스트가 갖는 1차 캐시 기능을 통해 @id의 값을 기준으로 기존 Entity 객체와

LoadedState에 저장된 Entity 객체를 비교에 기반하여 데이터베이스에 변경을 요청한다.

 

EntityManager 객체를 통해서 find() 메서드를 호출하여 Entity 객체를 찾는다.

*참조변수 em이 EntityManager 객체를 참조하고 있다.

클래스가 Memo이고, primary key가 1인 객체를 찾아 Memo 타입의 참조변수 memo가 참조하게 한다.

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

 

System.out.println("수정을 진행합니다.");

memo.setUsername("update");

 

System.out.println("트랜잭션 커밋 전");

 

// et.commit(); 커밋 후 flush() 메서드 호출됨

// 영속성 컨텍스트에 1차 캐시에 저장된 객체의 데이터 변경시

// JPA는 변경을 감지하며 변경된 데이터로 데이터베이스에 데이터를 저장한다.

// 이는 트랜잭션 환경에서 commit() 메서드를 호출한 이후에 수행된다.

et.commit(); 

System.out.println("트랜잭션 커밋 후");

 

entityinstance는 Entity 객체의 현재 상태를 의미하고,

loadedState는 영속성 컨텍스트에 저장된 Entity 객체의 최초 상태를 보관하고 있다.

 

Entity 상태

EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();

EntityTransaction et = em.getTransaction();
et.begin();

try {
	// 비영속 상태: 영속 컨텍스트에 해당 객체의 상태가 저장되어 있지 않은 상태
	Memo memo = new Memo();
    memo.setId(1L);
    memo.setUsername("Robbie");
    memo.setContents("비영속과 영속 상태");
    
    // EntityManager객체를 통해 persist() 메서드를 호출하면서 Memo타입 객체를 전달한다.
    // Memo타입 객체를 JPA가 영속성 컨텍스트에서 관리하게 된다.
    // 비영속 상태의 Entity 객체가 영속 상태로 변경된다. 
    // MANAGED가 영속 상태를 의미한다(영속 컨텍스트에서 해당 객체가 관리되고 있는 상태)
    em.persist(memo);
    
    et.commit();
    
    
    
} catch(Exception ex) {
	ex.printStackTrace();
    et.rollback();
} finally {
	em.close(); // 자원 해제
}

 

Entity 객체가 현재 영속성 컨텍스트에서 관리되고 있는 상태인지를 확인하는 메서드: EntityManager 객체를 통해 contains() 메서드를 호출하고, 해당 객체를 가리키는 참조변수를 전달한다.

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

 

영속성 컨텍스트에서 관리되고 있는(MANAGED) Entity 객체를 EntityManager 객체를 통해서 detach() 메서드를 호출하면서

해당 객체를 가리키고 있는 참조변수를 전달하면 영속성 컨텍스트에서 관리되고 있는 객체가 detached 상태가 된다.

detached 상태는 영속성 컨텍스트에서 해당 객체가 관리되다가 떨어져 나간 상태를 의미하며 준영속 상태라 불린다.

준영속 상태의 객체의 정보를 변경하게 되면 변경 감지가 이루어지지 않는다.

→ 준영속 상태의 객체의 정보를 변경한 후 commit() 메서드를 호출하더라도 데이터베이스에 영향을 주지 않는다.

MANAGED 상태의 객체만이 dirty checking(변경 감지)이 이루어진다.

 

EntityManager 객체를 통해 clear() 메서드를 호출하면

영속성 컨텍스트가 초기화되어 JPA가 관리하고 있던 Entity 객체 정보들이 모두 삭제된다.

 

clear() 메서드와 close() 메서드의 차이는

clear() 메서드를 호출했을 때 영속성 컨텍스트에 저장되어 있는 Entity 객체에 대한 정보는 모두 삭제, 영속성 컨텍스트를 계속 사용할 수 있음, 반면 EntityManager 객체를 통해 close() 메서드를 호출하게 되면 해당 객체를 더 이상 사용하지 않겠다는 것,

em.close(); 해당 코드가 작동된 후에 EntityManager 객체를 가리키는 참조변수를 통해서 Entity 객체의 상태를 저장하는 persist() 메서드 호출은 물론 영속성 컨텍스트가 관리하는 객체의 내용 변경과 같은 em.update() 메서드를 사용할 수 없게 된다. 

 

준영속 상태의 객체를 다시 영속상태로 변경할 때 merge() 메서드를 사용한다.

EntityManager객체를 통해 merge() 메서드를 호출할 때 준영속 상태의 객체를 참조하는 참조변수 또는 객체를 전달한다.

em.merge(entity)는 전달받은 Entity를 사용하여 새로운 영속상태의 Entity를 반환한다.

merge(entity)는 비영속, 준영속 상태의 Entity 객체를 모두 파라미터로 받을 수 있으며,

상황에 따라 데이터베이스에 저장을 하거나(insert) 수정을(update) 할 수 있다.

// 데이터베이스에 저장되어 있는 Memo 객체와 매핑된 테이블에서 primary key가 3인 하나의 행을 가져와
// Memo타입의 객체로 변환하여 참조변수 memo가 해당 객체를 가리키게 한다.
// 참조변수 memo가 비어있지 않다면 영속성 컨텍스트가 해당 객체를 관리하고 있다는 것을 의미하고,
// EntityManager 객체를 통해서 contains() 메서드 호출시 --> 파라미터로 해당 객체를 전달
// true를 반환한다. 이는 해당 객체가 영속 상태임을 의미한다(MANAGED)

// 참조변수 memo가 비어있지 않았을 때
Memo memo = em.find(Memo.class, 3);

System.out.println("em.contains(memo): "+em.contains(memo)); // true를 반환한다.

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

// false를 반환한다.
// memo가 참조하는 객체가 준영속 상태임을 의미한다.
System.out.println("em.contains(memo): "+em.contains(memo)); 


System.out.println("준영속 상태 memo 객체 값 변경");
memo.setContents("merge() 수정");

System.out.println("merge() 호출");
Memo mergeMemo = em.merge(memo);
System.out.println("mergeMemo.getContents(): "+mergeMemo.getContents());


// memo는 준영속 상태로 영속성 컨텍스트가 관리하는 객체가 아니다.
// mergeMemo는 영속 상태다.

et.commit();

 

스프링부트 환경에서 JPA가 작동하는 원리

 

스프링부트 환경에서 JPA를 사용하려면 JPA를 사용할 수 있도록 해당 라이브러리를 다운받아야 한다.

build.gradle의 dependencies JPA를 설정해준다.

// ------ build.gradle

dependencies {
	// JPA 설정
    // spring-boot-starter-data-jpa가 기본 값으로 hibernate 클래스를 가져온다.
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	// MySQL
    implementation 'mysql:mysql-connector-java:8.0.28'
}

# ------ resources > application.properties
# 데이터베이스를 연결할 수 있도록 정보를 적는다.
spring.datasource.url=jdbc:mysql://localhost:3306/memo
spring.datasource.username=계정명
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# ddl-auto에는 옵션이 5가지가 있다.
# create: 기존에 있던 테이블을 전부 삭제한 다음 다시 생성한다. drop + create
# create-drop: create의 기능을 가진채 종료시점에 테이블을 drop한다.
# update: 변경된 부분만 테이블에 반영한다.
# validate: Entity와 테이블이 정상적으로 매핑이 되었는지를 확인한다.
# none: 아무것도 하지 않는다.
spring.jpa.hibernate.ddl-auto=update

 

스프링부트 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해준다.

EntityManager 객체를 통해서 트랜잭션 환경을 얻어왔던 순수 자바와 달리

스프링부트 환경에서는 @Transactional 애너테이션으로 트랜잭션 환경을 쉽게 만들 수 있다.

클래스 또는 메서드 위에 @Transactional 애너테이션을 작성하는 것만으로도 트랜잭션 환경을 적용할 수 있다.

// 순수 자바 환경
// 1. resources > META-INF > persistence.xml 파일에 데이터베이스 관련정보 및
// EntityManagerFactory 객체를 생성할 수 있도록 persistence unit 을 작성해준다.

// 2. Persistence 객체가 persistence.xml 파일을 참고하여 EntityManagerFactory를 생성한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistenc unit name을 적어주기");

// 3. EntityManagerFactory 객체로부터 EntityManager 객체를 얻기
EntityManager em = emf.createEntityManager();

// 4. EntityManager 객체로부터 트랜잭션 객체를 얻기
EntityTransaction et = em.getTransaction();

// 5. 트랜잭션 시작하기
et.begin();

// --- 스프링부트 환경
@Test
// 클래스나 메서드 위에 @Transactional 어노테이션 작성하면 트랜잭션 환경을 설정할 수 있음
@Transactional
// 테스트 코드에서 @Transactional 어노테이션을 사용하면 테스트가 완료된 후에 롤백을 하기 때문에
// false 옵션을 추가해준다.
@Rollback(value=false)
void test(){
	/*...*/
}

 

Spring Data JPA

JPA를 쉽게 사용할 수 있도록 만들어놓은 하나의 모듈이다.

어플리케이션과 데이터베이스간 통신에 JPA를 사용할 때 EntityManagerFactory 객체를 얻고,

EntityManager 객체를 얻고, EntityTransaction 객체를 얻고, 트랜잭션 환경을 시작해야 하는 반면

Spring Data JPA를 사용할 때는 @Transactional 어노테이션만으로 트랜잭션 환경을 설정할 수 있는 편리함이 있다.