Spring : JPA 실습

요구사항 분석

  • 게시판 기능
    1. 게시글 조회
    2. 게시글 등록
    3. 게시글 수정
    4. 게시글 삭제
  • 회원 기능
    1. 구글 / 네이버 로그인
    2. 로그인한 사용자 글 작성 권한
    3. 본인 작성글에 대한 권한 관리

프로젝트에 Spring Data JPA 적용

Gradle


    // SpringBoot Security Starter
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // 웹 페이지
    compile('org.springframework.boot:spring-boot-starter-web')

    // lombok
    compile('org.projectlombok:lombok')

    // Jpa
    compile('org.springframework.boot:spring-boot-starter-data-jpa')

    // H2-Database
    compile('com.h2database:h2')

    // Session store
    compile('org.springframework.session:spring-session-jdbc')

    // Mustache
    compile('org.springframework.boot:spring-boot-starter-mustache')

    // Spring Security
    compile('org.springframework.boot:spring-boot-starter-oauth2-client')

    // Swagger
    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
    compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'

    // Maria DB
    compile('org.mariadb.jdbc:mariadb-java-client')

    // Spring Security Test
    testCompile('org.springframework.security:spring-security-test')

    // Test
    testCompile('org.springframework.boot:spring-boot-starter-test')

Domain

  • 도메인을 담을 패키지
  • 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역

기존 MyBatis: dao 와 조금 다르다. 그간 xml에 쿼리를 담고 클래스는 오로지 쿼리의 결과만 담던 일들이 모두 도메인 클래스에서 해결된다.

/**
 * 어노테이션 순서 : 주요 어노테이션을 클래스와 가깝게
 *
 * @Getter 롬복은 간편하지만 필수는 아님 - 코틀린은 data class 로 롬복 필요 없음
 * @NoArgsConstructor 파라미터가 없는 기본 생성자 생성 -> public Posts() {}
 * @Entity 실제 DB 테이블과 매칭될 클래스, Entity 클래스라고도 부름름
 */
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {

  /**
   * @Id 해당 테이블의 PK
   * @GeneratedValue 스프링 부트 2.0 에서는 IDENTITY 이 붙어야만 auto increment 실행
   */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  /**
   * @Column 테이블 칼럼 선언, 없어도 되지만 length, columnDefinition 옵션을 조정하기 위해서 사용
   */
  @Column(length = 500, nullable = false)
  private String title;

  @Column(columnDefinition = "TEXT", nullable = false)
  private String content;

  private String author;

  @Builder
  public Posts(String title, String content, String author) {
    this.title = title;
    this.content = content;
    this.author = author;
  }

  public void update(String title, String content) {
    this.title = title;
    this.content = content;
  }
}
  • Entity 클래스에서는 절대 Setter 메소드를 만들지 않습니다.
    • getter/setter를 무작정 생성하게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확히 구분할 수 없어 차후 기능 변경 시 복잡해진다.
    • 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야만 한다.
Example
// 나쁜 예
public class Order{
	public void setStatus(boolean status){
    		this.status = status
	}
    	public void 주문서비스_취소이벤트(){
        	order.setStatus(false);
        }
}
// 올바른 예
public class Order{
	public void cancelOrder(){
    		this.status = false
	}
    	public void 주문서비스_취소이벤트(){
        	order.cancelOrder();
        }
}
  • 그렇다면 Setter가 없는 이 상황에서 어떻게 값을 채워 DB에 insert 해야 할까요?
  • 생성자 대신 @Builder을 통해 제공되는 빌더 클래스를 사용한다.
  • 생성 시점에서 값을 채워주는 역할은 같지만 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확이 지정할 수 없다.
  • a,b의 위치를 변경해도 코드를 실행하기 전까지 문제를 찾을 수 없다. -> 시간 낭비, 스트레스 폭발 ....
public Example(String a, String b){
	this.a = a;
    	this.b = b;
}
  • 요로코롬 빌더 패턴으로 해주면 어느 필드에 어떤 값을 채워야할지 명확하게 인지할 수 있다.
Example.builder()
	 .a(a)
	 .b(b)
   	 .build();

PostsRepository

public interface PostsRepository extends JpaRepository<Posts, Long> {
 /**
   * 실제로 SpringDataJpa 에서 제공하는 기본 메소드로 해결 가능
   * @Query 가 가독성이 좋긴 하다.
   * 규모가 있는 프로젝트에서는 Querydsl 을 사용하는게 좋다.
   * 이유 :
   * 1. 타입 안전성이 보장된다.
   * 2. 국내 많은 회사에서 사용 중이다.(쿠팡, 배민 등.. JPA 를 쓰는 회사들)
   * 3. 레퍼런스가 많다 -> 검색 자료가 많아서 좋음
   */
  @Query("SELECT p from Posts p ORDER BY p.id DESC ")
  List<Posts> findAllDesc();
}
  • MyBatis 등에서 Dao라고 불리는 DB Layer 접근자이다.
  • JPA에서는 Repository라고 부르며 interface로 생성한다.
  • JpaRepository< Entity 클래스, PK 타입 > 를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.
  • Entity와 Repository는 함께 움직인다.

Spring Data JAP 테스트 코드 작성

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
`
  @Autowired
  PostsRepository postsRepository;
 `
  @After
  public void cleanup() {
    postsRepository.deleteAll();
  }
`
  @Test
  public void 게시글저장_불러오기() {
    //given
    String title = "테스트 게시글";
    String content = "테스트 본문";
`
    postsRepository.save(Posts.builder()
            .title(title)
            .content(content)
            .author("[email protected]")
            .build());
`
    //when
    List<Posts> postsList = postsRepository.findAll();
`
    //then
    Posts posts = postsList.get(0);
    assertThat(posts.getTitle()).isEqualTo(title);
    assertThat(posts.getContent()).isEqualTo(content);
  }
}
  • @After
    • Junit에서 단위 테스트가 끝날 때 마다 수행되는 메소드를 지정
    • 보통 테스트 간 데이터 침범을 막기 위해 사용
      • 테스트용 DB인 H2에 데이터가 남아있으면 안되므로 지워준다.

  • 실제 DB 가 적용된 과정 로그

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

  • API를 만들기 위해 총 3개의 클래스가 필요하다.
    1. Request 데이터를 받을 Dto
    2. API 요청을 받을 Controller
    3. 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

Spring Web 계층

  • Web Layer

    • 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemaker 등 뷰 템플릿 영역
    • @Filter, 인터셉터, 컨트롤러 어드바이스 등(@ControllerAdvice)등 외부 요청과 응답에 대한 전반적인 영역 을 이야기한다.
  • Service Layer

    • @Service에 사용되는 서비스 영역이다.
    • 일반적으로 Controller 와 Dao 중간 영역에서 사용
    • @Transactional이 사용되어야하는 영역
  • Repository

    • Database와 같이 데이터 저장소에 접근하는 영역
    • 기존 개발자는 Dao 영역으로 보면 이해가 쉽다.
  • Dtos

    • 계층 간 데이터 교환을 위한 객체
    • 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기한다.
  • Domain Model

    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 한다.
    • 예를 들어 택시 앱이라고 하면 배차, 탑승, 요금 등 모두 도메인이 될 수 있다.

이 5가지 레이어에서 비즈니스 처리를 담당해야할 곳은 Domain 이다.

JPA Auditing으로 생성시간/수정시간 자동화하기

// BaseTimeEntity.java
/**
 * 모든 Entity 의 상위 클래스가 된다.
 * createdDate, modifiedDate 를 자동으로 관리하는 역할.
 *
 * @MappedSuperclass BaseTimeEntity 를 상속할 경우 필드(createdDate, modifiedDate)들도 칼럼으로 인식하도록 한다.
 * @EntityListeners(AuditingEntityListener.class) BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.
 * @CreatedDate Entity 가 생성되어 저장될 때 시간이 자동 저장된다.
 * @LastModifiedDate 조회한 Entity 의 값을 변경할 때 시간이 자동 저장된다.
 */
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

  @CreatedDate
  private LocalDateTime createdDate;

  @LastModifiedDate
  private LocalDateTime modifiedDate;
}
// Posts.java
...
public class Posts extends BaseTimeEntity {
...

Test Code

  @Test
  public void BaseTimeEntity_등록() {
    //given
    LocalDateTime now = LocalDateTime.of(2019, 6, 4, 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(">>>>>>>>> createDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

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

좋은 웹페이지 즐겨찾기