Spring/그 외

domain, JpaRepository

ummchicken 2023. 1. 7. 16:19

출처 : 이동욱 개발자님 [스프링 부트와 AWS로 혼자 구현하는 웹 서비스]

 

 

내가 보려고 쓰는 Spring 프로젝트 구조

 

 


 

들어가기 전...

 

내가 보려고 쓰는 프로젝트 구조

 

 

main/java/...

 

 

main/resources/...

 

 


 

스프링 부트에서 JPA로 데이터베이스를 다뤄보자

 

왜 JPA를 쓸까?

 

 

 

국비학원에서 가르친 MyBatis... 

이것은 곧 SQL 매퍼(mapper)이다.

 

하지만 이것은 

실제로 개발하는 시간보다 SQL을 다루는 시간이 더 많았다.

 

분명 "객체지향 프로그래밍을 배웠는데, 왜 객체지향 프로그래밍을 못할까?"

→ 객체 모델링보다는 테이블 모델링에만 집중하고, 

객체를 단순히 테이블에 맞추어 데이터 전달 역할만 했다.

→ 이것은 곧 기형적인 형태이다.

 

 

어떻게 하면 관계형 데이터베이스를 이용하는 프로젝트에서 

객체지향 프로그래밍을 할 수 있을까?

 

문제의 해결책은 

JPA라는 자바 표준 ORM(Object Relational Mapping) 기술에 있다.

 

 

ORM SQL Mapper
객체를 매핑  SQL 쿼리를 매핑

 

 

즉, 개발자는 객체지향적으로 프로그래밍을 하고, 

JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다.

개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 

더는 SQL에 종속적인 개발을 하지 않아도 된다.

 

 

 


 

domain 패키지

도메인을 담을 패키지

 

💡 도메인이란?
→ 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 
혹은 문제 영역이다.

❗ 기존에 MyBatis 같은 쿼리 매퍼를 사용했다면 dao 패키지를 떠올리겠지만, 
dao 패키지와는 조금 결이 다르다.
→ 그간 xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이 
모두 도메인 클래스라고 불리는 곳에서 해결된다.

 

 

 

[Posts.java]

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import practicejpa.practicejpa01.domain.BaseTimeEntity;

import javax.persistence.*;

@Getter  // 6
@NoArgsConstructor  // 5
@Entity  // 1
public class Posts {

    @Id  // 2
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // 3
    private Long id;

    @Column(length = 500, nullable = false)  // 4
    private String title;

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

    private String author;

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

 

 

Posts 클래스는 실제 DB의 테이블과 매칭될 클래스이며, 
보통 Entity 클래스라고 한다.

JPA를 사용하면, DB 데이터에 작업할 경우 실제 쿼리를 날리기보다는, 
이 Entity 클래스의 수정을 통해 작업한다.

 

1. @Entity

  • 테이블과 링크될 클래스임을 나타낸다.
  • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.

 

 

2. @Id

  • 해당 테이블의 PK 필드를 나타낸다.

 

 

3. @GeneratedValue

  • PK의 생성 규칙을 나타낸다.
  • 스프링 부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 된다.

 

 

4. @Column

  • 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다.
  • 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
  • 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶거나, 타입을 TEXT로 변경하고 싶거나 등의 경우에 사용한다.

 

 

5. @NoArgsConstructor

  • 기본 생성자 자동 추가
  • public Posts() {}와 같은 효과

 

 

6. @Getter

  • 클래스 내 모든 필드의 Getter 메소드를 자동 생성

 

 

7. @Builder

  • 해당 클래스의 빌더 패턴 클래스를 생성
  • 생성자 상단에 선언 시, 생성자에 포함된 필드만 빌더에 포함

 

 

Setter 메소드가 없다.

 Entity 클래스에는 절대 Setter 메소드를 만들지 않는다.

대신, 해당 필드의 값 변경이 필요하면
명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 한다.

 

 

그럼 Setter가 없는 이 상황에서 

어떻게 값을 채워 DB에 삽입해야 할까?

→ 기본적인 구조는, 

생성자를 통해 최종값을 채운 후 DB에 삽입하는 것이다.

 

 

하지만 이 책에서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용한다.

(생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같다.

다만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다.)

 

 

※ 빌더 패턴(Builder pattern)이란?

객체를 정의하고 그 객체를 생성할 때 보통 생성자를 통해 생성하는 것을 생각한다.

Bag bag = new Bag("name", 1000, "memo");

 

하지만 생성자를 통해 객체를 생성하는데 몇가지 단점이 있어 별도 builder를 두는 방법이 있다.

빌더 패턴은 빌더의 필드 이름으로 값을 설정하기 때문에 순서에 종속적이지 않다.

Bag bag = Bag.builder()
		.name("name")
        	.money(1000)
        	.memo("memo")
        	.build();

→ 객체를 생성할 수 있는 빌더를 builder() 함수를 통해 얻고, 

거기에 셋팅하고 마지막에 build()를 통해 빌더를 작동시켜 객체를 생성한다.

출처 : https://pamyferret.tistory.com/67

 

 

 


 

JpaRepository

Posts 클래스로 Database를 접근하게 해줄 JpaRepository를 생성한다.

 

 

 

 

 

[PostsRepository.java]

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {

}

 

 

보통 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자이다.

JPA에선 Repository라고 부르며 인터페이스로 생성한다.

단순히 인터페이스를 생성 후, 
JpaRepository<Entity 클래스, PK 타입>를 상속하면 
기본적인 CRUD 메소드가 자동으로 생성된다.

 

 

 

🚨 주의할 점

Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다.

둘은 아주 밀접한 관계이고, 
Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없다.

Entity 클래스와 기본 Repository는 함께 움직여야 하므로
도메인 패키지에서 함께 관리한다.

 

 

 


 

Spring Data JPA 테스트 코드 작성하기

 

[PostRepository.test]

@SpringBootTest
class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @AfterEach  // 1
    public void cleanUp() {
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder()  // 2
                .title(title)
                .content(content)
                .author("ummchicken")
                .build());

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

        // then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }
    
}

 

 

1. @AfterEach

  • Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정
  • 여러 테스트가 동시에 수행되면, 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트가 실패할 수 있다.

 

 

2. postsRepository.save()

  • 테이블 posts에 insert / update 쿼리를 실행한다.
  • id 값이 있다면 update, 없다면 insert 쿼리가 실행된다.

 

 

3. postRepository.findAll()

  • 테이블 posts에 있는 모든 데이터를 조회해오는 메소드이다.