Spring/Blog-V1

스프링 35강. 스프링 구조 - OSIV, 영속화

JJJAEOoni 2022. 3. 18. 22:15
반응형

스프링 Layer별 구조

 

1. 필터

역할 : 걸러주는 애

 

필터와 컨트롤러 사이에서 DB Connection 객체를 가져온다.

== 세션 연결

(웹서버 아파치 톰캣 : request, session(id)으로 사용자 구분)

 

↓ OPEN SESSION / ↑ CLOSE SESSION

 

: OSIV(Open Session In View) = TRUE

 

 

 

2. 컨트롤러

@Controller

@RestController (String(text/html), 자바 오브젝트(application/json))

역할 : 클라이언트의 요청을 받아 View나 Data를 응답

 

 

PutMapping( ) { 서비스.송금( ) }

 

PutMapping( ) { 서비스.송금( ) }

 

put매핑 요청은 fetch밖에 못한다!

 

 

3. 서비스

@Service

역할 : 트랜잭션(일의 처리 단위) 관리

 

송금을 하나의 일의 단위라고 정한다면

보내는 사람 출금 update, 받는 사람 입금 update

총 update 두번이 일의 처리 단위이다.

 

송금( ) {update, update}

 

컨트롤러 입장에서 송금 메서드를 호출하게 되는 것이다.

서비스.송금( ) -> PutMapping

 

그러면 컨트롤러 입장에서는 서비스를 DI 해야 하고

서비스는 레파지토리를 DI 해야 한다.

 

서비스가 시작되는 타이밍에 데이터베이스 트랜잭션이 시작된다.

그리고 서비스가 끝날 때 트랜잭션이 끝나고 COMMIT 혹은 ROLLBACK 된다.

 

트랜잭션이 시작되었기 때문에 다른 클라이언트가 WRITE 요청을 했을 때 막히게 된다.

 

왜 필터와 컨트롤러 사이에서 디비가 연결되었을 때부터

트랜잭션이 시작되지 않았을까?

 

데이터베이스가 LOCK 걸리는 시간을 최소화시킬 수 있기 때문이다.

 

이게 레이어를 나누어놓은 이유이기도 하다.

 

그러면 반대로 DB 연결을 서비스부터 하면 안 될까?

세션이 열고 닫히는 게 서비스에서 일어나게 된다면

Lazy Loading(지연 로딩)이 불가능하다.

 

getter 호출과 동시에 null인 user 정보를 select 하고 View에 뿌려주는 애가

컨트롤러이기 때문에 컨트롤러 전에 세션이 연결되어야 한다.

 

세션을 컨트롤러까지 유지하는 전략을

OSIV(Open Session In View)라고 한다.

 

스프링의 기본 OSIV 전략은 true이다.

 

 

회원 수정( ) { User userEntity = findById(1)

                           userEntity.setUsername("love"); }

 

서비스 메서드 종료 == 트랜잭션 종료

=> 변경 감지 == 더티 체킹(JPA가 하는 일)

 

영속성 컨텍스트에서 관리되는 엔티티의 값이 변경되었을 경우

이를 자동으로 감지하여 DB에 영속화를 진행한다.

영속화할 때 jpa가 update 쿼리를 생성해 쿼리 저장소에 추가하며,

트랜잭션 종료 시 더티 체킹을 하여 DB로 강제 Flush 한다.

 

버퍼로 써서 DB에 보내는 것이다.

이때 원래 DB에는 id가 1번인 레코드가 존재하기 때문에 update 된다.

 

즉, 영속화 -> 영속화시킨 데이터 변경 -> 업데이트

 

 

4. 레파지토리

@Repository

역할 : DB에 연결해 데이터 가져오는 애

 

 

findAll( ), findById( ) : 리턴 User 오브젝트, deleteById( ), save( ), saveAll( ),

deleteAll( ) 얜 절대 쓰지 말자. 위험하다.

 

CRUD 메서드가 다 있는데 update( )가 없다.

 

save( )로도 업데이트가 가능하긴 하지만 사용하지 않을 것이다.

 

강제로 유저 오브젝트를 하나 만들고 id값을 지정해서 

id : 4

username : ssar

password : 5678

이 데이터를 save(user) 하면

 

DB에서 얘는 id를 가지고 있네?

DB에 확인해보니까 id 4가 이미 존재하네?

아 수정이구나! 하고 update 해준다.

 

이렇게 save를 사용하면 불편하다.

 

언제 불편하냐?

 

컬럼이 100개가 있는 테이블에서

77번째 컬럼만 값을 수정해야 할 때

77번째 빼고 모든 값을 다 넘겨줘야 한다.

 

그래서 save로는 update하지 않는다.

 

update 하기 위해서는 service 레이어를 거쳐야 한다.

 

 

 

5. 영속성 컨텍스트

 

User(1, "ssar", "1234", "ssar@nate.com") - Entity (리턴 User 오브젝트)

 

User(1, "love", "1234", "ssar@nate.com") username 변경 감지 - 아직 update 되지 않음

 

DB에 있는 데이터를 SELECT 해서 영속성 컨텍스트에 넣는 것 : 영속화

 

DB에서 ResultSet으로 리턴해줘도 오브젝트로 받는다.

아주 똑똑하네

 

6. DB

 

User 1번 레코드(행) 존재함 -> (리턴 ResultSet)

 

 

 

 


 

서비스 파일을 만들어보자.

 

클래스에 @Service를 달아주면 메모리에 뜨는데

트랜잭션은 시작하지 않는다.

 

달아줄 어노테이션이 하나 있다.

 

@Transactional

SELECT 하는데 트랜잭션이 시작되어 락걸릴 필요는 없으니까

WRITE 요청할 메서드에만 붙여준다.

 

메서드에 직접 붙여주는 어노테이션이다.

 

javax가 아닌 스프링 라이브러리니까 주의하자.

 

메서드가 끝나는 시점에 커밋이 완료된다.

 

package site.metacoding.dbproject.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import site.metacoding.dbproject.domain.user.User;
import site.metacoding.dbproject.domain.user.UserRepository;

@Service
public class UserService {
    
    // 서비스는 한글로 하는게 좋아용
    // 아래 스택들에 들어가는 코드를 비즈니스 로직이라고 한다.
    public String 유저네임중복검사(String username) {

    }

    public void 회원가입() {

    }

    public void 로그인() {

    }

    public void 유저정보보기() {

    }

    public void 유저수정() {

    }
}
package site.metacoding.dbproject.service;

import org.springframework.stereotype.Service;

@Service
public class PostService {
    public void 글목록보기() {

    }

    // 글상세보기, 글수정페이지
    public void 글상세보기() {

    }

    public void 글수정하기() {

    }
    
    public void 글삭제하기() {

    }

    public void 글쓰기() {

    }
}

 

 


 

하나씩 서비스로 메서드를 이사시켜보자.

 

// 서비스
// 글상세보기, 글수정페이지
public Post 글상세보기(Integer id) {

    // 박스에 담기 null 방지
    Optional<Post> postOp = postRepository.findById(id); // 유저정보

    // 핵심 로직 : 박스가 null인지 확인 => 나중에 try catch로 바꾸기!
    if (postOp.isPresent()) { // 박스안에 뭐가 있으면
        Post postEntity = postOp.get();
        return postEntity;

    } else { // 없으면 == isEmpty
        // 누군가 고의로 DELETE 하지 않는 이상 거의 타지 않는 오류
        return null;
    }
}
// 컨트롤러
// 글 상세보기 페이지 /post/{id} (삭제버튼 만들어두면 되니까 삭제페이지 필요 X)
@GetMapping("/post/{id}") // Get요청에 /post 제외시키기(인증 X)
public String detail(@PathVariable Integer id, Model model) { // int는 null이 없음, 초기값이 0
                                                              // Integer는 초기값이 null

    User principal = (User) session.getAttribute("principal");

    // 권한
    Post postEntity = postService.글상세보기(id); // 재활용, EAGER니까 user 가지고있음

    // 게시물이 없으면 error 페이지 이동
    if (postEntity == null) {
        return "error/page1";
    }

    // 로그인 안했을때 터짐 null.getId했던것
    if (principal != null) {
        // 권한 확인해서 view로 값 넘김
        if (principal.getId() == postEntity.getUser().getId()) { // 권한이 있다는 뜻
            model.addAttribute("pageOwner", true);
        } else {
            model.addAttribute("pageOwner", false);
        }
    }
    
    return "post/detail";
}

 

 

서비스의 글상세보기( )에서 postEntity를 가져올 때

EAGER 전략으로 가지고 온다면 OSIV가 필요 없다.

 

LAZY 전략일 때 필요한 게 OSIV이다.

 

지연 로딩이 되려면 컨트롤러에서 이미 세션이 열려있어야 하는 것이다!!

 

데이터를 리턴해줄 때는 ViewResolver가 아닌 MessageConverter가 발동하는데

메세지 컨버터는 자바 오브젝트를 json으로 변환시켜서 리턴하도록 작동하는데

자바 오브젝트의 데이터를 확인하고 json으로 변환시키기 때문에

Post의 getter를 다 호출하면서 json으로 만든다.

 

나는 Lazy loading을 한 적이 없는데

리턴시에 Lazy loading이 자동으로 일어난다.

 

왜?

메세지 컨버터가 getter를 다 때리고 있기 때문이다!!

 


 

 

노란색 log는 경고의 의미이다.

OSIV 가 true로 설정되어있다고 경고해주는 것이다.

 

jpa:
    open-in-view: true

 

내가 직접 true로 걸어줘서 알고 있다는 걸 알려주면 경고가 뜨지 않는다.

 

 

 

 

[출처]

 

https://cafe.naver.com/metacoding

 

메타코딩 : 네이버 카페

코린이들의 궁금증

cafe.naver.com

메타 코딩 유튜브

https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9

 

메타코딩

문의사항 : getinthere@naver.com 인스타그램 : https://www.instagram.com/meta4pm 깃헙 : https://github.com/codingspecialist 유료강좌 : https://www.easyupclass.com

www.youtube.com

 

 

 

반응형