이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책을 참고하여 작성되었습니다.

1. Spring web 계층


(출처)


  • Web Layer: @Controller와 JSP/Freemaker 등의 뷰 템플릿 영역이다. 외부 요청과 응답에대한 전반적인 영역이다.
  • Service Layer: @Service@Transactional이 사용되는 영역이며 컨트롤러와 Dao의 중간 영역에서 사용된다.
  • Repository Layer: DB처럼 데이터 저장소에 접근하는 영역이다. Dao 영역과 유사하다고한다.
  • Dtos: 계층 간 데이터 교환을 위한 객체의 영역이다. 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등을 말한다.
  • Domain Model
    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화 시킨 모델이다
    • 예를 들어, 택시앱이면 배차, 탑승, 요금 등이 모두 도메인이다.
    • @Entity가 사용된 영역 역시 도메인 모델이나 반드시 DB 테이블과 관계가 있어야 하는건 아니다. VO(Value Object) 또한 이 영역에 해당하기 때문이다.

2. 도메인에서의 비즈니스 처리

  • 트랜잭션 스크립트
    기존에 서비스로 처리하던 방식을 의미한다. 이렇게 하면 모든 로직이 서비스 클래스 내부에서 처리되어, 서비스 계층이 무의미해지며 객체가 단순히 데이터 덩어리가 된다. 그래서 이를 도메인 모델로 대체한다.
  • 도메인 모델에서
    각 객체가 각자 본인의 이벤트를 처리하며, 서비스 메서드는 트랜잭션과 도메인 간의 순서만 보장해준다.

3. 등록/수정/조회 API 만들기

1) 등록


PostsApiController.java

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto);
    }
}

PostsService.java

public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save (PostsSaveRequestDto requestsDto) {
        return postsRepository.save(requestsDto.toEntity()).getId();
    }
}
  • Bean을 주입 받는 방식
    @Autowired, setter, 생성자가 있는데 가장 권장하는 방식이 생성자이다. 생성자로 Bean 객체를 받는 방식으로 @AutoWired를 대신한다. 생성자는 @ReiquiredArgsConstructor가 대신 생성해준다.

PostsSaveRequestDto

@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;
    @Builder
    public PostsSaveRequestDto(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public Posts toEntity() {
        return Posts.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();
    }
}
  • Entity와 유사한 형태이다. Entity 클래스 대신 Request/Response 클래스로 사용하기 위함이다. 왜냐하면 Entity 클래스는 DB 테이블과 맞닿은 핵심 클래스이므로 이를 자주 수정하면 여러 클래스도 영행을 받기 때문에 Dto를 추가로 만들어서 사용한다. 이렇게 View Layer/DB Layer를 확실히 분리해야한다.

  • 테스트 코드 작성 시 JPA 기능 동작을 위해 @WebMvcTest 대신 @SpringBootTestTestRestTemplate 을 사용한다.

2) 수정/조회


PostsApiController.java

@RequiredArgsConstructor
@RestController
public class PostsApiController {
	...
    
    // 수정
    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
        return  postsService.update(id, requestDto);
    }
    
    // 조회
    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDto findById (@PathVariable Long id) {
        return postsService.findById(id);
    }
}

PostsResponseDto.java

@Getter
public class PostsResponseDto {
    ... // 필드 선언 생략
    
    public PostsResponseDto (Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}
  • PostsResponseDto는 Entity의 일부 필드만 사용하므로 생성자로 Entity를 받아 필요한 값을 필드에 넣는다.

PostsUpdateRequestDto.java

@Getter
public class PostsResponseDto {
    ... // 필드 선언 생략
    
    @Builder
    public PostsUpdateRequestDto(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

Posts.java

@Getter
public class PostsResponseDto {
    ... 
    
    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

PostsService.java

@Getter
public class PostsResponseDto {
    ... 
    
    @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;
    }

    public PostsResponseDto findById (Long id) {
        Posts entity = postsRepository.findById(id)
                .orElseThrow(() -> new
                        IllegalArgumentException("해당 게시글이 없습니다. id="+id));

        return new PostsResponseDto(entity);
    }
}

  • JPA의 영속성 컨텍스트
    • 엔티티를 영구 저장하는 환경인 논리적 개념이다.
    • 트랜잭션 안엣 DB에서 뎅터를 가져오면 해당 데이터는 영속성 컨텍스트가 유지되어있다.
    • 값 변셩시 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다.
    • 이를 더티 체킹이라고 한다.
    • 따라서 update 기능에서 쿼리를 날리지 않는다.

4. H2 웹콘솔 이용하기

이 부분은 Spring boot 버전 변화 후 이슈가 좀 발생 했었다.
application.properties에 spring.h2.console.enabled=true
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL
두 줄을 추가하니깐 성공했다. 만약 안 된다면 링크의 3번째 댓글의 스크린샷을 참고하여 직접 jdbc url을 붙여넣어보자.

5. JPA Auditing

각 게시글에 수정 날짜, 등록 날짜 등을 간편하게 등록하기 위해 사용한다.


BaseTimeEntity.java

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}
  • 해당 클래스는 모든 Entity의 상위 클래스가 되어 createdDate, modifiedDate를 자동으로 관리한다.
  • @MappedSuperclass: JPA Entity 클래스들이 해당 클래스 상속 시 필드들도 칼럼으로 인식하게 한다.
  • @EntityListeners(AuditingEntityListener.class): 해당 클래스에 Auditing 기능을 포함시킨다.
  • @CreatedDate: Entity의 생성 후 저장 시의 시간이 자동 저장된다.
  • @LastModifiedDate: Entity 변경 시의 시간이 자동 저장된다.

이후 Posts 클래스가 BaseTimeEntity를 상속 받고록하고, Application 클래스에 @EnableJpaAuditing을 추가해주면 기능이 제대로 활성화 된다.

profile
여러가지를 시도하는 학생입니다

0개의 댓글