Spring 웹 계층

Web Layer

  • Controller와 JSP 등의 뷰 템플릿 영역
  • 이외에도 Filter, Interceptor, ControllerAdvice등 외부 요청과 응답에 대한 전반적인 영역

Service Layer

  • Service에 사용되는 영역
  • 일반적으로 Controller와 Dao의 중간 영역에서 사용됨
  • Transcational이 사용되어야 하는 영역

Repository Layer

  • 기존의 Dao와 비슷한 영역
  • DB에 접근하는 영역

Dtos

  • DTO 계층 간에 데이터 교환을 위한 객체를 말하며 Dtos는 이들의 영역
  • 뷰 템플릿 엔진에서 사용될 객체나 Repostiory Layer에서 결과로 넘겨준 객체

Domain Model

  • 택시앱을 가정하면 배차, 탑승, 요금 등이 모두 도메인이 될 수 있음
  • @Entity가 사용된 영역 역시 도메인 모델
  • 다만 무조건 데이터베이스의 테이블과 관계가 있어야 하는 것은 아님
  • VO처럼 값 객체들도 이 영역에 해당함
  • 비지니스 처리를 담당

프로젝트 구조


생성자 주입 - @RequiredArgsConstructor

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;


    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }
...

@RequiredArgsConstructor

final이 선언된 모든 필드를 인자값으로 하는 생성자를 만들어줌

❓@RequiredArgsConstructor를 사용한 이유

Bean을 주입하는 3가지 방법
1. @Autowired  2. setter  3. 생성자

@Autowired는 권장하지 않으므로 생성자로 bean 객체를 받도록 함

생성자를 직접 쓰지 않고 롬복 어노테이션을 사용한 이유 ➜ 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 수정하는 번거로움을 해결하기 위함이다


❓ Entity class가 있음에도 DTO class 추가로 생성한 이유

  • Entity 클래스를 Request, Response 클래스로 사용하면 안됨
  • Veiw Layer와 DB Layer의 역할 분리가 필요함
  • Request, Response용 dto는 view를 위한 클래스(변경 多)
  • Controller에서 여러 테이블을 조인해서 줘야 할 경우도 빈번하므로 Entity 클래스만으로 표현하기 어려운 경우가 많음

PostsApiControllerTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)//랜덤포트
public class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate; //JPA 같이 테스트할 때 사용
...

❓ControllerTest에서 @WebMvcTest대신 @SpringBootTest를 사용한 이유

@WebMvcTest의 경우 JPA기능이 작동하지 않기 때문에 @SpringBootTest와 TestRestTemplate를 사용함


❓ update에서 별도로 DB에 save하지 않아도 테스트 통과하는 이유

Posts.java

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
PostsUpdateRequestDto.java

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {

    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
PostsService.java

   @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다, id=" + id));

        posts.update(requestDto.getTitle(), requestDto.getContent());

        return id;
    }

위 코드를 보면 DB에 update하는 쿼리에 대한 부분이 없다.
저렇게 작성해도 update가 되는 이유가 무엇일까?

Dirty Checking
Dirty Checking이란 상태 변경 검사이다.
JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 DB에 자동으로 반영해준다. 이때 변화가 있다면 최초 조회 상태를 기준으로 한다. JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어 놓는다. 그리고 트랜잭션이 끝나는 시점에서 이 스냅샷과 비교하여 다른 점이 있다면 Update Query를 DB에 전달한다. 이런 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용된다.


영속성 컨텍스트
엔티티를 영구 저장하는 환경

  • 준영속: detach된 엔티티
  • 비영속: DB에 반영되기 전 처음 생성된 엔티티

변경 부분만 update하고 싶을 경우
Entity 최상단에 @DynamicUpdate를 선언해주면 변경한 필드만 대응


JPA Auditing

보통 entity엔는 해당 데이터의 생성시간과 수정시간을 포함한다. (유지보수에 있어 중요한 정보이기 때문)
매번 DB에 insert, update하기 전 날짜 데이터를 등록/수정하는 코드가 여기저기 들어가게 된다.
반복되는 코드의 작성을 방지하기 위하여 JPA Auditing을 사용한다.

BaseTimeEntity.java

@Getter
@MappedSuperclass //createdDate, modifiedDate필드를 칼럼으로 인식
@EntityListeners(AuditingEntityListener.class) //클래스에 Auditing 기능 포함시킴
public class BaseTimeEntity {//모든 Entity의 상위 클래스로 Entity들의 createdDate, modifiedDate 관리

    @CreatedDate //Entity가 생성되어 저장될 때 시간 자동저장
    private LocalDateTime createdDate;

    @LastModifiedDate //조회한 Entity의 값을 변경할 때 시간 자동저장
    private LocalDateTime modifiedDate;

}
public class Posts extends BaseTimeEntity { Posts클래스가 BaseTimeEntity class 상속받음
@EnableJpaAuditing
public class JpaConfig { JPA Auditingd어노테이션 모두 활성화
}
PostsRepositoryTest.java

    @Test
    public void BaseTimeEntity_등록() {
        //given
        LocalDateTime now = LocalDateTime.of(2023, 5, 12, 0, 0, 0);
        postsRepository.save(Posts.builder().title("title").content("content").author("author").build());

        //when
        List<Posts> postsList = postsRepository.findAll();

        //then
        Posts posts = postsList.get(0);

        System.out.println(">>>>>>> createdDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

        assertThat(posts.getCreatedDate().isAfter(now));
        assertThat(posts.getModifiedDate().isAfter(now));
    }

test 성공




📄참고

https://jojoldu.tistory.com/415

profile
개발 공부노트

0개의 댓글